frontend cleanup
This commit is contained in:
+91
-83
@@ -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}`,
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user