Files
NetMesh/application/state/useWorkSurfaceHostEditor.ts

172 lines
4.7 KiB
TypeScript
Raw Permalink Normal View History

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { upsertHostById } from '../../domain/host';
import type { Host } from '../../types';
export type WorkSurfaceHostEditorMode = 'new' | 'edit';
export type WorkSurfaceHostEditorTarget =
| { mode: 'new'; defaultGroup: string | null; requestId: number }
| { mode: 'edit'; openedHost: Host; requestId: number };
interface UseWorkSurfaceHostEditorOptions {
hosts: Host[];
onUpdateHosts: (hosts: Host[]) => void;
onSaved?: (mode: WorkSurfaceHostEditorMode) => void;
}
export function buildWorkSurfaceHostEditorKey(target: WorkSurfaceHostEditorTarget): string {
if (target.mode === 'edit') {
return `edit:${target.openedHost.id}:${target.requestId}`;
}
return `new:${target.defaultGroup ?? 'root'}:${target.requestId}`;
}
export function shouldCloseDeletedWorkSurfaceHost(
hosts: Host[],
target: WorkSurfaceHostEditorTarget | null,
): boolean {
return target?.mode === 'edit'
&& !hosts.some((host) => host.id === target.openedHost.id);
}
function isPlainRecord(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function areDraftValuesEqual(left: unknown, right: unknown): boolean {
if (Object.is(left, right)) return true;
if (Array.isArray(left) || Array.isArray(right)) {
return Array.isArray(left)
&& Array.isArray(right)
&& left.length === right.length
&& left.every((value, index) => areDraftValuesEqual(value, right[index]));
}
if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
return leftKeys.length === rightKeys.length
&& leftKeys.every((key) => Object.hasOwn(right, key)
&& areDraftValuesEqual(left[key], right[key]));
}
function mergeWorkSurfaceDraftValue(
openedValue: unknown,
draftValue: unknown,
latestValue: unknown,
): unknown {
if (areDraftValuesEqual(draftValue, openedValue)) return latestValue;
if (!isPlainRecord(draftValue) || !isPlainRecord(latestValue)) {
return draftValue;
}
const openedRecord = isPlainRecord(openedValue) ? openedValue : {};
const merged: Record<string, unknown> = { ...latestValue };
const draftKeys = new Set([...Object.keys(openedRecord), ...Object.keys(draftValue)]);
for (const key of draftKeys) {
if (!Object.hasOwn(draftValue, key)) {
delete merged[key];
continue;
}
merged[key] = mergeWorkSurfaceDraftValue(
openedRecord[key],
draftValue[key],
latestValue[key],
);
}
return merged;
}
export function mergeWorkSurfaceHostDraft(
openedHost: Host,
draft: Host,
latestHost: Host,
): Host {
return mergeWorkSurfaceDraftValue(openedHost, draft, latestHost) as Host;
}
export function saveWorkSurfaceHostDraft(
hosts: Host[],
target: WorkSurfaceHostEditorTarget,
draft: Host,
): Host[] | null {
if (target.mode === 'new') {
return upsertHostById(hosts, draft);
}
const latestHost = hosts.find((host) => host.id === target.openedHost.id);
if (!latestHost) return null;
return upsertHostById(
hosts,
mergeWorkSurfaceHostDraft(target.openedHost, draft, latestHost),
);
}
export function useWorkSurfaceHostEditor({
hosts,
onUpdateHosts,
onSaved,
}: UseWorkSurfaceHostEditorOptions) {
const [target, setTarget] = useState<WorkSurfaceHostEditorTarget | null>(null);
const requestIdRef = useRef(0);
const nextRequestId = useCallback(() => {
requestIdRef.current += 1;
return requestIdRef.current;
}, []);
const openNew = useCallback((defaultGroup?: string | null) => {
setTarget({
mode: 'new',
defaultGroup: defaultGroup || null,
requestId: nextRequestId(),
});
}, [nextRequestId]);
const openEdit = useCallback((host: Host) => {
setTarget({ mode: 'edit', openedHost: host, requestId: nextRequestId() });
}, [nextRequestId]);
const close = useCallback(() => {
setTarget(null);
}, []);
const save = useCallback((draft: Host) => {
if (!target) return;
let saved = false;
onUpdateHosts((prevHosts) => {
const nextHosts = saveWorkSurfaceHostDraft(prevHosts, target, draft);
if (!nextHosts) return prevHosts;
saved = true;
return nextHosts;
});
if (!saved) {
close();
return;
}
onSaved?.(target.mode);
close();
}, [close, onSaved, onUpdateHosts, target]);
useEffect(() => {
if (shouldCloseDeletedWorkSurfaceHost(hosts, target)) {
close();
}
}, [close, hosts, target]);
const editorKey = useMemo(
() => (target ? buildWorkSurfaceHostEditorKey(target) : null),
[target],
);
return {
target,
editorKey,
openNew,
openEdit,
close,
save,
};
}