frontend lint
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2615
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack serve --mode development --open",
|
"dev": "webpack serve --mode development --open",
|
||||||
"build": "webpack --mode production",
|
"build": "webpack --mode production",
|
||||||
"lint": "echo \"No linting configured\""
|
"lint": "eslint src --ext .js,.jsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"@babel/core": "7.26.0",
|
"@babel/core": "7.26.0",
|
||||||
"@babel/preset-env": "7.26.0",
|
"@babel/preset-env": "7.26.0",
|
||||||
"@babel/preset-react": "7.26.3",
|
"@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",
|
"@svgr/webpack": "8.1.0",
|
||||||
"babel-loader": "9.2.1",
|
"babel-loader": "9.2.1",
|
||||||
"css-loader": "7.1.2",
|
"css-loader": "7.1.2",
|
||||||
|
|||||||
@@ -17,12 +17,8 @@ import PreviewZoomOverlay from './detail/PreviewZoomOverlay';
|
|||||||
import { getTagColorStyle } from './utils/colors';
|
import { getTagColorStyle } from './utils/colors';
|
||||||
import './DesktopWorkspace.css';
|
import './DesktopWorkspace.css';
|
||||||
|
|
||||||
const ITEM_WIDTH = 220;
|
|
||||||
const ITEM_HEIGHT = 260;
|
|
||||||
const CANVAS_PADDING = 24;
|
const CANVAS_PADDING = 24;
|
||||||
const ROTATION_RANGE = 7;
|
const ROTATION_RANGE = 7;
|
||||||
const JITTER_X = 26;
|
|
||||||
const JITTER_Y = 32;
|
|
||||||
const DEFAULT_CANVAS_WIDTH = 1024;
|
const DEFAULT_CANVAS_WIDTH = 1024;
|
||||||
const DEFAULT_CANVAS_HEIGHT = 680;
|
const DEFAULT_CANVAS_HEIGHT = 680;
|
||||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||||
@@ -90,7 +86,6 @@ const DesktopPreviewCard = ({
|
|||||||
prefetch = 3,
|
prefetch = 3,
|
||||||
onNavigatorSnapshot,
|
onNavigatorSnapshot,
|
||||||
shouldLoad = true,
|
shouldLoad = true,
|
||||||
imageSize = null,
|
|
||||||
}) => {
|
}) => {
|
||||||
const navigator = useAssetNavigator({
|
const navigator = useAssetNavigator({
|
||||||
document: doc,
|
document: doc,
|
||||||
@@ -230,9 +225,6 @@ const generateInitialLayout = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shelfOffset = Math.max(shelfWidth, 0);
|
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 spacingBuffer = Math.max(minSpacing, 0);
|
||||||
const placed = [];
|
const placed = [];
|
||||||
|
|
||||||
@@ -267,7 +259,7 @@ const generateInitialLayout = (
|
|||||||
return best;
|
return best;
|
||||||
};
|
};
|
||||||
|
|
||||||
entries.forEach((entry, index) => {
|
entries.forEach((entry) => {
|
||||||
const width = Number(entry.width) || 0;
|
const width = Number(entry.width) || 0;
|
||||||
const height = Number(entry.height) || 0;
|
const height = Number(entry.height) || 0;
|
||||||
if (!entry.id || width <= 0 || height <= 0) {
|
if (!entry.id || width <= 0 || height <= 0) {
|
||||||
@@ -874,18 +866,6 @@ const DesktopWorkspace = ({
|
|||||||
},
|
},
|
||||||
[resolvePreviewDimensions],
|
[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 signedDistance = (ax, ay, bx, by, px, py) => (bx - ax) * (py - ay) - (by - ay) * (px - ax);
|
||||||
|
|
||||||
const clipPolygonWithEdge = (subject, edgeStart, edgeEnd) => {
|
const clipPolygonWithEdge = (subject, edgeStart, edgeEnd) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
@@ -199,7 +199,6 @@ const PreviewStack = ({
|
|||||||
onItemActivate,
|
onItemActivate,
|
||||||
onOpenPreview,
|
onOpenPreview,
|
||||||
onZoomPreview,
|
onZoomPreview,
|
||||||
activeItemId = null,
|
|
||||||
}) => {
|
}) => {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
return <span className="meta">{emptyMessage}</span>;
|
return <span className="meta">{emptyMessage}</span>;
|
||||||
@@ -288,7 +287,6 @@ const DetailPanel = ({
|
|||||||
onTagAdd,
|
onTagAdd,
|
||||||
onTagRemove,
|
onTagRemove,
|
||||||
onRegenerateThumbnails,
|
onRegenerateThumbnails,
|
||||||
previewEntry,
|
|
||||||
onOpenPreview,
|
onOpenPreview,
|
||||||
onBulkTagAdd,
|
onBulkTagAdd,
|
||||||
onBulkTagRemove,
|
onBulkTagRemove,
|
||||||
@@ -296,7 +294,6 @@ const DetailPanel = ({
|
|||||||
onBulkCorrespondentAdd,
|
onBulkCorrespondentAdd,
|
||||||
onBulkCorrespondentRemove,
|
onBulkCorrespondentRemove,
|
||||||
onPromoteSelection,
|
onPromoteSelection,
|
||||||
activePreviewId = null,
|
|
||||||
onUpdateTitle = async () => false,
|
onUpdateTitle = async () => false,
|
||||||
ensureAssetUrl = null,
|
ensureAssetUrl = null,
|
||||||
getDocumentAsset = () => null,
|
getDocumentAsset = () => null,
|
||||||
@@ -542,9 +539,6 @@ const DetailPanel = ({
|
|||||||
const topCardinality = stackPreviewNavigator.cardinality;
|
const topCardinality = stackPreviewNavigator.cardinality;
|
||||||
const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0);
|
const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0);
|
||||||
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
|
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
|
||||||
const topOrdinal = stackPreviewNavigator.ordinal;
|
|
||||||
const topCanGoPrev = stackPreviewNavigator.canGoPrev;
|
|
||||||
const topCanGoNext = stackPreviewNavigator.canGoNext;
|
|
||||||
|
|
||||||
const bulkTagUnion = useMemo(() => {
|
const bulkTagUnion = useMemo(() => {
|
||||||
if (!selectedDocuments.length) return [];
|
if (!selectedDocuments.length) return [];
|
||||||
@@ -888,7 +882,6 @@ const DetailPanel = ({
|
|||||||
onItemActivate={handlePreviewActivate}
|
onItemActivate={handlePreviewActivate}
|
||||||
onOpenPreview={onOpenPreview}
|
onOpenPreview={onOpenPreview}
|
||||||
onZoomPreview={handleSingleZoom}
|
onZoomPreview={handleSingleZoom}
|
||||||
activeItemId={activePreviewId}
|
|
||||||
/>
|
/>
|
||||||
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
|
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
|
||||||
<div className="preview-pane__nav preview-pane__nav--overlay">
|
<div className="preview-pane__nav preview-pane__nav--overlay">
|
||||||
@@ -1093,9 +1086,7 @@ const DetailPanel = ({
|
|||||||
const renderBulk = () => {
|
const renderBulk = () => {
|
||||||
const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
|
const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
|
||||||
const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—';
|
const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—';
|
||||||
const topDoc = stackTopDocument;
|
|
||||||
const topDocIdLocal = topDocId;
|
const topDocIdLocal = topDocId;
|
||||||
const topOrdinal = stackPreviewNavigator.ordinal;
|
|
||||||
const topCardinalityLocal = topEffectiveCardinality;
|
const topCardinalityLocal = topEffectiveCardinality;
|
||||||
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
|
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
|
||||||
const topCanGoPrev = stackPreviewNavigator.canGoPrev;
|
const topCanGoPrev = stackPreviewNavigator.canGoPrev;
|
||||||
@@ -1115,7 +1106,6 @@ const DetailPanel = ({
|
|||||||
onItemActivate={handlePreviewActivate}
|
onItemActivate={handlePreviewActivate}
|
||||||
onOpenPreview={onOpenPreview}
|
onOpenPreview={onOpenPreview}
|
||||||
onZoomPreview={handleStackZoom}
|
onZoomPreview={handleStackZoom}
|
||||||
activeItemId={activePreviewId}
|
|
||||||
/>
|
/>
|
||||||
{topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
|
{topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
|
||||||
<div className="preview-pane__nav preview-pane__nav--overlay">
|
<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 TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||||
const DEFAULT_GRID_ICON_SIZE = 144;
|
const DEFAULT_GRID_ICON_SIZE = 144;
|
||||||
const DEFAULT_GRID_TITLE_SIZE = '11px';
|
|
||||||
const LIST_ICON_SIZE = 48;
|
const LIST_ICON_SIZE = 48;
|
||||||
|
|
||||||
const getPageCount = (doc) =>
|
const getPageCount = (doc) =>
|
||||||
@@ -198,7 +197,6 @@ const DocumentThumbnailImage = ({
|
|||||||
|
|
||||||
const DocumentsTable = ({
|
const DocumentsTable = ({
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
breadcrumbs,
|
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onShowSkeuoWorkspace = () => {},
|
onShowSkeuoWorkspace = () => {},
|
||||||
onRequestCreateFolder,
|
onRequestCreateFolder,
|
||||||
@@ -206,7 +204,6 @@ const DocumentsTable = ({
|
|||||||
subfolders,
|
subfolders,
|
||||||
documents,
|
documents,
|
||||||
searchResults,
|
searchResults,
|
||||||
isFilterActive,
|
|
||||||
onFolderSelect,
|
onFolderSelect,
|
||||||
onFolderDrop,
|
onFolderDrop,
|
||||||
onFolderDragOver,
|
onFolderDragOver,
|
||||||
@@ -220,7 +217,6 @@ const DocumentsTable = ({
|
|||||||
onDocumentRowClick,
|
onDocumentRowClick,
|
||||||
onDocumentOpen,
|
onDocumentOpen,
|
||||||
selectedDocumentIds,
|
selectedDocumentIds,
|
||||||
focusedDocumentId,
|
|
||||||
focusedRowKey,
|
focusedRowKey,
|
||||||
draggingDocumentIds = [],
|
draggingDocumentIds = [],
|
||||||
onDocumentDragStart,
|
onDocumentDragStart,
|
||||||
|
|||||||
+1
-248
@@ -397,33 +397,6 @@ const DocumentsLayout = ({ sidebarProps, children, sidebarCollapsed }) => (
|
|||||||
</main>
|
</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 AppLayout = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -445,9 +418,6 @@ const AppLayout = () => {
|
|||||||
({ message, variant }) => setStatusMessage(message, variant),
|
({ message, variant }) => setStatusMessage(message, variant),
|
||||||
[setStatusMessage],
|
[setStatusMessage],
|
||||||
);
|
);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
||||||
const collapseSidebar = useCallback(() => setSidebarCollapsed(true), []);
|
|
||||||
const expandSidebar = useCallback(() => setSidebarCollapsed(false), []);
|
|
||||||
const reportApiError = useApiError({
|
const reportApiError = useApiError({
|
||||||
onReport: handleApiReport,
|
onReport: handleApiReport,
|
||||||
});
|
});
|
||||||
@@ -616,12 +586,6 @@ const AppLayout = () => {
|
|||||||
}
|
}
|
||||||
const tagManager = tagManagerRef.current;
|
const tagManager = tagManagerRef.current;
|
||||||
|
|
||||||
|
|
||||||
const buildTagPayload = useCallback(
|
|
||||||
({ label, color } = {}) => tagManager.buildPayload({ label, color }),
|
|
||||||
[tagManager],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getDocumentAsset = useCallback((doc, type) => {
|
const getDocumentAsset = useCallback((doc, type) => {
|
||||||
if (!doc || !type) return null;
|
if (!doc || !type) return null;
|
||||||
return getAssetFromVersion(doc.current_version || null, type);
|
return getAssetFromVersion(doc.current_version || null, type);
|
||||||
@@ -1247,16 +1211,6 @@ const AppLayout = () => {
|
|||||||
[selectedRowKeys, updateSelectionOrder],
|
[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 folderOptions = useMemo(() => {
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
@@ -2498,73 +2452,6 @@ const AppLayout = () => {
|
|||||||
};
|
};
|
||||||
}, [appStatus, appDispatch, initializeAfterLogin]);
|
}, [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(
|
const bulkTagOperation = useCallback(
|
||||||
async ({ labels, action, documentIds }) => {
|
async ({ labels, action, documentIds }) => {
|
||||||
const normalized = labels.map((label) => label.trim()).filter((label) => label.length > 0);
|
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(
|
const handleBulkTagAddFromDetail = useCallback(
|
||||||
async ({ label, input, documentIds }) => {
|
async ({ label, input, documentIds }) => {
|
||||||
const trimmed = (label || '').trim();
|
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;
|
if (!file) return;
|
||||||
// FileList entry suppressed
|
// FileList entry suppressed
|
||||||
const relativePath =
|
const relativePath =
|
||||||
@@ -4554,54 +4366,6 @@ const AppLayout = () => {
|
|||||||
};
|
};
|
||||||
}, [handleTagRemove, setTagRemovalCursor]);
|
}, [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 () => {
|
const handleLogout = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -5333,7 +5097,6 @@ const AppLayout = () => {
|
|||||||
isFilterActive,
|
isFilterActive,
|
||||||
onLogout: handleLogout,
|
onLogout: handleLogout,
|
||||||
status,
|
status,
|
||||||
onCollapse: collapseSidebar,
|
|
||||||
tenantSlug,
|
tenantSlug,
|
||||||
tenants: tenantOptions,
|
tenants: tenantOptions,
|
||||||
activeTenantId: currentTenantId,
|
activeTenantId: currentTenantId,
|
||||||
@@ -5341,16 +5104,6 @@ const AppLayout = () => {
|
|||||||
onOpenSettings: openSettingsModal,
|
onOpenSettings: openSettingsModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sidebarPropsWithActions = useMemo(
|
|
||||||
() => ({
|
|
||||||
...sidebarProps,
|
|
||||||
onManageTags: openTagsModal,
|
|
||||||
onManageCorrespondents: openCorrespondentsModal,
|
|
||||||
onCollapse: collapseSidebar,
|
|
||||||
}),
|
|
||||||
[sidebarProps, openTagsModal, openCorrespondentsModal, collapseSidebar],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDetailPanelClose = useCallback(() => {
|
const handleDetailPanelClose = useCallback(() => {
|
||||||
setDetailPanelOpen(false);
|
setDetailPanelOpen(false);
|
||||||
clearDocumentSelection();
|
clearDocumentSelection();
|
||||||
|
|||||||
@@ -151,9 +151,6 @@ const Sidebar = ({
|
|||||||
onSearchSubmit,
|
onSearchSubmit,
|
||||||
onSearchClear,
|
onSearchClear,
|
||||||
isFilterActive,
|
isFilterActive,
|
||||||
appStatus,
|
|
||||||
loading,
|
|
||||||
previewActive,
|
|
||||||
onLogout,
|
onLogout,
|
||||||
status,
|
status,
|
||||||
onCollapse,
|
onCollapse,
|
||||||
|
|||||||
@@ -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 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 hslToHex = (h, s, l) => {
|
||||||
const normalizedH = ((h % 360) + 360) % 360;
|
const normalizedH = ((h % 360) + 360) % 360;
|
||||||
const sat = clamp01(s);
|
const sat = clamp01(s);
|
||||||
|
|||||||
Reference in New Issue
Block a user