refactor: introduce dedicated identifier types for improved clarity and type safety

This commit is contained in:
2025-11-24 23:47:36 +01:00
parent 22f0c040a2
commit 70e7bda87e
79 changed files with 1567 additions and 1572 deletions
@@ -8,7 +8,7 @@ type ApiClient = {
};
interface CorrespondentEntry {
id?: string | number;
id?: string;
name?: string;
[key: string]: unknown;
}
@@ -17,7 +17,7 @@ interface UseCorrespondentsOptions {
apiClient: ApiClient;
notifyApiError: (error: unknown, fallback: string) => void;
setStatusMessage: (message: string, variant?: string) => void;
tenantIdRef: MutableRefObject<string | number | null>;
tenantIdRef: MutableRefObject<string | null>;
mapDocumentCaches?: (mapper: (doc: any) => any) => void;
}
@@ -47,7 +47,7 @@ const useCorrespondents = ({
}, [apiClient, notifyApiError, tenantIdRef]);
const handleCorrespondentUpdate = useCallback(
async (correspondentId: string | number, changes: { name?: string }) => {
async (correspondentId: string, changes: { name?: string }) => {
if (correspondentId == null) {
throw new Error('Missing correspondent identifier.');
}
@@ -100,7 +100,7 @@ const useCorrespondents = ({
);
const handleCorrespondentDelete = useCallback(
async (correspondentId: string | number) => {
async (correspondentId: string) => {
if (correspondentId == null) {
throw new Error('Missing correspondent identifier.');
}
@@ -1,4 +1,5 @@
import { useCallback, useMemo } from 'react';
import type { Identifier } from '../../types/identifiers';
type ApiClient = {
@@ -7,13 +8,11 @@ type ApiClient = {
};
interface CorrespondentOption {
id?: string | number;
id?: string;
name?: string;
[key: string]: unknown;
}
type Identifier = string | number;
interface UseDocumentCorrespondentActionsArgs {
apiClient: ApiClient;
correspondents: CorrespondentOption[];
@@ -135,7 +134,7 @@ const useDocumentCorrespondentActions = ({
};
const handleCorrespondentAdd = useCallback(
async ({ document, name, input = null, option = null }: { document?: { id?: string | number }; name?: string; input?: HTMLInputElement | null; option?: CorrespondentOption | string | null }) => {
async ({ document, name, input = null, option = null }: { document?: { id?: string }; name?: string; input?: HTMLInputElement | null; option?: CorrespondentOption | string | null }) => {
if (!document?.id) {
throw new Error('Missing document for correspondent assignment.');
}
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
import type { DragEvent } from 'react';
import { createDocumentEntryKey, createFolderEntryKey } from '../../app/entryKey';
import type { FolderId, Identifier } from '../../types/identifiers';
type Identifier = string | number;
type FolderIdentifier = string | 'root';
type FolderIdentifier = FolderId | 'root';
type FolderInput = FolderIdentifier | number;
interface DocumentLike {
@@ -14,7 +14,7 @@ interface DocumentLike {
type ApplySelectionFn = (
keys: string[],
options?: { anchor?: string | null; interactedKeys?: string[] },
options?: { anchor: string | null; interactedKeys?: string[] },
) => void;
type HandleEntrySelectionFn = (
@@ -13,9 +13,9 @@ import {
trashDocument,
updateDocument,
} from '../../lib/apiClient';
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
type DocumentId = string | number;
type FolderId = DocumentId | 'root';
type FolderId = FolderIdentifier | 'root';
type NullableFolderId = FolderId | null;
type StatusLevel = 'success' | 'error' | 'info' | string;
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
type Identifier = string | number;
import type { Identifier } from '../../types/identifiers';
interface TagRecord {
id?: Identifier;
@@ -79,59 +79,59 @@ const useDocumentTagging = ({
}
try {
if (action === 'add') {
const createdIds: Identifier[] = [];
const createdTags: TagRecord[] = [];
for (const label of normalized) {
let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
if (!tag) {
const payload = tagManager.buildPayload({ label });
const response = await apiClient.post<{ id?: Identifier; label: string }>('/tags', payload);
tag = 'data' in response ? response.data : response;
await refreshTags();
if (action === 'add') {
const createdIds: Identifier[] = [];
const createdTags: TagRecord[] = [];
for (const label of normalized) {
let tag = tags.find((item) => item.label.toLowerCase() === label.toLowerCase()) || null;
if (!tag) {
const payload = tagManager.buildPayload({ label });
const response = await apiClient.post<{ id?: Identifier; label: string }>('/tags', payload);
tag = 'data' in response ? response.data : response;
await refreshTags();
}
createdIds.push(tag.id);
createdTags.push(tag);
}
createdIds.push(tag.id);
createdTags.push(tag);
}
tagIds = Array.from(new Set(createdIds));
tagIds = Array.from(new Set(createdIds));
if (updateDocumentCaches) {
const tagById = new Map<Identifier, TagRecord>();
tags.forEach((tag) => {
if (tag?.id != null) {
tagById.set(tag.id, tag);
}
});
createdTags.forEach((tag) => {
if (tag?.id != null) {
tagById.set(tag.id, tag);
}
});
targetDocumentIds.forEach((docId) => {
tagIds.forEach((tagId) => {
const cachedTag = tagById.get(tagId);
if (!cachedTag) {
return;
if (updateDocumentCaches) {
const tagById = new Map<Identifier, TagRecord>();
tags.forEach((tag) => {
if (tag?.id != null) {
tagById.set(tag.id, tag);
}
updateDocumentCaches(docId, (doc) => {
if (!doc) {
return doc;
});
createdTags.forEach((tag) => {
if (tag?.id != null) {
tagById.set(tag.id, tag);
}
});
targetDocumentIds.forEach((docId) => {
tagIds.forEach((tagId) => {
const cachedTag = tagById.get(tagId);
if (!cachedTag) {
return;
}
const currentTags = Array.isArray((doc as any).tags) ? (doc as any).tags : [];
if (currentTags.some((entry: any) => entry?.id === tagId)) {
return doc;
}
return {
...(doc as any),
tags: [...currentTags, { ...cachedTag }],
};
updateDocumentCaches(docId, (doc) => {
if (!doc) {
return doc;
}
const currentTags = Array.isArray((doc as any).tags) ? (doc as any).tags : [];
if (currentTags.some((entry: any) => entry?.id === tagId)) {
return doc;
}
return {
...(doc as any),
tags: [...currentTags, { ...cachedTag }],
};
});
});
});
});
}
}
}
tagIds = Array.from(new Set(tagIds));
tagIds = Array.from(new Set(tagIds));
if (!tagIds.length) {
return { ok: false, reason: 'no-tags' };
@@ -205,8 +205,7 @@ const useDocumentTagging = ({
if (result?.ok) {
const { tagCount, docsCount } = result;
setStatusMessage(
`Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${
docsCount === 1 ? '' : 's'
`Assigned ${tagCount} tag${tagCount === 1 ? '' : 's'} to ${docsCount} document${docsCount === 1 ? '' : 's'
}.`,
'success',
);
@@ -3,8 +3,8 @@ import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import useFileDrop from './useFileDrop';
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/workspaceUtils';
import { fetchDocument } from '../../lib/apiClient';
import type { Identifier } from '../../types/identifiers';
type Identifier = string | number;
type FolderId = Identifier | 'root' | null;
type FileEntry = {
+1 -2
View File
@@ -7,8 +7,7 @@ import {
useState,
} from 'react';
import DocumentsManager from '../../documents/DocumentsManager';
type DocumentId = string | number;
import type { DocumentId } from '../../types/identifiers';
interface DocumentLike {
id?: DocumentId;
@@ -52,6 +52,7 @@ import useWorkspaceTaxonomies from './useWorkspaceTaxonomies';
import useWorkspaceBreadcrumbs from './useWorkspaceBreadcrumbs';
import useWorkspaceDeskProps from './useWorkspaceDeskProps';
import useWorkspaceSelectionSync from './useWorkspaceSelectionSync';
import type { DocumentId, FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
const EntryType = Object.freeze({
document: 'document',
@@ -60,9 +61,7 @@ const EntryType = Object.freeze({
const noop = () => { };
type Identifier = string | number;
type DocumentId = Identifier;
type FolderId = Identifier | 'root';
type FolderId = FolderIdentifier | 'root';
interface DocumentLike {
id?: DocumentId | null;
+1 -1
View File
@@ -1,6 +1,6 @@
import { MutableRefObject, useEffect } from 'react';
type FolderId = string | number | 'root' | null;
type FolderId = string | 'root' | null;
interface DropOverlayState {
active: boolean;
+10 -8
View File
@@ -8,9 +8,9 @@ import {
createDocumentEntryKey,
createFolderEntryKey,
} from '../../app/entryKey';
import type { FolderId as FolderIdentifier, Identifier } from '../../types/identifiers';
type Identifier = string | number;
type FolderId = Identifier | 'root';
type FolderId = FolderIdentifier | 'root';
interface DocumentLike {
id?: Identifier | null;
@@ -126,8 +126,8 @@ const useFolderTree = ({
.filter(Boolean),
);
let nextDocKeys = [];
let mergedSelection = [];
let nextDocKeys: string[] = [];
let mergedSelection: string[] = [];
setSelectedEntries((previous) => {
const previousFolderKeys = previous
@@ -140,9 +140,11 @@ const useFolderTree = ({
});
const nextFocus = (() => {
const currentFocusedKey = createDocumentEntryKey(focusedDocumentId);
if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) {
return focusedDocumentId;
if (focusedDocumentId) {
const currentFocusedKey = createDocumentEntryKey(focusedDocumentId);
if (currentFocusedKey && availableDocKeySet.has(currentFocusedKey)) {
return focusedDocumentId;
}
}
if (nextDocKeys.length) {
const lastDocKey = nextDocKeys[nextDocKeys.length - 1];
@@ -172,7 +174,7 @@ const useFolderTree = ({
if (!targetId || targetId === 'root') {
setFolderNodes((prev: Map<FolderId, FolderTreeNode>) => {
const root = prev.get('root');
if (root?.expanded) return prev;
if (!root || root.expanded) return prev;
const next = new Map<FolderId, FolderTreeNode>(prev);
next.set('root', { ...root, expanded: true });
return next;
@@ -7,8 +7,8 @@ import {
moveFolder as moveFolderRequest,
renameFolder as renameFolderRequest,
} from '../../lib/apiClient';
import type { FolderId } from '../../types/identifiers';
type FolderId = string | number;
type FolderKey = FolderId | 'root';
interface FolderNode {
+6 -5
View File
@@ -1,4 +1,5 @@
import { MutableRefObject, useCallback, useState } from 'react';
import type { TagId, TenantId } from '../../types/identifiers';
type ApiClient = {
get: (path: string) => Promise<{ data: unknown }>
@@ -12,7 +13,7 @@ interface TagManagerInterface {
}
interface TagEntry {
id?: string | number;
id?: TagId;
label?: string;
color?: string | null;
[key: string]: unknown;
@@ -23,8 +24,8 @@ interface UseTagsOptions {
notifyApiError: (error: unknown, fallback: string) => void;
setStatusMessage: (message: string, variant?: string) => void;
tagManager: TagManagerInterface;
tenantIdRef: MutableRefObject<string | number | null>;
setActiveTagFilters: (updater: (prev: Array<string | number>) => Array<string | number>) => void;
tenantIdRef: MutableRefObject<TenantId | null>;
setActiveTagFilters: (updater: (prev: Array<TagId>) => Array<TagId>) => void;
mapDocumentCaches?: (mapper: (doc: any) => any) => void;
}
@@ -56,7 +57,7 @@ const useTags = ({
}, [apiClient, notifyApiError, tenantIdRef]);
const handleTagUpdate = useCallback(
async (tagId: string | number, changes: { label?: string; color?: string | null }) => {
async (tagId: TagId, changes: { label?: string; color?: string | null }) => {
if (tagId == null) {
throw new Error('Missing tag identifier.');
}
@@ -104,7 +105,7 @@ const useTags = ({
);
const handleTagDelete = useCallback(
async (tagId: string | number) => {
async (tagId: TagId) => {
if (tagId == null) {
throw new Error('Missing tag identifier.');
}
@@ -1,5 +1,6 @@
import { MutableRefObject, useCallback } from 'react';
import type { NavigateFunction } from 'react-router-dom';
import type { FolderId, TenantId } from '../../types/identifiers';
interface ApiClient {
get: (path: string) => Promise<{ data: unknown }>;
@@ -8,24 +9,24 @@ interface ApiClient {
}
interface TenantOption {
id?: string | number;
id?: TenantId;
name?: string;
}
interface UseTenantManagerOptions {
apiClient: ApiClient;
appDispatch: (action: any) => void;
currentTenantId: string | number | null;
currentTenantId: TenantId | null;
resetWorkspaceState: () => void;
setStatusMessage: (message: string, variant?: string) => void;
notifyApiError: (error: unknown, message: string) => void;
refreshTags: () => Promise<void>;
refreshCorrespondents: () => Promise<void>;
loadFolder: (folderId: string, options?: { preserveSearch?: boolean }) => Promise<void>;
loadFolder: (folderId: FolderId, options?: { preserveSearch?: boolean }) => Promise<void>;
handleDocumentsViewModeChange: (mode: string) => void;
navigate: NavigateFunction;
tokenRef?: MutableRefObject<string | null>;
tenantIdRef?: MutableRefObject<string | number | null>;
tenantIdRef?: MutableRefObject<TenantId | null>;
}
const useTenantManager = ({
@@ -1,8 +1,8 @@
import React, { useEffect, useMemo } from 'react';
import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
import type { FolderId as FolderIdentifier } from '../../types/identifiers';
type Identifier = string | number;
type FolderId = Identifier | 'root';
type FolderId = FolderIdentifier | 'root';
interface UseWorkspaceBreadcrumbsArgs {
selectedFolder: FolderId | null;
@@ -1,11 +1,10 @@
import { useCallback, useMemo } from 'react';
import type { MutableRefObject } from 'react';
import { createDocumentEntryKey } from '../../app/entryKey';
type Identifier = string | number;
import type { Identifier } from '../../types/identifiers';
interface ApplySelectionFn {
(keys: string[], options?: { anchor?: string | null; interactedKeys?: string[] }): unknown;
(keys: string[], options?: { anchor: string | null; interactedKeys?: string[] }): unknown;
}
interface UseWorkspaceDeskPropsArgs {
@@ -17,8 +16,8 @@ interface UseWorkspaceDeskPropsArgs {
applySelection: ApplySelectionFn;
showingSearchResults: boolean;
searchQuery: string;
activeTagFilters: Array<string | number>;
activeCorrespondentFilters: Array<string | number>;
activeTagFilters: Array<string>;
activeCorrespondentFilters: Array<string>;
selectedFolder: Identifier | 'root' | null;
promoteSelectionOrder: () => void;
handleDocumentTagDrop: (docId: Identifier, tagId: Identifier) => Promise<void> | void;
@@ -1,14 +1,13 @@
import { useEffect } from 'react';
import type { MutableRefObject } from 'react';
type Identifier = string | number;
import type { Identifier } from '../../types/identifiers';
interface UseWorkspaceSelectionSyncArgs {
showingSearchResults: boolean;
searchQuery: string;
setSelectedEntries: (entries: Array<string | number>) => void;
setSelectionOrder: (order: Array<string | number>) => void;
selectionOrderRef: MutableRefObject<Array<string | number>>;
setSelectedEntries: (entries: Array<string>) => void;
setSelectionOrder: (order: Array<string>) => void;
selectionOrderRef: MutableRefObject<Array<string>>;
selectionAnchorRef: MutableRefObject<Identifier | string | null>;
setFocusedDocumentId: (id: Identifier | null) => void;
selectedDocumentIds: Identifier[];
@@ -5,8 +5,7 @@ import TagManager from '../../tag_manager';
import useCorrespondents from './useCorrespondents';
import useDocumentCorrespondentActions from './useDocumentCorrespondentActions';
import useTags from './useTags';
type Identifier = string | number;
import type { Identifier } from '../../types/identifiers';
interface UseWorkspaceTaxonomiesArgs {
apiClient: any;
+3 -4
View File
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { resolveAssetUrl } from '../asset_manager';
type Identifier = string | number;
import type { Identifier } from '../types/identifiers';
type DocumentLike = {
id?: Identifier;
@@ -27,7 +26,7 @@ type AssetLike = {
type EnsureAssetUrl = (
documentId: Identifier,
asset: AssetLike,
options?: { force?: boolean; [key: string]: unknown },
options?: { force?: boolean;[key: string]: unknown },
) => Promise<unknown>;
type GetAsset = (document: DocumentLike, assetType: string) => AssetLike | null;
@@ -92,7 +91,7 @@ export const useAssetNavigator = ({
let cancelled = false;
setIsLoading(true);
ensureAssetUrl(documentId, asset, { force: true })
.catch(() => {})
.catch(() => { })
.finally(() => {
if (!cancelled) {
setIsLoading(false);