refactor: Standardize document and folder identification with a new entryKey module.

This commit is contained in:
2025-11-24 23:13:11 +01:00
parent 096c377897
commit 22f0c040a2
12 changed files with 146 additions and 202 deletions
+24
View File
@@ -0,0 +1,24 @@
// Entry key utilities for workspace selection
// Entry keys are strings in the format "document:id" or "folder:id"
const ENTRY_KEY_SEPARATOR = ':';
// Create entry key strings
export const createDocumentEntryKey = (documentId: string | number): string =>
`document${ENTRY_KEY_SEPARATOR}${documentId}`;
export const createFolderEntryKey = (folderId: string | number): string =>
`folder${ENTRY_KEY_SEPARATOR}${folderId}`;
// Type guards for entry key strings
export const isDocumentEntry = (key: string): boolean =>
key.split(ENTRY_KEY_SEPARATOR, 1)[0] === 'document';
export const isFolderEntry = (key: string): boolean =>
key.split(ENTRY_KEY_SEPARATOR, 1)[0] === 'folder';
// Extract ID from entry key string
export const getEntryId = (key: string): string => {
const parts = key.split(ENTRY_KEY_SEPARATOR);
return parts.slice(1).join(ENTRY_KEY_SEPARATOR);
};
+42 -51
View File
@@ -1,6 +1,12 @@
import { useCallback, useRef, useState } from 'react';
import {
createDocumentEntryKey,
createFolderEntryKey,
isDocumentEntry,
isFolderEntry,
getEntryId,
} from './entryKey';
type RowKey = string;
type DocumentId = string | number;
interface SelectionEventLike {
@@ -11,46 +17,36 @@ interface SelectionEventLike {
}
interface UseDocumentSelectionOptions {
resolveDocumentRowKey: (id: DocumentId | null) => RowKey | null;
resolveFolderRowKey: (id: DocumentId | null) => RowKey | null;
isDocumentRowKey: (key?: RowKey | null) => boolean;
isFolderRowKey: (key?: RowKey | null) => boolean;
getRowId: (key?: RowKey | null) => DocumentId | null;
initialEntries?: RowKey[];
initialEntries?: string[];
}
interface ApplySelectionOptions {
anchor?: RowKey | null;
interactedKeys?: RowKey[];
anchor?: string;
interactedKeys?: string[];
}
const DEFAULT_INITIAL_ENTRIES: RowKey[] = [];
const DEFAULT_INITIAL_ENTRIES: string[] = [];
export const useDocumentSelection = ({
resolveDocumentRowKey,
resolveFolderRowKey,
isDocumentRowKey,
isFolderRowKey,
getRowId,
initialEntries = DEFAULT_INITIAL_ENTRIES,
}: UseDocumentSelectionOptions) => {
const [selectedEntries, setSelectedEntries] = useState<RowKey[]>(initialEntries);
const [selectionOrder, setSelectionOrder] = useState<RowKey[]>(initialEntries);
const selectionOrderRef = useRef<RowKey[]>(initialEntries);
const selectionAnchorRef = useRef<RowKey | null>(null);
}: UseDocumentSelectionOptions = {}) => {
const [selectedEntries, setSelectedEntries] = useState<string[]>(initialEntries);
const [selectionOrder, setSelectionOrder] = useState<string[]>(initialEntries);
const selectionOrderRef = useRef<string[]>(initialEntries);
const selectionAnchorRef = useRef<string | null>(null);
const selectionInitializedRef = useRef(false);
const [focusedDocumentId, setFocusedDocumentId] = useState<DocumentId | null>(null);
const [focusedRowKey, setFocusedRowKey] = useState<RowKey | null>(null);
const [focusedDocumentId, setFocusedDocumentId] = useState<DocumentId | undefined>(undefined);
const [focusedRowKey, setFocusedRowKey] = useState<string | undefined>(undefined);
const visibleRowKeySetRef = useRef<Set<RowKey>>(new Set());
const navigableRowKeysRef = useRef<RowKey[]>([]);
const visibleRowKeySetRef = useRef<Set<string>>(new Set());
const navigableRowKeysRef = useRef<string[]>([]);
const configureSelectionEnvironment = useCallback(({
visibleRowKeySet,
navigableRowKeys,
}: {
visibleRowKeySet?: Set<RowKey>;
navigableRowKeys?: RowKey[];
visibleRowKeySet?: Set<string>;
navigableRowKeys?: string[];
}) => {
if (visibleRowKeySet) {
visibleRowKeySetRef.current = visibleRowKeySet;
@@ -60,7 +56,7 @@ export const useDocumentSelection = ({
}
}, []);
const updateSelectionOrder = useCallback((nextSelection: RowKey[], interactedKeys: RowKey[] = []) => {
const updateSelectionOrder = useCallback((nextSelection: string[], interactedKeys: string[] = []) => {
const nextSet = new Set(nextSelection);
const previousOrder = selectionOrderRef.current.filter((id) => nextSet.has(id));
const interacted = (interactedKeys || []).filter((id, index, array) => array.indexOf(id) === index);
@@ -93,23 +89,23 @@ export const useDocumentSelection = ({
const applySelection = useCallback(
(
rowKeys: Array<RowKey | null>,
rowKeys: Array<string | null>,
{ anchor, interactedKeys = [] }: ApplySelectionOptions = {},
) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const unique: RowKey[] = [];
const unique: string[] = [];
(rowKeys || []).forEach((key) => {
if (!key) return;
let canonicalKey: RowKey | null = null;
let canonicalKey: string | null = null;
if (visibleRowKeySet.has(key)) {
canonicalKey = key;
} else if (isDocumentRowKey(key)) {
const id = getRowId(key);
canonicalKey = id ? resolveDocumentRowKey(id) : null;
} else if (isFolderRowKey(key)) {
const id = getRowId(key);
canonicalKey = id ? resolveFolderRowKey(id) : null;
} else if (isDocumentEntry(key)) {
const id = getEntryId(key);
canonicalKey = id ? createDocumentEntryKey(id) : null;
} else if (isFolderEntry(key)) {
const id = getEntryId(key);
canonicalKey = id ? createFolderEntryKey(id) : null;
}
if (!canonicalKey || !visibleRowKeySet.has(canonicalKey)) {
@@ -131,18 +127,18 @@ export const useDocumentSelection = ({
const nextFocusedDocumentId: DocumentId | null = (() => {
if (focusedDocumentId) {
const focusKey = resolveDocumentRowKey(focusedDocumentId);
const focusKey = createDocumentEntryKey(focusedDocumentId);
if (focusKey && unique.includes(focusKey)) {
return focusedDocumentId;
}
}
if (resolvedAnchor && isDocumentRowKey(resolvedAnchor)) {
return getRowId(resolvedAnchor) ?? null;
if (resolvedAnchor && isDocumentEntry(resolvedAnchor)) {
return getEntryId(resolvedAnchor) ?? null;
}
const lastDocKey = [...unique].reverse().find((key) => isDocumentRowKey(key)) ?? null;
return lastDocKey ? getRowId(lastDocKey) ?? null : null;
const lastDocKey = [...unique].reverse().find((key) => isDocumentEntry(key)) ?? null;
return lastDocKey ? getEntryId(lastDocKey) ?? null : null;
})();
setFocusedDocumentId(nextFocusedDocumentId);
@@ -159,11 +155,6 @@ export const useDocumentSelection = ({
},
[
focusedDocumentId,
getRowId,
isDocumentRowKey,
isFolderRowKey,
resolveDocumentRowKey,
resolveFolderRowKey,
updateSelectionOrder,
],
);
@@ -174,7 +165,7 @@ export const useDocumentSelection = ({
}, [applySelection]);
const handleEntrySelection = useCallback(
(rowKey: RowKey | null, event?: SelectionEventLike) => {
(rowKey: string, event?: SelectionEventLike) => {
const visibleRowKeySet = visibleRowKeySetRef.current;
const navigableRowKeys = navigableRowKeysRef.current;
if (!rowKey || !visibleRowKeySet.has(rowKey)) {
@@ -200,8 +191,8 @@ export const useDocumentSelection = ({
anchorKey = rowKey;
}
let nextKeys: RowKey[] = [];
let interactedKeys: RowKey[] = [];
let nextKeys: string[] = [];
let interactedKeys: string[] = [];
if (shiftKey && anchorKey) {
const anchorIndex = navigableRowKeys.indexOf(anchorKey);
@@ -245,7 +236,7 @@ export const useDocumentSelection = ({
const promoteSelectionOrder = useCallback(
(docId?: DocumentId | null) => {
if (!docId) return;
const rowKey = resolveDocumentRowKey(docId);
const rowKey = createDocumentEntryKey(docId);
if (!rowKey) return;
if (!selectedEntries.includes(rowKey)) {
@@ -254,7 +245,7 @@ export const useDocumentSelection = ({
updateSelectionOrder(selectedEntries, [rowKey]);
},
[resolveDocumentRowKey, selectedEntries, updateSelectionOrder],
[selectedEntries, updateSelectionOrder],
);
return {
+16 -31
View File
@@ -1,19 +1,15 @@
import { useCallback, useMemo } from 'react';
import { useDocumentSelection } from './useDocumentSelection';
type RowKey = string;
import { isDocumentEntry, isFolderEntry, getEntryId } from './entryKey';
interface SelectionEntry {
rowKey?: RowKey;
entryKey?: string;
// Legacy field for compatibility
rowKey?: string;
[key: string]: unknown;
}
interface WorkspaceSelectionOptions {
resolveDocumentRowKey?: (id: string | number) => RowKey | null;
resolveFolderRowKey?: (id: string | number) => RowKey | null;
isDocumentRowKey?: (key: RowKey | SelectionEntry) => boolean;
isFolderRowKey?: (key: RowKey | SelectionEntry) => boolean;
getRowId?: (key: RowKey | SelectionEntry) => string | number | null;
onDocumentActivate?: (id: string | number) => void;
onInspectFolder?: (id: string | number) => void;
}
@@ -21,21 +17,10 @@ interface WorkspaceSelectionOptions {
const identity = <T,>(value: T) => value;
export const useWorkspaceSelection = ({
resolveDocumentRowKey,
resolveFolderRowKey,
isDocumentRowKey = () => false,
isFolderRowKey = () => false,
getRowId = () => null,
onDocumentActivate = identity,
onInspectFolder = identity,
}: WorkspaceSelectionOptions = {}) => {
const selection = useDocumentSelection({
resolveDocumentRowKey,
resolveFolderRowKey,
isDocumentRowKey,
isFolderRowKey,
getRowId,
});
const selection = useDocumentSelection();
const {
selectedEntries,
@@ -59,26 +44,26 @@ export const useWorkspaceSelection = ({
const selectedDocumentIds = useMemo(
() =>
selectedEntries
.filter((entry) => isDocumentRowKey(entry))
.map((entry) => getRowId(entry))
.filter((entry) => isDocumentEntry(entry))
.map((entry) => getEntryId(entry))
.filter(Boolean),
[selectedEntries, isDocumentRowKey, getRowId],
[selectedEntries],
);
const selectedFolderIds = useMemo(
() =>
selectedEntries
.filter((entry) => isFolderRowKey(entry))
.map((entry) => getRowId(entry))
.filter((entry) => isFolderEntry(entry))
.map((entry) => getEntryId(entry))
.filter(Boolean),
[selectedEntries, isFolderRowKey, getRowId],
[selectedEntries],
);
const selectEntry = useCallback(
(entry: SelectionEntry | string | null, event?: unknown) => {
(entry: SelectionEntry | string, event?: unknown) => {
const rowKey = entry && Object(entry) === entry
? (entry as SelectionEntry).rowKey ?? null
: (entry as string | null);
? (entry as SelectionEntry).rowKey ?? undefined
: (entry as string);
if (!rowKey) return;
handleEntrySelection(rowKey, event);
},
@@ -86,7 +71,7 @@ export const useWorkspaceSelection = ({
);
const inspectDocument = useCallback(
(documentId?: string | number | null) => {
(documentId?: string | number) => {
if (!documentId) return;
onDocumentActivate(documentId);
},
@@ -94,7 +79,7 @@ export const useWorkspaceSelection = ({
);
const inspectFolder = useCallback(
(folderId?: string | number | null) => {
(folderId?: string | number) => {
if (!folderId) return;
onInspectFolder(folderId);
},
-23
View File
@@ -4,29 +4,6 @@ export const DEFAULT_SORT_DIRECTION = 'asc';
export const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
export const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
const ROW_KEY_SEPARATOR = ':';
const DOCUMENT_ROW_PREFIX = 'document';
const FOLDER_ROW_PREFIX = 'folder';
const makeRowKey = (type, id) =>
id ? `${type}${ROW_KEY_SEPARATOR}${id}` : `${type}${ROW_KEY_SEPARATOR}`;
const getRowType = (key: string | null) => (key ?? '').split(ROW_KEY_SEPARATOR, 1)[0] ?? '';
export const getRowId = (key: string) => {
const parts = key.split(ROW_KEY_SEPARATOR);
return parts.slice(1).join(ROW_KEY_SEPARATOR);
};
export const isDocumentRowKey = (key) => getRowType(key) === DOCUMENT_ROW_PREFIX;
export const isFolderRowKey = (key) => getRowType(key) === FOLDER_ROW_PREFIX;
export const resolveDocumentRowKey = (documentId) =>
documentId ? makeRowKey(DOCUMENT_ROW_PREFIX, documentId) : null;
export const resolveFolderRowKey = (folderId) =>
folderId ? makeRowKey(FOLDER_ROW_PREFIX, folderId) : null;
export const hasFiles = (event) =>
Array.from(event.dataTransfer?.types || []).includes('Files');