lint frontend
This commit is contained in:
@@ -1,16 +1,21 @@
|
|||||||
import js from '@eslint/js';
|
import js from '@eslint/js';
|
||||||
|
import react from 'eslint-plugin-react';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
{
|
{
|
||||||
files: ['src/**/*.{js,jsx}'],
|
files: ['src/**/*.{js,jsx}'],
|
||||||
plugins: { 'react-hooks': reactHooks },
|
plugins: {
|
||||||
|
react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
},
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 'latest',
|
ecmaVersion: 'latest',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||||
globals: {
|
globals: {
|
||||||
|
// Browser APIs
|
||||||
window: 'readonly',
|
window: 'readonly',
|
||||||
document: 'readonly',
|
document: 'readonly',
|
||||||
console: 'readonly',
|
console: 'readonly',
|
||||||
@@ -33,11 +38,15 @@ export default [
|
|||||||
Image: 'readonly',
|
Image: 'readonly',
|
||||||
WebSocket: 'readonly',
|
WebSocket: 'readonly',
|
||||||
HTMLElement: 'readonly',
|
HTMLElement: 'readonly',
|
||||||
|
Element: 'readonly',
|
||||||
ClipboardItem: 'readonly',
|
ClipboardItem: 'readonly',
|
||||||
CSS: 'readonly',
|
CSS: 'readonly',
|
||||||
ResizeObserver: 'readonly',
|
ResizeObserver: 'readonly',
|
||||||
MutationObserver: 'readonly',
|
MutationObserver: 'readonly',
|
||||||
IntersectionObserver: 'readonly',
|
IntersectionObserver: 'readonly',
|
||||||
|
TextEncoder: 'readonly',
|
||||||
|
TextDecoder: 'readonly',
|
||||||
|
Buffer: 'readonly',
|
||||||
atob: 'readonly',
|
atob: 'readonly',
|
||||||
btoa: 'readonly',
|
btoa: 'readonly',
|
||||||
performance: 'readonly',
|
performance: 'readonly',
|
||||||
@@ -45,6 +54,9 @@ export default [
|
|||||||
queueMicrotask: 'readonly',
|
queueMicrotask: 'readonly',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
settings: {
|
||||||
|
react: { version: 'detect' },
|
||||||
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// Prevent the TDZ bug
|
// Prevent the TDZ bug
|
||||||
'no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
|
'no-use-before-define': ['error', { functions: false, classes: false, variables: true }],
|
||||||
@@ -53,10 +65,24 @@ export default [
|
|||||||
'react-hooks/rules-of-hooks': 'error',
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
|
|
||||||
|
// React JSX correctness
|
||||||
|
'react/jsx-key': 'error',
|
||||||
|
'react/jsx-no-duplicate-props': 'error',
|
||||||
|
'react/no-children-prop': 'error',
|
||||||
|
'react/no-danger-with-children': 'error',
|
||||||
|
'react/no-direct-mutation-state': 'error',
|
||||||
|
'react/no-unescaped-entities': 'warn',
|
||||||
|
|
||||||
// Turn off rules that are noisy without adding safety
|
// Turn off rules that are noisy without adding safety
|
||||||
|
'react/react-in-jsx-scope': 'off', // not needed with React 17+ JSX transform
|
||||||
|
'react/prop-types': 'off', // no PropTypes in this codebase
|
||||||
|
'react/no-unknown-property': 'off', // false positives with Three.js / custom attrs
|
||||||
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||||
'no-empty': 'off',
|
'no-empty': 'off',
|
||||||
'no-prototype-builtins': 'off',
|
'no-prototype-builtins': 'off',
|
||||||
},
|
},
|
||||||
|
linterOptions: {
|
||||||
|
reportUnusedDisableDirectives: 'off',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
1978
frontend/package-lock.json
generated
1978
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,7 @@
|
|||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"c8": "^10.1.3",
|
"c8": "^10.1.3",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.4",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -877,6 +877,11 @@ function Flow() {
|
|||||||
const journalContentRef = useRef('');
|
const journalContentRef = useRef('');
|
||||||
const reactFlow = useReactFlow();
|
const reactFlow = useReactFlow();
|
||||||
|
|
||||||
|
const scheduleAutoRun = useCallback(() => {
|
||||||
|
clearTimeout(autoRunTimer.current);
|
||||||
|
autoRunTimer.current = setTimeout(() => autoRunRef.current?.(), 300);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ── WebSocket ───────────────────────────────────────────────────────
|
// ── WebSocket ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
const updateNodeData = useCallback((nodeId, patch) => {
|
const updateNodeData = useCallback((nodeId, patch) => {
|
||||||
@@ -1648,6 +1653,14 @@ function Flow() {
|
|||||||
});
|
});
|
||||||
}, [reactFlow]);
|
}, [reactFlow]);
|
||||||
|
|
||||||
|
const openJournalTab = useCallback(() => {
|
||||||
|
setHelpTabs((prev) => {
|
||||||
|
if (prev.find((t) => t.label === 'Journal')) return prev;
|
||||||
|
return [...prev, { label: 'Journal', type: 'journal', content: journalContentRef.current }];
|
||||||
|
});
|
||||||
|
setActiveHelpTab('Journal');
|
||||||
|
}, []);
|
||||||
|
|
||||||
// ── Add node from context menu ──────────────────────────────────────
|
// ── Add node from context menu ──────────────────────────────────────
|
||||||
|
|
||||||
const addNode = useCallback((className, def) => {
|
const addNode = useCallback((className, def) => {
|
||||||
@@ -1797,11 +1810,6 @@ function Flow() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduleAutoRun = useCallback(() => {
|
|
||||||
clearTimeout(autoRunTimer.current);
|
|
||||||
autoRunTimer.current = setTimeout(() => autoRunRef.current?.(), 300);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onRuntimeValuesChange = useCallback((nodeId, patch, { scheduleRun = false } = {}) => {
|
const onRuntimeValuesChange = useCallback((nodeId, patch, { scheduleRun = false } = {}) => {
|
||||||
if (!patch || typeof patch !== 'object') return;
|
if (!patch || typeof patch !== 'object') return;
|
||||||
|
|
||||||
@@ -1958,14 +1966,6 @@ function Flow() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openJournalTab = useCallback(() => {
|
|
||||||
setHelpTabs((prev) => {
|
|
||||||
if (prev.find((t) => t.label === 'Journal')) return prev;
|
|
||||||
return [...prev, { label: 'Journal', type: 'journal', content: journalContentRef.current }];
|
|
||||||
});
|
|
||||||
setActiveHelpTab('Journal');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateTabContent = useCallback((label, content) => {
|
const updateTabContent = useCallback((label, content) => {
|
||||||
if (label === 'Journal') journalContentRef.current = content;
|
if (label === 'Journal') journalContentRef.current = content;
|
||||||
setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t));
|
setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t));
|
||||||
|
|||||||
@@ -980,13 +980,6 @@ function NodeTable({ rows }) {
|
|||||||
|
|
||||||
function CustomNode({ id, data }) {
|
function CustomNode({ id, data }) {
|
||||||
const ctx = useContext(NodeContext);
|
const ctx = useContext(NodeContext);
|
||||||
|
|
||||||
if (data.className === 'Group') {
|
|
||||||
return <GroupNode id={id} data={data} />;
|
|
||||||
}
|
|
||||||
if (data.className === 'TextNote') {
|
|
||||||
return <TextNoteNode id={id} data={data} />;
|
|
||||||
}
|
|
||||||
const def = data.definition;
|
const def = data.definition;
|
||||||
const scalarDisplay = formatScalarDisplay(data.scalarValue);
|
const scalarDisplay = formatScalarDisplay(data.scalarValue);
|
||||||
const processingTimeText = formatProcessingTime(data.processingTimeMs);
|
const processingTimeText = formatProcessingTime(data.processingTimeMs);
|
||||||
@@ -996,6 +989,7 @@ function CustomNode({ id, data }) {
|
|||||||
|
|
||||||
// Find the COORDPAIR input name (if any) so we can resolve live upstream positions
|
// Find the COORDPAIR input name (if any) so we can resolve live upstream positions
|
||||||
const coordPairInputName = React.useMemo(() => {
|
const coordPairInputName = React.useMemo(() => {
|
||||||
|
if (!def) return null;
|
||||||
const allInputs = { ...def.input.required, ...def.input.optional };
|
const allInputs = { ...def.input.required, ...def.input.optional };
|
||||||
for (const [name, spec] of Object.entries(allInputs)) {
|
for (const [name, spec] of Object.entries(allInputs)) {
|
||||||
const type = Array.isArray(spec) ? spec[0] : spec;
|
const type = Array.isArray(spec) ? spec[0] : spec;
|
||||||
@@ -1018,8 +1012,8 @@ function CustomNode({ id, data }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Parse inputs into data handles and widgets
|
// Parse inputs into data handles and widgets
|
||||||
const required = def.input.required || {};
|
const required = def?.input?.required || {};
|
||||||
const optional = def.input.optional || {};
|
const optional = def?.input?.optional || {};
|
||||||
|
|
||||||
const dataInputs = [];
|
const dataInputs = [];
|
||||||
const widgets = [];
|
const widgets = [];
|
||||||
@@ -1044,7 +1038,7 @@ function CustomNode({ id, data }) {
|
|||||||
|
|
||||||
// For manual-trigger nodes (Save), show progressive optional inputs:
|
// For manual-trigger nodes (Save), show progressive optional inputs:
|
||||||
// show field_N only if field_(N-1) is connected (or N==0).
|
// show field_N only if field_(N-1) is connected (or N==0).
|
||||||
const isProgressive = def.manual_trigger;
|
const isProgressive = def?.manual_trigger;
|
||||||
const connectedInputs = useStore(
|
const connectedInputs = useStore(
|
||||||
useCallback(
|
useCallback(
|
||||||
(s) => {
|
(s) => {
|
||||||
@@ -1075,6 +1069,13 @@ function CustomNode({ id, data }) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (data.className === 'Group') {
|
||||||
|
return <GroupNode id={id} data={data} />;
|
||||||
|
}
|
||||||
|
if (data.className === 'TextNote') {
|
||||||
|
return <TextNoteNode id={id} data={data} />;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [name, spec] of Object.entries(optional)) {
|
for (const [name, spec] of Object.entries(optional)) {
|
||||||
const [type, opts] = getSpecTypeAndOptions(spec);
|
const [type, opts] = getSpecTypeAndOptions(spec);
|
||||||
if (isProgressive && isDataSocketSpec(spec)) {
|
if (isProgressive && isDataSocketSpec(spec)) {
|
||||||
@@ -1210,7 +1211,7 @@ function CustomNode({ id, data }) {
|
|||||||
style={{ background: TYPE_COLORS[socketType] || 'var(--fallback-type)' }}
|
style={{ background: TYPE_COLORS[socketType] || 'var(--fallback-type)' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!!(
|
{(
|
||||||
(w.socketType && connectedInputs?.has(w.name))
|
(w.socketType && connectedInputs?.has(w.name))
|
||||||
|| (combinedInputName && connectedInputs?.has(combinedInputName))
|
|| (combinedInputName && connectedInputs?.has(combinedInputName))
|
||||||
) ? (
|
) ? (
|
||||||
@@ -1556,7 +1557,6 @@ function TextInputValueBox({ val, placeholder, nodeId, name, label, hideLabel, o
|
|||||||
>
|
>
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<input
|
<input
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
||||||
autoFocus
|
autoFocus
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function parseHeadings(md) {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const m = line.match(/^(#{1,6})\s+(.+)/);
|
const m = line.match(/^(#{1,6})\s+(.+)/);
|
||||||
if (m) {
|
if (m) {
|
||||||
const text = m[2].replace(/[*_`~\[\]]/g, '').trim();
|
const text = m[2].replace(/[*_`~[\]]/g, '').trim();
|
||||||
const id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/(^-|-$)/g, '');
|
const id = text.toLowerCase().replace(/[^\w]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
headings.push({ level: m[1].length, text, id });
|
headings.push({ level: m[1].length, text, id });
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,6 @@ function JournalTab({ content, onChange, onOpenDoc }) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Write your notes here (Markdown supported)…"
|
placeholder="Write your notes here (Markdown supported)…"
|
||||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
) : renderedHtml ? (
|
) : renderedHtml ? (
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const SI_PREFIX_MULTIPLIERS = {
|
|||||||
|
|
||||||
const NUMBER_WITH_UNIT_RE = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)?$/;
|
const NUMBER_WITH_UNIT_RE = /^([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)\s*(.*)?$/;
|
||||||
|
|
||||||
|
const PREFIXABLE_UNITS = new Set([
|
||||||
|
'm', 's', 'A', 'V', 'W', 'Hz', 'F', 'C', 'J', 'N', 'Pa', 'T', 'H', 'S', 'g', 'K', 'Ohm', 'ohm', 'Ω',
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a string like "1.5 nm" into { numeric: 1.5e-9, unit: "m" }.
|
* Parse a string like "1.5 nm" into { numeric: 1.5e-9, unit: "m" }.
|
||||||
* Returns null if the string does not start with a valid number.
|
* Returns null if the string does not start with a valid number.
|
||||||
@@ -56,10 +60,6 @@ const SI_PREFIXES = [
|
|||||||
{ exp: 24, prefix: 'Y' },
|
{ exp: 24, prefix: 'Y' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PREFIXABLE_UNITS = new Set([
|
|
||||||
'm', 's', 'A', 'V', 'W', 'Hz', 'F', 'C', 'J', 'N', 'Pa', 'T', 'H', 'S', 'g', 'K', 'Ohm', 'ohm', 'Ω',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const SUPERSCRIPT_DIGITS = {
|
const SUPERSCRIPT_DIGITS = {
|
||||||
'-': '⁻',
|
'-': '⁻',
|
||||||
'0': '⁰',
|
'0': '⁰',
|
||||||
|
|||||||
Reference in New Issue
Block a user