fix(ux): T4 polish wave — empty-contact filter, redirect-on-create, friendly stage errors
F19: client form drops empty-value contacts on submit; auto-promotes first remaining row to primary if none flagged. F20: new-interest dialog redirects to the detail page on create instead of bouncing back to the list. F21: stage-transition validation errors render with STAGE_LABELS — "Yacht is required before leaving the Enquiry stage." (was "yachtId is required before leaving stage=enquiry"). F22: blocked-stage marker swapped from the ⚑ unicode glyph to a Lucide AlertTriangle with aria-label. F25: documents-hub folder selection moves to ?folder=<id> querystring so deep-link / browser-back / refresh round-trip the current folder. F26: reopen-outcome action now toasts "Outcome cleared — interest is open again." F27: stage PATCH where target === current short-circuits to a no-op return; downstream callers don't see a phantom stage_change audit row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ChevronDown, ChevronRight, FileText, Plus, Upload } from 'lucide-react';
|
||||
|
||||
@@ -96,11 +97,30 @@ function findInTree(nodes: FolderNode[], id: string): FolderNode | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// URL encoding for the folder selection tri-state:
|
||||
// no `folder` param → undefined (hub root / "All documents")
|
||||
// `folder=root` → null (root folder only)
|
||||
// `folder=<uuid>` → string (specific folder)
|
||||
function decodeFolderParam(raw: string | null): string | null | undefined {
|
||||
if (raw == null) return undefined;
|
||||
if (raw === 'root') return null;
|
||||
return raw;
|
||||
}
|
||||
function encodeFolderParam(value: string | null | undefined): string | null {
|
||||
if (value === undefined) return null;
|
||||
if (value === null) return 'root';
|
||||
return value;
|
||||
}
|
||||
|
||||
export function DocumentsHub({ portSlug }: DocumentsHubProps) {
|
||||
// undefined = "All documents" (no folder selected / hub root)
|
||||
// null = root folder only
|
||||
// string = specific folder id
|
||||
const [selectedFolderId, setSelectedFolderId] = useState<string | null | undefined>(undefined);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const folderParam = searchParams.get('folder');
|
||||
const selectedFolderId = useMemo(() => decodeFolderParam(folderParam), [folderParam]);
|
||||
|
||||
const { data: tree = [] } = useDocumentFolders();
|
||||
|
||||
@@ -144,9 +164,20 @@ export function DocumentsHub({ portSlug }: DocumentsHubProps) {
|
||||
selectedFolder.entityId != null &&
|
||||
isEntityType(folderEntityType);
|
||||
|
||||
const handleFolderSelect = (id: string | null | undefined) => {
|
||||
setSelectedFolderId(id);
|
||||
};
|
||||
const handleFolderSelect = useCallback(
|
||||
(id: string | null | undefined) => {
|
||||
const next = new URLSearchParams(searchParams.toString());
|
||||
const encoded = encodeFolderParam(id);
|
||||
if (encoded == null) {
|
||||
next.delete('folder');
|
||||
} else {
|
||||
next.set('folder', encoded);
|
||||
}
|
||||
const qs = next.toString();
|
||||
router.replace((qs ? `${pathname}?${qs}` : pathname) as never, { scroll: false });
|
||||
},
|
||||
[router, pathname, searchParams],
|
||||
);
|
||||
|
||||
const sidebarFooter = (
|
||||
<PermissionGate resource="documents" action="manage_folders">
|
||||
|
||||
Reference in New Issue
Block a user