refactor: Delete isPlainObject type guard and replace its usages with direct typeof checks.

This commit is contained in:
2025-11-24 21:22:38 +01:00
parent 63be4663ab
commit f5ca74b63c
8 changed files with 86 additions and 95 deletions
+1
View File
@@ -7,6 +7,7 @@
"dev": "webpack serve --mode development --open", "dev": "webpack serve --mode development --open",
"build": "webpack --mode production", "build": "webpack --mode production",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx", "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"check": "tsc --noEmit && npm run lint",
"test:engine": "node --test tests/workspaceEngine.test.js" "test:engine": "node --test tests/workspaceEngine.test.js"
}, },
"dependencies": { "dependencies": {
@@ -12,7 +12,7 @@ import {
toIssuedTimestamp, toIssuedTimestamp,
} from '../utils/date'; } from '../utils/date';
import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary'; import { describeDocumentSummary, type DocumentSummaryRow } from './documentSummary';
import { isPlainObject } from '../utils/typeGuards';
import { useFolderManager } from '../folders/FolderManagerContext'; import { useFolderManager } from '../folders/FolderManagerContext';
type Identifier = string | number; type Identifier = string | number;
@@ -128,11 +128,11 @@ const resolveOptionName = (source?: QuickAddOption | string | null): string => {
if (!source) { if (!source) {
return ''; return '';
} }
if (isPlainObject(source)) { if (typeof source === 'string') {
const raw = source.name ?? source.label ?? ''; return source.trim();
return `${raw}`.trim();
} }
return `${source}`.trim(); const raw = source.name ?? source.label ?? '';
return `${raw}`.trim();
}; };
const normalizeQuickAddOption = (option?: QuickAddOption | string | null): QuickAddEntry | null => { const normalizeQuickAddOption = (option?: QuickAddOption | string | null): QuickAddEntry | null => {
@@ -140,17 +140,17 @@ const normalizeQuickAddOption = (option?: QuickAddOption | string | null): Quick
return null; return null;
} }
const label = (() => { const label = (() => {
if (isPlainObject(option)) { if (typeof option === 'string') {
const sourceLabel = option.label ?? option.name ?? ''; return option.trim();
return `${sourceLabel}`.trim();
} }
return `${option}`.trim(); const sourceLabel = option.label ?? option.name ?? '';
return `${sourceLabel}`.trim();
})(); })();
if (!label) { if (!label) {
return null; return null;
} }
return { return {
id: isPlainObject(option) && option.id ? option.id : label, id: typeof option !== 'string' && option.id ? option.id : label,
label, label,
original: option, original: option,
}; };
@@ -245,7 +245,7 @@ export const TagSection: React.FC<TagSectionProps> = ({
return; return;
} }
if (item.state === 'all' && onRemove) { if (item.state === 'all' && onRemove) {
const payload = isPlainObject(item.payload) const payload = item.payload && typeof item.payload === 'object' && 'id' in item.payload
? (item.payload as TagEntry) ? (item.payload as TagEntry)
: tags.find((tag) => (tag.id ?? tag.label) === item.id) ?? { id: item.id, label: item.label }; : tags.find((tag) => (tag.id ?? tag.label) === item.id) ?? { id: item.id, label: item.label };
onRemove(payload); onRemove(payload);
@@ -379,7 +379,7 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
return; return;
} }
if (item.state === 'all' && onRemove) { if (item.state === 'all' && onRemove) {
const payload = isPlainObject(item.payload) const payload = item.payload && typeof item.payload === 'object' && 'id' in item.payload
? (item.payload as CorrespondentEntry) ? (item.payload as CorrespondentEntry)
: entries.find((entry) => (entry.id ?? entry.name) === item.id) ?? { id: item.id, name: item.label }; : entries.find((entry) => (entry.id ?? entry.name) === item.id) ?? { id: item.id, name: item.label };
onRemove(payload); onRemove(payload);
@@ -393,7 +393,7 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
if (!resolvedName) { if (!resolvedName) {
return; return;
} }
const payload = isPlainObject(source) const payload = typeof source !== 'string'
? { ...source, name: resolvedName } ? { ...source, name: resolvedName }
: { id: null, name: resolvedName }; : { id: null, name: resolvedName };
onAdd({ name: resolvedName, option: payload, input: null }); onAdd({ name: resolvedName, option: payload, input: null });
@@ -405,26 +405,26 @@ export const CorrespondentSection: React.FC<CorrespondentSectionProps> = ({
<div className={containerClass}> <div className={containerClass}>
{hasEntries {hasEntries
? entries.map((entry) => { ? entries.map((entry) => {
const key = entry.id ?? entry.name; const key = entry.id ?? entry.name;
return ( return (
<span key={key} className="correspondent-pill"> <span key={key} className="correspondent-pill">
<span className="correspondent-pill__label"> <span className="correspondent-pill__label">
{entry.name} {entry.name}
{showCount && entry.count ? ` (${entry.count})` : ''} {showCount && entry.count ? ` (${entry.count})` : ''}
</span>
{onRemove ? (
<button
type="button"
className="correspondent-pill__remove"
onClick={() => onRemove(entry)}
aria-label={`Remove ${entry.name}`}
>
<IconX className="icon-inline" aria-hidden="true" />
</button>
) : null}
</span> </span>
); {onRemove ? (
}) <button
type="button"
className="correspondent-pill__remove"
onClick={() => onRemove(entry)}
aria-label={`Remove ${entry.name}`}
>
<IconX className="icon-inline" aria-hidden="true" />
</button>
) : null}
</span>
);
})
: !showQuickAdd && <span className="meta">No correspondents yet.</span>} : !showQuickAdd && <span className="meta">No correspondents yet.</span>}
{showQuickAdd ? ( {showQuickAdd ? (
<SelectionAssignmentMenu <SelectionAssignmentMenu
@@ -518,7 +518,7 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
if (active) { if (active) {
setFolderName(name); setFolderName(name);
} }
}).catch(() => {}); }).catch(() => { });
} }
return () => { return () => {
active = false; active = false;
@@ -674,21 +674,21 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
const titleMetaDisplay = editableTitle && isTitleEditing const titleMetaDisplay = editableTitle && isTitleEditing
? renderTitleEditForm('doc-title-edit--inline') ? renderTitleEditForm('doc-title-edit--inline')
: ( : (
<> <>
<span className="detail-meta__value">{document?.title}</span> <span className="detail-meta__value">{document?.title}</span>
{editableTitle ? ( {editableTitle ? (
<button <button
type="button" type="button"
className="icon-button" className="icon-button"
onClick={startTitleEdit} onClick={startTitleEdit}
aria-label="Edit title" aria-label="Edit title"
title="Edit title" title="Edit title"
> >
<EditIcon className="icon-inline" /> <EditIcon className="icon-inline" />
</button> </button>
) : null} ) : null}
</> </>
); );
const issuedDisplay = editableIssued && isIssuedEditing ? ( const issuedDisplay = editableIssued && isIssuedEditing ? (
<form className="doc-issued-edit" onSubmit={submitIssuedEdit}> <form className="doc-issued-edit" onSubmit={submitIssuedEdit}>
@@ -758,20 +758,20 @@ const DocumentSummarySection: React.FC<DocumentSummarySectionProps> = ({
onRemove={ onRemove={
onCorrespondentRemove onCorrespondentRemove
? (entry) => ? (entry) =>
onCorrespondentRemove({ onCorrespondentRemove({
documentId: document.id, documentId: document.id,
correspondentId: entry.id, correspondentId: entry.id,
}) })
: undefined : undefined
} }
onAdd={ onAdd={
onCorrespondentAdd onCorrespondentAdd
? ({ name, option }) => ? ({ name, option }) =>
onCorrespondentAdd({ onCorrespondentAdd({
document, document,
name, name,
option, option,
}) })
: undefined : undefined
} }
showCount showCount
@@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { isPlainObject } from '../../utils/typeGuards';
type ApiClient = { type ApiClient = {
post: (path: string, body?: unknown) => Promise<{ data: unknown }>; post: (path: string, body?: unknown) => Promise<{ data: unknown }>;
@@ -124,16 +124,14 @@ const useDocumentCorrespondentActions = ({
if (!option) { if (!option) {
return null; return null;
} }
if (isPlainObject(option) && 'id' in option) {
return option as CorrespondentOption;
}
if (typeof option === 'string') { if (typeof option === 'string') {
const trimmed = option.trim(); const trimmed = option.trim();
if (trimmed) { if (trimmed) {
return { id: null, name: trimmed }; return { id: null, name: trimmed };
} }
return null;
} }
return null; return option;
}; };
const handleCorrespondentAdd = useCallback( const handleCorrespondentAdd = useCallback(
@@ -1,5 +1,5 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { isPlainObject } from '../../utils/typeGuards';
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'; import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils'; import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey } from '../../app/appLayoutUtils';
import { import {
@@ -66,7 +66,7 @@ interface DocumentLike {
interface FolderContents { interface FolderContents {
documents?: DocumentLike[]; documents?: DocumentLike[];
subfolders?: Array<{ id?: FolderId; [key: string]: unknown }>; subfolders?: Array<{ id?: FolderId;[key: string]: unknown }>;
[key: string]: unknown; [key: string]: unknown;
} }
@@ -175,7 +175,7 @@ interface UseDocumentMutationsResult {
const normalizeDocumentId = (value: unknown): DocumentId | null => { const normalizeDocumentId = (value: unknown): DocumentId | null => {
if (!value) return null; if (!value) return null;
if (isPlainObject(value) && 'id' in value && value.id != null) { if (value && typeof value === 'object' && 'id' in value && value.id != null) {
return value.id as DocumentId; return value.id as DocumentId;
} }
return value as DocumentId; return value as DocumentId;
@@ -446,7 +446,7 @@ const useDocumentMutations = ({
}, },
[ [
token, token,
documentLookup,
removeDocumentsFromCaches, removeDocumentsFromCaches,
previewDocumentId, previewDocumentId,
closeDocumentPreview, closeDocumentPreview,
@@ -495,7 +495,8 @@ const useDocumentMutations = ({
); );
const handleDocumentIssuedUpdate = useCallback( const handleDocumentIssuedUpdate = useCallback(
async (documentId: DocumentId, nextIssuedDate: number | null) => {const payload = { issued_at: nextIssuedDate || null }; async (documentId: DocumentId, nextIssuedDate: number | null) => {
const payload = { issued_at: nextIssuedDate || null };
try { try {
const data = await updateDocument(documentId, payload); const data = await updateDocument(documentId, payload);
const updatedDocument = extractDocumentFromResponse?.(data); const updatedDocument = extractDocumentFromResponse?.(data);
@@ -7,7 +7,7 @@ import {
} from 'react'; } from 'react';
import type { JSX } from 'react'; import type { JSX } from 'react';
import { CheckIcon, ChevronDownIcon } from '../../ui/icons'; import { CheckIcon, ChevronDownIcon } from '../../ui/icons';
import { isPlainObject } from '../../utils/typeGuards';
type CapabilityValue = string | number; type CapabilityValue = string | number;
@@ -29,9 +29,7 @@ interface CapabilityDropdownProps {
summaryLabel?: string; summaryLabel?: string;
} }
const isCapabilityOption = (
option: CapabilityDropdownOption | CapabilityValue | null,
): option is CapabilityDropdownOption => isPlainObject(option);
const resolveCapabilityValue = ( const resolveCapabilityValue = (
option: CapabilityDropdownOption | CapabilityValue | null, option: CapabilityDropdownOption | CapabilityValue | null,
@@ -39,8 +37,8 @@ const resolveCapabilityValue = (
if (option == null) { if (option == null) {
return null; return null;
} }
if (!isCapabilityOption(option)) { if (typeof option !== 'object') {
return option as CapabilityValue; return option;
} }
if (option.value != null) { if (option.value != null) {
return option.value; return option.value;
@@ -170,7 +168,7 @@ const CapabilityDropdown = ({
} }
const label = formatLabel const label = formatLabel
? formatLabel(value) ? formatLabel(value)
: (isCapabilityOption(option) && option?.label) || String(value); : (typeof option === 'object' && option?.label) || String(value);
const selected = selectedValues.includes(value); const selected = selectedValues.includes(value);
return ( return (
<button <button
@@ -8,7 +8,7 @@ import React, {
import type { SettingsSectionConfig } from '../SettingsModal'; import type { SettingsSectionConfig } from '../SettingsModal';
import { IconX } from '../../ui/icons'; import { IconX } from '../../ui/icons';
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown'; import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
import { isPlainObject } from '../../utils/typeGuards';
type CapabilityValue = string | number; type CapabilityValue = string | number;
type CapabilitySetId = string | number; type CapabilitySetId = string | number;
@@ -55,15 +55,14 @@ interface CapabilitySetsSectionProps {
type CapabilityOptionInput = CapabilityDropdownOption | CapabilityValue | null; type CapabilityOptionInput = CapabilityDropdownOption | CapabilityValue | null;
const isCapabilityOption = (option: CapabilityOptionInput): option is CapabilityDropdownOption =>
isPlainObject(option);
const resolveCapabilityValue = (option: CapabilityOptionInput): CapabilityValue | null => { const resolveCapabilityValue = (option: CapabilityOptionInput): CapabilityValue | null => {
if (option == null) { if (option == null) {
return null; return null;
} }
if (!isCapabilityOption(option)) { if (typeof option === 'string' || typeof option === 'number') {
return option as CapabilityValue; return option;
} }
if (option.value != null) { if (option.value != null) {
return option.value as CapabilityValue; return option.value as CapabilityValue;
+10 -10
View File
@@ -1,10 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { CSSProperties, ReactNode, FormEvent, MutableRefObject } from 'react'; import type { CSSProperties, ReactNode, FormEvent, MutableRefObject } from 'react';
import { PlusIcon } from './icons'; import { PlusIcon } from './icons';
import { isPlainObject } from '../utils/typeGuards';
import useFloatingMenu from './useFloatingMenu'; import useFloatingMenu from './useFloatingMenu';
type QuickAddOption = string | number | { id?: string | number; label?: string; name?: string; [key: string]: unknown }; type QuickAddOption = string | number | { id?: string | number; label?: string; name?: string;[key: string]: unknown };
interface NormalizedOption { interface NormalizedOption {
id?: string | number; id?: string | number;
@@ -17,7 +17,7 @@ const normalizeOption = (option: QuickAddOption | null, index: number): Normaliz
if (option == null) { if (option == null) {
return null; return null;
} }
if (isPlainObject(option)) { if (typeof option === 'object') {
const label = option.label ?? option.name; const label = option.label ?? option.name;
if (label == null) { if (label == null) {
return null; return null;
@@ -190,19 +190,19 @@ const QuickAddMenu = ({
const menuClassName = 'menu menu--floating'; const menuClassName = 'menu menu--floating';
const anchoredMenuStyle = isAnchoredMenu && menuStyle const anchoredMenuStyle = isAnchoredMenu && menuStyle
? { ? {
top: menuStyle.top, top: menuStyle.top,
left: menuStyle.left, left: menuStyle.left,
...(menuStyle.width ? { width: menuStyle.width } : null), ...(menuStyle.width ? { width: menuStyle.width } : null),
} }
: undefined; : undefined;
const menuInlineStyle = (isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined) as CSSVarStyle | undefined; const menuInlineStyle = (isAnchoredMenu ? anchoredMenuStyle : menuStyle || undefined) as CSSVarStyle | undefined;
const hasFloatingWidthVar = Boolean(menuInlineStyle && Object.prototype.hasOwnProperty.call(menuInlineStyle, '--floating-min-width')); const hasFloatingWidthVar = Boolean(menuInlineStyle && Object.prototype.hasOwnProperty.call(menuInlineStyle, '--floating-min-width'));
const menuStyleWithVar: CSSVarStyle | undefined = hasFloatingWidthVar const menuStyleWithVar: CSSVarStyle | undefined = hasFloatingWidthVar
? menuInlineStyle ? menuInlineStyle
: { : {
...(menuInlineStyle || {}), ...(menuInlineStyle || {}),
'--floating-min-width': `${Math.max(menuMinWidth, 0)}px`, '--floating-min-width': `${Math.max(menuMinWidth, 0)}px`,
}; };
return ( return (
<div className={className ? `quick-add ${className}` : 'quick-add'}> <div className={className ? `quick-add ${className}` : 'quick-add'}>
-6
View File
@@ -1,6 +0,0 @@
const objectToString = Object.prototype.toString;
export const isPlainObject = (value: unknown): value is Record<string, unknown> =>
value !== null && !Array.isArray(value) && Object(value) === value;