This commit is contained in:
2025-11-14 02:35:17 +01:00
parent 6f4ff68062
commit a3c5494bf7
53 changed files with 269 additions and 340 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ const DocumentsRouteContent: React.FC = () => {
} = usePanelManager();
const safeSidebarProps = useMemo<Record<string, unknown>>(
() => (sidebarProps && typeof sidebarProps === 'object' ? sidebarProps : {}),
() => (sidebarProps && Object(sidebarProps) === sidebarProps ? sidebarProps : {}),
[sidebarProps],
);
+1 -1
View File
@@ -112,7 +112,7 @@ const UploadQueueOverlay = ({ queue = [], onClearQueue }: UploadQueueOverlayProp
<button
type="button"
className="icon-button ghost"
onClick={() => setCollapsed((value) => !value)}
onClick={() => setCollapsed(!collapsed)}
aria-label={collapsed ? 'Expand upload queue' : 'Collapse upload queue'}
>
{collapsed ? <BottombarExpandIcon size={16} /> : <BottombarCollapseIcon size={16} />}
+7 -7
View File
@@ -16,15 +16,15 @@ export const resolveApiPath = (path = '') => path;
const makeRowKey = (type, id) =>
id ? `${type}${ROW_KEY_SEPARATOR}${id}` : `${type}${ROW_KEY_SEPARATOR}`;
const getRowType = (key) => (
typeof key?.split === 'function' ? key.split(ROW_KEY_SEPARATOR, 1)[0] : ''
);
const normalizeRowKey = (key: string | number | null | undefined) => String(key ?? '');
const getRowType = (key) => normalizeRowKey(key).split(ROW_KEY_SEPARATOR, 1)[0] ?? '';
export const getRowId = (key) => {
if (typeof key?.indexOf !== 'function' || typeof key?.slice !== 'function') return '';
const separatorIndex = key.indexOf(ROW_KEY_SEPARATOR);
if (separatorIndex === -1) return key;
return key.slice(separatorIndex + 1);
const normalized = normalizeRowKey(key);
const separatorIndex = normalized.indexOf(ROW_KEY_SEPARATOR);
if (separatorIndex === -1) return normalized;
return normalized.slice(separatorIndex + 1);
};
export const isDocumentRowKey = (key) => getRowType(key) === DOCUMENT_ROW_PREFIX;
+1 -1
View File
@@ -43,7 +43,7 @@ type AppAction =
| { type: 'RESET_ERROR' }
| { type: 'SET_TENANTS'; tenants: Tenant[] };
const storage = typeof window !== 'undefined' ? window.sessionStorage : null;
const storage = window.sessionStorage;
const STORED_TOKEN = storage?.getItem('papercrate_token') ?? '';
let STORED_TENANT: Tenant = null;
+1 -1
View File
@@ -149,7 +149,7 @@ const useDocumentPreview = ({
const existing = previewEntries.get(documentId) || null;
const now = Date.now();
const expiresAt = typeof existing?.expiresAt === 'number' ? existing.expiresAt : null;
const expiresAt = Number.isFinite(existing?.expiresAt) ? Number(existing?.expiresAt) : null;
if (!force && existing && (!expiresAt || expiresAt > now)) {
return existing;
}
+3 -3
View File
@@ -137,7 +137,7 @@ export const useManagementModals = ({
const handleCorrespondentCreateSafe = useCallback<CorrespondentsPanelProps['onCreate']>(
async (payload) => {
if (typeof onCorrespondentCreate !== 'function') {
if (!onCorrespondentCreate) {
return undefined;
}
return onCorrespondentCreate(payload) ?? undefined;
@@ -147,7 +147,7 @@ export const useManagementModals = ({
const handleCorrespondentUpdateSafe = useCallback<CorrespondentsPanelProps['onUpdate']>(
async (id, payload) => {
if (typeof onCorrespondentUpdate !== 'function') {
if (!onCorrespondentUpdate) {
return;
}
await onCorrespondentUpdate(id, payload);
@@ -157,7 +157,7 @@ export const useManagementModals = ({
const handleCorrespondentDeleteSafe = useCallback<CorrespondentsPanelProps['onDelete']>(
async (id) => {
if (typeof onCorrespondentDelete !== 'function') {
if (!onCorrespondentDelete) {
return;
}
await onCorrespondentDelete(id);
+3 -3
View File
@@ -76,9 +76,9 @@ export const useWorkspaceSelection = ({
const selectEntry = useCallback(
(entry: SelectionEntry | string | null, event?: unknown) => {
const rowKey = typeof entry === 'string'
? entry
: entry?.rowKey ?? null;
const rowKey = entry && Object(entry) === entry
? (entry as SelectionEntry).rowKey ?? null
: (entry as string | null);
if (!rowKey) return;
handleEntrySelection(rowKey, event);
},
+17 -16
View File
@@ -81,15 +81,16 @@ const mergeAssetObjects = (
const merged = new Map<number, AssetObject>();
normalizeAssetObjects(existingObjects).forEach((entry) => {
if (typeof entry.ordinal === 'number') {
merged.set(entry.ordinal, { ...entry });
if (Number.isFinite(entry.ordinal)) {
merged.set(entry.ordinal as number, { ...entry });
}
});
normalizeAssetObjects(incomingObjects).forEach((entry) => {
if (typeof entry.ordinal === 'number') {
const current = merged.get(entry.ordinal) || {};
merged.set(entry.ordinal, { ...current, ...entry });
if (Number.isFinite(entry.ordinal)) {
const ordinal = entry.ordinal as number;
const current = merged.get(ordinal) || {};
merged.set(ordinal, { ...current, ...entry });
}
});
@@ -198,23 +199,23 @@ export const resolveDocumentAssetUrl = (
if (!doc || !type) {
return null;
}
const asset = typeof getAsset === 'function' ? getAsset(doc, type) : null;
const asset = getAsset ? getAsset(doc, type) : null;
if (!asset) {
return null;
}
const view = createAssetView(asset);
const object = view.getObject(objectOrdinal);
const url = object?.url || (objectOrdinal === 1 ? view.getPrimaryUrl() : null);
const expiresAt = typeof object?.expires_at === 'number'
? object.expires_at
: objectOrdinal === 1 && typeof asset.expiresAt === 'number'
? asset.expiresAt
const expiresAt = Number.isFinite(object?.expires_at)
? Number(object?.expires_at)
: objectOrdinal === 1 && Number.isFinite(asset.expiresAt)
? Number(asset.expiresAt)
: null;
const now = Date.now();
if (url && (!expiresAt || expiresAt > now)) {
return url;
}
if (doc.id && asset.id && typeof ensureAssetUrl === 'function') {
if (doc.id && asset.id && ensureAssetUrl) {
const force = Boolean(url && expiresAt && expiresAt <= now);
const options: { force: boolean; start?: number | null; limit?: number | null; [key: string]: unknown } = {
force,
@@ -397,7 +398,7 @@ class AssetManager {
const baseAsset = this.assetCache.get(asset.id) || asset;
const view = createAssetView(baseAsset);
const assetExpiresAt = typeof baseAsset.expiresAt === 'number' ? baseAsset.expiresAt : null;
const assetExpiresAt = Number.isFinite(baseAsset.expiresAt) ? Number(baseAsset.expiresAt) : null;
const now = Date.now();
const isOrdinalSatisfied = (ordinal) => {
@@ -408,8 +409,8 @@ class AssetManager {
if (!object.url) {
return false;
}
if (typeof object.expires_at === 'number') {
return object.expires_at > now;
if (Number.isFinite(object.expires_at)) {
return Number(object.expires_at) > now;
}
if (ordinal === 1 && baseAsset.url && (!assetExpiresAt || assetExpiresAt > now)) {
return true;
@@ -460,8 +461,8 @@ class AssetManager {
const combined = { ...cachedEntry, ...asset, ...data, objects: mergedObjects };
const view = createAssetView(combined);
const primaryObject = view.getPrimaryObject();
const expiresAt = typeof primaryObject?.expires_at === 'number'
? primaryObject.expires_at
const expiresAt = Number.isFinite(primaryObject?.expires_at)
? Number(primaryObject?.expires_at)
: Date.now() + this.assetPresignTtlMs;
const cardinality = (() => {
const reported = Number(data.cardinality ?? asset.cardinality ?? cachedEntry?.cardinality);
@@ -125,7 +125,7 @@ function CorrespondentsPanel({
if (!usage) {
return '0';
}
const total = typeof usage.total === 'number' ? usage.total : 0;
const total = Number.isFinite(usage.total) ? Number(usage.total) : 0;
return total.toString();
}, []);
+1 -8
View File
@@ -419,14 +419,7 @@ const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
commitSize();
if (typeof window.ResizeObserver === 'undefined') {
window.addEventListener('resize', commitSize);
return () => {
window.removeEventListener('resize', commitSize);
};
}
const observer = new window.ResizeObserver(commitSize);
const observer = new ResizeObserver(commitSize);
observer.observe(container);
return () => observer.disconnect();
}, [engine]);
+1 -7
View File
@@ -10,13 +10,7 @@ const openDatabase = (): Promise<IDBDatabase> => {
}
currentDbPromise.value = new Promise((resolve, reject) => {
const dbApi = typeof window !== 'undefined' ? window.indexedDB : null;
if (!dbApi) {
reject(new Error('IndexedDB not available'));
return;
}
const request = dbApi.open(DB_NAME, DB_VERSION);
const request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
const db = request.result;
@@ -141,7 +141,7 @@ export const useDeskTagInteractions = ({
const { x: pointerX, y: pointerY } = getPointerPosition(event, { fallbackToPage: false });
const { clone, offsetX, offsetY } = createDragPreview(event.currentTarget, pointerX, pointerY) || {};
if (clone && typeof event.dataTransfer.setDragImage === 'function') {
if (clone) {
event.dataTransfer.setDragImage(clone, offsetX || 0, offsetY || 0);
}
+1 -5
View File
@@ -165,13 +165,9 @@ const getEventTargetElement = (event?: PointerEventLike | null): Element | null
if (!event) {
return null;
}
const ElementCtor = typeof window !== 'undefined' ? window.Element : null;
if (!ElementCtor) {
return null;
}
const nativeEvent = 'nativeEvent' in event ? (event as ReactPointerEvent).nativeEvent : null;
const candidate = (event.target as Element | null) || (nativeEvent ? (nativeEvent.target as Element | null) : null);
return candidate instanceof ElementCtor ? candidate : null;
return candidate instanceof Element ? candidate : null;
};
const useDocumentDrag = (options: UseDocumentDragOptions) => {
+15 -20
View File
@@ -618,7 +618,7 @@ export class WorkspaceEngine {
updateConfig({ allowLayoutPersistence, tenantId, viewId }: WorkspaceEngineOptions): void {
const allowChanged =
typeof allowLayoutPersistence === 'boolean'
allowLayoutPersistence !== undefined
&& allowLayoutPersistence !== this.allowLayoutPersistence;
const tenantChanged = tenantId !== undefined && tenantId !== this.tenantId;
const viewChanged = viewId !== undefined && viewId !== this.viewId;
@@ -682,15 +682,11 @@ export class WorkspaceEngine {
}
setEnsureDocumentSize(fn: EnsureDocumentSize): void {
if (typeof fn === 'function') {
this.ensureDocumentSize = fn;
}
this.ensureDocumentSize = fn;
}
setResolveBaseMetrics(fn: ResolveBaseMetrics): void {
if (typeof fn === 'function') {
this.resolveBaseMetrics = fn;
}
this.resolveBaseMetrics = fn;
}
setItemRefs(ref: ItemRefs | null | undefined): void {
@@ -782,18 +778,14 @@ export class WorkspaceEngine {
updateLayoutEntry(
docId: string | number | null,
updater:
| LayoutEntry
| null
| undefined
| ((previous: LayoutEntry | null) => LayoutEntry | null | undefined),
updater: (previous: LayoutEntry | null) => LayoutEntry | null | undefined,
): void {
if (docId == null) {
return;
}
const key = String(docId);
const previous = this.layout.get(key) || null;
const next = typeof updater === 'function' ? updater(previous) : updater;
const next = updater(previous);
if (!next) {
this.layout.delete(key);
} else {
@@ -939,7 +931,9 @@ export class WorkspaceEngine {
let angularVelocity = simulationState.angularVelocity + torqueAcceleration * dt;
angularVelocity = clamp(angularVelocity, -MAX_ANGULAR_VELOCITY, MAX_ANGULAR_VELOCITY);
const dampingConstant = typeof dampingOverride === 'number' ? dampingOverride : ANGULAR_DAMPING;
const dampingConstant = Number.isFinite(dampingOverride)
? Number(dampingOverride)
: ANGULAR_DAMPING;
const dampingFactor = Math.exp(-dampingConstant * dt);
angularVelocity *= dampingFactor;
@@ -1154,8 +1148,12 @@ export class WorkspaceEngine {
if (existing) {
const defaultCenterX = (minCenterX + maxCenterX) / 2;
const defaultCenterY = (minCenterY + maxCenterY) / 2;
const prevCenterX = typeof existing.centerX === 'number' ? existing.centerX : defaultCenterX;
const prevCenterY = typeof existing.centerY === 'number' ? existing.centerY : defaultCenterY;
const prevCenterX = Number.isFinite(existing.centerX)
? Number(existing.centerX)
: defaultCenterX;
const prevCenterY = Number.isFinite(existing.centerY)
? Number(existing.centerY)
: defaultCenterY;
const centerX = clamp(prevCenterX, minCenterX, maxCenterX);
const centerY = clamp(prevCenterY, minCenterY, maxCenterY);
const rotation = existing.rotation ?? 0;
@@ -1201,9 +1199,6 @@ export class WorkspaceEngine {
recalcVisibleDocIds(): void {
const ensureSize = this.ensureDocumentSize;
if (typeof ensureSize !== 'function') {
return;
}
const layoutMap = this.layout;
const canvasWidth = this.canvasSize.width || DESK_DEFAULT_CANVAS_WIDTH;
@@ -1418,7 +1413,7 @@ export const useWorkspaceSnapshot = (
useSyncExternalStoreHook: UseSyncExternalStoreHook,
): WorkspaceSnapshot => {
const useSyncExternalStore = useSyncExternalStoreHook;
if (typeof useSyncExternalStore !== 'function') {
if (!useSyncExternalStore) {
throw new Error('useWorkspaceSnapshot requires useSyncExternalStore hook');
}
return useSyncExternalStore(
+2 -5
View File
@@ -28,7 +28,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
display = null,
onClose = noop,
}) => {
const portalTarget = typeof document !== 'undefined' ? document.body : null;
const portalTarget = document.body;
const [isNativeScale, setIsNativeScale] = useState(false);
const [naturalSize, setNaturalSize] = useState<NaturalSize>({ width: null, height: null });
const [renderBackdrop, setRenderBackdrop] = useState(false);
@@ -135,9 +135,6 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
}, [open, isNativeScale, naturalSize.width, naturalSize.height]);
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
if (!open) {
previouslyFocusedRef.current?.focus?.();
previouslyFocusedRef.current = null;
@@ -247,7 +244,7 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
toggleZoomAtPoint(event.clientX ?? 0, event.clientY ?? 0);
};
if (!renderBackdrop || !activeDisplay?.url || !portalTarget) {
if (!renderBackdrop || !activeDisplay?.url) {
return null;
}
+8 -7
View File
@@ -88,13 +88,14 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
const contentConfig = contentConfigProp || null;
const contentEnabled = Boolean(contentConfig && (contentConfig.enabled ?? true));
const loadContent = contentConfig?.loadContent ?? null;
const showContentTab = Boolean(contentConfig && (contentConfig.forceDisplay ?? contentEnabled));
const [contentState, setContentState] = useState<ContentState | null>(() => {
if (!contentConfig) {
return null;
}
if (!contentEnabled || typeof contentConfig.loadContent !== 'function') {
if (!contentEnabled || !loadContent) {
return { status: contentEnabled ? 'idle' : 'unavailable', data: null, error: null };
}
return { status: 'idle', data: null, error: null };
@@ -106,7 +107,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
return undefined;
}
if (!contentEnabled || typeof contentConfig.loadContent !== 'function') {
if (!contentEnabled || !loadContent) {
setContentState({ status: contentEnabled ? 'idle' : 'unavailable', data: null, error: null });
return undefined;
}
@@ -116,7 +117,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
setContentState({ status: 'loading', data: null, error: null });
Promise.resolve(contentConfig.loadContent({ signal: controller.signal }))
Promise.resolve(loadContent({ signal: controller.signal }))
.then((result) => {
if (cancelled) {
return;
@@ -143,7 +144,7 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
controller.abort();
contentConfig.onCancel?.();
};
}, [contentConfig, contentEnabled, showContentTab, document?.id, resetKey]);
}, [contentConfig, contentEnabled, showContentTab, document?.id, resetKey, loadContent]);
const renderSummarySection = useCallback(() => (
<DocumentSummarySection
@@ -345,10 +346,10 @@ const DocumentInfoPanel: React.FC<DocumentInfoPanelProps> = ({
if (!tab) {
return null;
}
if (typeof tab.render === 'function') {
return tab.render(context);
if (!tab.render) {
return null;
}
return null;
return tab.render(context);
};
const isControlled = controlledActiveTab !== undefined && controlledActiveTab !== null;
@@ -9,6 +9,7 @@ import {
toIssuedTimestamp,
} from '../utils/date';
import { describeDocumentSummary } from './documentSummary';
import { isPlainObject } from '../utils/typeGuards';
type Identifier = string | number;
@@ -110,43 +111,33 @@ interface QuickAddEntry {
original: QuickAddOption | string;
}
const resolveOptionName = (source: unknown): string => {
const resolveOptionName = (source?: QuickAddOption | string | null): string => {
if (!source) {
return '';
}
if (typeof source === 'string') {
return source.trim();
if (isPlainObject(source)) {
const raw = source.name ?? source.label ?? '';
return `${raw}`.trim();
}
if (typeof source === 'object') {
const candidate = source as { name?: string; label?: string; trim?: () => string };
if (typeof candidate.name === 'string' && candidate.name.trim()) {
return candidate.name.trim();
}
if (typeof candidate.label === 'string' && candidate.label.trim()) {
return candidate.label.trim();
}
if (typeof candidate.trim === 'function') {
const viaTrim = candidate.trim();
if (typeof viaTrim === 'string' && viaTrim.trim()) {
return viaTrim.trim();
}
}
}
return '';
return `${source}`.trim();
};
const normalizeQuickAddOption = (option: QuickAddOption | string | null | undefined): QuickAddEntry | null => {
if (option == null) {
return null;
}
const isObject = typeof option === 'object';
const labelSource = isObject ? option.label ?? option.name ?? '' : option;
const label = typeof labelSource === 'string' ? labelSource.trim() : '';
const label = (() => {
if (isPlainObject(option)) {
const sourceLabel = option.label ?? option.name ?? '';
return `${sourceLabel}`.trim();
}
return `${option}`.trim();
})();
if (!label) {
return null;
}
return {
id: isObject && option.id ? option.id : label,
id: isPlainObject(option) && option.id ? option.id : label,
label,
original: option,
};
@@ -170,10 +161,7 @@ export const TagSection: React.FC<TagSectionProps> = ({
const handleSelect = useCallback(
(option: { label?: string; name?: string } | string | null) => {
if (!onAdd) return;
const labelSource = option && typeof option === 'object'
? option.label ?? option.name ?? ''
: option;
const label = typeof labelSource === 'string' ? labelSource.trim() : '';
const label = resolveOptionName(option as QuickAddOption | string | null);
if (!label) {
return;
}
@@ -372,13 +360,13 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
if (!onAdd || !item) {
return;
}
const source = item.payload ?? item;
const source = (item.payload ?? item) as QuickAddOption | string | null;
const resolvedName = resolveOptionName(source);
if (!resolvedName) {
return;
}
const payload = (source && typeof source === 'object')
? { ...(source as Record<string, unknown>), name: resolvedName }
const payload = isPlainObject(source)
? { ...source, name: resolvedName }
: { id: null, name: resolvedName };
onAdd({ name: resolvedName, option: payload, input: null });
},
@@ -38,12 +38,12 @@ const useLazyVisibility = (
return undefined;
}
if (typeof window === 'undefined' || !('IntersectionObserver' in window)) {
if (!window.IntersectionObserver) {
setIsVisible(true);
return undefined;
}
const observer = new IntersectionObserver(
const observer = new window.IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
+1 -1
View File
@@ -175,7 +175,7 @@ const DocumentsGrid: React.FC<DocumentsGridProps> = ({
const classes = ['document-card', 'folder-card'];
if (isDraggingFolder) classes.push('is-dragging');
if (isSelectedFolder) classes.push('selected');
const canRenameFolder = folder.id !== 'root' && typeof onFolderRename === 'function';
const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
const isFolderEditing = editingFolderId === folder.id;
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
+1 -1
View File
@@ -186,7 +186,7 @@ const DocumentsList: React.FC<DocumentsListProps> = ({
const isDraggingFolder = draggedFolderId === folder.id;
const isSelectedFolder = selectedFolderIdsSet?.has(folder.id);
const rowKey = `folder:${folder.id}`;
const canRenameFolder = folder.id !== 'root' && typeof onFolderRename === 'function';
const canRenameFolder = Boolean(onFolderRename) && folder.id !== 'root';
const isFolderEditing = editingFolderId === folder.id;
const folderDraftValue = isFolderEditing ? folderDraft : folder.name;
const trimmedFolderDraft = isFolderEditing ? folderDraft.trim() : '';
@@ -64,12 +64,14 @@ const normalizeItems = (items?: SelectionAssignmentMenuItem[]): NormalizedSelect
: item.state === 'partial'
? 'partial'
: 'none';
const numericCount = Number.isFinite(item.count) ? Number(item.count) : null;
const numericTotal = Number.isFinite(item.total) ? Number(item.total) : null;
return {
id: item.id ?? trimmedLabel,
label: trimmedLabel,
state,
count: typeof item.count === 'number' ? item.count : null,
total: typeof item.total === 'number' ? item.total : null,
count: numericCount,
total: numericTotal,
payload: item.payload ?? item,
};
})
@@ -160,7 +162,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
const handleToggle = useCallback(
async (item: NormalizedSelectionAssignmentItem) => {
if (!item || typeof onToggle !== 'function') {
if (!item || !onToggle) {
return;
}
setPending(true);
@@ -179,7 +181,7 @@ const SelectionAssignmentMenu: React.FC<SelectionAssignmentMenuProps> = ({
const handleCreate = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault?.();
if (typeof onCreate !== 'function') {
if (!onCreate) {
return;
}
const value = query.trim();
@@ -89,6 +89,13 @@ const normalizeDocumentList = (selectedIds?: SelectedIdList): DocumentId[] =>
? selectedIds.filter((value): value is DocumentId => value !== null && value !== undefined)
: [];
const isRecord = (value: unknown): value is Record<string, unknown> => value != null && Object(value) === value;
const splitLabelSegments = (input: unknown): string[] => {
const text = `${input ?? ''}`.trim();
return text ? text.split('/') : [];
};
const buildFolderTreeOptions = (tree?: FolderTreeNode[] | null): SelectionAssignmentMenuItem[] => {
const entries: SelectionAssignmentMenuItem[] = [];
@@ -346,7 +353,9 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const documentCount = documentIdList.length;
const folderCount = folderIdList.length;
const totalCount = typeof selectionCount === 'number' ? selectionCount : documentCount + folderCount;
const totalCount = Number.isFinite(selectionCount)
? Number(selectionCount)
: documentCount + folderCount;
const selectedDocuments = useMemo<DocumentLike[]>(() => {
if (!documentIdList.length || !(documentLookupMap instanceof Map)) {
@@ -370,7 +379,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
return null;
}
const label = option?.label || option?.payload?.label || option?.name || String(id);
const segments = typeof label === 'string' ? label.split('/') : [label];
const segments = splitLabelSegments(label);
const depth = Math.max(segments.length - 1, 0);
return {
id,
@@ -399,7 +408,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const renderFolderLabel = useCallback((item: SelectionAssignmentMenuItem) => {
const payload = (item?.payload as { segments?: string[]; depth?: number }) || {};
const segments = payload.segments || (typeof item.label === 'string' ? item.label.split('/') : []);
const segments = payload.segments || splitLabelSegments(item.label);
const depth = payload.depth ?? Math.max(segments.length - 1, 0);
const clampedDepth = Math.min(depth, 6);
const indentWidth = clampedDepth > 0 ? clampedDepth * 0.9 : 0;
@@ -481,12 +490,12 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const handleMoveSelectionToFolder = useCallback(
async (option: unknown) => {
if (!documentIdList.length || typeof onMoveDocumentsToFolder !== 'function') {
if (!documentIdList.length || !onMoveDocumentsToFolder) {
return;
}
const candidate = option as { id?: DocumentId; value?: DocumentId } | DocumentId | null | undefined;
const value = typeof candidate === 'object'
? candidate?.id ?? candidate?.value ?? null
const value = isRecord(candidate)
? (candidate?.id ?? candidate?.value ?? null)
: candidate;
if (!value && value !== 0) {
return;
@@ -506,7 +515,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const showPrimaryButtons = Boolean(onBulkReanalyze || onDeleteSelection || onClearSelection);
const moveMenu = typeof onMoveDocumentsToFolder === 'function' ? (
const moveMenu = onMoveDocumentsToFolder ? (
<SelectionAssignmentMenu
label="Move"
triggerContent={(
@@ -534,7 +543,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
const primaryButtons = showPrimaryButtons ? (
<div className="panel-floating__buttons">
{typeof onBulkReanalyze === 'function' ? (
{onBulkReanalyze ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
@@ -546,7 +555,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
<AnalyzeIcon className="icon-inline" />
</button>
) : null}
{typeof onDeleteSelection === 'function' ? (
{onDeleteSelection ? (
<button
type="button"
className="icon-button danger panel-floating-actions__button"
@@ -557,7 +566,7 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
<TrashIcon className="icon-inline" />
</button>
) : null}
{typeof onClearSelection === 'function' ? (
{onClearSelection ? (
<button
type="button"
className="icon-button panel-floating-actions__button"
+4 -9
View File
@@ -61,16 +61,11 @@ export interface DocumentSummary {
const coercePageCount = (metadata: DocumentMetadata | null | undefined): number | null => {
const raw = metadata?.page_count;
if (typeof raw === 'number') {
return Number.isFinite(raw) && raw >= 0 ? raw : null;
if (raw == null || raw === '') {
return null;
}
if (raw != null && raw !== '') {
const parsed = Number.parseInt(String(raw), 10);
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
}
return null;
const parsed = Number.parseInt(String(raw), 10);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
};
const sanitizeArray = <T>(entries: (T | null | undefined)[] | null | undefined): T[] =>
@@ -192,7 +192,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
if (!doc || !doc.id) {
return;
}
const previewAsset = typeof getDocumentAsset === 'function' ? getDocumentAsset(doc, 'preview') : null;
const previewAsset = getDocumentAsset(doc, 'preview');
if (!previewAsset) {
return;
}
@@ -207,12 +207,8 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return;
}
if (event) {
if (typeof event.preventDefault === 'function') {
event.preventDefault();
}
if (typeof event.stopPropagation === 'function') {
event.stopPropagation();
}
event.preventDefault();
event.stopPropagation();
}
if (event?.altKey) {
handleDocumentPreviewZoom(doc);
@@ -489,16 +485,14 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
const handleDocumentClick = useCallback(
(doc, event) => {
if (!doc || suppressDocumentClickRef.current) {
if (!doc || suppressDocumentClickRef.current || !onEntryPointer) {
return;
}
if (typeof onEntryPointer === 'function') {
onEntryPointer(
{ type: EntryType.document, id: doc.id, key: `document:${doc.id}`, document: doc },
event,
);
}
onEntryPointer(
{ type: EntryType.document, id: doc.id, key: `document:${doc.id}`, document: doc },
event,
);
},
[onEntryPointer],
);
@@ -509,7 +503,7 @@ const DocumentsPanel: React.FC<DocumentsPanelProps> = ({
return;
}
if (typeof onEntryPointer === 'function') {
if (onEntryPointer) {
onEntryPointer(
{ type: EntryType.folder, id: folder.id, key: `folder:${folder.id}`, folder },
event,
@@ -49,7 +49,7 @@ export const createDocumentsTableHeaderActions = ({
? 'Sorting Z → A. Click to switch to ascending.'
: 'Sorting A → Z. Click to switch to descending.';
const includeDescendantsToggle = isFilterActive && typeof onToggleIncludeDescendants === 'function'
const includeDescendantsToggle = isFilterActive && onToggleIncludeDescendants
? (
<button
type="button"
@@ -66,11 +66,11 @@ export const createDocumentsTableHeaderActions = ({
)
: null;
const sortControls = typeof onSortFieldChange === 'function'
const sortControls = onSortFieldChange
? (
<div className="documents-actions__sort-group">
<SortFieldQuickMenu sortField={sortField} onChange={onSortFieldChange} />
{typeof onSortDirectionToggle === 'function' ? (
{onSortDirectionToggle ? (
<button
type="button"
className="icon-button documents-toolbar__toggle documents-sort__direction"
+11 -9
View File
@@ -85,12 +85,13 @@ type DragEventLike = DragEvent | DataTransfer | {
};
export const parseTagTransferPayload = (input: DataTransfer | DragEventLike | null): TagPayload | null => {
let dataTransfer: DataTransfer | null = null;
if (input) {
if (typeof DataTransfer !== 'undefined' && input instanceof DataTransfer) {
dataTransfer = input;
} else if (typeof input === 'object' && 'dataTransfer' in input && input.dataTransfer) {
dataTransfer = input.dataTransfer;
let dataTransfer: DataTransfer | null = null;
if (input instanceof DataTransfer) {
dataTransfer = input;
} else if (input && Object(input) === input && 'dataTransfer' in (input as Record<string, unknown>)) {
const candidate = (input as { dataTransfer?: DataTransfer | null }).dataTransfer;
if (candidate) {
dataTransfer = candidate;
}
}
const raw = readTagTransferData(dataTransfer || null);
@@ -112,10 +113,11 @@ export const isTagTransferEvent = (event?: DragEventLike | null): boolean => {
return false;
}
let types: DOMStringList | ReadonlyArray<string> | undefined;
if (typeof DataTransfer !== 'undefined' && event instanceof DataTransfer) {
if (event instanceof DataTransfer) {
types = event.types;
} else if ('dataTransfer' in event && event.dataTransfer) {
types = event.dataTransfer.types;
} else if (Object(event) === event && 'dataTransfer' in (event as Record<string, unknown>)) {
const payload = (event as { dataTransfer?: DataTransfer | null }).dataTransfer;
types = payload?.types;
}
if (!types) {
return false;
+1 -1
View File
@@ -9,7 +9,7 @@ export const isPrimaryPointerEvent = (event?: PointerEventLike | null): boolean
if (!event) {
return true;
}
if (typeof event.button === 'number' && event.button !== 0) {
if (event.button !== 0) {
return false;
}
const type = event?.type?.toLowerCase?.() ?? '';
@@ -53,7 +53,7 @@ const useCorrespondents = ({
}
const payload: Record<string, unknown> = {};
if (typeof changes?.name?.trim === 'function') {
if (changes?.name != null) {
const trimmed = changes.name.trim();
if (!trimmed) {
throw new Error('Correspondent name cannot be empty.');
@@ -120,9 +120,7 @@ const useCorrespondents = ({
await apiClient.delete(`/correspondents/${correspondentId}`);
await refreshCorrespondents();
if (typeof mapDocumentCaches === 'function') {
mapDocumentCaches(stripFromDoc);
}
mapDocumentCaches?.(stripFromDoc);
setStatusMessage('Correspondent deleted.', 'success');
return true;
@@ -1,4 +1,5 @@
import { useCallback, useMemo } from 'react';
import { isPlainObject, isStringValue } from '../../utils/typeGuards';
type ApiClient = {
post: (path: string, body?: unknown) => Promise<{ data: unknown }>;
@@ -99,10 +100,10 @@ const useDocumentCorrespondentActions = ({
if (!option) {
return null;
}
if (typeof option === 'object' && 'id' in option) {
if (isPlainObject(option) && 'id' in option) {
return option as CorrespondentOption;
}
if (typeof option === 'string') {
if (isStringValue(option)) {
const trimmed = option.trim();
if (trimmed) {
return { id: null, name: trimmed };
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef } from 'react';
import type { DragEvent } from 'react';
import { isPlainObject, isFunctionValue } from '../../utils/typeGuards';
type Identifier = string | number;
type FolderIdentifier = Identifier | 'root';
@@ -144,11 +145,17 @@ const useDocumentDragHandlers = ({
}
} else {
const payload = item.payload;
const folderId = (payload && typeof payload === 'object' && 'id' in payload)
? (payload as { id?: FolderIdentifier }).id
: (typeof (payload as { trim?: () => string })?.trim === 'function'
? (payload as { trim: () => string }).trim()
: null);
const folderId = (() => {
if (isPlainObject(payload) && 'id' in payload) {
return (payload as { id?: FolderIdentifier }).id ?? null;
}
const maybeTrim = (payload as { trim?: () => string })?.trim;
if (isFunctionValue(maybeTrim)) {
const nextValue = maybeTrim.call(payload);
return nextValue || null;
}
return null;
})();
const rowEl = folderId
? (document.getElementById(`folder-row-${folderId}`)
|| document.getElementById(`folder-card-${folderId}`))
@@ -205,10 +212,9 @@ const useDocumentDragHandlers = ({
const handleDocumentDragStart = useCallback(
(event: DragEvent<HTMLElement>, documentOrId: DocumentLike | Identifier | null | undefined) => {
const documentId =
(documentOrId as DocumentLike)?.id ?? (typeof documentOrId === 'string' || typeof documentOrId === 'number'
? documentOrId
: null);
const documentId: Identifier | null = Object(documentOrId) === documentOrId
? (documentOrId as DocumentLike)?.id ?? null
: (documentOrId as Identifier | null);
if (!documentId) {
return;
}
@@ -1,4 +1,5 @@
import { useCallback } from 'react';
import { isPlainObject } from '../../utils/typeGuards';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils';
@@ -173,7 +174,7 @@ interface UseDocumentMutationsResult {
const normalizeDocumentId = (value: unknown): DocumentId | null => {
if (!value) return null;
if (typeof value === 'object' && value !== null && 'id' in value && value.id != null) {
if (isPlainObject(value) && 'id' in value && value.id != null) {
return value.id as DocumentId;
}
return value as DocumentId;
@@ -562,8 +563,8 @@ const useDocumentMutations = ({
}
await api.post(`/documents/${document.id}/tags`, { tag_ids: [tag.id] });
setStatusMessage('Tag assigned.', 'success');
if (input && typeof input === 'object') {
input.value = '';
if (input && Object(input) === input && 'value' in (input as Record<string, unknown>)) {
(input as { value?: string }).value = '';
}
await refreshCurrentFolder();
} catch (error) {
@@ -582,12 +583,16 @@ const useDocumentMutations = ({
const resolveTagForCache = (): Tag | null => {
const lookupTag = tagLookupById.get(tagId);
const source = lookupTag ?? tagData;
if (!source || source.id == null || typeof source.label?.trim !== 'function') {
if (!source || source.id == null) {
return null;
}
const labelText = `${source.label ?? ''}`.trim();
if (!labelText) {
return null;
}
return {
id: source.id,
label: source.label,
label: labelText,
color: Object.prototype.hasOwnProperty.call(source, 'color') ? (source as Tag).color ?? null : null,
};
};
@@ -222,7 +222,9 @@ const useDocumentTagging = ({
},
);
const payload = 'data' in response ? response.data : response;
const queued = typeof payload?.queued === 'number' ? payload.queued : targetIds.length;
const queued = Number.isFinite(payload?.queued)
? Number(payload.queued)
: targetIds.length;
setStatusMessage(
`Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`,
'success',
@@ -322,7 +322,7 @@ const useDocumentUploads = ({
items.map(async (item, index) => {
if (item.kind !== 'file') return;
const fileFromItem = typeof item.getAsFile === 'function' ? item.getAsFile() : null;
const fileFromItem = item.getAsFile?.() ?? null;
if (fileFromItem) {
const relativePath = (fileFromItem as File & { webkitRelativePath?: string })?.webkitRelativePath ?? '';
const segments = relativePath
@@ -334,9 +334,9 @@ const useDocumentUploads = ({
pushFile(fileFromItem, segments);
}
if (typeof item.webkitGetAsEntry === 'function') {
if ((item as ExtendedDataTransferItem).webkitGetAsEntry) {
try {
const entry = item.webkitGetAsEntry();
const entry = (item as ExtendedDataTransferItem).webkitGetAsEntry?.();
if (entry) {
await walkEntry(entry, []);
return;
+1 -4
View File
@@ -20,9 +20,6 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio
const mapDocumentCaches = useCallback(
(mapper: (doc: DocumentLike) => DocumentLike | undefined) => {
if (typeof mapper !== 'function') {
return;
}
const applyToList = (list?: DocumentLike[] | null) => {
let changed = false;
@@ -81,7 +78,7 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio
const updateDocumentCaches = useCallback(
(documentId, updater) => {
if (!documentId || typeof updater !== 'function') {
if (!documentId) {
return;
}
@@ -153,19 +153,10 @@ const useDocumentsWorkspace = ({
} = appState;
const tenantRecord = (tenant ?? null) as TenantOption | null;
const tenantName: string | null = typeof tenantRecord?.name === 'string'
? tenantRecord.name
: typeof tenantRecord?.slug === 'string'
? tenantRecord.slug
: null;
const tenantNameCandidate = tenantRecord?.name ?? tenantRecord?.slug ?? null;
const tenantName = tenantNameCandidate ? String(tenantNameCandidate) : null;
const currentTenantId: Identifier | null = (() => {
const value = tenantRecord?.id;
if (typeof value === 'string' || typeof value === 'number') {
return value as Identifier;
}
return null;
})();
const currentTenantId: Identifier | null = (tenantRecord?.id ?? null) as Identifier | null;
const tenantOptions: TenantOption[] = Array.isArray(tenantOptionsRaw)
? (tenantOptionsRaw as TenantOption[])
@@ -306,11 +297,9 @@ const useDocumentsWorkspace = ({
folderContentsRef.current = folderContents;
}, [folderContents]);
const setSearchResultsRef = useRef(() => {});
const setSearchResultsRef = useRef<(value: unknown) => void>(() => {});
const setSearchResultsProxy = useCallback((value) => {
if (typeof setSearchResultsRef.current === 'function') {
setSearchResultsRef.current(value);
}
setSearchResultsRef.current(value);
}, []);
const {
@@ -1290,7 +1279,9 @@ const useDocumentsWorkspace = ({
if (docOrId == null) {
return;
}
const docId = typeof docOrId === 'object' ? docOrId?.id : docOrId;
const docId: Identifier | null = Object(docOrId) === docOrId
? (docOrId as DocumentLike)?.id ?? null
: (docOrId as Identifier | null);
if (docId == null) {
return;
}
+3 -10
View File
@@ -311,16 +311,9 @@ const useFolderTree = ({
if (Array.isArray(child?.subfolders)) {
return child.subfolders.length > 0;
}
if (typeof child?.has_children === 'boolean') {
return child.has_children;
}
if (typeof child?.hasChildren === 'boolean') {
return child.hasChildren;
}
if (typeof childNode?.hasChildren === 'boolean') {
return childNode.hasChildren;
}
return false;
const flag = [child?.has_children, child?.hasChildren, childNode?.hasChildren]
.find((value) => value != null);
return Boolean(flag);
})();
next.set(childId, {
id: childId,
+2 -4
View File
@@ -62,7 +62,7 @@ const useTags = ({
}
const payload: Record<string, unknown> = {};
if (typeof changes?.label?.trim === 'function') {
if (changes?.label != null) {
payload.label = changes.label;
}
if (Object.prototype.hasOwnProperty.call(changes, 'color')) {
@@ -124,9 +124,7 @@ const useTags = ({
return { ...doc, tags: nextTags };
};
if (typeof mapDocumentCaches === 'function') {
mapDocumentCaches(stripTagFromDoc);
}
mapDocumentCaches?.(stripTagFromDoc);
await refreshTags();
setStatusMessage('Tag deleted.', 'success');
+11 -11
View File
@@ -49,15 +49,13 @@ interface UseAssetNavigatorOptions {
defaultOrdinal?: number;
}
type SetOrdinalArg = number | ((prev: number) => number);
interface AssetNavigatorReturn {
document: DocumentLike | null | undefined;
documentId: Identifier | null;
asset: AssetLike | null;
assetType: string;
ordinal: number;
setOrdinal: (next: SetOrdinalArg) => void;
setOrdinal: (next: number) => void;
goPrev: () => void;
goNext: () => void;
canGoPrev: boolean;
@@ -90,7 +88,7 @@ export const useAssetNavigator = ({
const documentId = (document?.id ?? null) as Identifier | null;
const asset = useMemo<AssetLike | null>(() => {
if (!document || typeof getAsset !== 'function') {
if (!document || !getAsset) {
return null;
}
return getAsset(document, assetType) || null;
@@ -109,17 +107,19 @@ export const useAssetNavigator = ({
}, [documentId, assetType, defaultOrdinal]);
const setOrdinal = useCallback(
(next: SetOrdinalArg) => {
setOrdinalInternal((prev) => {
const target = typeof next === 'function' ? next(prev) : next;
return clampOrdinalValue(target, cardinality, defaultOrdinal);
});
(next: number) => {
setOrdinalInternal(clampOrdinalValue(next, cardinality, defaultOrdinal));
},
[cardinality, defaultOrdinal],
);
const goPrev = useCallback(() => setOrdinal((value) => value - 1), [setOrdinal]);
const goNext = useCallback(() => setOrdinal((value) => value + 1), [setOrdinal]);
const goPrev = useCallback(() => {
setOrdinalInternal((prev) => clampOrdinalValue(prev - 1, cardinality, defaultOrdinal));
}, [cardinality, defaultOrdinal]);
const goNext = useCallback(() => {
setOrdinalInternal((prev) => clampOrdinalValue(prev + 1, cardinality, defaultOrdinal));
}, [cardinality, defaultOrdinal]);
const objects = view.getObjects();
const currentObject = view.getObject(ordinal);
+8 -10
View File
@@ -157,7 +157,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
);
const hasOcr = useMemo(() => {
if (!document || typeof getDocumentAsset !== 'function') {
if (!document || !getDocumentAsset) {
return false;
}
return Boolean(getDocumentAsset(document, 'ocr-text'));
@@ -191,7 +191,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
);
const loadOcrContent = useCallback(async ({ signal }: { signal?: AbortSignal } = {}) => {
if (!document || !hasOcr || typeof getDocumentAsset !== 'function') {
if (!document || !hasOcr || !getDocumentAsset) {
return '';
}
@@ -204,7 +204,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
const asset = getDocumentAsset(document, 'ocr-text');
let url = updateUrl();
if (!url && document.id && asset?.id && typeof ensureAssetUrl === 'function') {
if (!url && document.id && asset?.id && ensureAssetUrl) {
await ensureAssetUrl(document.id, asset, { start: 1, limit: 1 });
if (signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
@@ -274,7 +274,7 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
}, [previewEntry?.url, document?.id]);
useEffect(() => {
if (typeof hydrateDocument === 'function' && document?.id) {
if (hydrateDocument && document?.id) {
hydrateDocument(document.id);
}
}, [hydrateDocument, document?.id]);
@@ -318,7 +318,7 @@ const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(n
);
const breadcrumbs = useMemo(() => {
if (!document || typeof resolveFolderPath !== 'function') {
if (!document || !resolveFolderPath) {
return [];
}
const folderSegments = resolveFolderPath(document.folder_id);
@@ -397,7 +397,7 @@ const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(n
canZoom: Boolean(zoomDisplay),
});
const collapseButton = isSidebarVariant && typeof onCollapsePanel === 'function'
const collapseButton = isSidebarVariant && onCollapsePanel
? (
<button
type="button"
@@ -411,7 +411,7 @@ const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(n
)
: null;
const maximizeButton = isSidebarVariant && typeof onMaximizePanel === 'function'
const maximizeButton = isSidebarVariant && onMaximizePanel
? (
<button
type="button"
@@ -587,9 +587,7 @@ export const createDocumentViewerSurface = ({
return null;
}
const sidebarToggle = typeof renderSidebarToggle === 'function'
? renderSidebarToggle()
: null;
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
return {
key: 'preview',
@@ -57,16 +57,6 @@ export const useViewerLayoutMode = (
measure();
if (typeof ResizeObserver === 'undefined') {
window.addEventListener('resize', measure);
return () => {
if (frame) {
cancelAnimationFrame(frame);
}
window.removeEventListener('resize', measure);
};
}
const observer = new ResizeObserver((entries) => {
if (!entries.length) {
return;
+1 -1
View File
@@ -71,7 +71,7 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
if (activeSectionConfig.component) {
const SectionComponent = activeSectionConfig.component;
sectionContent = <SectionComponent {...sectionProps} />;
} else if (typeof activeSectionConfig.render === 'function') {
} else if (activeSectionConfig.render) {
sectionContent = activeSectionConfig.render(sectionProps);
}
}
@@ -7,6 +7,7 @@ import {
} from 'react';
import type { JSX } from 'react';
import { CheckIcon, ChevronDownIcon } from '../../ui/icons';
import { isPlainObject } from '../../utils/typeGuards';
type CapabilityValue = string | number;
@@ -28,14 +29,18 @@ interface CapabilityDropdownProps {
summaryLabel?: string;
}
const isCapabilityOption = (
option: CapabilityDropdownOption | CapabilityValue | null | undefined,
): option is CapabilityDropdownOption => isPlainObject(option);
const resolveCapabilityValue = (
option: CapabilityDropdownOption | CapabilityValue | null | undefined,
): CapabilityValue | null => {
if (option == null) {
return null;
}
if (typeof option === 'string' || typeof option === 'number') {
return option;
if (!isCapabilityOption(option)) {
return option as CapabilityValue;
}
if (option.value != null) {
return option.value;
@@ -166,9 +171,9 @@ const CapabilityDropdown = ({
if (value == null) {
return null;
}
const label = typeof formatLabel === 'function'
const label = formatLabel
? formatLabel(value)
: (typeof option === 'object' && option?.label) || String(value);
: (isCapabilityOption(option) && option?.label) || String(value);
const selected = selectedValues.includes(value);
return (
<button
@@ -96,7 +96,7 @@ const ApiTokensSection = ({
const [newTokenExpires, setNewTokenExpires] = useState('');
const [newTokenCapabilitySetId, setNewTokenCapabilitySetId] = useState<string>('');
const [formError, setFormError] = useState<string | null>(null);
const supportsClipboardWrite = typeof navigator !== 'undefined' && Boolean(navigator?.clipboard?.writeText);
const supportsClipboardWrite = Boolean(navigator.clipboard?.writeText);
const [canCopyToken, setCanCopyToken] = useState<boolean>(supportsClipboardWrite);
const [copyFeedback, setCopyFeedback] = useState<CopyFeedbackState | null>(null);
@@ -125,12 +125,13 @@ const ApiTokensSection = ({
const capabilitySelectionOptions = useMemo<CapabilitySelectionOption[]>(() => (
Array.isArray(capabilities)
? capabilities.map((capability) => {
if (typeof capability !== 'string') {
return { value: String(capability ?? ''), label: String(capability ?? '') };
const capabilityText = `${capability ?? ''}`;
if (!capabilityText.includes(':')) {
return { value: capability, label: capabilityText };
}
const [namespace, action] = capability.split(':');
const [namespace, action] = capabilityText.split(':');
if (!namespace || !action) {
return { value: capability, label: capability };
return { value: capability, label: capabilityText };
}
const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`;
const formattedAction = action.replace(/_/g, ' ');
@@ -193,9 +194,6 @@ const ApiTokensSection = ({
});
try {
if (typeof navigator === 'undefined') {
throw new Error('Clipboard API unavailable');
}
await navigator.clipboard.writeText(createdToken);
showSuccess();
return;
@@ -214,8 +212,7 @@ const ApiTokensSection = ({
useEffect(() => {
setCopyFeedback(null);
const hasClipboard = typeof navigator !== 'undefined' && Boolean(navigator?.clipboard?.writeText);
setCanCopyToken(hasClipboard);
setCanCopyToken(Boolean(navigator.clipboard?.writeText));
}, [createdToken]);
const handleNewCapabilitySetChange = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
@@ -8,6 +8,7 @@ import React, {
import type { SettingsSectionConfig } from '../SettingsModal';
import { IconX } from '../../ui/icons';
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
import { isPlainObject } from '../../utils/typeGuards';
type CapabilityValue = string | number;
type CapabilitySetId = string | number;
@@ -54,12 +55,15 @@ interface CapabilitySetsSectionProps {
type CapabilityOptionInput = CapabilityDropdownOption | CapabilityValue | null | undefined;
const isCapabilityOption = (option: CapabilityOptionInput): option is CapabilityDropdownOption =>
isPlainObject(option);
const resolveCapabilityValue = (option: CapabilityOptionInput): CapabilityValue | null => {
if (option == null) {
return null;
}
if (typeof option === 'string' || typeof option === 'number') {
return option;
if (!isCapabilityOption(option)) {
return option as CapabilityValue;
}
if (option.value != null) {
return option.value as CapabilityValue;
@@ -99,12 +103,13 @@ const CapabilitySetsSection: React.FC<CapabilitySetsSectionProps> = ({
const capabilitySelectionOptions = useMemo<CapabilityDropdownOption[]>(() => (
(capabilities ?? []).map((capability) => {
if (typeof capability !== 'string') {
return { value: capability, label: String(capability) };
const capabilityLabel = `${capability ?? ''}`;
if (!capabilityLabel.includes(':')) {
return { value: capability, label: capabilityLabel };
}
const [namespace, action] = capability.split(':');
const [namespace, action] = capabilityLabel.split(':');
if (!namespace || !action) {
return { value: capability, label: capability };
return { value: capability, label: capabilityLabel };
}
const formattedNamespace = `${namespace.charAt(0).toUpperCase()}${namespace.slice(1)}`;
const formattedAction = action.replace(/_/g, ' ');
@@ -161,7 +166,7 @@ const CapabilitySetsSection: React.FC<CapabilitySetsSectionProps> = ({
label: set.label || set.slug || set.id,
capabilities: Array.isArray(set.capabilities) ? set.capabilities : [],
isSystem: Boolean(set?.is_system),
version: typeof set?.cap_version === 'number' ? set.cap_version : null,
version: Number.isFinite(set?.cap_version) ? Number(set.cap_version) : null,
})),
[capabilitySets],
);
@@ -492,7 +497,7 @@ const CapabilitySetsSection: React.FC<CapabilitySetsSectionProps> = ({
: '—'}
</td>
<td>{isSystem ? 'Yes' : 'No'}</td>
<td>{typeof set.cap_version === 'number' ? set.cap_version : '—'}</td>
<td>{Number.isFinite(set.cap_version) ? Number(set.cap_version) : '—'}</td>
<td className="settings-table__actions">
{isSystem ? (
<span className="settings-status">System set</span>
+3 -5
View File
@@ -35,11 +35,9 @@ const useCapabilitySets = ({ api, notifyApiError, setStatusMessage, token }: Use
const applyCapabilitySets = useCallback((updater: CapabilitySet[] | ((prev: CapabilitySet[]) => CapabilitySet[])) => {
setCapabilitySets((previous) => {
const base = Array.isArray(previous) ? [...previous] : [];
const next = typeof updater === 'function'
? updater(base)
: Array.isArray(updater)
? [...updater]
: base;
const next = Array.isArray(updater)
? [...updater]
: updater(base);
const supportsLabels = next.some((item) => Object.prototype.hasOwnProperty.call(item || {}, 'label'));
setSupportsCapabilitySetLabels(supportsLabels);
return next;
+1 -3
View File
@@ -683,9 +683,7 @@ const Sidebar: React.FC<SidebarProps> = ({
sidebarClassNames.push('sidebar--suppressed');
}
const tenantMenuContent = tenantMenuOpen
&& tenantMenuStyle
&& typeof document !== 'undefined'
const tenantMenuContent = tenantMenuOpen && tenantMenuStyle
? createPortal(
<div
className={menuClassName}
+1 -5
View File
@@ -257,11 +257,7 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
}
}, [collapsed]);
const setCollapsed = useCallback((value) => {
if (typeof value === 'function') {
setCollapsedState((prev) => Boolean(value(prev)));
return;
}
const setCollapsed = useCallback((value: boolean) => {
setCollapsedState(Boolean(value));
}, []);
+2 -2
View File
@@ -92,7 +92,7 @@ function TagsPanel({
const handleDelete = useCallback(
async (tag) => {
if (!tag?.id || typeof onDeleteTag !== 'function') {
if (!tag?.id || !onDeleteTag) {
return;
}
@@ -115,7 +115,7 @@ function TagsPanel({
const handleCreate = useCallback(
async (event) => {
event.preventDefault();
if (typeof onCreateTag !== 'function') {
if (!onCreateTag) {
return;
}
+1 -1
View File
@@ -17,7 +17,7 @@ const normalizeEntries = (entries) =>
}
const id = entry.id ?? entry.value ?? index;
const label = entry.label ?? entry.name ?? entry.title ?? '';
const onClick = typeof entry.onClick === 'function' ? entry.onClick : null;
const onClick = entry.onClick ? entry.onClick : null;
return label ? { id, label, onClick, raw: entry } : null;
})
.filter(Boolean);
+2 -1
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ReactNode, FormEvent, MutableRefObject } from 'react';
import { PlusIcon } from './icons';
import { isPlainObject } from '../utils/typeGuards';
import useFloatingMenu from './useFloatingMenu';
type QuickAddOption = string | number | { id?: string | number; label?: string; name?: string; [key: string]: unknown };
@@ -16,7 +17,7 @@ const normalizeOption = (option: QuickAddOption | null | undefined, index: numbe
if (option == null) {
return null;
}
if (typeof option === 'object') {
if (isPlainObject(option)) {
const label = option.label ?? option.name;
if (label == null) {
return null;
+1 -1
View File
@@ -87,7 +87,7 @@ export const EditIcon: TablerIconComponent = ({ className, size = '1em', stroke
);
export const FolderIcon: TablerIconComponent = ({ className, size = 16, title, ...rest }) => {
const dimensionProps = typeof size === 'number' ? { width: size, height: size } : {};
const dimensionProps = Number.isFinite(size) ? { width: Number(size), height: Number(size) } : {};
return (
<FolderSvg
+10
View File
@@ -0,0 +1,10 @@
const objectToString = Object.prototype.toString;
export const isPlainObject = (value: unknown): value is Record<string, unknown> =>
value !== null && !Array.isArray(value) && Object(value) === value;
export const isStringValue = (value: unknown): value is string =>
objectToString.call(value) === '[object String]';
export const isFunctionValue = <T extends (...args: unknown[]) => unknown>(value: unknown): value is T =>
objectToString.call(value) === '[object Function]';
+10 -37
View File
@@ -1,11 +1,5 @@
/* global PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, BufferSource, PublicKeyCredentialUserEntity, PublicKeyCredentialDescriptor, AuthenticatorSelectionCriteria, AuthenticatorTransport, AuthenticationExtensionsClientOutputs */
type BufferCtor = {
from: (input: string, encoding: string) => {
toString: (encoding: string) => string;
};
};
type CreationChallengeResponse = {
publicKey?: PublicKeyCredentialCreationOptions & {
challenge?: string | BufferSource;
@@ -32,6 +26,8 @@ type AuthenticationCredential = PublicKeyCredential & {
response: AuthenticatorAssertionResponse;
};
import { isStringValue } from './typeGuards';
const base64urlToBase64 = (value: string = ''): string => {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
const padding = normalized.length % 4;
@@ -45,32 +41,9 @@ const base64urlToBase64 = (value: string = ''): string => {
const base64ToBase64url = (value: string = ''): string =>
value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
const getWindowObject = (): (Window & typeof globalThis) | null =>
(typeof window !== 'undefined' ? window : null);
const decodeBase64 = (value: string): string => window.atob(value);
const decodeBase64 = (value: string): string => {
const win = getWindowObject();
if (win?.atob) {
return win.atob(value);
}
const bufferCtor = (globalThis as typeof globalThis & { Buffer?: BufferCtor }).Buffer;
if (bufferCtor) {
return bufferCtor.from(value, 'base64').toString('binary');
}
throw new Error('No base64 decoder available.');
};
const encodeBase64 = (binary: string): string => {
const win = getWindowObject();
if (win?.btoa) {
return win.btoa(binary);
}
const bufferCtor = (globalThis as typeof globalThis & { Buffer?: BufferCtor }).Buffer;
if (bufferCtor) {
return bufferCtor.from(binary, 'binary').toString('base64');
}
throw new Error('No base64 encoder available.');
};
const encodeBase64 = (binary: string): string => window.btoa(binary);
export const base64urlToUint8Array = (value?: string | null): Uint8Array => {
const base64 = base64urlToBase64(value || '');
@@ -119,7 +92,7 @@ export const arrayBufferToBase64url = (
};
export const isWebAuthnAvailable = (): boolean =>
Boolean(typeof navigator !== 'undefined' && navigator?.credentials?.create && navigator.credentials.get);
Boolean(navigator.credentials?.create && navigator.credentials.get);
export const preparePublicKeyCreationOptions = (
challengeResponse: CreationChallengeResponse,
@@ -130,14 +103,14 @@ export const preparePublicKeyCreationOptions = (
const publicKey: PublicKeyCredentialCreationOptions = { ...challengeResponse.publicKey };
if (publicKey.challenge && typeof publicKey.challenge === 'string') {
if (publicKey.challenge && isStringValue(publicKey.challenge)) {
publicKey.challenge = base64urlToBufferSource(publicKey.challenge);
}
if (publicKey.user?.id) {
publicKey.user = {
...publicKey.user,
id: typeof publicKey.user.id === 'string'
id: isStringValue(publicKey.user.id)
? base64urlToBufferSource(publicKey.user.id)
: publicKey.user.id,
};
@@ -146,7 +119,7 @@ export const preparePublicKeyCreationOptions = (
if (Array.isArray(publicKey.excludeCredentials)) {
publicKey.excludeCredentials = publicKey.excludeCredentials.map((descriptor) => ({
...descriptor,
id: typeof descriptor.id === 'string'
id: isStringValue(descriptor.id)
? base64urlToBufferSource(descriptor.id)
: descriptor.id,
}));
@@ -168,14 +141,14 @@ export const preparePublicKeyRequestOptions = (
const publicKey: PublicKeyCredentialRequestOptions = { ...challengeResponse.publicKey };
if (publicKey.challenge && typeof publicKey.challenge === 'string') {
if (publicKey.challenge && isStringValue(publicKey.challenge)) {
publicKey.challenge = base64urlToBufferSource(publicKey.challenge);
}
if (Array.isArray(publicKey.allowCredentials)) {
publicKey.allowCredentials = publicKey.allowCredentials.map((descriptor) => ({
...descriptor,
id: typeof descriptor.id === 'string'
id: isStringValue(descriptor.id)
? base64urlToBufferSource(descriptor.id)
: descriptor.id,
}));