refactor
This commit is contained in:
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() : '';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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));
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]';
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user