From cd9a8a020483bd1ead71701bcea523a9af752ba0 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Tue, 11 Nov 2025 15:31:42 +0100 Subject: [PATCH 1/3] fix --- frontend/src/styles/detail/detail-panels.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/styles/detail/detail-panels.css b/frontend/src/styles/detail/detail-panels.css index c30b43a..a51f469 100644 --- a/frontend/src/styles/detail/detail-panels.css +++ b/frontend/src/styles/detail/detail-panels.css @@ -74,10 +74,6 @@ flex: 1; } -.detail-panel__content .document-viewer-panel__body { - padding: 0.5rem 0.5rem 0; -} - .detail-section__header { display: flex; align-items: center; From 1193c715790ff1608a3d397bce28bbb9fa4dd3bd Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Tue, 11 Nov 2025 15:59:17 +0100 Subject: [PATCH 2/3] tag picker --- frontend/src/desktop/createDesktopSurface.js | 3 +- .../src/documents/SelectionAssignmentMenu.jsx | 62 +++++++++---------- .../documents/panel/createDocumentsSurface.js | 3 +- frontend/src/styles/detail/detail-panels.css | 19 +++++- frontend/src/styles/sidebar/sidebar.css | 1 + 5 files changed, 53 insertions(+), 35 deletions(-) diff --git a/frontend/src/desktop/createDesktopSurface.js b/frontend/src/desktop/createDesktopSurface.js index 0ca4a33..2b1dd9a 100644 --- a/frontend/src/desktop/createDesktopSurface.js +++ b/frontend/src/desktop/createDesktopSurface.js @@ -84,12 +84,13 @@ const createDesktopSurface = ({ const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null; const detail = detailOpen && detailProps ? (() => { - const { onClose, onOpenPreview, ...restDetailProps } = detailProps; + const { onClose, onOpenPreview, tags: tagOptions, ...restDetailProps } = detailProps; return ( ); diff --git a/frontend/src/documents/SelectionAssignmentMenu.jsx b/frontend/src/documents/SelectionAssignmentMenu.jsx index e5ff17b..6bd0aa6 100644 --- a/frontend/src/documents/SelectionAssignmentMenu.jsx +++ b/frontend/src/documents/SelectionAssignmentMenu.jsx @@ -1,10 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useFloatingMenu from '../ui/useFloatingMenu'; -import { - PlusIcon, - CheckIcon, - CircleDashedCheckIcon, -} from '../ui/icons'; +import { CheckIcon, CircleDashedCheckIcon, PlusIcon } from '../ui/icons'; const STATE_ORDER = { all: 0, @@ -117,7 +113,8 @@ const SelectionAssignmentMenu = ({ ); const handleCreate = useCallback( - async () => { + async (event) => { + event?.preventDefault?.(); if (typeof onCreate !== 'function') { return; } @@ -144,9 +141,12 @@ const SelectionAssignmentMenu = ({ ); const canCreate = Boolean(onCreate); - const showCreateOption = canCreate - && query.trim().length > 0 - && !existingLabels.has(query.trim().toLowerCase()); + const trimmedQuery = query.trim(); + const queryKey = trimmedQuery.toLowerCase(); + const canSubmitCreate = canCreate + && trimmedQuery.length > 0 + && !existingLabels.has(queryKey) + && !pending; const handleTriggerClick = useCallback(() => { if (disabled) { @@ -184,15 +184,28 @@ const SelectionAssignmentMenu = ({ data-floating-position >
- setQuery(event.target.value)} - placeholder={placeholder} - aria-label={placeholder} - disabled={pending} - /> +
+ setQuery(event.target.value)} + placeholder={placeholder} + aria-label={placeholder} + disabled={pending} + /> + {canCreate ? ( + + ) : null} +
{filteredItems.length ? ( @@ -237,19 +250,6 @@ const SelectionAssignmentMenu = ({
{emptyMessage}
)}
- {showCreateOption ? ( - - ) : null} ) : null} diff --git a/frontend/src/documents/panel/createDocumentsSurface.js b/frontend/src/documents/panel/createDocumentsSurface.js index 794e2f4..a0ee776 100644 --- a/frontend/src/documents/panel/createDocumentsSurface.js +++ b/frontend/src/documents/panel/createDocumentsSurface.js @@ -93,12 +93,13 @@ const createDocumentsSurface = ({ const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null; const detail = detailOpen && detailProps ? (() => { - const { onClose, onOpenPreview, ...restDetailProps } = detailProps; + const { onClose, onOpenPreview, tags: tagOptions, ...restDetailProps } = detailProps; return ( ); diff --git a/frontend/src/styles/detail/detail-panels.css b/frontend/src/styles/detail/detail-panels.css index a51f469..f5584c6 100644 --- a/frontend/src/styles/detail/detail-panels.css +++ b/frontend/src/styles/detail/detail-panels.css @@ -191,7 +191,14 @@ border-bottom: 1px solid var(--border-subtle); } -.selection-assignment__header input { +.selection-assignment__form { + display: flex; + gap: 0.4rem; + align-items: stretch; +} + +.selection-assignment__form input { + flex: 1 1 auto; width: 100%; padding: 0.35rem 0.6rem; border: 1px solid var(--border-subtle); @@ -201,12 +208,20 @@ font-size: 0.95rem; } -.selection-assignment__header input:focus-visible { +.selection-assignment__form input:focus-visible { outline: none; border-color: color-mix(in oklch, var(--accent) 60%, transparent); box-shadow: 0 0 0 1px color-mix(in oklch, var(--accent) 35%, transparent); } +.selection-assignment__add { + align-self: stretch; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 2rem; +} + .selection-assignment__list { max-height: max(240px, 50vh); overflow-y: auto; diff --git a/frontend/src/styles/sidebar/sidebar.css b/frontend/src/styles/sidebar/sidebar.css index a1a8479..0b8ae83 100644 --- a/frontend/src/styles/sidebar/sidebar.css +++ b/frontend/src/styles/sidebar/sidebar.css @@ -247,6 +247,7 @@ .menu__list { overflow-y: auto; + max-height: min(20rem, 60vh); padding: 0.25rem; display: flex; flex-direction: column; From d46951d12e40710fc4d30655c906bd934e1ce905 Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Tue, 11 Nov 2025 16:22:50 +0100 Subject: [PATCH 3/3] fix --- .../src/documents/DocumentSummarySection.jsx | 201 +++++++++++++++--- .../src/documents/SelectionAssignmentMenu.jsx | 6 +- frontend/src/styles/sidebar/sidebar.css | 2 +- 3 files changed, 176 insertions(+), 33 deletions(-) diff --git a/frontend/src/documents/DocumentSummarySection.jsx b/frontend/src/documents/DocumentSummarySection.jsx index b040c32..5793167 100644 --- a/frontend/src/documents/DocumentSummarySection.jsx +++ b/frontend/src/documents/DocumentSummarySection.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EditIcon, IconX, PlusIcon } from '../ui/icons'; -import QuickAddMenu from '../ui/QuickAddMenu'; +import SelectionAssignmentMenu from './SelectionAssignmentMenu'; import { getTagColorStyle } from '../utils/colors'; import { formatDate, @@ -34,6 +34,29 @@ export const buildCorrespondentOptions = (entries = []) => { const normalizeOptions = (options) => (Array.isArray(options) ? options : []); +const normalizeQuickAddOption = (option) => { + if (option == null) { + return null; + } + if (typeof option === 'string') { + const label = option.trim(); + return label ? { id: label, label, original: option } : null; + } + const label = typeof option.label === 'string' + ? option.label.trim() + : typeof option.name === 'string' + ? option.name.trim() + : ''; + if (!label) { + return null; + } + return { + id: option.id ?? label, + label, + original: option, + }; +}; + export const TagSection = ({ tags = [], onRemove, @@ -63,10 +86,71 @@ export const TagSection = ({ [onAdd], ); - const normalizedOptions = useMemo(() => normalizeOptions(datalistOptions), [datalistOptions]); + const normalizedOptions = useMemo( + () => + normalizeOptions(datalistOptions) + .map((option) => normalizeQuickAddOption(option)) + .filter(Boolean), + [datalistOptions], + ); const containerClass = className ? `tag-list ${className}` : 'tag-list'; const showQuickAdd = Boolean(onAdd); + const assignmentItems = useMemo(() => { + const map = new Map(); + + normalizedOptions.forEach((option) => { + const label = option?.label?.trim(); + if (!label) { + return; + } + const key = label.toLowerCase(); + if (map.has(key)) { + return; + } + map.set(key, { + id: option.id ?? label, + label, + state: 'none', + payload: option.original ?? { label }, + }); + }); + + tags.forEach((tag) => { + const label = typeof tag?.label === 'string' ? tag.label.trim() : ''; + if (!label) { + return; + } + const key = label.toLowerCase(); + const payload = { id: tag.id, label, color: tag.color ?? null }; + if (map.has(key)) { + const entry = map.get(key); + entry.state = 'all'; + entry.payload = payload; + return; + } + map.set(key, { + id: tag.id ?? label, + label, + state: 'all', + payload, + }); + }); + + return Array.from(map.values()); + }, [normalizedOptions, tags]); + + const handleAssignmentSelect = useCallback( + (item) => { + if (!item) { + return; + } + const payload = item.payload ?? { label: item.label }; + handleSelect(payload); + }, + [handleSelect], + ); + return (
{tags.map((tag) => { @@ -89,14 +173,17 @@ export const TagSection = ({ ); })} {showQuickAdd ? ( - handleSelect(normalized || original)} + @@ -125,31 +212,82 @@ export const CorrespondentSection = ({ [onAdd], ); - const handleSelect = useCallback( - (original, normalized) => { - if (!onAdd) return; - const source = normalized && typeof normalized === 'object' ? normalized : original; + const normalizedOptions = useMemo( + () => + normalizeOptions(datalistOptions) + .map((option) => normalizeQuickAddOption(option)) + .filter(Boolean), + [datalistOptions], + ); + const hasEntries = entries && entries.length > 0; + const showQuickAdd = Boolean(onAdd); + const containerClass = className ? `correspondent-list ${className}` : 'correspondent-list'; + + const assignmentItems = useMemo(() => { + const map = new Map(); + + normalizedOptions.forEach((option) => { + const label = option?.label?.trim(); + if (!label) { + return; + } + const key = label.toLowerCase(); + if (map.has(key)) { + return; + } + map.set(key, { + id: option.id ?? label, + label, + state: 'none', + payload: option.original ?? { name: label }, + }); + }); + + entries.forEach((entry) => { + const label = typeof entry?.name === 'string' ? entry.name.trim() : ''; + if (!label) { + return; + } + const key = label.toLowerCase(); + const payload = { id: entry.id, name: label }; + if (map.has(key)) { + const item = map.get(key); + item.state = 'all'; + item.payload = payload; + return; + } + map.set(key, { + id: entry.id ?? label, + label, + state: 'all', + payload, + }); + }); + + return Array.from(map.values()); + }, [normalizedOptions, entries]); + + const handleAssignmentSelect = useCallback( + (item) => { + if (!onAdd || !item) { + return; + } + const source = item.payload ?? item; const resolvedName = - (source && typeof source.name === 'string' && source.name.trim()) || - (typeof source === 'string' ? source.trim() : '') || - (source && typeof source.label === 'string' ? source.label.trim() : ''); + (source && typeof source.name === 'string' && source.name.trim()) + || (typeof source === 'string' ? source.trim() : '') + || (source && typeof source.label === 'string' ? source.label.trim() : ''); if (!resolvedName) { return; } - const payload = - source && typeof source === 'object' - ? { ...source, name: resolvedName } - : { id: null, name: resolvedName }; + const payload = typeof source === 'object' + ? { ...source, name: resolvedName } + : { id: null, name: resolvedName }; onAdd({ name: resolvedName, option: payload, input: null }); }, [onAdd], ); - const normalizedOptions = useMemo(() => normalizeOptions(datalistOptions), [datalistOptions]); - const hasEntries = entries && entries.length > 0; - const showQuickAdd = Boolean(onAdd); - const containerClass = className ? `correspondent-list ${className}` : 'correspondent-list'; - return (
{hasEntries @@ -176,14 +314,17 @@ export const CorrespondentSection = ({ }) : !showQuickAdd && No correspondents yet.} {showQuickAdd ? ( - handleSelect(original, normalized)} + diff --git a/frontend/src/documents/SelectionAssignmentMenu.jsx b/frontend/src/documents/SelectionAssignmentMenu.jsx index 6bd0aa6..ca75ed8 100644 --- a/frontend/src/documents/SelectionAssignmentMenu.jsx +++ b/frontend/src/documents/SelectionAssignmentMenu.jsx @@ -31,10 +31,12 @@ const SelectionAssignmentMenu = ({ disabled = false, className, triggerContent = null, + triggerClassName = 'quick-add__chip quick-add__trigger panel-floating-actions__trigger', showStateIndicators = true, showCounts = true, onOpenMenu = null, renderItemLabel = null, + positionStrategy = 'absolute', }) => { const anchorRef = useRef(null); const inputRef = useRef(null); @@ -51,7 +53,7 @@ const SelectionAssignmentMenu = ({ } = useFloatingMenu({ anchorRef, align: 'center', - positionStrategy: 'absolute', + positionStrategy, minWidth: 220, }); @@ -163,7 +165,7 @@ const SelectionAssignmentMenu = ({