frontend cleanup

This commit is contained in:
2025-10-29 00:02:44 +01:00
parent 8554fbd6e5
commit b4282c2b4b
4 changed files with 164 additions and 88 deletions
+91 -83
View File
@@ -29,6 +29,7 @@ import TagsPanel from './tags/TagsPanel';
import CorrespondentsPanel from './correspondents/CorrespondentsPanel';
import TagManager from './tag_manager';
import Sidebar from './sidebar/Sidebar';
import SettingsModal from './settings/SettingsModal';
import { ChevronsRightIcon } from './ui/icons';
import { createDocumentsSurface } from './documents/DocumentsTable';
import { createPreviewSurface } from './preview/PreviewWorkspace';
@@ -338,12 +339,12 @@ const LoginView = ({
<div className="login-card__tenant-list">
{tenantSelection.tenants.map((tenant) => (
<button
key={tenant.tenant_id}
key={tenant.id}
type="button"
onClick={() => onSelectTenant?.(tenant)}
disabled={Boolean(selectingTenantId)}
className={
selectingTenantId === tenant.tenant_id
selectingTenantId === tenant.id
? 'login-card__tenant-button is-loading'
: 'login-card__tenant-button'
}
@@ -440,7 +441,7 @@ const AppLayout = () => {
const previewDocumentId = routeDocumentId;
const { status: appStatus, token, tenant, tenants: tenantOptions = [] } = appState;
const tenantSlug = tenant?.slug || null;
const currentTenantId = tenant?.tenant_id || null;
const currentTenantId = tenant?.id || null;
const [status, setStatus] = useState(null);
const setStatusMessage = useCallback((message, variant = 'info') => {
setStatus(message ? { message, variant } : null);
@@ -464,6 +465,7 @@ const AppLayout = () => {
const [isCreateFolderModalOpen, setCreateFolderModalOpen] = useState(false);
const [isTagsModalOpen, setTagsModalOpen] = useState(false);
const [isCorrespondentsModalOpen, setCorrespondentsModalOpen] = useState(false);
const [isSettingsModalOpen, setSettingsModalOpen] = useState(false);
const [newFolderName, setNewFolderName] = useState('');
const [createFolderError, setCreateFolderError] = useState('');
const [creatingFolder, setCreatingFolder] = useState(false);
@@ -2004,38 +2006,6 @@ const AppLayout = () => {
],
);
const loadFolder = useCallback(
async (folderId, { showLoading = true, preserveSearch = false } = {}) => {
const targetId = folderId || 'root';
setSelectedFolder(targetId);
if (showLoading) setLoading(true);
try {
const contents = await ensureFolderData(targetId, { force: true, prefetchDepth: 1 });
if (targetId !== 'root') {
try {
await ensureFolderData('root', {
force: false,
includeDocuments: false,
prefetchDepth: 1,
});
} catch (error) {
console.warn('Failed to refresh root folder tree', error);
}
}
applySelectedFolder(targetId, contents);
if (!preserveSearch) {
setSearchResults(null);
}
} catch (error) {
notifyApiError(error, 'Failed to load folder contents.');
} finally {
if (showLoading) setLoading(false);
}
},
[ensureFolderData, applySelectedFolder, notifyApiError],
);
const expandFolderAncestors = useCallback(
(targetId) => {
if (!targetId || targetId === 'root') {
@@ -2097,51 +2067,73 @@ const AppLayout = () => {
return;
}
const visited = new Set();
const stack = [];
let currentId = targetId;
let guard = 0;
while (currentId && currentId !== 'root' && guard < 32) {
guard += 1;
if (visited.has(currentId)) {
break;
}
visited.add(currentId);
const node = folderNodes.get(currentId);
if (!node) {
stack.push(currentId);
break;
const fetchAncestor = async (folderId, guard = 0) => {
if (!folderId || folderId === 'root' || guard > 32) {
return;
}
const parentId = node.parentId ?? 'root';
if (!parentId || parentId === 'root') {
break;
const existing = folderNodes.get(folderId);
if (existing?.loaded) {
return;
}
stack.push(parentId);
currentId = parentId;
}
while (stack.length) {
const ancestorId = stack.pop();
if (!ancestorId || ancestorId === 'root') {
continue;
try {
const contents = await ensureFolderData(folderId, {
includeDocuments: false,
force: false,
prefetchDepth: 0,
});
const parentId = contents?.folder?.parent_id ?? 'root';
if (parentId && parentId !== 'root') {
await fetchAncestor(parentId, guard + 1);
}
} catch (error) {
console.warn('Failed to ensure ancestor folder for navigation', folderId, error);
}
if (!folderNodes.has(ancestorId)) {
};
await fetchAncestor(targetId, 0);
},
[folderNodes, ensureFolderData],
);
const loadFolder = useCallback(
async (folderId, { showLoading = true, preserveSearch = false } = {}) => {
const targetId = folderId || 'root';
setSelectedFolder(targetId);
await ensureFolderAncestorsLoaded(targetId);
expandFolderAncestors(targetId);
if (showLoading) setLoading(true);
try {
const contents = await ensureFolderData(targetId, { force: true, prefetchDepth: 1 });
if (targetId !== 'root') {
try {
await ensureFolderData(ancestorId, {
includeDocuments: false,
await ensureFolderData('root', {
force: false,
includeDocuments: false,
prefetchDepth: 1,
});
} catch (error) {
console.warn('Failed to ensure ancestor folder for navigation', ancestorId, error);
console.warn('Failed to refresh root folder tree', error);
}
}
applySelectedFolder(targetId, contents);
if (!preserveSearch) {
setSearchResults(null);
}
} catch (error) {
notifyApiError(error, 'Failed to load folder contents.');
} finally {
if (showLoading) setLoading(false);
}
},
[folderNodes, ensureFolderData],
[
ensureFolderData,
applySelectedFolder,
notifyApiError,
ensureFolderAncestorsLoaded,
expandFolderAncestors,
],
);
const selectFolder = useCallback(
@@ -2200,10 +2192,7 @@ const AppLayout = () => {
const hasData = folderContents.has(targetParam);
if (targetParam !== selectedFolder || !hasData) {
loadFolder(targetParam, {
showLoading: !routeDocumentId,
preserveSearch: isFilterActive,
});
selectFolder(targetParam, { immediate: true });
}
}, [
token,
@@ -2211,9 +2200,9 @@ const AppLayout = () => {
routeFolderId,
routeDocumentId,
selectedFolder,
loadFolder,
folderContents,
isFilterActive,
selectFolder,
]);
const refreshCurrentFolder = useCallback(async () => {
@@ -4048,6 +4037,14 @@ const AppLayout = () => {
setCorrespondentsModalOpen(false);
}, []);
const openSettingsModal = useCallback(() => {
setSettingsModalOpen(true);
}, []);
const closeSettingsModal = useCallback(() => {
setSettingsModalOpen(false);
}, []);
const handleCreateFolderSubmit = useCallback(
async (event) => {
event.preventDefault();
@@ -4111,10 +4108,11 @@ const AppLayout = () => {
useEffect(() => {
setTagsModalOpen(false);
setCorrespondentsModalOpen(false);
setSettingsModalOpen(false);
}, [location.pathname]);
useEffect(() => {
if (!isTagsModalOpen && !isCorrespondentsModalOpen) {
if (!isTagsModalOpen && !isCorrespondentsModalOpen && !isSettingsModalOpen) {
return;
}
const handleKeyDown = (event) => {
@@ -4124,6 +4122,8 @@ const AppLayout = () => {
closeTagsModal();
} else if (isCorrespondentsModalOpen) {
closeCorrespondentsModal();
} else if (isSettingsModalOpen) {
closeSettingsModal();
}
}
};
@@ -4131,7 +4131,14 @@ const AppLayout = () => {
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isTagsModalOpen, isCorrespondentsModalOpen, closeTagsModal, closeCorrespondentsModal]);
}, [
isTagsModalOpen,
isCorrespondentsModalOpen,
isSettingsModalOpen,
closeTagsModal,
closeCorrespondentsModal,
closeSettingsModal,
]);
useEffect(() => {
if (!token) return undefined;
@@ -4943,8 +4950,7 @@ const AppLayout = () => {
const handleTenantSelect = useCallback(
async (tenantOption, { refreshOnly = false } = {}) => {
const requestedTenantId =
tenantOption?.tenant_id || tenantOption?.tenantId || tenantOption?.id || null;
const requestedTenantId = tenantOption?.id ?? null;
if (!refreshOnly && (!requestedTenantId || requestedTenantId === currentTenantId)) {
return;
}
@@ -4980,7 +4986,7 @@ const AppLayout = () => {
api.defaults.headers.common.Authorization = `Bearer ${data.access_token}`;
tokenRef.current = data.access_token;
tenantIdRef.current = data?.tenant?.tenant_id || null;
tenantIdRef.current = data?.tenant?.id ?? null;
if (Array.isArray(data?.tenants)) {
appDispatch({ type: 'SET_TENANTS', tenants: data.tenants });
@@ -4992,10 +4998,8 @@ const AppLayout = () => {
await Promise.all([refreshTags(), refreshCorrespondents()]);
await loadFolder('root', { showLoading: false, preserveSearch: false });
setStatusMessage(
`Switched to ${data?.tenant?.slug || data?.tenant?.tenant_id || 'tenant'}.`,
'info',
);
const tenantLabel = data?.tenant?.slug || data?.tenant?.id || 'tenant';
setStatusMessage(`Switched to ${tenantLabel}.`, 'info');
} catch (error) {
notifyApiError(error, 'Failed to switch tenant.');
} finally {
@@ -5102,6 +5106,7 @@ const AppLayout = () => {
tenants: tenantOptions,
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
onOpenSettings: openSettingsModal,
};
const sidebarPropsWithActions = useMemo(
@@ -5215,6 +5220,7 @@ const AppLayout = () => {
resolveApiPath,
openTagsModal,
openCorrespondentsModal,
openSettingsModal,
}),
[
token,
@@ -5255,6 +5261,7 @@ const AppLayout = () => {
resolveApiPath,
openTagsModal,
openCorrespondentsModal,
openSettingsModal,
],
);
@@ -5397,6 +5404,7 @@ const AppLayout = () => {
</div>
</div>
)}
<SettingsModal open={isSettingsModalOpen} onClose={closeSettingsModal} />
</div>
</AppShellContext.Provider>
);
@@ -5661,15 +5669,15 @@ const LoginRoute = () => {
const handleTenantSelect = useCallback(
async (tenant) => {
if (!tenantSelection?.selectionToken || !tenant?.tenant_id) {
if (!tenantSelection?.selectionToken || !tenant?.id) {
return;
}
try {
setSelectingTenantId(tenant.tenant_id);
setSelectingTenantId(tenant.id);
const { data } = await api.post(
'/auth/select-tenant',
{ tenant_id: tenant.tenant_id },
{ tenant_id: tenant.id },
{
headers: {
Authorization: `Bearer ${tenantSelection.selectionToken}`,
+22 -4
View File
@@ -7,6 +7,8 @@ import {
ChevronsLeftIcon,
LogoutIcon,
ChevronDownIcon,
SettingsIcon,
CheckIcon,
} from '../ui/icons';
import { getTagColorStyle } from '../utils/colors';
@@ -159,6 +161,7 @@ const Sidebar = ({
tenants = [],
activeTenantId = null,
onSelectTenant,
onOpenSettings,
}) => {
const sortedCorrespondents = useMemo(
() =>
@@ -232,7 +235,7 @@ const Sidebar = ({
const handleTenantSelect = useCallback(
(tenant) => {
const targetId = tenant?.tenant_id || tenant?.tenantId || tenant?.id || null;
const targetId = tenant?.id || null;
if (!targetId) {
return;
}
@@ -247,6 +250,11 @@ const Sidebar = ({
onLogout?.();
}, [onLogout]);
const handleSettingsFromMenu = useCallback(() => {
setTenantMenuOpen(false);
onOpenSettings?.();
}, [onOpenSettings]);
const renderNodes = useCallback(
(ids, depth) =>
ids.map((id) => {
@@ -317,7 +325,7 @@ const Sidebar = ({
<span className="menu__empty">No tenants available.</span>
) : (
tenants.map((tenant) => {
const tenantId = tenant?.tenant_id || tenant?.tenantId || tenant?.id || null;
const tenantId = tenant?.id || null;
const isActive = tenantId === activeTenantId;
return (
<button
@@ -327,16 +335,26 @@ const Sidebar = ({
onClick={() => handleTenantSelect(tenant)}
role="menuitem"
>
<span className="menu__check-slot">
{isActive ? <CheckIcon size={16} /> : null}
</span>
<span className="menu__label">
{tenant?.slug || tenant?.name || tenantId || 'Tenant'}
{tenant?.slug || tenant?.name || tenantId || 'Tenant'}
</span>
{isActive ? <span className="menu__active-indicator">Active</span> : null}
</button>
);
})
)}
</div>
<div className="menu__footer">
<button
type="button"
className="menu__settings"
onClick={handleSettingsFromMenu}
>
<SettingsIcon size={16} />
Settings
</button>
<button
type="button"
className="menu__logout"
+31 -1
View File
@@ -1191,7 +1191,7 @@ button.danger:hover:not([disabled]) {
.menu .menu__item {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
gap: 0.6rem;
width: 100%;
padding: 0.55rem 1rem;
@@ -1230,6 +1230,14 @@ button.danger:hover:not([disabled]) {
text-overflow: ellipsis;
}
.menu__check-slot {
width: 1rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent, #2563eb);
}
.menu__active-indicator {
font-size: 0.75rem;
color: var(--muted);
@@ -1245,6 +1253,28 @@ button.danger:hover:not([disabled]) {
.menu__footer {
border-top: 1px solid var(--border-muted, var(--border));
padding: 0.35rem 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.menu__settings {
width: 100%;
display: inline-flex;
align-items: center;
gap: 0.4rem;
border: none;
background: none;
color: inherit;
font: inherit;
cursor: pointer;
padding: 0.4rem 0.5rem;
border-radius: 0.35rem;
}
.menu__settings:hover,
.menu__settings:focus-visible {
background: var(--sidebar-hover-bg);
}
.menu__logout {
+20
View File
@@ -21,6 +21,8 @@ import {
IconLogout,
IconChevronDown,
IconX,
IconSettings,
IconCheck,
} from '@tabler/icons-react';
import FolderSvg from '../assets/folder.svg';
@@ -195,6 +197,24 @@ export const CloseIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) =>
/>
);
export const SettingsIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconSettings
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const CheckIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconCheck
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const AnalyzeIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconAnalyze
className={composeClassName('icon', className)}