mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-10-23 03:37:01 +09:00
Migrate frontend JavaScript to TypeScript (#3950)
* Migrate frontend JavaScript to TypeScript * Fix Docker frontend TypeScript checks * Address TypeScript migration review feedback * Avoid duplicate cron system admin during patches
This commit is contained in:
1 parent
a47d36adcc
commit
0ac0cdafe0
40 files changed
+883
-208
No files matched your search
@@ -83,6 +83,7 @@ COPY src/themes/huraga/package.json src/themes/huraga/package.json
|
||||
|
||||
RUN --mount=type=cache,target=/root/.npm CYPRESS_INSTALL_BINARY=0 npm ci
|
||||
|
||||
COPY tsconfig.json ./
|
||||
COPY src/themes/admin_default ./src/themes/admin_default
|
||||
COPY src/themes/huraga ./src/themes/huraga
|
||||
COPY src/modules ./src/modules
|
||||
|
||||
@@ -1,4 +1,42 @@
|
||||
function injectCSRFToken(params, token) {
|
||||
type ApiParams = FormData | Record<string, unknown> | string | null | undefined;
|
||||
type RequestBody = FormData | string | null;
|
||||
|
||||
interface ParsedResponse {
|
||||
payload: unknown;
|
||||
rawText: string;
|
||||
}
|
||||
|
||||
interface ApiErrorPayload {
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
result?: unknown;
|
||||
}
|
||||
|
||||
function isApiResponsePayload(payload: unknown): payload is ApiErrorPayload | null {
|
||||
return payload === null || (typeof payload === 'object' && !Array.isArray(payload));
|
||||
}
|
||||
|
||||
interface MinimalResponse {
|
||||
ok?: boolean;
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
text?: () => Promise<string>;
|
||||
}
|
||||
|
||||
interface ApiErrorLike {
|
||||
code?: string;
|
||||
message?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface ApiTimeoutOptions {
|
||||
timeoutMs?: number;
|
||||
timeoutMessage?: string | null;
|
||||
}
|
||||
|
||||
function injectCSRFToken<T extends ApiParams>(params: T, token: string): T {
|
||||
if (params instanceof FormData) {
|
||||
if (!params.has('CSRFToken')) {
|
||||
params.append('CSRFToken', token);
|
||||
@@ -11,29 +49,29 @@ function injectCSRFToken(params, token) {
|
||||
return params;
|
||||
}
|
||||
|
||||
function buildRequestBody(method, params, url) {
|
||||
function buildRequestBody(method: string, params: ApiParams, url: URL): { body: RequestBody; isFormData: boolean } {
|
||||
const methodLower = method.toLowerCase();
|
||||
const isFormData = params instanceof FormData;
|
||||
let body = null;
|
||||
let body: RequestBody = null;
|
||||
|
||||
if (methodLower === 'get') {
|
||||
if (isFormData) {
|
||||
for (const [key, value] of params.entries()) {
|
||||
if (key !== 'CSRFToken') {
|
||||
url.searchParams.append(key, value);
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
} else if (params && typeof params === 'object') {
|
||||
Object.keys(params)
|
||||
.filter((key) => key !== 'CSRFToken')
|
||||
.forEach((key) => url.searchParams.append(key, params[key]));
|
||||
.forEach((key) => url.searchParams.append(key, String(params[key])));
|
||||
} else if (params) {
|
||||
url.search = params;
|
||||
url.search = String(params);
|
||||
url.searchParams.delete('CSRFToken');
|
||||
}
|
||||
} else if (['post', 'put', 'patch', 'delete'].includes(methodLower)) {
|
||||
if (isFormData || typeof params === 'string') {
|
||||
body = params;
|
||||
body = params as RequestBody;
|
||||
} else {
|
||||
body = JSON.stringify(params);
|
||||
}
|
||||
@@ -42,8 +80,16 @@ function buildRequestBody(method, params, url) {
|
||||
return { body, isFormData };
|
||||
}
|
||||
|
||||
function buildHeaders({ url, body, isFormData, csrfToken, origin }) {
|
||||
const headers = {
|
||||
function buildHeaders(
|
||||
{ url, body, isFormData, csrfToken, origin }: {
|
||||
url: URL;
|
||||
body: RequestBody;
|
||||
isFormData: boolean;
|
||||
csrfToken?: string | null;
|
||||
origin: string;
|
||||
},
|
||||
): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
if (url.origin === origin) {
|
||||
@@ -56,12 +102,12 @@ function buildHeaders({ url, body, isFormData, csrfToken, origin }) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function parseResponseBody(response) {
|
||||
async function parseResponseBody(response: MinimalResponse): Promise<ParsedResponse> {
|
||||
if (response.status === 204) {
|
||||
return { payload: null, rawText: '' };
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const text = await response.text?.() ?? '';
|
||||
if (!text) {
|
||||
return { payload: null, rawText: '' };
|
||||
}
|
||||
@@ -73,10 +119,11 @@ async function parseResponseBody(response) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateHttpResponse(response, parsed) {
|
||||
function validateHttpResponse(response: MinimalResponse, parsed: ParsedResponse): ApiErrorPayload | null {
|
||||
if (!response.ok) {
|
||||
const error = new Error(parsed.payload?.error?.message || `HTTP error ${response.status}: ${response.statusText}`);
|
||||
error.code = parsed.payload?.error?.code || `http_${response.status}`;
|
||||
const payload = isApiResponsePayload(parsed.payload) ? parsed.payload : null;
|
||||
const error = new Error(payload?.error?.message || `HTTP error ${response.status}: ${response.statusText}`);
|
||||
error.code = payload?.error?.code || `http_${response.status}`;
|
||||
error.status = response.status;
|
||||
error.rawBody = parsed.rawText;
|
||||
throw error;
|
||||
@@ -86,10 +133,10 @@ function validateHttpResponse(response, parsed) {
|
||||
throw new Error('Invalid or non-JSON response from server');
|
||||
}
|
||||
|
||||
return parsed.payload;
|
||||
return isApiResponsePayload(parsed.payload) ? parsed.payload : null;
|
||||
}
|
||||
|
||||
function interpretResponse(payload) {
|
||||
function interpretResponse(payload: ApiErrorPayload | null) {
|
||||
if (!payload) {
|
||||
return null;
|
||||
}
|
||||
@@ -103,7 +150,7 @@ function interpretResponse(payload) {
|
||||
return payload.result;
|
||||
}
|
||||
|
||||
function normalizeApiError(error, { timeoutMs, timeoutMessage }) {
|
||||
function normalizeApiError(error: ApiErrorLike, { timeoutMs = 30000, timeoutMessage = null }: ApiTimeoutOptions = {}) {
|
||||
if (error.name === 'AbortError') {
|
||||
return {
|
||||
message: timeoutMessage || `Request timed out after ${timeoutMs / 1000} seconds`,
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
/**
|
||||
* FOSSBilling API for JavaScript.
|
||||
*
|
||||
@@ -8,7 +9,7 @@
|
||||
* with this source code in the file LICENSE
|
||||
*/
|
||||
|
||||
import { parseDataAttr } from './parse-data-attr.mjs';
|
||||
import { parseDataAttr } from './parse-data-attr.mts';
|
||||
import {
|
||||
injectCSRFToken,
|
||||
buildRequestBody,
|
||||
@@ -17,11 +18,11 @@ import {
|
||||
validateHttpResponse,
|
||||
interpretResponse,
|
||||
normalizeApiError,
|
||||
} from './api-helpers.mjs';
|
||||
} from './api-helpers.mts';
|
||||
import {
|
||||
dispatchLinkAction,
|
||||
createLinkLoadingState,
|
||||
} from './link-helpers.mjs';
|
||||
} from './link-helpers.mts';
|
||||
|
||||
/**
|
||||
* Tools for the API wrapper.
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
/**
|
||||
* FOSSBilling browser runtime.
|
||||
*
|
||||
File renamed without changes.
File renamed without changes.
@@ -9,11 +9,11 @@ import {
|
||||
normalizeApiError,
|
||||
parseResponseBody,
|
||||
validateHttpResponse,
|
||||
} from '../api-helpers.mjs';
|
||||
} from '../api-helpers.mts';
|
||||
|
||||
describe('API request helpers', () => {
|
||||
test('injects a missing CSRF token without replacing an existing one', () => {
|
||||
const params = {};
|
||||
const params: Record<string, unknown> = {};
|
||||
assert.equal(injectCSRFToken(params, 'token'), params);
|
||||
assert.equal(params.CSRFToken, 'token');
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('API response helpers', () => {
|
||||
{ ok: false, status: 422, statusText: 'Invalid' },
|
||||
{ payload: { error: { message: 'Bad input', code: 'bad_input' } }, rawText: 'body' },
|
||||
),
|
||||
(error) => error.message === 'Bad input' && error.code === 'bad_input' && error.status === 422,
|
||||
(error: any) => error.message === 'Bad input' && error.code === 'bad_input' && error.status === 422,
|
||||
);
|
||||
assert.throws(
|
||||
() => validateHttpResponse({ ok: true }, { payload: null, rawText: '<html>' }),
|
||||
@@ -116,7 +116,7 @@ describe('API response helpers', () => {
|
||||
assert.equal(interpretResponse({ result: 0 }), 0);
|
||||
assert.throws(
|
||||
() => interpretResponse({ error: { message: 'Denied', code: 'denied' } }),
|
||||
(error) => error.message === 'Denied' && error.code === 'denied',
|
||||
(error: any) => error.message === 'Denied' && error.code === 'denied',
|
||||
);
|
||||
});
|
||||
|
||||
+28
-26
@@ -1,52 +1,54 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from 'node:test';
|
||||
|
||||
import { createLinkLoadingState, dispatchLinkAction } from '../link-helpers.mjs';
|
||||
import { createLinkLoadingState, dispatchLinkAction } from '../link-helpers.mts';
|
||||
|
||||
globalThis.window = globalThis;
|
||||
const testGlobal = globalThis as any;
|
||||
|
||||
const createClassList = (classes = []) => ({
|
||||
testGlobal.window = testGlobal;
|
||||
|
||||
const createClassList = (classes: string[] = []) => ({
|
||||
classes: new Set(classes),
|
||||
add(value) { this.classes.add(value); },
|
||||
remove(value) { this.classes.delete(value); },
|
||||
contains(value) { return this.classes.has(value); },
|
||||
add(value: string) { this.classes.add(value); },
|
||||
remove(value: string) { this.classes.delete(value); },
|
||||
contains(value: string) { return this.classes.has(value); },
|
||||
});
|
||||
|
||||
const createElement = () => ({
|
||||
const createElement = (): any => ({
|
||||
className: '',
|
||||
attributes: {},
|
||||
classList: createClassList(),
|
||||
getAttribute(name) { return this.attributes[name] ?? null; },
|
||||
setAttribute(name, value) { this.attributes[name] = String(value); },
|
||||
getAttribute(name: string) { return this.attributes[name] ?? null; },
|
||||
setAttribute(name: string, value: string) { this.attributes[name] = String(value); },
|
||||
});
|
||||
|
||||
globalThis.document = {
|
||||
testGlobal.document = {
|
||||
createElement,
|
||||
createTextNode: (text) => ({ textContent: text }),
|
||||
createTextNode: (text: string) => ({ textContent: text }),
|
||||
querySelector: () => null,
|
||||
};
|
||||
|
||||
const createLink = ({ disabled = false, parent = null } = {}) => ({
|
||||
const createLink = ({ disabled = false, parent = null }: { disabled?: boolean; parent?: any } = {}): any => ({
|
||||
innerHTML: '<span>Original</span>',
|
||||
attributes: {},
|
||||
classList: createClassList(disabled ? ['disabled'] : []),
|
||||
parentElement: parent,
|
||||
getAttribute(name) { return this.attributes[name] ?? null; },
|
||||
setAttribute(name, value) { this.attributes[name] = String(value); },
|
||||
removeAttribute(name) { delete this.attributes[name]; },
|
||||
replaceChildren(...children) { this.children = children; },
|
||||
getAttribute(name: string) { return this.attributes[name] ?? null; },
|
||||
setAttribute(name: string, value: string) { this.attributes[name] = String(value); },
|
||||
removeAttribute(name: string) { delete this.attributes[name]; },
|
||||
replaceChildren(...children: any[]) { this.children = children; },
|
||||
closest() { return parent; },
|
||||
});
|
||||
|
||||
describe('dispatchLinkAction', () => {
|
||||
test('dispatches links without a modal directly', () => {
|
||||
let request;
|
||||
let request: unknown;
|
||||
dispatchLinkAction({}, '/endpoint', null, (...args) => { request = args; });
|
||||
assert.deepEqual(request, ['GET', '/endpoint']);
|
||||
});
|
||||
|
||||
test('uses native confirm when the modal library is unavailable', () => {
|
||||
let request;
|
||||
let request: unknown;
|
||||
window.confirm = (message) => {
|
||||
assert.equal(message, 'Continue?');
|
||||
return true;
|
||||
@@ -78,7 +80,7 @@ describe('dispatchLinkAction', () => {
|
||||
return 'Because';
|
||||
};
|
||||
|
||||
let request;
|
||||
let request: unknown;
|
||||
dispatchLinkAction(
|
||||
{ modal: { type: 'prompt', key: 'reason', label: 'Reason', value: 'Default' } },
|
||||
'/endpoint',
|
||||
@@ -89,9 +91,9 @@ describe('dispatchLinkAction', () => {
|
||||
});
|
||||
|
||||
test('configures library confirm and prompt callbacks', () => {
|
||||
const created = [];
|
||||
const modals = { create: (config) => created.push(config) };
|
||||
const requests = [];
|
||||
const created: any[] = [];
|
||||
const modals = { create: (config: any) => created.push(config) };
|
||||
const requests: unknown[] = [];
|
||||
|
||||
dispatchLinkAction(
|
||||
{ modal: { type: 'confirm', title: 'Delete', button: 'Delete', buttonColor: 'danger' } },
|
||||
@@ -157,7 +159,7 @@ describe('createLinkLoadingState', () => {
|
||||
children: [],
|
||||
appendChild(child) { this.children.push(child); },
|
||||
};
|
||||
const removed = [];
|
||||
const removed: unknown[] = [];
|
||||
const listeners = new Map();
|
||||
window.addEventListener = (name, listener) => listeners.set(name, listener);
|
||||
window.removeEventListener = (name, listener) => {
|
||||
@@ -167,11 +169,11 @@ describe('createLinkLoadingState', () => {
|
||||
};
|
||||
|
||||
const originalCreateElement = document.createElement;
|
||||
document.createElement = () => {
|
||||
const element = originalCreateElement();
|
||||
document.createElement = (() => {
|
||||
const element = originalCreateElement('span');
|
||||
element.remove = () => removed.push(element);
|
||||
return element;
|
||||
};
|
||||
}) as typeof document.createElement;
|
||||
|
||||
const state = createLinkLoadingState(createLink({ parent }));
|
||||
state.set({
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from 'node:test';
|
||||
|
||||
import { parseDataAttr } from '../parse-data-attr.mjs';
|
||||
import { parseDataAttr } from '../parse-data-attr.mts';
|
||||
|
||||
const parse = (value) => parseDataAttr(JSON.stringify(value));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
/**
|
||||
* Default FOSSBilling rich text editor adapter.
|
||||
*
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ensureDir,
|
||||
sharedLoaders,
|
||||
writeAssetManifest,
|
||||
} from './tools/esbuild-helpers.mjs';
|
||||
} from './tools/esbuild-helpers.mts';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = resolve(__dirname, '..');
|
||||
@@ -20,21 +20,21 @@ async function build() {
|
||||
await ensureDir(resolve(outputDir, 'editor'));
|
||||
|
||||
await buildJsFile({
|
||||
entryPoint: resolve(__dirname, 'core/fossbilling.js'),
|
||||
entryPoint: resolve(__dirname, 'core/fossbilling.ts'),
|
||||
outfile: resolve(outputDir, 'js/fossbilling.js'),
|
||||
isProduction,
|
||||
drop: [],
|
||||
});
|
||||
|
||||
await buildJsFile({
|
||||
entryPoint: resolve(__dirname, 'core/api.js'),
|
||||
entryPoint: resolve(__dirname, 'core/api.ts'),
|
||||
outfile: resolve(outputDir, 'js/api.js'),
|
||||
isProduction,
|
||||
drop: [],
|
||||
});
|
||||
|
||||
await buildJsFile({
|
||||
entryPoint: resolve(__dirname, 'editor/ckeditor.js'),
|
||||
entryPoint: resolve(__dirname, 'editor/ckeditor.ts'),
|
||||
outfile: resolve(outputDir, 'editor/ckeditor.js'),
|
||||
isProduction,
|
||||
drop: [],
|
||||
@@ -3,7 +3,7 @@ import { dirname, extname, join, relative, resolve } from 'path';
|
||||
import { readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { parseManifest, extractIconReferencesFromContent, computeIconErrors } from './icon-check-helpers.mjs';
|
||||
import { parseManifest, extractIconReferencesFromContent, computeIconErrors } from './icon-check-helpers.mts';
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
|
||||
@@ -32,7 +32,7 @@ const themes = [
|
||||
},
|
||||
];
|
||||
|
||||
const sourceExtensions = new Set(['.twig', '.js', '.mjs', '.php']);
|
||||
const sourceExtensions = new Set(['.twig', '.ts', '.php']);
|
||||
|
||||
function walkFiles(dir) {
|
||||
const files = [];
|
||||
@@ -1,59 +0,0 @@
|
||||
import { readdirSync } from 'fs';
|
||||
import { extname, join, resolve } from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const scanRoots = [
|
||||
resolve(rootDir, 'frontend'),
|
||||
resolve(rootDir, 'src/themes/admin_default'),
|
||||
resolve(rootDir, 'src/themes/huraga'),
|
||||
];
|
||||
const sourceExtensions = new Set(['.js', '.mjs']);
|
||||
|
||||
function walkFiles(dir) {
|
||||
const files = [];
|
||||
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
|
||||
if (path.includes('/assets/build/') || path.includes('/node_modules/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...walkFiles(path));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sourceExtensions.has(extname(entry.name))) {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
const files = scanRoots.flatMap(walkFiles).sort();
|
||||
const failures = [];
|
||||
|
||||
for (const file of files) {
|
||||
const result = spawnSync(process.execPath, ['--check', file], {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
failures.push({ file, stderr: result.stderr.trim() });
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
for (const failure of failures) {
|
||||
console.error(`${failure.file}\n${failure.stderr}`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Checked JavaScript syntax for ${files.length} files`);
|
||||
@@ -5,8 +5,48 @@ import { PurgeCSS } from 'purgecss';
|
||||
import { dirname, join, resolve } from 'path';
|
||||
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'fs/promises';
|
||||
import * as esbuild from 'esbuild';
|
||||
import type { BuildOptions, Drop, Loader, Plugin } from 'esbuild';
|
||||
|
||||
export const sharedLoaders = {
|
||||
export type LoaderMap = Record<string, Loader>;
|
||||
|
||||
interface ThemeBuildPaths {
|
||||
buildDir: string;
|
||||
jsDir: string;
|
||||
cssDir: string;
|
||||
symbolDir: string;
|
||||
}
|
||||
|
||||
interface PurgeOptions {
|
||||
themePath?: string;
|
||||
enabled?: boolean;
|
||||
area?: 'admin' | 'client';
|
||||
additionalStandardSafelist?: RegExp[];
|
||||
}
|
||||
|
||||
interface CssBuildOptions {
|
||||
entryPoint: string;
|
||||
outfile: string;
|
||||
nodeModulesDir: string;
|
||||
isProduction: boolean;
|
||||
loader?: LoaderMap;
|
||||
themePath?: string;
|
||||
purge?: Omit<PurgeOptions, 'themePath' | 'enabled'>;
|
||||
}
|
||||
|
||||
interface JsBuildOptions {
|
||||
entryPoint: string;
|
||||
outfile?: string;
|
||||
outdir?: string;
|
||||
entryNames?: string;
|
||||
chunkNames?: string;
|
||||
isProduction: boolean;
|
||||
loader?: LoaderMap;
|
||||
drop?: Drop[];
|
||||
splitting?: boolean;
|
||||
format?: BuildOptions['format'];
|
||||
}
|
||||
|
||||
export const sharedLoaders: LoaderMap = {
|
||||
'.svg': 'file',
|
||||
'.woff': 'file',
|
||||
'.woff2': 'file',
|
||||
@@ -15,7 +55,7 @@ export const sharedLoaders = {
|
||||
'.webp': 'file',
|
||||
};
|
||||
|
||||
export function sassPlugin(nodeModulesDir, isProduction) {
|
||||
export function sassPlugin(nodeModulesDir: string, isProduction: boolean): Plugin {
|
||||
return {
|
||||
name: 'sass',
|
||||
setup(build) {
|
||||
@@ -37,24 +77,24 @@ export function sassPlugin(nodeModulesDir, isProduction) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureDir(dir) {
|
||||
export async function ensureDir(dir: string) {
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
export async function removeDirContents(dir) {
|
||||
export async function removeDirContents(dir: string) {
|
||||
try {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
await rm(join(dir, entry.name), { recursive: true, force: true });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemeBuildPaths(themePath) {
|
||||
export function getThemeBuildPaths(themePath: string): ThemeBuildPaths {
|
||||
const buildDir = resolve(themePath, 'assets/build');
|
||||
|
||||
return {
|
||||
@@ -65,14 +105,14 @@ export function getThemeBuildPaths(themePath) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareThemeBuildDirs(paths) {
|
||||
export async function prepareThemeBuildDirs(paths: ThemeBuildPaths) {
|
||||
await removeDirContents(paths.buildDir);
|
||||
await ensureDir(paths.jsDir);
|
||||
await ensureDir(paths.cssDir);
|
||||
await ensureDir(paths.symbolDir);
|
||||
}
|
||||
|
||||
export async function postprocessCssFile(cssPath, isProduction) {
|
||||
export async function postprocessCssFile(cssPath: string, isProduction: boolean) {
|
||||
const css = await readFile(cssPath, 'utf8');
|
||||
const mapPath = `${cssPath}.map`;
|
||||
let prevMap;
|
||||
@@ -81,7 +121,7 @@ export async function postprocessCssFile(cssPath, isProduction) {
|
||||
try {
|
||||
prevMap = await readFile(mapPath, 'utf8');
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -99,7 +139,7 @@ export async function postprocessCssFile(cssPath, isProduction) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function purgeCssFile(cssFilePath, options = {}) {
|
||||
export async function purgeCssFile(cssFilePath: string, options: PurgeOptions) {
|
||||
const {
|
||||
themePath,
|
||||
enabled = false,
|
||||
@@ -111,6 +151,10 @@ export async function purgeCssFile(cssFilePath, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!themePath) {
|
||||
throw new Error('PurgeCSS requires a themePath when enabled');
|
||||
}
|
||||
|
||||
try {
|
||||
const css = await readFile(cssFilePath, 'utf8');
|
||||
const modulesPath = resolve(themePath, '../../modules');
|
||||
@@ -119,10 +163,10 @@ export async function purgeCssFile(cssFilePath, options = {}) {
|
||||
const purgeCSSResult = await new PurgeCSS().purge({
|
||||
content: [
|
||||
`${themePath}/html/**/*.twig`,
|
||||
`${themePath}/assets/**/*.js`,
|
||||
`${themePath}/assets/**/*.{js,ts}`,
|
||||
`${modulesPath}/*/templates/${moduleArea}/**/*.twig`,
|
||||
],
|
||||
css: [{ raw: css, extension: 'css' }],
|
||||
css: [{ raw: css, extension: 'css' } as unknown as { raw: string }],
|
||||
safelist: {
|
||||
standard: [
|
||||
/^fi-/,
|
||||
@@ -168,11 +212,12 @@ export async function purgeCssFile(cssFilePath, options = {}) {
|
||||
console.log(`PurgeCSS applied to ${cssFilePath.split('/').pop()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`PurgeCSS failed for ${cssFilePath.split('/').pop()}: ${error.message}`);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`PurgeCSS failed for ${cssFilePath.split('/').pop()}: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
function formatBytes(bytes: number) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes}b`;
|
||||
}
|
||||
@@ -189,12 +234,12 @@ function formatBytes(bytes) {
|
||||
return `${size.toFixed(1)}${unit}`;
|
||||
}
|
||||
|
||||
export async function logFileSize(filePath, label = filePath.split('/').pop()) {
|
||||
export async function logFileSize(filePath: string, label = filePath.split('/').pop()) {
|
||||
const fileStat = await stat(filePath);
|
||||
console.log(` ${label}: ${formatBytes(fileStat.size)}`);
|
||||
}
|
||||
|
||||
export async function buildCssFile(options) {
|
||||
export async function buildCssFile(options: CssBuildOptions) {
|
||||
const {
|
||||
entryPoint,
|
||||
outfile,
|
||||
@@ -222,6 +267,10 @@ export async function buildCssFile(options) {
|
||||
await postprocessCssFile(outfile, isProduction);
|
||||
|
||||
if (purge) {
|
||||
if (isProduction && !themePath) {
|
||||
throw new Error(`Cannot purge ${outfile}: themePath is required`);
|
||||
}
|
||||
|
||||
await purgeCssFile(outfile, {
|
||||
themePath,
|
||||
enabled: isProduction,
|
||||
@@ -232,7 +281,7 @@ export async function buildCssFile(options) {
|
||||
await logFileSize(outfile, `${outfile.split('/').pop()} after CSS post-processing`);
|
||||
}
|
||||
|
||||
export async function buildJsFile(options) {
|
||||
export async function buildJsFile(options: JsBuildOptions) {
|
||||
const {
|
||||
entryPoint,
|
||||
outfile,
|
||||
@@ -246,7 +295,7 @@ export async function buildJsFile(options) {
|
||||
format = splitting ? 'esm' : undefined,
|
||||
} = options;
|
||||
|
||||
const buildOptions = {
|
||||
const buildOptions: BuildOptions = {
|
||||
entryPoints: [entryPoint],
|
||||
bundle: true,
|
||||
platform: 'browser',
|
||||
@@ -280,6 +329,6 @@ export async function buildJsFile(options) {
|
||||
await esbuild.build(buildOptions);
|
||||
}
|
||||
|
||||
export async function writeAssetManifest(buildDir, manifest) {
|
||||
export async function writeAssetManifest(buildDir: string, manifest: Record<string, string>) {
|
||||
await writeFile(join(buildDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
||||
}
|
||||
File renamed without changes.
@@ -124,10 +124,12 @@ export async function generateIconSprite(options) {
|
||||
spriter.add(filePath, fileName, content);
|
||||
}
|
||||
|
||||
const result = await new Promise((res, rej) => {
|
||||
type SpriteCompileResult = { symbol: { sprite: { contents: string } } };
|
||||
|
||||
const result = await new Promise<SpriteCompileResult>((res, rej) => {
|
||||
spriter.compile((error, result) => {
|
||||
if (error) rej(error);
|
||||
else res(result);
|
||||
else res(result as SpriteCompileResult);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import {
|
||||
computeIconErrors,
|
||||
extractIconReferencesFromContent,
|
||||
parseManifest,
|
||||
} from '../icon-check-helpers.mjs';
|
||||
} from '../icon-check-helpers.mts';
|
||||
|
||||
describe('icon manifest helpers', () => {
|
||||
test('normalizes manifest entries and tracks dynamic icons', () => {
|
||||
Vendored
+159
@@ -0,0 +1,159 @@
|
||||
type ApiParams = Record<string, unknown> | FormData | string | null | undefined;
|
||||
type ApiSuccessHandler = (result: unknown) => void;
|
||||
type ApiErrorHandler = (error: { message: string; code?: string }) => void;
|
||||
|
||||
interface FOSSBillingApiNamespace {
|
||||
get(endpoint: string, params?: ApiParams, successHandler?: ApiSuccessHandler, errorHandler?: ApiErrorHandler): void;
|
||||
post(endpoint: string, params?: ApiParams, successHandler?: ApiSuccessHandler, errorHandler?: ApiErrorHandler): void;
|
||||
put(endpoint: string, params?: ApiParams, successHandler?: ApiSuccessHandler, errorHandler?: ApiErrorHandler): void;
|
||||
patch(endpoint: string, params?: ApiParams, successHandler?: ApiSuccessHandler, errorHandler?: ApiErrorHandler): void;
|
||||
delete(endpoint: string, params?: ApiParams, successHandler?: ApiSuccessHandler, errorHandler?: ApiErrorHandler): void;
|
||||
}
|
||||
|
||||
interface FOSSBillingEditorInstance {
|
||||
destroy?: () => void | Promise<void>;
|
||||
getData: () => string;
|
||||
setData: (data: string) => void;
|
||||
model?: {
|
||||
document?: {
|
||||
on?: (event: string, callback: () => void) => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface FOSSBillingEditorAdapter {
|
||||
create: (element: HTMLElement, options?: Record<string, unknown>) => Promise<FOSSBillingEditorInstance>;
|
||||
}
|
||||
|
||||
interface FOSSBillingEditorRegistry {
|
||||
all(): FOSSBillingEditorInstance[];
|
||||
create(element: HTMLElement, options?: Record<string, unknown>): Promise<FOSSBillingEditorInstance>;
|
||||
get(elementOrName: HTMLElement | string): FOSSBillingEditorInstance | null;
|
||||
registerAdapter(name: string, adapter: FOSSBillingEditorAdapter): void;
|
||||
syncForm(form: HTMLFormElement, formData?: FormData): void;
|
||||
validateForm(form: HTMLFormElement): boolean;
|
||||
}
|
||||
|
||||
interface FOSSBillingRuntime {
|
||||
api?: {
|
||||
admin: FOSSBillingApiNamespace;
|
||||
client: FOSSBillingApiNamespace;
|
||||
guest: FOSSBillingApiNamespace;
|
||||
makeRequest: (
|
||||
method: string,
|
||||
url: string,
|
||||
params?: ApiParams,
|
||||
successHandler?: ApiSuccessHandler,
|
||||
errorHandler?: ApiErrorHandler,
|
||||
enableLoader?: boolean,
|
||||
timeoutMs?: number,
|
||||
timeoutMessage?: string | null,
|
||||
) => void;
|
||||
_afterComplete: (element: Element, result: unknown) => void;
|
||||
};
|
||||
backToTop?: () => void;
|
||||
charts?: {
|
||||
renderTimeSeriesSparkline: (target: HTMLElement | string, data: unknown, options?: Record<string, unknown>) => unknown;
|
||||
};
|
||||
cookieCreate?: (name: string, value: string, days?: number) => void;
|
||||
cookieRead?: (name: string) => string | null;
|
||||
detectTimezone?: () => string;
|
||||
editor?: FOSSBillingEditorRegistry;
|
||||
initTimezone?: () => void;
|
||||
message?: (message: string, type?: string) => void;
|
||||
ready?: (callback: (runtime: FOSSBillingRuntime) => void) => void;
|
||||
tools?: {
|
||||
getBaseURL: (url: string) => string;
|
||||
getCSRFToken: () => string | null;
|
||||
isJSON: (jsonString: string) => boolean;
|
||||
serializeFormData: (formData: FormData) => string;
|
||||
serializeFormDataToJSON: (formData: FormData) => string;
|
||||
serializeFormDataToObject: (formData: FormData) => Record<string, unknown>;
|
||||
};
|
||||
ui?: {
|
||||
notify: (message: string, type?: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface ModalCreateOptions {
|
||||
confirmButton?: string;
|
||||
confirmButtonColor?: string;
|
||||
confirmCallback?: () => void;
|
||||
content?: string;
|
||||
label?: string;
|
||||
promptConfirmCallback?: (value: string) => void;
|
||||
title?: string;
|
||||
type?: string;
|
||||
value?: string;
|
||||
}
|
||||
|
||||
interface ModalsRuntime {
|
||||
create(options: ModalCreateOptions): unknown;
|
||||
create(type: string, options: ModalCreateOptions): unknown;
|
||||
hideAll(): void;
|
||||
}
|
||||
|
||||
interface BootstrapConstructor<T = unknown> {
|
||||
new (element: Element, options?: Record<string, unknown>): T;
|
||||
getInstance?: (element: Element) => T | null;
|
||||
getOrCreateInstance?: (element: Element) => T;
|
||||
}
|
||||
|
||||
interface BootstrapRuntime {
|
||||
Collapse: BootstrapConstructor;
|
||||
Modal: BootstrapConstructor<{ hide: () => void; show: () => void }>;
|
||||
Popover: BootstrapConstructor<{ hide: () => void; show: () => void }>;
|
||||
Tab: BootstrapConstructor<{ show: () => void }>;
|
||||
Toast: BootstrapConstructor<{ show: () => void }>;
|
||||
Tooltip: BootstrapConstructor<{ dispose: () => void; hide: () => void; show: () => void }>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const bootstrap: BootstrapRuntime;
|
||||
const FOSSBilling: FOSSBillingRuntime;
|
||||
const Modals: ModalsRuntime | undefined;
|
||||
|
||||
interface Window {
|
||||
FOSSBilling: FOSSBillingRuntime;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
code?: string;
|
||||
rawBody?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
var bootstrap: BootstrapRuntime;
|
||||
var flashMessage: ((options: { message?: string; reload?: boolean | string; type?: string }) => void) | undefined;
|
||||
var FOSSBilling: FOSSBillingRuntime;
|
||||
var Modals: ModalsRuntime | undefined;
|
||||
var TomSelect: unknown;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const css: string;
|
||||
export default css;
|
||||
}
|
||||
|
||||
declare module '*?raw' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '@ckeditor/*/dist/*.css' {
|
||||
const css: string;
|
||||
export default css;
|
||||
}
|
||||
|
||||
declare module 'bootstrap/dist/js/bootstrap.esm.js' {
|
||||
export const Collapse: BootstrapRuntime['Collapse'];
|
||||
export const Modal: BootstrapRuntime['Modal'];
|
||||
export const Popover: BootstrapRuntime['Popover'];
|
||||
export const Tab: BootstrapRuntime['Tab'];
|
||||
export const Toast: BootstrapRuntime['Toast'];
|
||||
export const Tooltip: BootstrapRuntime['Tooltip'];
|
||||
}
|
||||
|
||||
declare module 'sortable-tablesort/dist/sortable.min.js';
|
||||
|
||||
export {};
|
||||
Generated
+402
-1
@@ -11,6 +11,7 @@
|
||||
"src/themes/admin_default"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.3",
|
||||
"autoprefixer": "10.5.2",
|
||||
"ckeditor5": "48.3.0",
|
||||
"esbuild": "0.28.1",
|
||||
@@ -18,9 +19,11 @@
|
||||
"postcss": "8.5.16",
|
||||
"purgecss": "8.0.0",
|
||||
"sass": "1.101.0",
|
||||
"svg-sprite": "2.0.4"
|
||||
"svg-sprite": "2.0.4",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11"
|
||||
}
|
||||
},
|
||||
@@ -2134,6 +2137,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
|
||||
"integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/triple-beam": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||
@@ -2148,6 +2161,346 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript/typescript-aix-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-darwin-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-freebsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
|
||||
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-loong64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
|
||||
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-mips64el": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
|
||||
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-ppc64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
|
||||
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-riscv64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
|
||||
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-s390x": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
|
||||
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-linux-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-netbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-openbsd-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-sunos-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-arm64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
|
||||
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript/typescript-win32-x64": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
|
||||
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz",
|
||||
@@ -5269,6 +5622,48 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
|
||||
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@typescript/typescript-aix-ppc64": "7.0.2",
|
||||
"@typescript/typescript-darwin-arm64": "7.0.2",
|
||||
"@typescript/typescript-darwin-x64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-freebsd-x64": "7.0.2",
|
||||
"@typescript/typescript-linux-arm": "7.0.2",
|
||||
"@typescript/typescript-linux-arm64": "7.0.2",
|
||||
"@typescript/typescript-linux-loong64": "7.0.2",
|
||||
"@typescript/typescript-linux-mips64el": "7.0.2",
|
||||
"@typescript/typescript-linux-ppc64": "7.0.2",
|
||||
"@typescript/typescript-linux-riscv64": "7.0.2",
|
||||
"@typescript/typescript-linux-s390x": "7.0.2",
|
||||
"@typescript/typescript-linux-x64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-netbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-arm64": "7.0.2",
|
||||
"@typescript/typescript-openbsd-x64": "7.0.2",
|
||||
"@typescript/typescript-sunos-x64": "7.0.2",
|
||||
"@typescript/typescript-win32-arm64": "7.0.2",
|
||||
"@typescript/typescript-win32-x64": "7.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
@@ -5645,6 +6040,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tabler/icons": "3.44.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
}
|
||||
},
|
||||
"src/themes/huraga": {
|
||||
@@ -5656,6 +6054,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tabler/icons": "3.44.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -6,14 +6,14 @@
|
||||
"scripts": {
|
||||
"build": "npm run check && npm run build-core && npm run build-themes",
|
||||
"build:production": "NODE_ENV=production npm run build",
|
||||
"build-core": "node ./frontend/esbuild.mjs",
|
||||
"build-core": "node ./frontend/esbuild.mts",
|
||||
"build-themes": "npm run build-admin_default && npm run build -w huraga",
|
||||
"build-huraga": "npm run build -w huraga",
|
||||
"build-admin_default": "npm run build -w admin_default",
|
||||
"check": "npm run check-js-syntax && npm run check-icons",
|
||||
"check-js-syntax": "node ./frontend/tools/check-js-syntax.mjs",
|
||||
"check-icons": "node ./frontend/tools/check-icons.mjs",
|
||||
"test:js": "node --test 'frontend/core/tests/**/*.test.mjs' 'frontend/tools/tests/**/*.test.mjs'",
|
||||
"check": "npm run check-types && npm run check-icons",
|
||||
"check-types": "tsc --noEmit",
|
||||
"check-icons": "node ./frontend/tools/check-icons.mts",
|
||||
"test:js": "node --test 'frontend/core/tests/**/*.test.mts' 'frontend/tools/tests/**/*.test.mts'",
|
||||
"cy:open": "npm exec --yes cypress@15.15.0 -- open",
|
||||
"cy:run": "npm exec --yes cypress@15.15.0 -- run",
|
||||
"cy:run:record": "npm exec --yes cypress@15.15.0 -- run --record"
|
||||
@@ -30,6 +30,7 @@
|
||||
"src/themes/admin_default"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.13.3",
|
||||
"autoprefixer": "10.5.2",
|
||||
"ckeditor5": "48.3.0",
|
||||
"esbuild": "0.28.1",
|
||||
@@ -37,7 +38,8 @@
|
||||
"postcss": "8.5.16",
|
||||
"purgecss": "8.0.0",
|
||||
"sass": "1.101.0",
|
||||
"svg-sprite": "2.0.4"
|
||||
"svg-sprite": "2.0.4",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
@@ -45,6 +47,7 @@
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24",
|
||||
"npm": ">=11"
|
||||
}
|
||||
}
|
||||
@@ -2100,7 +2100,17 @@ class UpdatePatcher implements InjectionAwareInterface
|
||||
}
|
||||
|
||||
if ($this->tableHasColumn('admin', 'role')) {
|
||||
$this->executeSql("UPDATE `admin` SET `system_name` = 'cron' WHERE `role` = 'cron' AND (`system_name` IS NULL OR `system_name` = '') ORDER BY `id` ASC LIMIT 1;");
|
||||
$this->executeSql("
|
||||
UPDATE `admin`
|
||||
SET `system_name` = 'cron'
|
||||
WHERE `role` = 'cron'
|
||||
AND (`system_name` IS NULL OR `system_name` = '')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM (SELECT `id` FROM `admin` WHERE `system_name` = 'cron' LIMIT 1) AS existing_cron
|
||||
)
|
||||
ORDER BY `id` ASC
|
||||
LIMIT 1;
|
||||
");
|
||||
$this->executeSql('ALTER TABLE `admin` DROP COLUMN `role`;');
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -1,6 +1,7 @@
|
||||
import './js/ui/modals';
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
import './js/ui/modals.ts';
|
||||
import * as tabler from '@tabler/core/js/tabler.js';
|
||||
import './js/fossbilling';
|
||||
import './js/fossbilling.ts';
|
||||
|
||||
globalThis.bootstrap = tabler.bootstrap;
|
||||
|
||||
@@ -103,14 +104,14 @@ function loadAdminFeature(loader, name) {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.querySelector('.datepicker')) {
|
||||
loadAdminFeature(async () => {
|
||||
const { default: initDatepickers } = await import('./js/datepicker');
|
||||
const { default: initDatepickers } = await import('./js/datepicker.ts');
|
||||
initDatepickers();
|
||||
}, 'datepickers');
|
||||
}
|
||||
|
||||
if (document.querySelector('.js-locale-selector, .autocomplete-selector, .canned_response_select')) {
|
||||
loadAdminFeature(async () => {
|
||||
const { default: initTomSelectControls } = await import('./js/tomselect');
|
||||
const { default: initTomSelectControls } = await import('./js/tomselect.ts');
|
||||
initTomSelectControls();
|
||||
}, 'Tom Select controls');
|
||||
}
|
||||
@@ -123,7 +124,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (document.querySelector('#theme-settings')) {
|
||||
loadAdminFeature(async () => {
|
||||
const { default: initThemeSettings } = await import('./js/ui/theme_settings');
|
||||
const { default: initThemeSettings } = await import('./js/ui/theme_settings.ts');
|
||||
initThemeSettings();
|
||||
}, 'theme settings');
|
||||
}
|
||||
+22
-9
@@ -1,7 +1,20 @@
|
||||
import Litepicker from 'litepicker';
|
||||
|
||||
type LitepickerOptions = ConstructorParameters<typeof Litepicker>[0];
|
||||
|
||||
interface LitepickerDate {
|
||||
format: (format: string) => string;
|
||||
}
|
||||
|
||||
type DateRangeInput = HTMLInputElement & {
|
||||
dataset: DOMStringMap & {
|
||||
nameFrom: string;
|
||||
nameTo: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function initDatepickers() {
|
||||
document.querySelectorAll('.datepicker').forEach(element => {
|
||||
document.querySelectorAll<HTMLInputElement>('.datepicker').forEach(element => {
|
||||
element.autocomplete = 'off';
|
||||
new Litepicker({
|
||||
element: element,
|
||||
@@ -12,16 +25,16 @@ export default function initDatepickers() {
|
||||
minYear: 1930,
|
||||
months: !!element.dataset.pickMonth,
|
||||
years: !!element.dataset.pickYear
|
||||
},
|
||||
} as LitepickerOptions['dropdowns'],
|
||||
singleMode: !isRanger(element),
|
||||
delimiter: ' to ',
|
||||
buttonText: {
|
||||
previousMonth: '<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 6l-6 6l6 6"></path></svg>',
|
||||
nextMonth: '<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M9 6l6 6l-6 6"></path></svg>',
|
||||
reset: '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-clock-x" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M20.926 13.15a9 9 0 1 0 -7.835 7.784"></path><path d="M12 7v5l2 2"></path><path d="M22 22l-5 -5"></path><path d="M17 22l5 -5"></path></svg>',
|
||||
},
|
||||
} as LitepickerOptions['buttonText'],
|
||||
setup: (picker) => {
|
||||
picker.on('selected', (date, dateTo) => {
|
||||
picker.on('selected', (date: LitepickerDate, dateTo?: LitepickerDate) => {
|
||||
if (isRanger(element) && dateTo) {
|
||||
setDateToEl(element.dataset.nameFrom, date.format('YYYY-MM-DD'), element);
|
||||
setDateToEl(element.dataset.nameTo, dateTo.format('YYYY-MM-DD'), element);
|
||||
@@ -33,19 +46,19 @@ export default function initDatepickers() {
|
||||
});
|
||||
}
|
||||
|
||||
function isRanger(element) {
|
||||
return element.dataset.nameFrom && element.dataset.nameTo;
|
||||
function isRanger(element: HTMLInputElement): element is DateRangeInput {
|
||||
return Boolean(element.dataset.nameFrom && element.dataset.nameTo);
|
||||
}
|
||||
|
||||
function setDateToEl(name, value, siblingEl = null) {
|
||||
let element = document.getElementById(name);
|
||||
function setDateToEl(name: string, value: string, siblingEl: HTMLElement) {
|
||||
let element = document.getElementById(name) as HTMLInputElement | null;
|
||||
if (element === null) {
|
||||
element = document.createElement('input');
|
||||
element.type = 'hidden';
|
||||
element.id = name;
|
||||
element.name = name;
|
||||
element.value = value;
|
||||
siblingEl.parentNode.insertBefore(element, siblingEl.nextSibling);
|
||||
siblingEl.parentNode!.insertBefore(element, siblingEl.nextSibling);
|
||||
return;
|
||||
}
|
||||
element.value = value;
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
import backToTop from "./ui/backToTop";
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
import backToTop from "./ui/backToTop.ts";
|
||||
|
||||
function renderTimeSeriesSparkline(...args) {
|
||||
return import("./ui/charts").then(({ renderTimeSeriesSparkline: renderChart }) => renderChart(...args));
|
||||
return import("./ui/charts.ts").then(({ renderTimeSeriesSparkline: renderChart }) => renderChart(...args));
|
||||
}
|
||||
|
||||
globalThis.FOSSBilling = Object.assign(globalThis.FOSSBilling || {}, {
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
import TomSelect from 'tom-select';
|
||||
import { getCSRFToken, getBaseURL } from './utils';
|
||||
import { getCSRFToken, getBaseURL } from './utils.ts';
|
||||
globalThis.TomSelect = TomSelect;
|
||||
|
||||
const createTomSelectTemplate = (data, escape, options = {}) => {
|
||||
File renamed without changes.
+1
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
const chartInstances = new WeakMap();
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
/**
|
||||
* JavaScript for the FOSSBilling modals. No jQuery required.
|
||||
*
|
||||
+1
-1
@@ -78,7 +78,7 @@ export default function initThemeSettings() {
|
||||
hrEl.classList.add('my-3');
|
||||
headingEl.classList.add('mb-3');
|
||||
el.before(hrEl); el.before(headingEl);
|
||||
headingEl.innerText = el.innerText;
|
||||
headingEl.textContent = el.textContent;
|
||||
el.remove();
|
||||
});
|
||||
|
||||
+20
-17
@@ -12,7 +12,7 @@
|
||||
* Get CSRF token from cookie
|
||||
* @returns {string|null} CSRF token or null if not found
|
||||
*/
|
||||
export function getCSRFToken() {
|
||||
export function getCSRFToken(): string | null {
|
||||
const match = document.cookie.match(/csrf_token=([^;]*)/);
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export function getCSRFToken() {
|
||||
* @param {string} path - Relative path
|
||||
* @returns {string} Absolute URL
|
||||
*/
|
||||
export function getBaseURL(path) {
|
||||
export function getBaseURL(path: string): string {
|
||||
if (!path) return '';
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
return path;
|
||||
@@ -36,7 +36,7 @@ export function getBaseURL(path) {
|
||||
* @param {HTMLElement|Document} [scope=document] - Scope to search within
|
||||
* @returns {HTMLElement|null} Found element or null
|
||||
*/
|
||||
export function safeQuerySelector(selector, scope = document) {
|
||||
export function safeQuerySelector(selector: string, scope: ParentNode = document): Element | null {
|
||||
try {
|
||||
return scope.querySelector(selector);
|
||||
} catch (error) {
|
||||
@@ -51,7 +51,7 @@ export function safeQuerySelector(selector, scope = document) {
|
||||
* @param {HTMLElement|Document} [scope=document] - Scope to search within
|
||||
* @returns {NodeList|[]} Found elements or empty array
|
||||
*/
|
||||
export function safeQuerySelectorAll(selector, scope = document) {
|
||||
export function safeQuerySelectorAll(selector: string, scope: ParentNode = document): NodeListOf<Element> | [] {
|
||||
try {
|
||||
return scope.querySelectorAll(selector);
|
||||
} catch (error) {
|
||||
@@ -66,9 +66,9 @@ export function safeQuerySelectorAll(selector, scope = document) {
|
||||
* @param {number} wait - Time to wait in milliseconds
|
||||
* @returns {Function} Debounced function
|
||||
*/
|
||||
export function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
export function debounce<T extends (...args: unknown[]) => void>(func: T, wait: number) {
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
return function executedFunction(this: unknown, ...args: Parameters<T>) {
|
||||
const context = this;
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
@@ -85,10 +85,10 @@ export function debounce(func, wait) {
|
||||
* @param {number} limit - Time limit in milliseconds
|
||||
* @returns {Function} Throttled function
|
||||
*/
|
||||
export function throttle(func, limit) {
|
||||
let lastFunc;
|
||||
let lastRan;
|
||||
return function(...args) {
|
||||
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number) {
|
||||
let lastFunc: ReturnType<typeof setTimeout> | undefined;
|
||||
let lastRan: number | undefined;
|
||||
return function(this: unknown, ...args: Parameters<T>) {
|
||||
const context = this;
|
||||
if (!lastRan) {
|
||||
func.apply(context, args);
|
||||
@@ -110,7 +110,7 @@ export function throttle(func, limit) {
|
||||
* @param {HTMLElement} element - Element to check
|
||||
* @returns {boolean} True if element is in viewport
|
||||
*/
|
||||
export function isElementInViewport(element) {
|
||||
export function isElementInViewport(element: Element | null): boolean {
|
||||
if (!element) return false;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
@@ -127,7 +127,7 @@ export function isElementInViewport(element) {
|
||||
* @param {HTMLElement|string} target - Element or selector
|
||||
* @param {Object} [options] - Scroll options
|
||||
*/
|
||||
export function scrollToElement(target, options = {}) {
|
||||
export function scrollToElement(target: Element | string, options: ScrollIntoViewOptions = {}) {
|
||||
const element = typeof target === 'string' ? safeQuerySelector(target) : target;
|
||||
if (!element) return;
|
||||
|
||||
@@ -146,7 +146,7 @@ export function scrollToElement(target, options = {}) {
|
||||
* @param {number} [decimals=2] - Number of decimals
|
||||
* @returns {string} Formatted string
|
||||
*/
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return '0 Bytes';
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
@@ -165,14 +165,17 @@ export function formatBytes(bytes, decimals = 2) {
|
||||
* @param {...Object} objects - Objects to merge
|
||||
* @returns {Object} Merged object
|
||||
*/
|
||||
export function deepMerge(...objects) {
|
||||
const result = {};
|
||||
export function deepMerge(...objects: Array<Record<string, unknown>>): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const obj of objects) {
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
||||
result[key] = deepMerge(result[key] || {}, obj[key]);
|
||||
const current = typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key])
|
||||
? result[key] as Record<string, unknown>
|
||||
: {};
|
||||
result[key] = deepMerge(current, obj[key] as Record<string, unknown>);
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
@@ -7,14 +7,14 @@ import {
|
||||
prepareThemeBuildDirs,
|
||||
sharedLoaders,
|
||||
writeAssetManifest,
|
||||
} from '../../../frontend/tools/esbuild-helpers.mjs';
|
||||
import { buildIconSprite } from '../../../frontend/tools/icon-sprite.mjs';
|
||||
} from '../../../frontend/tools/esbuild-helpers.mts';
|
||||
import { buildIconSprite } from '../../../frontend/tools/icon-sprite.mts';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const rootDir = resolve(__dirname, '../../..');
|
||||
const nodeModulesDir = resolve(rootDir, 'node_modules');
|
||||
const adminLoaders = { ...sharedLoaders, '.svg': 'dataurl' };
|
||||
const adminLoaders = { ...sharedLoaders, '.svg': 'dataurl' } as const;
|
||||
|
||||
async function build() {
|
||||
console.log(`Building admin_default theme (${isProduction ? 'production' : 'development'}) with esbuild ...`);
|
||||
@@ -62,7 +62,7 @@ async function build() {
|
||||
});
|
||||
|
||||
await buildJsFile({
|
||||
entryPoint: resolve(__dirname, 'assets/fossbilling.js'),
|
||||
entryPoint: resolve(__dirname, 'assets/fossbilling.ts'),
|
||||
outdir: paths.jsDir,
|
||||
entryNames: '[name]',
|
||||
chunkNames: 'chunks/[name]-[hash]',
|
||||
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"name": "admin_default",
|
||||
"scripts": {
|
||||
"build": "node ./esbuild.mjs",
|
||||
"dev": "node ./esbuild.mjs"
|
||||
"build": "node ./esbuild.mts",
|
||||
"dev": "node ./esbuild.mts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/core": "^1.2.0",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
import { Tooltip, Toast, Modal, Collapse, Tab } from 'bootstrap/dist/js/bootstrap.esm.js';
|
||||
import './js/utils';
|
||||
import initTheme from './js/ui/theme.js';
|
||||
import initPhoneInput from './js/phone-input.js';
|
||||
import './js/utils.ts';
|
||||
import initTheme from './js/ui/theme.ts';
|
||||
import initPhoneInput from './js/phone-input.ts';
|
||||
|
||||
globalThis.bootstrap = { Tooltip, Toast, Modal, Collapse, Tab };
|
||||
|
||||
@@ -126,7 +127,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const languageSelector = document.querySelector('.js-locale-selector');
|
||||
if (languageSelector) {
|
||||
// Dynamically import TomSelect module with error handling
|
||||
import('./js/tomselect.js')
|
||||
import('./js/tomselect.ts')
|
||||
.then(module => {
|
||||
if (typeof module.default === 'function') {
|
||||
module.default();
|
||||
+7
-5
@@ -1,7 +1,8 @@
|
||||
import intlTelInput from 'intl-tel-input';
|
||||
import type { Iso2 } from 'intl-tel-input';
|
||||
|
||||
export default function initPhoneInput() {
|
||||
const inputs = document.querySelectorAll('.js-phone-input');
|
||||
const inputs = document.querySelectorAll<HTMLInputElement>('.js-phone-input');
|
||||
|
||||
if (inputs.length === 0) {
|
||||
return;
|
||||
@@ -9,20 +10,21 @@ export default function initPhoneInput() {
|
||||
|
||||
const countries = intlTelInput.getAllCountries();
|
||||
const supportedCountries = new Set(countries.map((country) => country.iso2));
|
||||
const isSupportedCountry = (country: string): country is Iso2 => supportedCountries.has(country as Iso2);
|
||||
|
||||
inputs.forEach((input) => {
|
||||
const form = input.closest('form') || document;
|
||||
const countryCodeInput = form.querySelector('input[name="phone_cc"], input[name$="[phone_cc]"]');
|
||||
const countryCodeInput = form.querySelector<HTMLInputElement>('input[name="phone_cc"], input[name$="[phone_cc]"]');
|
||||
|
||||
if (!countryCodeInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const initialPhoneCountryCode = countryCodeInput.value.trim();
|
||||
const countrySelect = form.querySelector('select[name="country"], select[name$="[country]"]');
|
||||
const countrySelect = form.querySelector<HTMLSelectElement>('select[name="country"], select[name$="[country]"]');
|
||||
const selectedCountry = (countrySelect?.value || input.dataset.initialCountry || '').toLowerCase();
|
||||
const initialCountry = countries.find((country) => country.dialCode === initialPhoneCountryCode)?.iso2
|
||||
|| (supportedCountries.has(selectedCountry) ? selectedCountry : '');
|
||||
|| (isSupportedCountry(selectedCountry) ? selectedCountry : '');
|
||||
|
||||
const iti = intlTelInput(input, {
|
||||
initialCountry,
|
||||
@@ -45,7 +47,7 @@ export default function initPhoneInput() {
|
||||
|
||||
countrySelect?.addEventListener('change', () => {
|
||||
const country = countrySelect.value.toLowerCase();
|
||||
if (supportedCountries.has(country)) {
|
||||
if (isSupportedCountry(country)) {
|
||||
iti.setSelectedCountry(country);
|
||||
syncFields();
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-nocheck -- Runtime DOM/widget integration; converted to TS without changing behavior.
|
||||
/**
|
||||
* Huraga theme TomSelect setup - lazy loaded only when needed
|
||||
*/
|
||||
File renamed without changes.
@@ -1,11 +1,10 @@
|
||||
/**
|
||||
* Huraga theme utilities - minimal version of FOSSBilling utilities
|
||||
* Only includes functionality actually used by the Huraga theme
|
||||
*/
|
||||
|
||||
globalThis.FOSSBilling = Object.assign(globalThis.FOSSBilling || {}, {
|
||||
message: (message, type = "info") => {
|
||||
let color;
|
||||
window.FOSSBilling = Object.assign(window.FOSSBilling || {}, {
|
||||
message: (message: string, type = "info") => {
|
||||
let color: string;
|
||||
switch (type) {
|
||||
case "error":
|
||||
color = "danger";
|
||||
@@ -30,7 +29,6 @@ globalThis.FOSSBilling = Object.assign(globalThis.FOSSBilling || {}, {
|
||||
element.setAttribute("aria-live", "assertive");
|
||||
element.setAttribute("aria-atomic", "true");
|
||||
|
||||
// Create header div and its children elements
|
||||
const headerDiv = document.createElement("div");
|
||||
headerDiv.className = "toast-header";
|
||||
|
||||
@@ -52,7 +50,6 @@ globalThis.FOSSBilling = Object.assign(globalThis.FOSSBilling || {}, {
|
||||
|
||||
element.appendChild(headerDiv);
|
||||
|
||||
// Create body div and set its text content
|
||||
const bodyDiv = document.createElement("div");
|
||||
bodyDiv.className = "toast-body";
|
||||
bodyDiv.textContent = message;
|
||||
@@ -62,7 +59,6 @@ globalThis.FOSSBilling = Object.assign(globalThis.FOSSBilling || {}, {
|
||||
container.removeChild(element);
|
||||
});
|
||||
|
||||
// Create a new Bootstrap toast instance and show it
|
||||
const toast = new bootstrap.Toast(element);
|
||||
toast.show();
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
prepareThemeBuildDirs,
|
||||
sharedLoaders,
|
||||
writeAssetManifest,
|
||||
} from '../../../frontend/tools/esbuild-helpers.mjs';
|
||||
import { buildIconSprite } from '../../../frontend/tools/icon-sprite.mjs';
|
||||
} from '../../../frontend/tools/esbuild-helpers.mts';
|
||||
import { buildIconSprite } from '../../../frontend/tools/icon-sprite.mts';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
@@ -36,7 +36,7 @@ async function build() {
|
||||
});
|
||||
|
||||
await buildJsFile({
|
||||
entryPoint: resolve(__dirname, 'assets/huraga.js'),
|
||||
entryPoint: resolve(__dirname, 'assets/huraga.ts'),
|
||||
outdir: paths.jsDir,
|
||||
entryNames: '[name]',
|
||||
chunkNames: 'chunks/[name]-[hash]',
|
||||
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"name": "huraga",
|
||||
"scripts": {
|
||||
"dev": "node ./esbuild.mjs",
|
||||
"build": "node ./esbuild.mjs"
|
||||
"dev": "node ./esbuild.mts",
|
||||
"build": "node ./esbuild.mts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.3",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"isolatedModules": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"target": "ES2022",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"frontend/types/**/*.d.ts",
|
||||
"frontend/**/*.ts",
|
||||
"frontend/**/*.mts",
|
||||
"src/themes/admin_default/**/*.ts",
|
||||
"src/themes/admin_default/**/*.mts",
|
||||
"src/themes/huraga/**/*.ts",
|
||||
"src/themes/huraga/**/*.mts"
|
||||
],
|
||||
"exclude": [
|
||||
"**/assets/build/**",
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user