mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Refactor array type annotations to use shorthand syntax
Replaces verbose Array<T> type annotations with the shorthand T[] across multiple frontend files for consistency and readability. Also includes minor improvements such as using optional chaining, simplifying type casts, and updating dependency versions in package.json.
This commit is contained in:
parent
bde0ec5ece
commit
4e2beab35b
38
frontend/package-lock.json
generated
38
frontend/package-lock.json
generated
@ -49,17 +49,17 @@
|
|||||||
"license-report": "^6.8.0",
|
"license-report": "^6.8.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^5.4.149",
|
"pdfjs-dist": "^5.4.149",
|
||||||
"posthog-js": "^1.268.4",
|
"posthog-js": "^1.268.5",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-i18next": "^15.7.3",
|
"react-i18next": "^16.0.0",
|
||||||
"react-router-dom": "^7.9.2",
|
"react-router-dom": "^7.9.2",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
"@iconify-json/material-symbols": "^1.2.38",
|
"@iconify-json/material-symbols": "^1.2.39",
|
||||||
"@iconify/utils": "^3.0.2",
|
"@iconify/utils": "^3.0.2",
|
||||||
"@playwright/test": "^1.55.1",
|
"@playwright/test": "^1.55.1",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
@ -1653,9 +1653,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@iconify-json/material-symbols": {
|
"node_modules/@iconify-json/material-symbols": {
|
||||||
"version": "1.2.38",
|
"version": "1.2.39",
|
||||||
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.38.tgz",
|
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.39.tgz",
|
||||||
"integrity": "sha512-I13hrSxRJG3ZwIhBTMXMXxxGAlooqZzivF/TQasvRMBeBFDjGK5+IcCzhEApKZlqiWOK3Sqx2Rf7ihiFS/zNvw==",
|
"integrity": "sha512-spjiB1I5jPi6hV5b/QyC4zO8GRYGCbb6/DaHm754NJFqNli6bsYDpN4HYVl67XhU49rYljvJyNc/6lYEf+jokA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -4581,9 +4581,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.6",
|
"version": "2.8.7",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz",
|
||||||
"integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
|
"integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
@ -5699,9 +5699,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.223",
|
"version": "1.5.224",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.223.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz",
|
||||||
"integrity": "sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==",
|
"integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
@ -10042,9 +10042,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/posthog-js": {
|
"node_modules/posthog-js": {
|
||||||
"version": "1.268.4",
|
"version": "1.268.5",
|
||||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.4.tgz",
|
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.5.tgz",
|
||||||
"integrity": "sha512-kbE8SeH4Hi6uETEzO4EVANULz1ncw+PXC/SMfDdByf4Qt0a/AKoxjlGCZwHuZuflQmBfTwwQcjHeQxnmIxti1A==",
|
"integrity": "sha512-IRhFBeCKkl4bapbxmLvWedKUOG7Fh9jJab718qm7ce8j66LWaPiX7mEi/iuoYLYRU3wD6mWFFiWmeXh6prczRg==",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@posthog/core": "1.2.1",
|
"@posthog/core": "1.2.1",
|
||||||
@ -10364,16 +10364,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-i18next": {
|
"node_modules/react-i18next": {
|
||||||
"version": "15.7.3",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz",
|
||||||
"integrity": "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==",
|
"integrity": "sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6",
|
"@babel/runtime": "^7.27.6",
|
||||||
"html-parse-stringify": "^3.0.1"
|
"html-parse-stringify": "^3.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"i18next": ">= 25.4.1",
|
"i18next": ">= 25.5.2",
|
||||||
"react": ">= 16.8.0",
|
"react": ">= 16.8.0",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
|
@ -45,10 +45,10 @@
|
|||||||
"license-report": "^6.8.0",
|
"license-report": "^6.8.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^5.4.149",
|
"pdfjs-dist": "^5.4.149",
|
||||||
"posthog-js": "^1.268.4",
|
"posthog-js": "^1.268.5",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-i18next": "^15.7.3",
|
"react-i18next": "^16.0.0",
|
||||||
"react-router-dom": "^7.9.2",
|
"react-router-dom": "^7.9.2",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
"@iconify-json/material-symbols": "^1.2.38",
|
"@iconify-json/material-symbols": "^1.2.39",
|
||||||
"@iconify/utils": "^3.0.2",
|
"@iconify/utils": "^3.0.2",
|
||||||
"@playwright/test": "^1.55.1",
|
"@playwright/test": "^1.55.1",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
|
@ -8,7 +8,7 @@ export { useToast, ToastProvider, ToastRenderer };
|
|||||||
let _api: ReturnType<typeof createImperativeApi> | null = null;
|
let _api: ReturnType<typeof createImperativeApi> | null = null;
|
||||||
|
|
||||||
function createImperativeApi() {
|
function createImperativeApi() {
|
||||||
const subscribers: Array<(fn: any) => void> = [];
|
const subscribers: ((fn: any) => void)[] = [];
|
||||||
let api: any = null;
|
let api: any = null;
|
||||||
return {
|
return {
|
||||||
provide(instance: any) {
|
provide(instance: any) {
|
||||||
|
@ -9,7 +9,7 @@ import NoToolsFound from './shared/NoToolsFound';
|
|||||||
import "./toolPicker/ToolPicker.css";
|
import "./toolPicker/ToolPicker.css";
|
||||||
|
|
||||||
interface SearchResultsProps {
|
interface SearchResultsProps {
|
||||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
filteredTools: { item: [string, ToolRegistryEntry]; matchedText?: string }[];
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import { renderToolButtons } from "./shared/renderToolButtons";
|
|||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
filteredTools: { item: [string, ToolRegistryEntry]; matchedText?: string }[];
|
||||||
isSearching?: boolean;
|
isSearching?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ export const renderToolButtons = (
|
|||||||
subcategory: SubcategoryGroup,
|
subcategory: SubcategoryGroup,
|
||||||
selectedToolKey: string | null,
|
selectedToolKey: string | null,
|
||||||
onSelect: (id: string) => void,
|
onSelect: (id: string) => void,
|
||||||
showSubcategoryHeader: boolean = true,
|
showSubcategoryHeader = true,
|
||||||
disableNavigation: boolean = false,
|
disableNavigation = false,
|
||||||
searchResults?: Array<{ item: [string, any]; matchedText?: string }>
|
searchResults?: { item: [string, any]; matchedText?: string }[]
|
||||||
) => {
|
) => {
|
||||||
// Create a map of matched text for quick lookup
|
// Create a map of matched text for quick lookup
|
||||||
const matchedTextMap = new Map<string, string>();
|
const matchedTextMap = new Map<string, string>();
|
||||||
|
@ -101,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
handleReaderToggle: () => void;
|
handleReaderToggle: () => void;
|
||||||
|
|
||||||
// Computed values
|
// Computed values
|
||||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
|
filteredTools: { item: [string, ToolRegistryEntry]; matchedText?: string }[]; // Filtered by search
|
||||||
isPanelVisible: boolean;
|
isPanelVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +223,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
|||||||
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
|
// Filter tools based on search query with fuzzy matching (name, description, id, synonyms)
|
||||||
const filteredTools = useMemo(() => {
|
const filteredTools = useMemo(() => {
|
||||||
if (!toolRegistry) return [];
|
if (!toolRegistry) return [];
|
||||||
return filterToolRegistryByQuery(toolRegistry as Record<string, ToolRegistryEntry>, state.searchQuery);
|
return filterToolRegistryByQuery(toolRegistry, state.searchQuery);
|
||||||
}, [toolRegistry, state.searchQuery]);
|
}, [toolRegistry, state.searchQuery]);
|
||||||
|
|
||||||
const isPanelVisible = useMemo(() =>
|
const isPanelVisible = useMemo(() =>
|
||||||
|
@ -10,7 +10,7 @@ const buildFormData = (parameters: MergeParameters, files: File[]): FormData =>
|
|||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
});
|
});
|
||||||
// Provide stable client file IDs (align with files order)
|
// Provide stable client file IDs (align with files order)
|
||||||
const clientIds: string[] = files.map((f: any) => String((f as any).fileId || f.name));
|
const clientIds: string[] = files.map((f: any) => String((f).fileId || f.name));
|
||||||
formData.append('clientFileIds', JSON.stringify(clientIds));
|
formData.append('clientFileIds', JSON.stringify(clientIds));
|
||||||
formData.append("sortType", "orderProvided"); // Always use orderProvided since UI handles sorting
|
formData.append("sortType", "orderProvided"); // Always use orderProvided since UI handles sorting
|
||||||
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
|
formData.append("removeCertSign", parameters.removeDigitalSignature.toString());
|
||||||
|
@ -199,7 +199,7 @@ export const useToolOperation = <TParams>(
|
|||||||
// Listen for global error file id events from HTTP interceptor during this run
|
// Listen for global error file id events from HTTP interceptor during this run
|
||||||
let externalErrorFileIds: string[] = [];
|
let externalErrorFileIds: string[] = [];
|
||||||
const errorListener = (e: Event) => {
|
const errorListener = (e: Event) => {
|
||||||
const detail = (e as CustomEvent)?.detail as any;
|
const detail = (e as CustomEvent)?.detail;
|
||||||
if (detail?.fileIds) {
|
if (detail?.fileIds) {
|
||||||
externalErrorFileIds = Array.isArray(detail.fileIds) ? detail.fileIds : [];
|
externalErrorFileIds = Array.isArray(detail.fileIds) ? detail.fileIds : [];
|
||||||
}
|
}
|
||||||
@ -416,7 +416,7 @@ export const useToolOperation = <TParams>(
|
|||||||
let parsed: any = payload;
|
let parsed: any = payload;
|
||||||
if (typeof payload === 'string') {
|
if (typeof payload === 'string') {
|
||||||
try { parsed = JSON.parse(payload); } catch { parsed = payload; }
|
try { parsed = JSON.parse(payload); } catch { parsed = payload; }
|
||||||
} else if (payload && typeof (payload as any).text === 'function') {
|
} else if (payload && typeof (payload).text === 'function') {
|
||||||
// Blob or Response-like object from axios when responseType='blob'
|
// Blob or Response-like object from axios when responseType='blob'
|
||||||
const text = await (payload as Blob).text();
|
const text = await (payload as Blob).text();
|
||||||
try { parsed = JSON.parse(text); } catch { parsed = text; }
|
try { parsed = JSON.parse(text); } catch { parsed = text; }
|
||||||
|
@ -24,7 +24,7 @@ export interface ToolSection {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useToolSections(
|
export function useToolSections(
|
||||||
filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>,
|
filteredTools: { item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }[],
|
||||||
searchQuery?: string
|
searchQuery?: string
|
||||||
) {
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -102,7 +102,7 @@ export function useToolSections(
|
|||||||
const subMap = {} as SubcategoryIdMap;
|
const subMap = {} as SubcategoryIdMap;
|
||||||
const seen = new Set<string /* FIX ME: Should be ToolId */>();
|
const seen = new Set<string /* FIX ME: Should be ToolId */>();
|
||||||
filteredTools.forEach(({ item: [id, tool] }) => {
|
filteredTools.forEach(({ item: [id, tool] }) => {
|
||||||
const toolId = id as string /* FIX ME: Should be ToolId */;
|
const toolId = id /* FIX ME: Should be ToolId */;
|
||||||
if (seen.has(toolId)) return;
|
if (seen.has(toolId)) return;
|
||||||
seen.add(toolId);
|
seen.add(toolId);
|
||||||
const sub = tool.subcategoryId;
|
const sub = tool.subcategoryId;
|
||||||
@ -113,7 +113,7 @@ export function useToolSections(
|
|||||||
|
|
||||||
// If a search query is present, always order subcategories by first occurrence in
|
// If a search query is present, always order subcategories by first occurrence in
|
||||||
// the ranked filteredTools list so the top-ranked tools' subcategory appears first.
|
// the ranked filteredTools list so the top-ranked tools' subcategory appears first.
|
||||||
if (searchQuery && searchQuery.trim()) {
|
if (searchQuery?.trim()) {
|
||||||
const order: SubcategoryId[] = [];
|
const order: SubcategoryId[] = [];
|
||||||
filteredTools.forEach(({ item: [_, tool] }) => {
|
filteredTools.forEach(({ item: [_, tool] }) => {
|
||||||
const sc = tool.subcategoryId;
|
const sc = tool.subcategoryId;
|
||||||
|
@ -149,7 +149,7 @@ const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use
|
|||||||
const { title, body } = extractAxiosErrorMessage(error);
|
const { title, body } = extractAxiosErrorMessage(error);
|
||||||
|
|
||||||
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
// Normalize response data ONCE, reuse for both ID extraction and special-toast matching
|
||||||
const raw = (error?.response?.data) as any;
|
const raw = (error?.response?.data);
|
||||||
let normalized: unknown = raw;
|
let normalized: unknown = raw;
|
||||||
try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); }
|
try { normalized = await normalizeAxiosErrorData(raw); } catch (e) { console.debug('normalizeAxiosErrorData', e); }
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ const __INTERCEPTOR_ID__ = apiClient?.interceptors?.response?.use
|
|||||||
const isSpecial =
|
const isSpecial =
|
||||||
status === 422 ||
|
status === 422 ||
|
||||||
status === 409 || // often actionable conflicts
|
status === 409 || // often actionable conflicts
|
||||||
/Failed files:/.test(body) ||
|
body.includes('Failed files:') ||
|
||||||
/invalid\/corrupted file\(s\)/i.test(body);
|
/invalid\/corrupted file\(s\)/i.test(body);
|
||||||
|
|
||||||
if (isSpecial && url) {
|
if (isSpecial && url) {
|
||||||
|
@ -82,4 +82,4 @@ export type JsonValue =
|
|||||||
| JsonValue[]
|
| JsonValue[]
|
||||||
| { [key: string]: JsonValue };
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
export type JsonObject = { [key: string]: JsonValue };
|
export type JsonObject = Record<string, JsonValue>;
|
||||||
|
@ -82,8 +82,8 @@ export function isFuzzyMatch(query: string, target: string, minScore?: number):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience: rank a list of items by best score across provided getters
|
// Convenience: rank a list of items by best score across provided getters
|
||||||
export function rankByFuzzy<T>(items: T[], query: string, getters: Array<(item: T) => string>, minScore?: number): Array<{ item: T; score: number; matchedText?: string }>{
|
export function rankByFuzzy<T>(items: T[], query: string, getters: ((item: T) => string)[], minScore?: number): { item: T; score: number; matchedText?: string }[]{
|
||||||
const results: Array<{ item: T; score: number; matchedText?: string }> = [];
|
const results: { item: T; score: number; matchedText?: string }[] = [];
|
||||||
const threshold = typeof minScore === 'number' ? minScore : minScoreForQuery(query);
|
const threshold = typeof minScore === 'number' ? minScore : minScoreForQuery(query);
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
let best = 0;
|
let best = 0;
|
||||||
|
@ -18,10 +18,10 @@ export function filterToolRegistryByQuery(
|
|||||||
const nq = normalizeForSearch(query);
|
const nq = normalizeForSearch(query);
|
||||||
const threshold = minScoreForQuery(query);
|
const threshold = minScoreForQuery(query);
|
||||||
|
|
||||||
const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = [];
|
const exactName: { id: string; tool: ToolRegistryEntry; pos: number }[] = [];
|
||||||
const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = [];
|
const exactSyn: { id: string; tool: ToolRegistryEntry; text: string; pos: number }[] = [];
|
||||||
const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
const fuzzyName: { id: string; tool: ToolRegistryEntry; score: number; text: string }[] = [];
|
||||||
const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
const fuzzySyn: { id: string; tool: ToolRegistryEntry; score: number; text: string }[] = [];
|
||||||
|
|
||||||
for (const [id, tool] of entries) {
|
for (const [id, tool] of entries) {
|
||||||
const nameNorm = normalizeForSearch(tool.name || '');
|
const nameNorm = normalizeForSearch(tool.name || '');
|
||||||
|
Loading…
Reference in New Issue
Block a user