dockerfiles, TagsWorkspace
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM rust:1-slim AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libjpeg-dev \
|
||||||
|
libpng-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY src ./src
|
||||||
|
COPY migrations ./migrations
|
||||||
|
COPY tests ./tests
|
||||||
|
COPY diesel.toml ./
|
||||||
|
|
||||||
|
RUN cargo build --release --bin paperless-backend --bin worker
|
||||||
|
RUN cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
libssl3 \
|
||||||
|
libpq5 \
|
||||||
|
libjpeg62-turbo \
|
||||||
|
libpng16-16 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& useradd --system --create-home --uid 10001 appuser
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/release/paperless-backend /usr/local/bin/paperless-backend
|
||||||
|
COPY --from=builder /app/target/release/worker /usr/local/bin/paperless-worker
|
||||||
|
COPY --from=builder /usr/local/cargo/bin/diesel /usr/local/bin/diesel
|
||||||
|
COPY migrations ./migrations
|
||||||
|
COPY diesel.toml ./
|
||||||
|
|
||||||
|
ENV RUST_LOG=info
|
||||||
|
USER appuser
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/paperless-backend"]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=build /app/dist ./
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
-179
@@ -4,6 +4,7 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
useContext,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
@@ -12,8 +13,11 @@ import {
|
|||||||
Navigate,
|
Navigate,
|
||||||
Route,
|
Route,
|
||||||
Routes,
|
Routes,
|
||||||
|
Outlet,
|
||||||
|
useLocation,
|
||||||
|
useMatch,
|
||||||
useNavigate,
|
useNavigate,
|
||||||
useParams,
|
matchPath,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
|
|
||||||
@@ -1174,7 +1178,13 @@ const Sidebar = ({
|
|||||||
onFolderDragStart,
|
onFolderDragStart,
|
||||||
onFolderDragEnd,
|
onFolderDragEnd,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
|
onShowTags,
|
||||||
|
tags = [],
|
||||||
}) => {
|
}) => {
|
||||||
|
const handleShowTags = onShowTags || (() => {});
|
||||||
|
const tagsRouteMatch = useMatch('/tags');
|
||||||
|
const isTagsRoute = Boolean(tagsRouteMatch);
|
||||||
|
|
||||||
const renderNodes = useCallback(
|
const renderNodes = useCallback(
|
||||||
(ids, depth) =>
|
(ids, depth) =>
|
||||||
ids.map((id) => {
|
ids.map((id) => {
|
||||||
@@ -1249,8 +1259,8 @@ const Sidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`sidebar-link${isTagsView ? ' active' : ''}`}
|
className={`sidebar-link${isTagsRoute ? ' active' : ''}`}
|
||||||
onClick={onShowTags}
|
onClick={handleShowTags}
|
||||||
>
|
>
|
||||||
All tags
|
All tags
|
||||||
</button>
|
</button>
|
||||||
@@ -1282,6 +1292,17 @@ const TagsPanel = ({ tags, onRefresh, onUpdateTag }) => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const colorPickerValue = useMemo(() => {
|
||||||
|
if (!draftColor) {
|
||||||
|
return '#3366ff';
|
||||||
|
}
|
||||||
|
const match = HEX_COLOR_PATTERN.exec(draftColor.trim());
|
||||||
|
if (!match) {
|
||||||
|
return '#3366ff';
|
||||||
|
}
|
||||||
|
return `#${match[1].toLowerCase()}`;
|
||||||
|
}, [draftColor]);
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (!editingId) return;
|
if (!editingId) return;
|
||||||
|
|
||||||
@@ -1391,13 +1412,12 @@ const TagsPanel = ({ tags, onRefresh, onUpdateTag }) => {
|
|||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="tags-table__color-editor">
|
<div className="tags-table__color-editor">
|
||||||
<input
|
<input
|
||||||
className="tags-table__color-field"
|
type="color"
|
||||||
value={draftColor}
|
className="tags-table__color-picker"
|
||||||
|
value={colorPickerValue}
|
||||||
onChange={(event) => setDraftColor(event.target.value)}
|
onChange={(event) => setDraftColor(event.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder="#3366ff"
|
|
||||||
spellCheck="false"
|
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
|
aria-label="Pick tag color"
|
||||||
/>
|
/>
|
||||||
{draftColor && (
|
{draftColor && (
|
||||||
<button
|
<button
|
||||||
@@ -1470,51 +1490,17 @@ const MainLayout = ({ sidebarProps, children, className = 'app-main' }) => (
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DocumentsWorkspace = ({
|
const AppShellContext = React.createContext(null);
|
||||||
sidebarProps,
|
|
||||||
previewActive,
|
|
||||||
previewWorkspaceDocument,
|
|
||||||
previewWorkspaceDetail,
|
|
||||||
previewWorkspaceEntry,
|
|
||||||
closeDocumentPreview,
|
|
||||||
handleThumbnailRegeneration,
|
|
||||||
documentsTableProps,
|
|
||||||
detailPanelProps,
|
|
||||||
}) => {
|
|
||||||
if (previewActive && previewWorkspaceDocument) {
|
|
||||||
return (
|
|
||||||
<main className="preview-main">
|
|
||||||
<PreviewWorkspace
|
|
||||||
document={previewWorkspaceDocument}
|
|
||||||
detail={previewWorkspaceDetail}
|
|
||||||
previewEntry={previewWorkspaceEntry}
|
|
||||||
onClose={closeDocumentPreview}
|
|
||||||
onRegenerateThumbnails={handleThumbnailRegeneration}
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const AppLayout = () => {
|
||||||
<MainLayout sidebarProps={sidebarProps}>
|
const navigate = useNavigate();
|
||||||
<DocumentsTable {...documentsTableProps} />
|
const location = useLocation();
|
||||||
<DetailPanel {...detailPanelProps} />
|
const folderMatch = matchPath('/folders/:folderId', location.pathname);
|
||||||
</MainLayout>
|
const nestedDocMatch = matchPath('/folders/:folderId/documents/:documentId', location.pathname);
|
||||||
);
|
const docMatch = nestedDocMatch || matchPath('/documents/:documentId', location.pathname);
|
||||||
};
|
const routeFolderId =
|
||||||
|
folderMatch?.params?.folderId || nestedDocMatch?.params?.folderId || null;
|
||||||
const TagsWorkspace = ({ sidebarProps, tags, onRefresh, onUpdateTag }) => (
|
const routeDocumentId = docMatch?.params?.documentId || null;
|
||||||
<MainLayout sidebarProps={sidebarProps} className="tags-main">
|
|
||||||
<TagsPanel tags={tags} onRefresh={onRefresh} onUpdateTag={onUpdateTag} />
|
|
||||||
</MainLayout>
|
|
||||||
);
|
|
||||||
|
|
||||||
function App({
|
|
||||||
routeFolderId = null,
|
|
||||||
routeDocumentId = null,
|
|
||||||
navigate,
|
|
||||||
mode = 'documents',
|
|
||||||
}) {
|
|
||||||
const [token, setToken] = useState(() => window.localStorage.getItem('paperless_token') || '');
|
const [token, setToken] = useState(() => window.localStorage.getItem('paperless_token') || '');
|
||||||
const [status, setStatus] = useState(null);
|
const [status, setStatus] = useState(null);
|
||||||
const setStatusMessage = useCallback((message, variant = 'info') => {
|
const setStatusMessage = useCallback((message, variant = 'info') => {
|
||||||
@@ -3789,15 +3775,7 @@ function App({
|
|||||||
return pool.find((doc) => doc.id === previewDocumentId) || null;
|
return pool.find((doc) => doc.id === previewDocumentId) || null;
|
||||||
}, [previewDocumentId, previewWorkspaceDetail, searchResults, documents]);
|
}, [previewDocumentId, previewWorkspaceDetail, searchResults, documents]);
|
||||||
|
|
||||||
const isTagsView = mode === 'tags';
|
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
|
||||||
const previewActive = !isTagsView && Boolean(previewDocumentId && previewWorkspaceDocument);
|
|
||||||
|
|
||||||
const goToTagsView = useCallback(() => {
|
|
||||||
if (!navigate || isTagsView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
navigate('/tags');
|
|
||||||
}, [navigate, isTagsView]);
|
|
||||||
|
|
||||||
const sidebarProps = {
|
const sidebarProps = {
|
||||||
folderNodes,
|
folderNodes,
|
||||||
@@ -3812,8 +3790,7 @@ function App({
|
|||||||
onFolderDragStart: handleFolderDragStart,
|
onFolderDragStart: handleFolderDragStart,
|
||||||
onFolderDragEnd: handleFolderDragEnd,
|
onFolderDragEnd: handleFolderDragEnd,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
onShowTags: goToTagsView,
|
onShowTags: () => navigate('/tags'),
|
||||||
isTagsView,
|
|
||||||
tags,
|
tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3865,10 +3842,58 @@ function App({
|
|||||||
activePreviewId,
|
activePreviewId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
token,
|
||||||
|
status,
|
||||||
|
dropOverlayState,
|
||||||
|
handleBulkReanalyze,
|
||||||
|
handleLogout,
|
||||||
|
sidebarProps,
|
||||||
|
tags,
|
||||||
|
refreshTags,
|
||||||
|
handleTagUpdate,
|
||||||
|
previewActive,
|
||||||
|
previewWorkspaceDocument,
|
||||||
|
previewWorkspaceDetail,
|
||||||
|
previewWorkspaceEntry,
|
||||||
|
closeDocumentPreview,
|
||||||
|
handleThumbnailRegeneration,
|
||||||
|
documentsTableProps,
|
||||||
|
detailPanelProps,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
token,
|
||||||
|
status,
|
||||||
|
dropOverlayState,
|
||||||
|
handleBulkReanalyze,
|
||||||
|
handleLogout,
|
||||||
|
sidebarProps,
|
||||||
|
tags,
|
||||||
|
refreshTags,
|
||||||
|
handleTagUpdate,
|
||||||
|
previewActive,
|
||||||
|
previewWorkspaceDocument,
|
||||||
|
previewWorkspaceDetail,
|
||||||
|
previewWorkspaceEntry,
|
||||||
|
closeDocumentPreview,
|
||||||
|
handleThumbnailRegeneration,
|
||||||
|
documentsTableProps,
|
||||||
|
detailPanelProps,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className="app-shell">
|
||||||
{token ? (
|
<LoginView onSubmit={handleLogin} status={status} />
|
||||||
<>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShellContext.Provider value={contextValue}>
|
||||||
|
<div className="app-shell">
|
||||||
<DropOverlay
|
<DropOverlay
|
||||||
active={dropOverlayState.active}
|
active={dropOverlayState.active}
|
||||||
folderName={dropOverlayState.folderName}
|
folderName={dropOverlayState.folderName}
|
||||||
@@ -3902,8 +3927,36 @@ function App({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className={previewActive ? 'preview-main' : 'app-main'}>
|
<Outlet />
|
||||||
{previewActive && previewWorkspaceDocument ? (
|
</div>
|
||||||
|
</AppShellContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAppShell = () => {
|
||||||
|
const context = useContext(AppShellContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('AppShellContext not found. Ensure routes are nested under AppLayout.');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DocumentsRoute = () => {
|
||||||
|
const {
|
||||||
|
sidebarProps,
|
||||||
|
previewActive,
|
||||||
|
previewWorkspaceDocument,
|
||||||
|
previewWorkspaceDetail,
|
||||||
|
previewWorkspaceEntry,
|
||||||
|
closeDocumentPreview,
|
||||||
|
handleThumbnailRegeneration,
|
||||||
|
documentsTableProps,
|
||||||
|
detailPanelProps,
|
||||||
|
} = useAppShell();
|
||||||
|
|
||||||
|
if (previewActive && previewWorkspaceDocument) {
|
||||||
|
return (
|
||||||
|
<main className="preview-main">
|
||||||
<PreviewWorkspace
|
<PreviewWorkspace
|
||||||
document={previewWorkspaceDocument}
|
document={previewWorkspaceDocument}
|
||||||
detail={previewWorkspaceDetail}
|
detail={previewWorkspaceDetail}
|
||||||
@@ -3911,134 +3964,41 @@ function App({
|
|||||||
onClose={closeDocumentPreview}
|
onClose={closeDocumentPreview}
|
||||||
onRegenerateThumbnails={handleThumbnailRegeneration}
|
onRegenerateThumbnails={handleThumbnailRegeneration}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Sidebar
|
|
||||||
folderNodes={folderNodes}
|
|
||||||
onToggle={folderClickHandlers.onToggle}
|
|
||||||
onSelect={folderClickHandlers.onSelect}
|
|
||||||
onDrop={folderClickHandlers.onDrop}
|
|
||||||
onDragOver={folderClickHandlers.onDragOver}
|
|
||||||
onDragLeave={folderClickHandlers.onDragLeave}
|
|
||||||
onCreateFolder={handleFolderCreate}
|
|
||||||
onDeleteFolder={handleFolderDelete}
|
|
||||||
selectedFolder={selectedFolder}
|
|
||||||
onFolderDragStart={handleFolderDragStart}
|
|
||||||
onFolderDragEnd={handleFolderDragEnd}
|
|
||||||
draggedFolderId={draggedFolderId}
|
|
||||||
/>
|
|
||||||
<DocumentsTable
|
|
||||||
currentFolderName={currentFolderName}
|
|
||||||
breadcrumbs={breadcrumbs}
|
|
||||||
onRefresh={refreshCurrentFolder}
|
|
||||||
subfolders={currentSubfolders}
|
|
||||||
documents={documents}
|
|
||||||
searchResults={searchResults}
|
|
||||||
isFilterActive={isFilterActive}
|
|
||||||
onFolderSelect={selectFolder}
|
|
||||||
onFolderDrop={folderClickHandlers.onDrop}
|
|
||||||
onFolderDragOver={folderClickHandlers.onDragOver}
|
|
||||||
onFolderDragLeave={folderClickHandlers.onDragLeave}
|
|
||||||
onFolderDragStart={handleFolderDragStart}
|
|
||||||
onFolderDragEnd={handleFolderDragEnd}
|
|
||||||
draggedFolderId={draggedFolderId}
|
|
||||||
onFolderDelete={handleFolderDelete}
|
|
||||||
onDocumentRowClick={handleDocumentRowClick}
|
|
||||||
onDocumentOpen={openDocumentPreview}
|
|
||||||
selectedDocumentIds={selectedDocumentIds}
|
|
||||||
focusedDocumentId={focusedDocumentId}
|
|
||||||
draggingDocumentIds={draggedDocumentIds}
|
|
||||||
onDocumentDragStart={(event, documentId) => {
|
|
||||||
const selection = selectedDocumentIds.includes(documentId)
|
|
||||||
? selectedDocumentIds
|
|
||||||
: [documentId];
|
|
||||||
if (!selectedDocumentIds.includes(documentId)) {
|
|
||||||
applySelection([documentId], {
|
|
||||||
anchor: documentId,
|
|
||||||
interactedIds: [documentId],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setDraggedDocumentIds(selection);
|
|
||||||
event.dataTransfer.effectAllowed = 'move';
|
|
||||||
try {
|
|
||||||
event.dataTransfer.setData(
|
|
||||||
'application/x-paperless-doc-list',
|
|
||||||
JSON.stringify(selection),
|
|
||||||
);
|
|
||||||
} catch (
|
|
||||||
// eslint-disable-next-line no-empty
|
|
||||||
error
|
|
||||||
) {}
|
|
||||||
event.currentTarget.classList.add('dragging');
|
|
||||||
}}
|
|
||||||
onDocumentDragEnd={(event) => {
|
|
||||||
setDraggedDocumentIds([]);
|
|
||||||
event.currentTarget.classList.remove('dragging');
|
|
||||||
}}
|
|
||||||
filterBar={filterBar}
|
|
||||||
/>
|
|
||||||
<DetailPanel
|
|
||||||
selectedDocuments={orderedSelectedDocuments}
|
|
||||||
detailMap={documentDetails}
|
|
||||||
tags={tags}
|
|
||||||
onTagAdd={handleTagAdd}
|
|
||||||
onTagRemove={handleTagRemove}
|
|
||||||
onRegenerateThumbnails={handleThumbnailRegeneration}
|
|
||||||
previewEntry={selectedPreviewEntry}
|
|
||||||
onOpenPreview={openDocumentPreview}
|
|
||||||
onBulkTagAdd={handleBulkTagAddFromDetail}
|
|
||||||
onBulkTagRemove={handleBulkTagRemoveFromDetail}
|
|
||||||
onBulkMove={handleBulkMoveFromDetail}
|
|
||||||
onBulkReanalyze={handleBulkSelectionReanalyze}
|
|
||||||
folderOptions={folderOptions}
|
|
||||||
defaultMoveTarget={defaultMoveTarget}
|
|
||||||
onPromoteSelection={promoteSelectionOrder}
|
|
||||||
activePreviewId={activePreviewId}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<LoginView onSubmit={handleLogin} status={status} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoutedDocumentsApp = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const params = useParams();
|
|
||||||
const folderId = params.folderId ?? null;
|
|
||||||
const documentId = params.documentId ?? null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<App
|
<MainLayout sidebarProps={sidebarProps}>
|
||||||
mode="documents"
|
<DocumentsTable {...documentsTableProps} />
|
||||||
routeFolderId={folderId}
|
<DetailPanel {...detailPanelProps} />
|
||||||
routeDocumentId={documentId}
|
</MainLayout>
|
||||||
navigate={navigate}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoutedTagsApp = () => {
|
const TagsRoute = () => {
|
||||||
const navigate = useNavigate();
|
const { sidebarProps, tags, refreshTags, handleTagUpdate } = useAppShell();
|
||||||
return <App mode="tags" navigate={navigate} />;
|
return (
|
||||||
|
<MainLayout sidebarProps={sidebarProps} className="tags-main">
|
||||||
|
<TagsPanel tags={tags} onRefresh={refreshTags} onUpdateTag={handleTagUpdate} />
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppRouter = () => (
|
const AppRouter = () => (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route element={<AppLayout />}>
|
||||||
<Route path="/" element={<Navigate to="/folders" replace />} />
|
<Route path="/" element={<Navigate to="/folders" replace />} />
|
||||||
<Route path="/folders" element={<RoutedDocumentsApp />} />
|
<Route path="/folders" element={<DocumentsRoute />} />
|
||||||
<Route path="/folders/:folderId" element={<RoutedDocumentsApp />} />
|
<Route path="/folders/:folderId" element={<DocumentsRoute />} />
|
||||||
<Route path="/documents/:documentId" element={<RoutedDocumentsApp />} />
|
<Route path="/documents/:documentId" element={<DocumentsRoute />} />
|
||||||
<Route
|
<Route
|
||||||
path="/folders/:folderId/documents/:documentId"
|
path="/folders/:folderId/documents/:documentId"
|
||||||
element={<RoutedDocumentsApp />}
|
element={<DocumentsRoute />}
|
||||||
/>
|
/>
|
||||||
<Route path="/tags" element={<RoutedTagsApp />} />
|
<Route path="/tags" element={<TagsRoute />} />
|
||||||
<Route path="*" element={<Navigate to="/folders" replace />} />
|
<Route path="*" element={<Navigate to="/folders" replace />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -361,9 +361,13 @@ button.icon-button.ghost:hover:not([disabled]) {
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-table__color-field {
|
.tags-table__color-picker {
|
||||||
width: 7rem;
|
width: 2.25rem;
|
||||||
font-family: var(--monospace, 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace);
|
height: 2.25rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-table__edit-controls {
|
.tags-table__edit-controls {
|
||||||
|
|||||||
Reference in New Issue
Block a user