refactor: Standardize document and folder identification with a new entryKey module.
This commit is contained in:
@@ -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);
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user