frontend lint

This commit is contained in:
2025-10-29 17:40:11 +01:00
parent 1cc0aafc1a
commit ec6e7f04f2
10 changed files with 2655 additions and 319 deletions
+2
View File
@@ -0,0 +1,2 @@
dist
node_modules
+31
View File
@@ -0,0 +1,31 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"no-use-before-define": [
"error",
{ "functions": false, "classes": true, "variables": true }
],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off"
}
}
+2615
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"dev": "webpack serve --mode development --open",
"build": "webpack --mode production",
"lint": "echo \"No linting configured\""
"lint": "eslint src --ext .js,.jsx"
},
"dependencies": {
"@fontsource/inter": "^5.2.8",
@@ -20,6 +20,9 @@
"@babel/core": "7.26.0",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"eslint": "8.57.0",
"eslint-plugin-react": "7.37.1",
"eslint-plugin-react-hooks": "4.6.0",
"@svgr/webpack": "8.1.0",
"babel-loader": "9.2.1",
"css-loader": "7.1.2",
+1 -21
View File
@@ -17,12 +17,8 @@ import PreviewZoomOverlay from './detail/PreviewZoomOverlay';
import { getTagColorStyle } from './utils/colors';
import './DesktopWorkspace.css';
const ITEM_WIDTH = 220;
const ITEM_HEIGHT = 260;
const CANVAS_PADDING = 24;
const ROTATION_RANGE = 7;
const JITTER_X = 26;
const JITTER_Y = 32;
const DEFAULT_CANVAS_WIDTH = 1024;
const DEFAULT_CANVAS_HEIGHT = 680;
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
@@ -90,7 +86,6 @@ const DesktopPreviewCard = ({
prefetch = 3,
onNavigatorSnapshot,
shouldLoad = true,
imageSize = null,
}) => {
const navigator = useAssetNavigator({
document: doc,
@@ -230,9 +225,6 @@ const generateInitialLayout = (
}
const shelfOffset = Math.max(shelfWidth, 0);
const usableWidth = Math.max(canvasWidth - shelfOffset - padding * 2, 1);
const usableHeight = Math.max(canvasHeight - padding * 2, 1);
const spacingBuffer = Math.max(minSpacing, 0);
const placed = [];
@@ -267,7 +259,7 @@ const generateInitialLayout = (
return best;
};
entries.forEach((entry, index) => {
entries.forEach((entry) => {
const width = Number(entry.width) || 0;
const height = Number(entry.height) || 0;
if (!entry.id || width <= 0 || height <= 0) {
@@ -874,18 +866,6 @@ const DesktopWorkspace = ({
},
[resolvePreviewDimensions],
);
const polygonArea = (points) => {
let area = 0;
const count = points.length;
for (let index = 0; index < count; index += 1) {
const next = (index + 1) % count;
area += points[index].x * points[next].y;
area -= points[next].x * points[index].y;
}
return area / 2;
};
const signedDistance = (ax, ay, bx, by, px, py) => (bx - ax) * (py - ay) - (by - ay) * (px - ax);
const clipPolygonWithEdge = (subject, edgeStart, edgeEnd) => {
+1 -11
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
DownloadIcon,
EditIcon,
@@ -199,7 +199,6 @@ const PreviewStack = ({
onItemActivate,
onOpenPreview,
onZoomPreview,
activeItemId = null,
}) => {
if (!items.length) {
return <span className="meta">{emptyMessage}</span>;
@@ -288,7 +287,6 @@ const DetailPanel = ({
onTagAdd,
onTagRemove,
onRegenerateThumbnails,
previewEntry,
onOpenPreview,
onBulkTagAdd,
onBulkTagRemove,
@@ -296,7 +294,6 @@ const DetailPanel = ({
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onPromoteSelection,
activePreviewId = null,
onUpdateTitle = async () => false,
ensureAssetUrl = null,
getDocumentAsset = () => null,
@@ -542,9 +539,6 @@ const DetailPanel = ({
const topCardinality = stackPreviewNavigator.cardinality;
const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0);
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
const topOrdinal = stackPreviewNavigator.ordinal;
const topCanGoPrev = stackPreviewNavigator.canGoPrev;
const topCanGoNext = stackPreviewNavigator.canGoNext;
const bulkTagUnion = useMemo(() => {
if (!selectedDocuments.length) return [];
@@ -888,7 +882,6 @@ const DetailPanel = ({
onItemActivate={handlePreviewActivate}
onOpenPreview={onOpenPreview}
onZoomPreview={handleSingleZoom}
activeItemId={activePreviewId}
/>
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
<div className="preview-pane__nav preview-pane__nav--overlay">
@@ -1093,9 +1086,7 @@ const DetailPanel = ({
const renderBulk = () => {
const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—';
const topDoc = stackTopDocument;
const topDocIdLocal = topDocId;
const topOrdinal = stackPreviewNavigator.ordinal;
const topCardinalityLocal = topEffectiveCardinality;
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
const topCanGoPrev = stackPreviewNavigator.canGoPrev;
@@ -1115,7 +1106,6 @@ const DetailPanel = ({
onItemActivate={handlePreviewActivate}
onOpenPreview={onOpenPreview}
onZoomPreview={handleStackZoom}
activeItemId={activePreviewId}
/>
{topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
<div className="preview-pane__nav preview-pane__nav--overlay">
@@ -17,7 +17,6 @@ import {
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
const DEFAULT_GRID_ICON_SIZE = 144;
const DEFAULT_GRID_TITLE_SIZE = '11px';
const LIST_ICON_SIZE = 48;
const getPageCount = (doc) =>
@@ -198,7 +197,6 @@ const DocumentThumbnailImage = ({
const DocumentsTable = ({
currentFolderName,
breadcrumbs,
onRefresh,
onShowSkeuoWorkspace = () => {},
onRequestCreateFolder,
@@ -206,7 +204,6 @@ const DocumentsTable = ({
subfolders,
documents,
searchResults,
isFilterActive,
onFolderSelect,
onFolderDrop,
onFolderDragOver,
@@ -220,7 +217,6 @@ const DocumentsTable = ({
onDocumentRowClick,
onDocumentOpen,
selectedDocumentIds,
focusedDocumentId,
focusedRowKey,
draggingDocumentIds = [],
onDocumentDragStart,
+1 -248
View File
@@ -397,33 +397,6 @@ const DocumentsLayout = ({ sidebarProps, children, sidebarCollapsed }) => (
</main>
);
const MainContentShell = ({
className,
header = {},
sidebarToggleButton = null,
children,
}) => {
const { title = '', subtitle = null, leading = null, actions = null } = header || {};
return (
<div className={className}>
<div className="panel-header main-content__header">
<div className="panel-actions main-content__actions">
{sidebarToggleButton}
{leading}
<h2 className="main-content__title">
{title}
{subtitle ? <span className="main-content__subtitle">{subtitle}</span> : null}
</h2>
<div className="spacer" />
{actions}
</div>
</div>
{children}
</div>
);
};
const AppLayout = () => {
const navigate = useNavigate();
const location = useLocation();
@@ -445,9 +418,6 @@ const AppLayout = () => {
({ message, variant }) => setStatusMessage(message, variant),
[setStatusMessage],
);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const collapseSidebar = useCallback(() => setSidebarCollapsed(true), []);
const expandSidebar = useCallback(() => setSidebarCollapsed(false), []);
const reportApiError = useApiError({
onReport: handleApiReport,
});
@@ -616,12 +586,6 @@ const AppLayout = () => {
}
const tagManager = tagManagerRef.current;
const buildTagPayload = useCallback(
({ label, color } = {}) => tagManager.buildPayload({ label, color }),
[tagManager],
);
const getDocumentAsset = useCallback((doc, type) => {
if (!doc || !type) return null;
return getAssetFromVersion(doc.current_version || null, type);
@@ -1247,16 +1211,6 @@ const AppLayout = () => {
[selectedRowKeys, updateSelectionOrder],
);
const visibleSelectedCount = useMemo(
() => selectedDocumentIds.filter((id) => visibleDocumentIds.includes(id)).length,
[selectedDocumentIds, visibleDocumentIds],
);
const allDocumentsSelected =
visibleDocumentIds.length > 0 &&
visibleSelectedCount === visibleDocumentIds.length;
const someDocumentsSelected =
visibleSelectedCount > 0 && !allDocumentsSelected;
const folderOptions = useMemo(() => {
const cache = new Map();
@@ -2498,73 +2452,6 @@ const AppLayout = () => {
};
}, [appStatus, appDispatch, initializeAfterLogin]);
const handleBulkMoveSubmit = useCallback(
async (event) => {
event.preventDefault();
if (!selectedDocumentIds.length) {
setStatusMessage('Select documents before moving.', 'error');
return;
}
const form = new FormData(event.currentTarget);
const target = form.get('target')?.toString() || 'root';
const folderId = target === 'root' ? null : target;
if (!refreshOnly) {
setLoading(true);
}
try {
await api.post('/documents/bulk/move', {
document_ids: selectedDocumentIds,
folder_id: folderId,
});
const count = selectedDocumentIds.length;
const suffix = count === 1 ? '' : 's';
const folderLabel =
folderId === null
? DEFAULT_FOLDER_NAME
: folderLabelMap.get(target) || 'target folder';
setStatusMessage(
`Moved ${count} document${suffix} to ${folderLabel}.`,
'success',
);
applySelection([], { anchor: null });
await refreshCurrentFolder();
if (folderId && folderId !== selectedFolder) {
await ensureFolderData(folderId, { force: true, prefetchDepth: 1 });
}
event.currentTarget.reset();
} catch (error) {
const message = error.response?.data?.error || 'Failed to move documents.';
notifyApiError(error, message);
} finally {
setLoading(false);
}
},
[
api,
selectedDocumentIds,
folderLabelMap,
applySelection,
refreshCurrentFolder,
ensureFolderData,
notifyApiError,
selectedFolder,
setStatusMessage,
],
);
const parseTagInput = useCallback((value) => {
return value
.split(',')
.map((item) => item.trim())
.filter(Boolean);
}, []);
const bulkTagOperation = useCallback(
async ({ labels, action, documentIds }) => {
const normalized = labels.map((label) => label.trim()).filter((label) => label.length > 0);
@@ -2650,81 +2537,6 @@ const AppLayout = () => {
],
);
const handleBulkTagAdd = useCallback(
async (event) => {
event.preventDefault();
if (!selectedDocumentIds.length) {
setStatusMessage('Select documents before assigning tags.', 'error');
return;
}
const form = new FormData(event.currentTarget);
const raw = form.get('tags')?.toString().trim() || '';
const labels = parseTagInput(raw);
if (!labels.length) {
setStatusMessage('Enter at least one tag label.', 'error');
return;
}
const result = await bulkTagOperation({ labels, action: 'add' });
if (result?.ok) {
const { tagCount, docsCount } = result;
setStatusMessage(
`Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${
docsCount === 1 ? '' : 's'
}.`,
'success',
);
event.currentTarget.reset();
} else if (result?.reason === 'no-labels') {
setStatusMessage('Enter at least one tag label.', 'error');
}
},
[
selectedDocumentIds,
setStatusMessage,
parseTagInput,
bulkTagOperation,
],
);
const handleBulkTagRemove = useCallback(
async (event) => {
event.preventDefault();
if (!selectedDocumentIds.length) {
setStatusMessage('Select documents before removing tags.', 'error');
return;
}
const form = new FormData(event.currentTarget);
const raw = form.get('tags')?.toString().trim() || '';
const labels = parseTagInput(raw);
if (!labels.length) {
setStatusMessage('Enter at least one tag label to remove.', 'error');
return;
}
const result = await bulkTagOperation({ labels, action: 'remove' });
if (result?.ok) {
const { docsCount } = result;
setStatusMessage(
`Removed tags from ${docsCount} document${docsCount === 1 ? '' : 's'}.`,
'success',
);
event.currentTarget.reset();
} else if (result?.reason === 'no-labels') {
setStatusMessage('Enter at least one tag label to remove.', 'error');
} else if (result?.reason === 'tag-missing') {
setStatusMessage(`Tag “${result.label}” not found.`, 'error');
}
},
[
selectedDocumentIds,
setStatusMessage,
parseTagInput,
bulkTagOperation,
],
);
const handleBulkTagAddFromDetail = useCallback(
async ({ label, input, documentIds }) => {
const trimmed = (label || '').trim();
@@ -3371,7 +3183,7 @@ const AppLayout = () => {
}),
);
Array.from(dataTransfer.files || []).forEach((file, index) => {
Array.from(dataTransfer.files || []).forEach((file) => {
if (!file) return;
// FileList entry suppressed
const relativePath =
@@ -4554,54 +4366,6 @@ const AppLayout = () => {
};
}, [handleTagRemove, setTagRemovalCursor]);
const handleLogin = useCallback(
async (event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
const payload = {
username: form.get('username')?.toString().trim(),
password: form.get('password')?.toString() || '',
};
if (!payload.username || !payload.password) {
setStatusMessage('Username and password are required.', 'error');
return;
}
try {
setLoading(true);
appDispatch({ type: 'LOGIN_REQUEST' });
const { data } = await api.post('/auth/login', payload);
if (data?.access_token && Array.isArray(data?.tenants)) {
appDispatch({
type: 'TENANT_SELECTION_REQUIRED',
selectionToken: data.access_token,
tenants: data.tenants,
});
setStatusMessage('Select a tenant to continue.', 'info');
return;
}
if (!data?.access_token) {
throw new Error('Invalid login response.');
}
appDispatch({
type: 'LOGIN_SUCCESS',
token: data.access_token,
tenant: data.tenant || null,
});
setStatusMessage('Login successful.', 'success');
} catch (error) {
const message = error?.response?.data?.error || 'Login failed. Check credentials.';
appDispatch({ type: 'LOGIN_FAILURE', error: message });
notifyApiError(error, message);
} finally {
setLoading(false);
}
},
[appDispatch, notifyApiError, setStatusMessage],
);
const handleLogout = useCallback(async () => {
try {
setLoading(true);
@@ -5333,7 +5097,6 @@ const AppLayout = () => {
isFilterActive,
onLogout: handleLogout,
status,
onCollapse: collapseSidebar,
tenantSlug,
tenants: tenantOptions,
activeTenantId: currentTenantId,
@@ -5341,16 +5104,6 @@ const AppLayout = () => {
onOpenSettings: openSettingsModal,
};
const sidebarPropsWithActions = useMemo(
() => ({
...sidebarProps,
onManageTags: openTagsModal,
onManageCorrespondents: openCorrespondentsModal,
onCollapse: collapseSidebar,
}),
[sidebarProps, openTagsModal, openCorrespondentsModal, collapseSidebar],
);
const handleDetailPanelClose = useCallback(() => {
setDetailPanelOpen(false);
clearDocumentSelection();
-3
View File
@@ -151,9 +151,6 @@ const Sidebar = ({
onSearchSubmit,
onSearchClear,
isFilterActive,
appStatus,
loading,
previewActive,
onLogout,
status,
onCollapse,
-31
View File
@@ -4,37 +4,6 @@ const clamp01 = (value) => Math.min(1, Math.max(0, value));
const clampRange = (value, min, max) => Math.min(max, Math.max(min, value));
const gammaEncode = (channel) =>
channel <= 0.0031308 ? 12.92 * channel : 1.055 * Math.pow(channel, 1 / 2.4) - 0.055;
const oklchToHex = (l, c, h) => {
const hr = (h * Math.PI) / 180;
const a = Math.cos(hr) * c;
const b = Math.sin(hr) * c;
const l1 = l + 0.3963377774 * a + 0.2158037573 * b;
const m1 = l - 0.1055613458 * a - 0.0638541728 * b;
const s1 = l - 0.0894841775 * a - 1.291485548 * b;
const l3 = l1 ** 3;
const m3 = m1 ** 3;
const s3 = s1 ** 3;
const r = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
const g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
const bLin = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3;
if ([r, g, bLin].some((channel) => channel < 0 || channel > 1)) {
return null;
}
const sr = Math.round(clamp01(gammaEncode(r)) * 255);
const sg = Math.round(clamp01(gammaEncode(g)) * 255);
const sb = Math.round(clamp01(gammaEncode(bLin)) * 255);
return `#${((sr << 16) | (sg << 8) | sb).toString(16).padStart(6, '0')}`;
};
const hslToHex = (h, s, l) => {
const normalizedH = ((h % 360) + 360) % 360;
const sat = clamp01(s);