Stabilize admin state and submission metadata
This commit is contained in:
parent
215ead0768
commit
bd2087ba5f
@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@goodgrief/cue-engine": "file:../../packages/cue-engine",
|
"@goodgrief/cue-engine": "file:../../packages/cue-engine",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.3",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vitest": "^4.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
339
apps/admin/src/app/admin-state.test.ts
Normal file
339
apps/admin/src/app/admin-state.test.ts
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { createCueRuntimeState } from "@goodgrief/cue-engine";
|
||||||
|
import {
|
||||||
|
createEmptyRepositoryState,
|
||||||
|
defaultCueStack,
|
||||||
|
defaultSceneDefinitions,
|
||||||
|
type Cue,
|
||||||
|
type PhotoAsset,
|
||||||
|
type Submission
|
||||||
|
} from "@goodgrief/shared-types";
|
||||||
|
import {
|
||||||
|
adminReducer,
|
||||||
|
createCueDraft,
|
||||||
|
createMetadataDraft,
|
||||||
|
initialAdminUiState,
|
||||||
|
sortCues
|
||||||
|
} from "./admin-state";
|
||||||
|
|
||||||
|
const scene = defaultSceneDefinitions[0]!;
|
||||||
|
const otherScene = defaultSceneDefinitions.find((entry) => entry.id !== scene.id)!;
|
||||||
|
|
||||||
|
const cue = (id: string, orderIndex: number, notes: string): Cue => ({
|
||||||
|
...defaultCueStack[0]!,
|
||||||
|
id,
|
||||||
|
orderIndex,
|
||||||
|
sceneDefinitionId: scene.id,
|
||||||
|
notes
|
||||||
|
});
|
||||||
|
|
||||||
|
const submission = (id: string, caption: string): Submission => ({
|
||||||
|
id,
|
||||||
|
source: "live",
|
||||||
|
submittedAt: "2026-04-10T00:00:00.000Z",
|
||||||
|
status: "approved_all",
|
||||||
|
consentId: `consent-${id}`,
|
||||||
|
displayName: `Display ${id}`,
|
||||||
|
caption
|
||||||
|
});
|
||||||
|
|
||||||
|
const asset = (
|
||||||
|
id: string,
|
||||||
|
submissionId: string,
|
||||||
|
moderationStatus: PhotoAsset["moderationStatus"] = "approved"
|
||||||
|
): PhotoAsset => ({
|
||||||
|
id,
|
||||||
|
submissionId,
|
||||||
|
originalKey: `/uploads/${id}.jpg`,
|
||||||
|
mimeType: "image/jpeg",
|
||||||
|
processingStatus: "ready",
|
||||||
|
moderationStatus,
|
||||||
|
createdAt: "2026-04-10T00:00:00.000Z"
|
||||||
|
});
|
||||||
|
|
||||||
|
const repositoryWithCues = (cues: Cue[]) => ({
|
||||||
|
...createEmptyRepositoryState(),
|
||||||
|
scenes: defaultSceneDefinitions,
|
||||||
|
cues: sortCues(cues),
|
||||||
|
photoAssets: [],
|
||||||
|
submissions: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const bootState = (cues: Cue[], selectedCueId = cues[0]?.id ?? null) => {
|
||||||
|
const repository = repositoryWithCues(cues);
|
||||||
|
const selectedCue = selectedCueId ? repository.cues.find((entry) => entry.id === selectedCueId) : null;
|
||||||
|
return adminReducer(initialAdminUiState, {
|
||||||
|
type: "bootstrapLoaded",
|
||||||
|
payload: repository,
|
||||||
|
initial: {
|
||||||
|
cueState: selectedCue ? createCueRuntimeState(repository.cues) : createCueRuntimeState([]),
|
||||||
|
selectedSceneId: scene.id,
|
||||||
|
selectedAssetIds: [],
|
||||||
|
previewParams: scene.defaultParams,
|
||||||
|
activePresetId: scene.defaultPresetId,
|
||||||
|
cueDraft: createCueDraft(selectedCue, scene)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("adminReducer cue reconciliation", () => {
|
||||||
|
it("moves a cue immediately while keeping the moved cue selected and dirty draft intact", () => {
|
||||||
|
const cues = [cue("cue-a", 0, "A"), cue("cue-b", 1, "B"), cue("cue-c", 2, "C")];
|
||||||
|
const dirtyState = {
|
||||||
|
...bootState(cues, "cue-b"),
|
||||||
|
cueState: createCueRuntimeState(cues),
|
||||||
|
cueDraft: {
|
||||||
|
...createCueDraft(cues[1], scene),
|
||||||
|
notes: "Unsaved label"
|
||||||
|
},
|
||||||
|
cueDraftDirty: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const moving = adminReducer(dirtyState, { type: "cueMoveOptimistic", cueId: "cue-b", direction: "up" });
|
||||||
|
|
||||||
|
expect(moving.repository?.cues.map((entry) => entry.id)).toEqual(["cue-b", "cue-a", "cue-c"]);
|
||||||
|
expect(moving.cueState.previewCueId).toBe("cue-b");
|
||||||
|
expect(moving.cueState.armedCueId).toBe("cue-b");
|
||||||
|
expect(moving.cueMoveInFlight).toBe(true);
|
||||||
|
expect(moving.cueDraftDirty).toBe(true);
|
||||||
|
expect(moving.cueDraft.notes).toBe("Unsaved label");
|
||||||
|
|
||||||
|
const stalePoll = adminReducer(moving, {
|
||||||
|
type: "liveLoaded",
|
||||||
|
cues,
|
||||||
|
pendingCount: 1,
|
||||||
|
approvedCount: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stalePoll.repository?.cues.map((entry) => entry.id)).toEqual(["cue-b", "cue-a", "cue-c"]);
|
||||||
|
expect(stalePoll.pendingCount).toBe(1);
|
||||||
|
expect(stalePoll.approvedCount).toBe(2);
|
||||||
|
|
||||||
|
const moved = adminReducer(moving, {
|
||||||
|
type: "cueMoveSucceeded",
|
||||||
|
cueId: "cue-b",
|
||||||
|
cues: [cue("cue-b", 0, "B"), cue("cue-a", 1, "A"), cue("cue-c", 2, "C")]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(moved.repository?.cues.map((entry) => entry.id)).toEqual(["cue-b", "cue-a", "cue-c"]);
|
||||||
|
expect(moved.cueState.previewCueId).toBe("cue-b");
|
||||||
|
expect(moved.cueState.armedCueId).toBe("cue-b");
|
||||||
|
expect(moved.cueMoveInFlight).toBe(false);
|
||||||
|
expect(moved.cueDraftDirty).toBe(true);
|
||||||
|
expect(moved.cueDraft.notes).toBe("Unsaved label");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refreshes clean cue drafts from live polling but preserves dirty drafts", () => {
|
||||||
|
const cues = [cue("cue-a", 0, "A"), cue("cue-b", 1, "B")];
|
||||||
|
const cleanState = {
|
||||||
|
...bootState(cues, "cue-b"),
|
||||||
|
cueState: createCueRuntimeState(cues),
|
||||||
|
cueDraft: createCueDraft(cues[1], scene)
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanRefresh = adminReducer(cleanState, {
|
||||||
|
type: "liveLoaded",
|
||||||
|
cues: [cue("cue-a", 0, "A"), cue("cue-b", 1, "B updated")],
|
||||||
|
pendingCount: 0,
|
||||||
|
approvedCount: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const dirtyRefresh = adminReducer(
|
||||||
|
{
|
||||||
|
...cleanState,
|
||||||
|
cueDraft: {
|
||||||
|
...cleanState.cueDraft,
|
||||||
|
notes: "Unsaved local edit"
|
||||||
|
},
|
||||||
|
cueDraftDirty: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "liveLoaded",
|
||||||
|
cues: [cue("cue-a", 0, "A"), cue("cue-b", 1, "B updated")],
|
||||||
|
pendingCount: 0,
|
||||||
|
approvedCount: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(cleanRefresh.cueDraft.notes).toBe("B updated");
|
||||||
|
expect(dirtyRefresh.cueDraft.notes).toBe("Unsaved local edit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adminReducer library reconciliation", () => {
|
||||||
|
const baseSubmission = submission("submission-a", "Caption");
|
||||||
|
|
||||||
|
it("prunes unavailable selected media while preserving dirty metadata drafts", () => {
|
||||||
|
const state = {
|
||||||
|
...bootState([cue("cue-a", 0, "A")]),
|
||||||
|
selectedAssetIds: ["asset-a", "asset-b"],
|
||||||
|
metadataAssetId: "asset-a",
|
||||||
|
metadataDraft: {
|
||||||
|
...createMetadataDraft(baseSubmission),
|
||||||
|
caption: "Unsaved caption"
|
||||||
|
},
|
||||||
|
metadataDirty: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconciled = adminReducer(state, {
|
||||||
|
type: "libraryLoaded",
|
||||||
|
photoAssets: [asset("asset-a", baseSubmission.id, "approved"), asset("asset-b", baseSubmission.id, "hold")],
|
||||||
|
submissions: [
|
||||||
|
{
|
||||||
|
...baseSubmission,
|
||||||
|
caption: "Server caption"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
collections: state.repository?.collections ?? []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(reconciled.selectedAssetIds).toEqual(["asset-a"]);
|
||||||
|
expect(reconciled.metadataAssetId).toBe("asset-a");
|
||||||
|
expect(reconciled.metadataDraft.caption).toBe("Unsaved caption");
|
||||||
|
expect(reconciled.approvedCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adminReducer preview and operator controls", () => {
|
||||||
|
it("selects a scene as one clean preview transition", () => {
|
||||||
|
const state = bootState([cue("cue-a", 0, "A")], "cue-a");
|
||||||
|
const selected = adminReducer(state, {
|
||||||
|
type: "previewSceneSelected",
|
||||||
|
scene: otherScene,
|
||||||
|
presetId: "preset-test",
|
||||||
|
params: otherScene.defaultParams,
|
||||||
|
assetIds: ["asset-a", "asset-b"]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(selected.selectedSceneId).toBe(otherScene.id);
|
||||||
|
expect(selected.sceneBrowserFilter).toBe(otherScene.sceneFamily);
|
||||||
|
expect(selected.activePresetId).toBe("preset-test");
|
||||||
|
expect(selected.previewParams).toBe(otherScene.defaultParams);
|
||||||
|
expect(selected.selectedAssetIds).toEqual(["asset-a", "asset-b"]);
|
||||||
|
expect(selected.metadataAssetId).toBe("asset-a");
|
||||||
|
expect(selected.cueDraft.id).toBeNull();
|
||||||
|
expect(selected.cueDraftDirty).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps media selection ordering deterministic and marks saved cue drafts dirty", () => {
|
||||||
|
const state = {
|
||||||
|
...bootState([cue("cue-a", 0, "A")], "cue-a"),
|
||||||
|
selectedAssetIds: ["asset-a", "asset-b", "asset-c"],
|
||||||
|
metadataAssetId: "asset-a",
|
||||||
|
cueDraftDirty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const promoted = adminReducer(state, {
|
||||||
|
type: "selectedAssetPromoted",
|
||||||
|
assetId: "asset-b",
|
||||||
|
status: "Anchor image updated."
|
||||||
|
});
|
||||||
|
const reordered = adminReducer(promoted, {
|
||||||
|
type: "selectedAssetReordered",
|
||||||
|
assetId: "asset-b",
|
||||||
|
direction: "later"
|
||||||
|
});
|
||||||
|
const removed = adminReducer(reordered, {
|
||||||
|
type: "selectedAssetRemoved",
|
||||||
|
assetId: "asset-b"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promoted.selectedAssetIds).toEqual(["asset-b", "asset-a", "asset-c"]);
|
||||||
|
expect(promoted.metadataAssetId).toBe("asset-b");
|
||||||
|
expect(promoted.cueDraftDirty).toBe(true);
|
||||||
|
expect(reordered.selectedAssetIds).toEqual(["asset-a", "asset-b", "asset-c"]);
|
||||||
|
expect(removed.selectedAssetIds).toEqual(["asset-a", "asset-c"]);
|
||||||
|
expect(removed.metadataAssetId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks cue drafts dirty when operator params or cue fields change", () => {
|
||||||
|
const state = {
|
||||||
|
...bootState([cue("cue-a", 0, "A")], "cue-a"),
|
||||||
|
cueDraftDirty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const paramChanged = adminReducer(state, {
|
||||||
|
type: "previewParamChanged",
|
||||||
|
path: "scenicTreatment.hue",
|
||||||
|
value: 0.42
|
||||||
|
});
|
||||||
|
const cueChanged = adminReducer(state, {
|
||||||
|
type: "cueDraftChanged",
|
||||||
|
field: "notes",
|
||||||
|
value: "Operator label"
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(paramChanged.previewParams?.scenicTreatment.hue).toBe(0.42);
|
||||||
|
expect(paramChanged.cueDraftDirty).toBe(true);
|
||||||
|
expect(cueChanged.cueDraft.notes).toBe("Operator label");
|
||||||
|
expect(cueChanged.cueDraftDirty).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("adminReducer metadata and upload controls", () => {
|
||||||
|
it("preserves dirty metadata for the same target and hydrates when the target changes", () => {
|
||||||
|
const submissionA = submission("submission-a", "Saved A");
|
||||||
|
const submissionB = submission("submission-b", "Saved B");
|
||||||
|
const focused = adminReducer(bootState([cue("cue-a", 0, "A")]), {
|
||||||
|
type: "metadataTargetSelected",
|
||||||
|
assetId: "asset-a",
|
||||||
|
submission: submissionA
|
||||||
|
});
|
||||||
|
const edited = adminReducer(focused, {
|
||||||
|
type: "metadataDraftChanged",
|
||||||
|
field: "caption",
|
||||||
|
value: "Local edit"
|
||||||
|
});
|
||||||
|
const sameTargetRefresh = adminReducer(edited, {
|
||||||
|
type: "metadataTargetSelected",
|
||||||
|
assetId: "asset-a",
|
||||||
|
submission: {
|
||||||
|
...submissionA,
|
||||||
|
caption: "Server refresh"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const changedTarget = adminReducer(sameTargetRefresh, {
|
||||||
|
type: "metadataTargetSelected",
|
||||||
|
assetId: "asset-b",
|
||||||
|
submission: submissionB
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(focused.metadataDraft.caption).toBe("Saved A");
|
||||||
|
expect(sameTargetRefresh.metadataDraft.caption).toBe("Local edit");
|
||||||
|
expect(sameTargetRefresh.metadataDirty).toBe(true);
|
||||||
|
expect(changedTarget.metadataDraft.caption).toBe("Saved B");
|
||||||
|
expect(changedTarget.metadataDirty).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resets upload fields and optionally adds uploaded media to the current cue draft", () => {
|
||||||
|
const state = {
|
||||||
|
...bootState([cue("cue-a", 0, "A")], "cue-a"),
|
||||||
|
selectedAssetIds: ["asset-a"],
|
||||||
|
uploadName: "Name",
|
||||||
|
uploadCaption: "Caption",
|
||||||
|
uploadPromptAnswer: "Prompt",
|
||||||
|
uploadAddToSelection: false,
|
||||||
|
cueDraftDirty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploaded = adminReducer(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
uploadAddToSelection: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "adminUploadSucceeded",
|
||||||
|
assetId: "asset-new",
|
||||||
|
addToSelection: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(uploaded.selectedAssetIds).toEqual(["asset-new", "asset-a"]);
|
||||||
|
expect(uploaded.metadataAssetId).toBe("asset-new");
|
||||||
|
expect(uploaded.uploadName).toBe("");
|
||||||
|
expect(uploaded.uploadCaption).toBe("");
|
||||||
|
expect(uploaded.uploadPromptAnswer).toBe("");
|
||||||
|
expect(uploaded.uploadAddToSelection).toBe(true);
|
||||||
|
expect(uploaded.cueDraftDirty).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
931
apps/admin/src/app/admin-state.ts
Normal file
931
apps/admin/src/app/admin-state.ts
Normal file
@ -0,0 +1,931 @@
|
|||||||
|
import {
|
||||||
|
armCue,
|
||||||
|
createCueRuntimeState,
|
||||||
|
takeCue,
|
||||||
|
triggerSafeScene,
|
||||||
|
type CueRuntimeState
|
||||||
|
} from "@goodgrief/cue-engine";
|
||||||
|
import { effectPresetLibrary } from "@goodgrief/effects";
|
||||||
|
import type { ProgramOutputState } from "../features/live/output-sync";
|
||||||
|
import {
|
||||||
|
moveCueInOrder,
|
||||||
|
sortCuesByOrder,
|
||||||
|
setSceneParamValue,
|
||||||
|
upsertCueInOrder,
|
||||||
|
Cue,
|
||||||
|
CueTransition,
|
||||||
|
type CueUpsertPayload,
|
||||||
|
PhotoAsset,
|
||||||
|
RepositoryState,
|
||||||
|
SceneDefinition,
|
||||||
|
SceneParamGroups,
|
||||||
|
type SceneParamScalar,
|
||||||
|
Submission
|
||||||
|
} from "@goodgrief/shared-types";
|
||||||
|
|
||||||
|
export interface CueDraftState {
|
||||||
|
id: string | null;
|
||||||
|
notes: string;
|
||||||
|
triggerMode: Cue["triggerMode"];
|
||||||
|
transitionInStyle: CueTransition["style"];
|
||||||
|
transitionInDurationMs: number;
|
||||||
|
transitionOutStyle: CueTransition["style"];
|
||||||
|
transitionOutDurationMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataDraftState {
|
||||||
|
contributorName: string;
|
||||||
|
lovedOneName: string;
|
||||||
|
displayName: string;
|
||||||
|
caption: string;
|
||||||
|
promptAnswer: string;
|
||||||
|
notes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SceneBrowserFilter = "all" | SceneDefinition["sceneFamily"];
|
||||||
|
export type WorkspaceMode = "show" | "build";
|
||||||
|
export type ShowUtilityTab = "controls" | "notes" | "media" | "moderation";
|
||||||
|
export type BuildLibraryTab = "approved" | "pending" | "upload";
|
||||||
|
|
||||||
|
export const defaultCueTransition: CueTransition = {
|
||||||
|
style: "dissolve",
|
||||||
|
durationMs: 4000
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createCueDraft = (cue?: Cue | null, scene?: SceneDefinition): CueDraftState => ({
|
||||||
|
id: cue?.id ?? null,
|
||||||
|
notes: cue?.notes ?? scene?.name ?? "",
|
||||||
|
triggerMode: cue?.triggerMode ?? "manual",
|
||||||
|
transitionInStyle: cue?.transitionIn.style ?? defaultCueTransition.style,
|
||||||
|
transitionInDurationMs: cue?.transitionIn.durationMs ?? defaultCueTransition.durationMs,
|
||||||
|
transitionOutStyle: cue?.transitionOut.style ?? "mist_reveal",
|
||||||
|
transitionOutDurationMs: cue?.transitionOut.durationMs ?? 4000
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createMetadataDraft = (submission?: Submission | null): MetadataDraftState => ({
|
||||||
|
contributorName: submission?.contributorName ?? "",
|
||||||
|
lovedOneName: submission?.lovedOneName ?? "",
|
||||||
|
displayName: submission?.displayName ?? "",
|
||||||
|
caption: submission?.caption ?? "",
|
||||||
|
promptAnswer: submission?.promptAnswer ?? "",
|
||||||
|
notes: submission?.notes ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sortCues = sortCuesByOrder;
|
||||||
|
export const moveCueInList = moveCueInOrder;
|
||||||
|
|
||||||
|
export const getApprovedAssets = (payload: Pick<RepositoryState, "photoAssets">) =>
|
||||||
|
payload.photoAssets.filter((asset) => asset.moderationStatus === "approved");
|
||||||
|
|
||||||
|
export const getPendingModerationAssets = (photoAssets: PhotoAsset[], submissions: Submission[]) =>
|
||||||
|
photoAssets.filter((asset) => {
|
||||||
|
if (asset.moderationStatus !== "pending") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submission = submissions.find((entry) => entry.id === asset.submissionId);
|
||||||
|
return submission?.source !== "admin_upload";
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getPendingModerationCount = (photoAssets: PhotoAsset[], submissions: Submission[]) =>
|
||||||
|
getPendingModerationAssets(photoAssets, submissions).length;
|
||||||
|
|
||||||
|
export const filterAvailableAssetIds = (payload: Pick<RepositoryState, "photoAssets">, assetIds: string[]) => {
|
||||||
|
const available = new Set(getApprovedAssets(payload).map((asset) => asset.id));
|
||||||
|
return assetIds.filter((assetId) => available.has(assetId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InitialLiveState {
|
||||||
|
cueState: CueRuntimeState;
|
||||||
|
selectedSceneId: string;
|
||||||
|
selectedAssetIds: string[];
|
||||||
|
previewParams?: SceneParamGroups;
|
||||||
|
activePresetId: string;
|
||||||
|
cueDraft: CueDraftState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUiState {
|
||||||
|
repository: RepositoryState | null;
|
||||||
|
workspaceMode: WorkspaceMode;
|
||||||
|
showUtilityTab: ShowUtilityTab;
|
||||||
|
buildLibraryTab: BuildLibraryTab;
|
||||||
|
cueState: CueRuntimeState;
|
||||||
|
programOutputState: ProgramOutputState | null;
|
||||||
|
selectedSceneId: string;
|
||||||
|
sceneBrowserFilter: SceneBrowserFilter;
|
||||||
|
selectedAssetIds: string[];
|
||||||
|
metadataAssetId: string | null;
|
||||||
|
metadataDraft: MetadataDraftState;
|
||||||
|
metadataDirty: boolean;
|
||||||
|
metadataSaving: boolean;
|
||||||
|
metadataHydrationKey: string | null;
|
||||||
|
previewParams: SceneParamGroups | null;
|
||||||
|
activePresetId: string;
|
||||||
|
cueDraft: CueDraftState;
|
||||||
|
cueDraftDirty: boolean;
|
||||||
|
mediaSearch: string;
|
||||||
|
uploadName: string;
|
||||||
|
uploadCaption: string;
|
||||||
|
uploadPromptAnswer: string;
|
||||||
|
uploadFile: File | null;
|
||||||
|
uploadAddToSelection: boolean;
|
||||||
|
status: string;
|
||||||
|
pendingCount: number;
|
||||||
|
approvedCount: number;
|
||||||
|
cueMoveInFlight: boolean;
|
||||||
|
cueMutationInFlight: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdminFieldUpdater<K extends keyof AdminUiState> =
|
||||||
|
| AdminUiState[K]
|
||||||
|
| ((current: AdminUiState[K]) => AdminUiState[K]);
|
||||||
|
|
||||||
|
export type AdminAction =
|
||||||
|
| {
|
||||||
|
type: "setField";
|
||||||
|
field: keyof AdminUiState;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "bootstrapLoaded";
|
||||||
|
payload: RepositoryState;
|
||||||
|
initial: InitialLiveState;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "repositoryHydrated";
|
||||||
|
payload: RepositoryState;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "liveLoaded";
|
||||||
|
cues: Cue[];
|
||||||
|
pendingCount: number;
|
||||||
|
approvedCount: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "libraryLoaded";
|
||||||
|
photoAssets: PhotoAsset[];
|
||||||
|
submissions: Submission[];
|
||||||
|
collections: RepositoryState["collections"];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "statusChanged";
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewSceneSelected";
|
||||||
|
scene: SceneDefinition;
|
||||||
|
presetId: string;
|
||||||
|
params: SceneParamGroups;
|
||||||
|
assetIds: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewCueSelected";
|
||||||
|
cue: Cue;
|
||||||
|
scene: SceneDefinition;
|
||||||
|
presetId: string;
|
||||||
|
params: SceneParamGroups;
|
||||||
|
assetIds: string[];
|
||||||
|
armPreview?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "generatedCueDraftLoaded";
|
||||||
|
draft: CueUpsertPayload;
|
||||||
|
scene: SceneDefinition;
|
||||||
|
presetId: string;
|
||||||
|
params: SceneParamGroups;
|
||||||
|
assetIds: string[];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewReset";
|
||||||
|
initial: InitialLiveState;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewPresetApplied";
|
||||||
|
presetId: string;
|
||||||
|
params: SceneParamGroups;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewParamsReplaced";
|
||||||
|
params: SceneParamGroups;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewParamChanged";
|
||||||
|
path: string;
|
||||||
|
value: SceneParamScalar;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueDraftChanged";
|
||||||
|
field: keyof CueDraftState;
|
||||||
|
value: CueDraftState[keyof CueDraftState];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetsReplaced";
|
||||||
|
assetIds: string[];
|
||||||
|
metadataAssetId?: string | null;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetsCleared";
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetToggled";
|
||||||
|
assetId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetFocused";
|
||||||
|
assetId: string | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetPromoted";
|
||||||
|
assetId: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetReordered";
|
||||||
|
assetId: string;
|
||||||
|
direction: "earlier" | "later";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "selectedAssetRemoved";
|
||||||
|
assetId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "approvedAssetRemoved";
|
||||||
|
assetId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "metadataTargetSelected";
|
||||||
|
assetId: string | null;
|
||||||
|
submission?: Submission | null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "metadataDraftChanged";
|
||||||
|
field: keyof MetadataDraftState;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "metadataDraftReset";
|
||||||
|
submission?: Submission | null;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "metadataSaveStarted";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "metadataSaveFinished";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "adminUploadSucceeded";
|
||||||
|
assetId: string | null;
|
||||||
|
addToSelection: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "blackoutSet";
|
||||||
|
blackout: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "previewCueSkipped";
|
||||||
|
cueId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "programCueTaken";
|
||||||
|
previewUsesArmedCue: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "safeCueTriggered";
|
||||||
|
cueId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueMoveOptimistic";
|
||||||
|
cueId: string;
|
||||||
|
direction: "up" | "down";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueMoveSucceeded";
|
||||||
|
cues: Cue[];
|
||||||
|
cueId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueMoveFailed";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueMutationStarted";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueMutationFinished";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueUpsertSucceeded";
|
||||||
|
cue: Cue;
|
||||||
|
scene?: SceneDefinition;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "cueDeleted";
|
||||||
|
cueId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "metadataSaved";
|
||||||
|
submission: Submission;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialAdminUiState: AdminUiState = {
|
||||||
|
repository: null,
|
||||||
|
workspaceMode: "show",
|
||||||
|
showUtilityTab: "controls",
|
||||||
|
buildLibraryTab: "approved",
|
||||||
|
cueState: createCueRuntimeState([]),
|
||||||
|
programOutputState: null,
|
||||||
|
selectedSceneId: "",
|
||||||
|
sceneBrowserFilter: "all",
|
||||||
|
selectedAssetIds: [],
|
||||||
|
metadataAssetId: null,
|
||||||
|
metadataDraft: createMetadataDraft(),
|
||||||
|
metadataDirty: false,
|
||||||
|
metadataSaving: false,
|
||||||
|
metadataHydrationKey: null,
|
||||||
|
previewParams: null,
|
||||||
|
activePresetId: effectPresetLibrary[0]?.id ?? "",
|
||||||
|
cueDraft: createCueDraft(),
|
||||||
|
cueDraftDirty: false,
|
||||||
|
mediaSearch: "",
|
||||||
|
uploadName: "",
|
||||||
|
uploadCaption: "",
|
||||||
|
uploadPromptAnswer: "",
|
||||||
|
uploadFile: null,
|
||||||
|
uploadAddToSelection: true,
|
||||||
|
status: "Connecting to local show state...",
|
||||||
|
pendingCount: 0,
|
||||||
|
approvedCount: 0,
|
||||||
|
cueMoveInFlight: false,
|
||||||
|
cueMutationInFlight: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRepositoryCues = (repository: RepositoryState | null, cues: Cue[]) =>
|
||||||
|
repository ? { ...repository, cues: sortCues(cues) } : repository;
|
||||||
|
|
||||||
|
const reconcileCueDraft = (state: AdminUiState, cues: Cue[]) => {
|
||||||
|
if (!state.cueDraft.id || state.cueDraftDirty || !state.repository) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cue = cues.find((entry) => entry.id === state.cueDraft.id);
|
||||||
|
const scene = cue ? state.repository.scenes.find((entry) => entry.id === cue.sceneDefinitionId) : undefined;
|
||||||
|
if (!cue || !scene) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueDraft: createCueDraft(undefined, state.repository.scenes.find((sceneEntry) => sceneEntry.id === state.selectedSceneId)),
|
||||||
|
cueDraftDirty: false,
|
||||||
|
status: "Selected cue changed externally. Preview is now an unsaved draft."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueDraft: createCueDraft(cue, scene)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const reconcileCues = (state: AdminUiState, cues: Cue[]) => {
|
||||||
|
const sorted = sortCues(cues);
|
||||||
|
const cueIds = new Set(sorted.map((cue) => cue.id));
|
||||||
|
const previewCueId = state.cueState.previewCueId && cueIds.has(state.cueState.previewCueId)
|
||||||
|
? state.cueState.previewCueId
|
||||||
|
: sorted[0]?.id ?? null;
|
||||||
|
const armedCueId = state.cueState.armedCueId && cueIds.has(state.cueState.armedCueId)
|
||||||
|
? state.cueState.armedCueId
|
||||||
|
: previewCueId;
|
||||||
|
const currentCueId = state.cueState.currentCueId && cueIds.has(state.cueState.currentCueId)
|
||||||
|
? state.cueState.currentCueId
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return reconcileCueDraft(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
repository: updateRepositoryCues(state.repository, sorted),
|
||||||
|
cueState: {
|
||||||
|
...state.cueState,
|
||||||
|
cueStack: sorted,
|
||||||
|
previewCueId,
|
||||||
|
armedCueId,
|
||||||
|
currentCueId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sorted
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const upsertCueIntoList = (cues: Cue[], cue: Cue) => {
|
||||||
|
return upsertCueInOrder(cues, cue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveItem = <T,>(items: T[], fromIndex: number, toIndex: number) => {
|
||||||
|
if (
|
||||||
|
fromIndex < 0 ||
|
||||||
|
toIndex < 0 ||
|
||||||
|
fromIndex >= items.length ||
|
||||||
|
toIndex >= items.length ||
|
||||||
|
fromIndex === toIndex
|
||||||
|
) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = [...items];
|
||||||
|
const [item] = next.splice(fromIndex, 1);
|
||||||
|
next.splice(toIndex, 0, item);
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
const markCueDraftDirty = (state: AdminUiState) =>
|
||||||
|
state.cueDraft.id ? { ...state, cueDraftDirty: true } : state;
|
||||||
|
|
||||||
|
const setSelectedAssets = (
|
||||||
|
state: AdminUiState,
|
||||||
|
assetIds: string[],
|
||||||
|
metadataAssetId: string | null | undefined = assetIds[0] ?? state.metadataAssetId,
|
||||||
|
status?: string
|
||||||
|
) =>
|
||||||
|
markCueDraftDirty({
|
||||||
|
...state,
|
||||||
|
selectedAssetIds: assetIds,
|
||||||
|
metadataAssetId: metadataAssetId ?? null,
|
||||||
|
...(typeof status === "string" ? { status } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadataHydrationKey = (assetId: string | null, submission?: Submission | null) =>
|
||||||
|
assetId && submission ? `${assetId}:${submission.id}` : assetId;
|
||||||
|
|
||||||
|
export const adminReducer = (state: AdminUiState, action: AdminAction): AdminUiState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "setField": {
|
||||||
|
const current = state[action.field] as unknown;
|
||||||
|
const nextValue =
|
||||||
|
typeof action.value === "function"
|
||||||
|
? (action.value as (current: unknown) => unknown)(current)
|
||||||
|
: action.value;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[action.field]: nextValue
|
||||||
|
} as AdminUiState;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "statusChanged":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
status: action.status
|
||||||
|
};
|
||||||
|
|
||||||
|
case "bootstrapLoaded":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
repository: {
|
||||||
|
...action.payload,
|
||||||
|
cues: sortCues(action.payload.cues)
|
||||||
|
},
|
||||||
|
pendingCount: getPendingModerationCount(action.payload.photoAssets, action.payload.submissions),
|
||||||
|
approvedCount: getApprovedAssets(action.payload).length,
|
||||||
|
cueState: {
|
||||||
|
...action.initial.cueState,
|
||||||
|
cueStack: sortCues(action.initial.cueState.cueStack)
|
||||||
|
},
|
||||||
|
selectedSceneId: action.initial.selectedSceneId,
|
||||||
|
sceneBrowserFilter: "all",
|
||||||
|
selectedAssetIds: action.initial.selectedAssetIds,
|
||||||
|
metadataAssetId: action.initial.selectedAssetIds[0] ?? null,
|
||||||
|
metadataDraft: createMetadataDraft(),
|
||||||
|
metadataDirty: false,
|
||||||
|
metadataHydrationKey: null,
|
||||||
|
previewParams: action.initial.previewParams ?? null,
|
||||||
|
activePresetId: action.initial.activePresetId,
|
||||||
|
cueDraft: action.initial.cueDraft,
|
||||||
|
cueDraftDirty: false,
|
||||||
|
status: "Ready. Local show state loaded."
|
||||||
|
};
|
||||||
|
|
||||||
|
case "repositoryHydrated":
|
||||||
|
return reconcileCues(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
repository: {
|
||||||
|
...action.payload,
|
||||||
|
cues: sortCues(action.payload.cues)
|
||||||
|
},
|
||||||
|
pendingCount: getPendingModerationCount(action.payload.photoAssets, action.payload.submissions),
|
||||||
|
approvedCount: getApprovedAssets(action.payload).length,
|
||||||
|
selectedAssetIds: filterAvailableAssetIds(action.payload, state.selectedAssetIds)
|
||||||
|
},
|
||||||
|
action.payload.cues
|
||||||
|
);
|
||||||
|
|
||||||
|
case "liveLoaded": {
|
||||||
|
const countedState = {
|
||||||
|
...state,
|
||||||
|
pendingCount: action.pendingCount,
|
||||||
|
approvedCount: action.approvedCount
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.cueMoveInFlight || state.cueMutationInFlight) {
|
||||||
|
return countedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reconcileCues(
|
||||||
|
countedState,
|
||||||
|
action.cues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "libraryLoaded": {
|
||||||
|
if (!state.repository) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableIds = new Set(
|
||||||
|
action.photoAssets
|
||||||
|
.filter((asset) => asset.moderationStatus === "approved")
|
||||||
|
.map((asset) => asset.id)
|
||||||
|
);
|
||||||
|
const knownIds = new Set(action.photoAssets.map((asset) => asset.id));
|
||||||
|
const nextMetadataSubmission = state.metadataAssetId
|
||||||
|
? action.submissions.find((submission) =>
|
||||||
|
action.photoAssets.some(
|
||||||
|
(asset) => asset.id === state.metadataAssetId && asset.submissionId === submission.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
repository: {
|
||||||
|
...state.repository,
|
||||||
|
photoAssets: action.photoAssets,
|
||||||
|
submissions: action.submissions,
|
||||||
|
collections: action.collections
|
||||||
|
},
|
||||||
|
pendingCount: getPendingModerationCount(action.photoAssets, action.submissions),
|
||||||
|
approvedCount: action.photoAssets.filter((asset) => asset.moderationStatus === "approved").length,
|
||||||
|
selectedAssetIds: state.selectedAssetIds.filter((assetId) => availableIds.has(assetId)),
|
||||||
|
metadataAssetId: state.metadataAssetId && knownIds.has(state.metadataAssetId) ? state.metadataAssetId : null,
|
||||||
|
metadataDraft:
|
||||||
|
state.metadataDirty || !nextMetadataSubmission
|
||||||
|
? state.metadataDraft
|
||||||
|
: createMetadataDraft(nextMetadataSubmission),
|
||||||
|
metadataHydrationKey:
|
||||||
|
state.metadataDirty || !state.metadataAssetId || !nextMetadataSubmission
|
||||||
|
? state.metadataHydrationKey
|
||||||
|
: metadataHydrationKey(state.metadataAssetId, nextMetadataSubmission)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "previewSceneSelected":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedSceneId: action.scene.id,
|
||||||
|
sceneBrowserFilter: action.scene.sceneFamily,
|
||||||
|
selectedAssetIds: action.assetIds,
|
||||||
|
metadataAssetId: action.assetIds[0] ?? null,
|
||||||
|
previewParams: action.params,
|
||||||
|
activePresetId: action.presetId,
|
||||||
|
cueDraft: createCueDraft(undefined, action.scene),
|
||||||
|
cueDraftDirty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case "previewCueSelected":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueState: action.armPreview ?? true ? armCue(state.cueState, action.cue.id) : state.cueState,
|
||||||
|
selectedSceneId: action.scene.id,
|
||||||
|
sceneBrowserFilter: action.scene.sceneFamily,
|
||||||
|
selectedAssetIds: action.assetIds,
|
||||||
|
metadataAssetId: action.assetIds[0] ?? null,
|
||||||
|
previewParams: action.params,
|
||||||
|
activePresetId: action.presetId,
|
||||||
|
cueDraft: createCueDraft(action.cue, action.scene),
|
||||||
|
cueDraftDirty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case "generatedCueDraftLoaded":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
selectedSceneId: action.scene.id,
|
||||||
|
sceneBrowserFilter: action.scene.sceneFamily,
|
||||||
|
selectedAssetIds: action.assetIds,
|
||||||
|
metadataAssetId: action.assetIds[0] ?? null,
|
||||||
|
previewParams: action.params,
|
||||||
|
activePresetId: action.presetId,
|
||||||
|
cueDraft: {
|
||||||
|
id: null,
|
||||||
|
notes: action.draft.notes ?? action.scene.name,
|
||||||
|
triggerMode: action.draft.triggerMode,
|
||||||
|
transitionInStyle: action.draft.transitionIn.style,
|
||||||
|
transitionInDurationMs: action.draft.transitionIn.durationMs,
|
||||||
|
transitionOutStyle: action.draft.transitionOut.style,
|
||||||
|
transitionOutDurationMs: action.draft.transitionOut.durationMs
|
||||||
|
},
|
||||||
|
cueDraftDirty: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case "previewReset":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueState: action.initial.cueState,
|
||||||
|
selectedSceneId: action.initial.selectedSceneId,
|
||||||
|
sceneBrowserFilter: "all",
|
||||||
|
selectedAssetIds: action.initial.selectedAssetIds,
|
||||||
|
metadataAssetId: action.initial.selectedAssetIds[0] ?? null,
|
||||||
|
previewParams: action.initial.previewParams ?? null,
|
||||||
|
activePresetId: action.initial.activePresetId,
|
||||||
|
cueDraft: action.initial.cueDraft,
|
||||||
|
cueDraftDirty: false,
|
||||||
|
status: "Reset to safe hold on program and opening cue in preview."
|
||||||
|
};
|
||||||
|
|
||||||
|
case "previewPresetApplied":
|
||||||
|
return markCueDraftDirty({
|
||||||
|
...state,
|
||||||
|
activePresetId: action.presetId,
|
||||||
|
previewParams: action.params,
|
||||||
|
...(action.status ? { status: action.status } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
case "previewParamsReplaced":
|
||||||
|
return markCueDraftDirty({
|
||||||
|
...state,
|
||||||
|
previewParams: action.params,
|
||||||
|
...(action.status ? { status: action.status } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
case "previewParamChanged":
|
||||||
|
return state.previewParams
|
||||||
|
? markCueDraftDirty({
|
||||||
|
...state,
|
||||||
|
previewParams: setSceneParamValue(state.previewParams, action.path, action.value)
|
||||||
|
})
|
||||||
|
: state;
|
||||||
|
|
||||||
|
case "cueDraftChanged":
|
||||||
|
return markCueDraftDirty({
|
||||||
|
...state,
|
||||||
|
cueDraft: {
|
||||||
|
...state.cueDraft,
|
||||||
|
[action.field]: action.value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case "selectedAssetsReplaced":
|
||||||
|
return setSelectedAssets(state, action.assetIds, action.metadataAssetId, action.status);
|
||||||
|
|
||||||
|
case "selectedAssetsCleared":
|
||||||
|
return setSelectedAssets(state, [], null, action.status);
|
||||||
|
|
||||||
|
case "selectedAssetToggled": {
|
||||||
|
const nextAssetIds = state.selectedAssetIds.includes(action.assetId)
|
||||||
|
? state.selectedAssetIds.filter((candidate) => candidate !== action.assetId)
|
||||||
|
: [...state.selectedAssetIds, action.assetId].slice(-12);
|
||||||
|
return setSelectedAssets(state, nextAssetIds, action.assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "selectedAssetFocused":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataAssetId: action.assetId
|
||||||
|
};
|
||||||
|
|
||||||
|
case "selectedAssetPromoted": {
|
||||||
|
const index = state.selectedAssetIds.indexOf(action.assetId);
|
||||||
|
return setSelectedAssets(
|
||||||
|
state,
|
||||||
|
index <= 0 ? state.selectedAssetIds : moveItem(state.selectedAssetIds, index, 0),
|
||||||
|
action.assetId,
|
||||||
|
action.status
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "selectedAssetReordered": {
|
||||||
|
const index = state.selectedAssetIds.indexOf(action.assetId);
|
||||||
|
if (index === -1) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextIndex = action.direction === "earlier" ? index - 1 : index + 1;
|
||||||
|
return setSelectedAssets(state, moveItem(state.selectedAssetIds, index, nextIndex), action.assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "selectedAssetRemoved":
|
||||||
|
return setSelectedAssets(
|
||||||
|
state,
|
||||||
|
state.selectedAssetIds.filter((candidate) => candidate !== action.assetId),
|
||||||
|
state.metadataAssetId === action.assetId ? null : state.metadataAssetId
|
||||||
|
);
|
||||||
|
|
||||||
|
case "approvedAssetRemoved":
|
||||||
|
if (!state.selectedAssetIds.includes(action.assetId) && state.metadataAssetId !== action.assetId) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return setSelectedAssets(
|
||||||
|
state,
|
||||||
|
state.selectedAssetIds.filter((candidate) => candidate !== action.assetId),
|
||||||
|
state.metadataAssetId === action.assetId ? null : state.metadataAssetId
|
||||||
|
);
|
||||||
|
|
||||||
|
case "metadataTargetSelected": {
|
||||||
|
const nextKey = metadataHydrationKey(action.assetId, action.submission);
|
||||||
|
const targetChanged = state.metadataHydrationKey !== nextKey;
|
||||||
|
if (!action.assetId) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataAssetId: null,
|
||||||
|
metadataHydrationKey: null,
|
||||||
|
metadataDraft: state.metadataDirty ? state.metadataDraft : createMetadataDraft()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataAssetId: action.assetId,
|
||||||
|
metadataHydrationKey: nextKey,
|
||||||
|
metadataDraft:
|
||||||
|
targetChanged || !state.metadataDirty
|
||||||
|
? createMetadataDraft(action.submission ?? undefined)
|
||||||
|
: state.metadataDraft,
|
||||||
|
metadataDirty: targetChanged ? false : state.metadataDirty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case "metadataDraftChanged":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataDraft: {
|
||||||
|
...state.metadataDraft,
|
||||||
|
[action.field]: action.value
|
||||||
|
},
|
||||||
|
metadataDirty: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case "metadataDraftReset":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataDraft: createMetadataDraft(action.submission ?? undefined),
|
||||||
|
metadataDirty: false,
|
||||||
|
...(action.status ? { status: action.status } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
case "metadataSaveStarted":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataSaving: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case "metadataSaveFinished":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
metadataSaving: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case "adminUploadSucceeded": {
|
||||||
|
const selectedAssetIds =
|
||||||
|
action.addToSelection && action.assetId
|
||||||
|
? [action.assetId, ...state.selectedAssetIds.filter((assetId) => assetId !== action.assetId)].slice(0, 12)
|
||||||
|
: state.selectedAssetIds;
|
||||||
|
const next = {
|
||||||
|
...state,
|
||||||
|
selectedAssetIds,
|
||||||
|
metadataAssetId: action.assetId ?? state.metadataAssetId,
|
||||||
|
metadataDirty: false,
|
||||||
|
uploadFile: null,
|
||||||
|
uploadName: "",
|
||||||
|
uploadCaption: "",
|
||||||
|
uploadPromptAnswer: "",
|
||||||
|
uploadAddToSelection: true
|
||||||
|
};
|
||||||
|
return action.addToSelection && action.assetId ? markCueDraftDirty(next) : next;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "blackoutSet":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueState: {
|
||||||
|
...state.cueState,
|
||||||
|
blackout: action.blackout
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case "previewCueSkipped":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueState: armCue(state.cueState, action.cueId)
|
||||||
|
};
|
||||||
|
|
||||||
|
case "programCueTaken":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueState: action.previewUsesArmedCue
|
||||||
|
? takeCue(state.cueState)
|
||||||
|
: {
|
||||||
|
...state.cueState,
|
||||||
|
blackout: false,
|
||||||
|
safeSceneActive: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
case "safeCueTriggered":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueState: triggerSafeScene(state.cueState, action.cueId)
|
||||||
|
};
|
||||||
|
|
||||||
|
case "cueMoveOptimistic": {
|
||||||
|
const currentCues = state.repository?.cues ?? state.cueState.cueStack;
|
||||||
|
return reconcileCues(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
cueMoveInFlight: true,
|
||||||
|
cueState: armCue(state.cueState, action.cueId)
|
||||||
|
},
|
||||||
|
moveCueInList(currentCues, action.cueId, action.direction)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "cueMoveSucceeded":
|
||||||
|
return reconcileCues(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
cueMoveInFlight: false,
|
||||||
|
cueState: armCue(state.cueState, action.cueId),
|
||||||
|
status: "Cue moved."
|
||||||
|
},
|
||||||
|
action.cues
|
||||||
|
);
|
||||||
|
|
||||||
|
case "cueMoveFailed":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueMoveInFlight: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case "cueMutationStarted":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueMutationInFlight: true
|
||||||
|
};
|
||||||
|
|
||||||
|
case "cueMutationFinished":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
cueMutationInFlight: false
|
||||||
|
};
|
||||||
|
|
||||||
|
case "cueUpsertSucceeded": {
|
||||||
|
const currentCues = state.repository?.cues ?? state.cueState.cueStack;
|
||||||
|
const cues = upsertCueIntoList(currentCues, action.cue);
|
||||||
|
return reconcileCues(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
cueMutationInFlight: false,
|
||||||
|
cueDraft: createCueDraft(action.cue, action.scene),
|
||||||
|
cueDraftDirty: false,
|
||||||
|
cueState: armCue(state.cueState, action.cue.id)
|
||||||
|
},
|
||||||
|
cues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "cueDeleted": {
|
||||||
|
const cues = sortCues((state.repository?.cues ?? state.cueState.cueStack).filter((cue) => cue.id !== action.cueId));
|
||||||
|
const deletedIndex = state.cueState.cueStack.findIndex((cue) => cue.id === action.cueId);
|
||||||
|
const nextCue = cues[Math.min(Math.max(deletedIndex, 0), Math.max(cues.length - 1, 0))] ?? null;
|
||||||
|
return reconcileCues(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
cueMutationInFlight: false,
|
||||||
|
cueDraft: createCueDraft(undefined, state.repository?.scenes.find((scene) => scene.id === state.selectedSceneId)),
|
||||||
|
cueDraftDirty: false,
|
||||||
|
cueState: nextCue ? armCue(state.cueState, nextCue.id) : state.cueState
|
||||||
|
},
|
||||||
|
cues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "metadataSaved":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
repository: state.repository
|
||||||
|
? {
|
||||||
|
...state.repository,
|
||||||
|
submissions: state.repository.submissions.map((submission) =>
|
||||||
|
submission.id === action.submission.id ? action.submission : submission
|
||||||
|
)
|
||||||
|
}
|
||||||
|
: state.repository,
|
||||||
|
metadataDraft: createMetadataDraft(action.submission),
|
||||||
|
metadataDirty: false,
|
||||||
|
metadataSaving: false,
|
||||||
|
metadataHydrationKey: metadataHydrationKey(state.metadataAssetId, action.submission)
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -100,9 +100,14 @@ export const updateCue = async (cueId: string, payload: CueUpsertPayload) =>
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const moveCue = async (cueId: string, payload: CueMovePayload) => {
|
export const moveCue = async (cueId: string, payload: CueMovePayload) =>
|
||||||
await postVoid(`/api/cues/${cueId}/move`, payload);
|
requestJson<Cue[]>(`/api/cues/${cueId}/move`, {
|
||||||
};
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
export const generateCue = async (payload: CueGeneratePayload) =>
|
export const generateCue = async (payload: CueGeneratePayload) =>
|
||||||
requestJson<CueUpsertPayload>("/api/cues/generate", {
|
requestJson<CueUpsertPayload>("/api/cues/generate", {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export interface CreateSubmissionResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateSubmissionInput {
|
export interface CreateSubmissionInput {
|
||||||
|
contributorName?: string;
|
||||||
|
lovedOneName?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
promptAnswer?: string;
|
promptAnswer?: string;
|
||||||
@ -29,7 +31,10 @@ export const createSubmission = async (
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", input.file);
|
formData.append("file", input.file);
|
||||||
formData.append("displayName", input.displayName ?? "");
|
formData.append("contributorName", input.contributorName ?? "");
|
||||||
|
formData.append("lovedOneName", input.lovedOneName ?? "");
|
||||||
|
// Compatibility label for older deployed APIs that only know displayName.
|
||||||
|
formData.append("displayName", input.displayName ?? input.lovedOneName ?? input.contributorName ?? "");
|
||||||
formData.append("caption", input.caption ?? "");
|
formData.append("caption", input.caption ?? "");
|
||||||
formData.append("promptAnswer", input.promptAnswer ?? "");
|
formData.append("promptAnswer", input.promptAnswer ?? "");
|
||||||
formData.append("allowArchive", String(input.allowArchive));
|
formData.append("allowArchive", String(input.allowArchive));
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useState } from "react";
|
|||||||
import { createSubmission } from "./api";
|
import { createSubmission } from "./api";
|
||||||
|
|
||||||
export interface SubmissionFormState {
|
export interface SubmissionFormState {
|
||||||
displayName: string;
|
contributorName: string;
|
||||||
|
lovedOneName: string;
|
||||||
caption: string;
|
caption: string;
|
||||||
promptAnswer: string;
|
promptAnswer: string;
|
||||||
allowArchive: boolean;
|
allowArchive: boolean;
|
||||||
@ -11,7 +12,8 @@ export interface SubmissionFormState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SubmissionFormState = {
|
const initialState: SubmissionFormState = {
|
||||||
displayName: "",
|
contributorName: "",
|
||||||
|
lovedOneName: "",
|
||||||
caption: "",
|
caption: "",
|
||||||
promptAnswer: "",
|
promptAnswer: "",
|
||||||
allowArchive: false,
|
allowArchive: false,
|
||||||
@ -56,7 +58,8 @@ export const useSubmissionForm = () => {
|
|||||||
try {
|
try {
|
||||||
await createSubmission(
|
await createSubmission(
|
||||||
{
|
{
|
||||||
displayName: state.displayName,
|
contributorName: state.contributorName,
|
||||||
|
lovedOneName: state.lovedOneName,
|
||||||
caption: state.caption,
|
caption: state.caption,
|
||||||
promptAnswer: state.promptAnswer,
|
promptAnswer: state.promptAnswer,
|
||||||
allowArchive: state.allowArchive,
|
allowArchive: state.allowArchive,
|
||||||
|
|||||||
@ -32,13 +32,25 @@ export const SubmissionRoute = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="submission-field">
|
<div className="submission-field">
|
||||||
<label htmlFor="displayName">Name or initials (optional)</label>
|
<label htmlFor="contributorName">Your name (optional)</label>
|
||||||
<input
|
<input
|
||||||
id="displayName"
|
id="contributorName"
|
||||||
type="text"
|
type="text"
|
||||||
value={state.displayName}
|
value={state.contributorName}
|
||||||
maxLength={80}
|
maxLength={80}
|
||||||
onChange={(event) => updateField("displayName", event.target.value)}
|
autoComplete="name"
|
||||||
|
onChange={(event) => updateField("contributorName", event.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="submission-field">
|
||||||
|
<label htmlFor="lovedOneName">Name of your loved one (optional)</label>
|
||||||
|
<input
|
||||||
|
id="lovedOneName"
|
||||||
|
type="text"
|
||||||
|
value={state.lovedOneName}
|
||||||
|
maxLength={80}
|
||||||
|
onChange={(event) => updateField("lovedOneName", event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
371
package-lock.json
generated
371
package-lock.json
generated
@ -34,7 +34,8 @@
|
|||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.3",
|
||||||
"@vitejs/plugin-react": "^4.4.1",
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5",
|
||||||
|
"vitest": "^4.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/submission": {
|
"apps/submission": {
|
||||||
@ -1933,6 +1934,13 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tweenjs/tween.js": {
|
"node_modules/@tweenjs/tween.js": {
|
||||||
"version": "23.1.3",
|
"version": "23.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
@ -1985,6 +1993,24 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chai": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/deep-eql": "*",
|
||||||
|
"assertion-error": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/deep-eql": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@ -2073,6 +2099,119 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vitest/expect": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.1.0",
|
||||||
|
"@types/chai": "^5.2.2",
|
||||||
|
"@vitest/spy": "4.1.4",
|
||||||
|
"@vitest/utils": "4.1.4",
|
||||||
|
"chai": "^6.2.2",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/mocker": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/spy": "4.1.4",
|
||||||
|
"estree-walker": "^3.0.3",
|
||||||
|
"magic-string": "^0.30.21"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"msw": "^2.4.9",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"msw": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/pretty-format": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/runner": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/utils": "4.1.4",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/snapshot": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.4",
|
||||||
|
"@vitest/utils": "4.1.4",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"pathe": "^2.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/spy": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vitest/utils": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/pretty-format": "4.1.4",
|
||||||
|
"convert-source-map": "^2.0.0",
|
||||||
|
"tinyrainbow": "^3.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@webgpu/types": {
|
"node_modules/@webgpu/types": {
|
||||||
"version": "0.1.69",
|
"version": "0.1.69",
|
||||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||||
@ -2119,6 +2258,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assertion-error": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/atomic-sleep": {
|
"node_modules/atomic-sleep": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||||
@ -2237,6 +2386,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/chai": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@ -2383,6 +2542,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/es-module-lexer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.27.7",
|
"version": "0.27.7",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||||
@ -2441,6 +2607,26 @@
|
|||||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expect-type": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-decode-uri-component": {
|
"node_modules/fast-decode-uri-component": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
|
||||||
@ -2856,6 +3042,16 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/meshoptimizer": {
|
"node_modules/meshoptimizer": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz",
|
||||||
@ -2932,6 +3128,17 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/obug": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/sxzz",
|
||||||
|
"https://opencollective.com/debug"
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/on-exit-leak-free": {
|
"node_modules/on-exit-leak-free": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||||
@ -2981,6 +3188,13 @@
|
|||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pathe": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@ -3423,6 +3637,13 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/siginfo": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
@ -3472,6 +3693,13 @@
|
|||||||
"node": ">= 10.x"
|
"node": ">= 10.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stackback": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
@ -3481,6 +3709,13 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/std-env": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/thread-stream": {
|
"node_modules/thread-stream": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz",
|
||||||
@ -3499,6 +3734,23 @@
|
|||||||
"integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
|
"integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tinybench": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/tinyexec": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
@ -3516,6 +3768,16 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tinyrainbow": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/toad-cache": {
|
"node_modules/toad-cache": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
|
||||||
@ -4172,6 +4434,96 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vitest": {
|
||||||
|
"version": "4.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz",
|
||||||
|
"integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vitest/expect": "4.1.4",
|
||||||
|
"@vitest/mocker": "4.1.4",
|
||||||
|
"@vitest/pretty-format": "4.1.4",
|
||||||
|
"@vitest/runner": "4.1.4",
|
||||||
|
"@vitest/snapshot": "4.1.4",
|
||||||
|
"@vitest/spy": "4.1.4",
|
||||||
|
"@vitest/utils": "4.1.4",
|
||||||
|
"es-module-lexer": "^2.0.0",
|
||||||
|
"expect-type": "^1.3.0",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"picomatch": "^4.0.3",
|
||||||
|
"std-env": "^4.0.0-rc.1",
|
||||||
|
"tinybench": "^2.9.0",
|
||||||
|
"tinyexec": "^1.0.2",
|
||||||
|
"tinyglobby": "^0.2.15",
|
||||||
|
"tinyrainbow": "^3.1.0",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||||
|
"why-is-node-running": "^2.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vitest": "vitest.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/vitest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@edge-runtime/vm": "*",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||||
|
"@vitest/browser-playwright": "4.1.4",
|
||||||
|
"@vitest/browser-preview": "4.1.4",
|
||||||
|
"@vitest/browser-webdriverio": "4.1.4",
|
||||||
|
"@vitest/coverage-istanbul": "4.1.4",
|
||||||
|
"@vitest/coverage-v8": "4.1.4",
|
||||||
|
"@vitest/ui": "4.1.4",
|
||||||
|
"happy-dom": "*",
|
||||||
|
"jsdom": "*",
|
||||||
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@edge-runtime/vm": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@opentelemetry/api": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-playwright": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-preview": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/browser-webdriverio": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-istanbul": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/coverage-v8": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/ui": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"happy-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jsdom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@ -4187,6 +4539,23 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/why-is-node-running": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"siginfo": "^2.0.0",
|
||||||
|
"stackback": "0.0.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"why-is-node-running": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
38
packages/shared-types/src/cue-order.ts
Normal file
38
packages/shared-types/src/cue-order.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import type { Cue } from "./entities";
|
||||||
|
|
||||||
|
export const indexCueOrder = (cues: Cue[]) =>
|
||||||
|
cues.map((cue, index) => ({
|
||||||
|
...cue,
|
||||||
|
orderIndex: index
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const sortCuesByOrder = (cues: Cue[]) =>
|
||||||
|
indexCueOrder([...cues].sort((left, right) => left.orderIndex - right.orderIndex));
|
||||||
|
|
||||||
|
export const moveCueInOrder = (cues: Cue[], cueId: string, direction: "up" | "down") => {
|
||||||
|
const sorted = sortCuesByOrder(cues);
|
||||||
|
const currentIndex = sorted.findIndex((cue) => cue.id === cueId);
|
||||||
|
if (currentIndex < 0) {
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swapIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
|
||||||
|
if (swapIndex < 0 || swapIndex >= sorted.length) {
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = [...sorted];
|
||||||
|
[next[currentIndex], next[swapIndex]] = [next[swapIndex]!, next[currentIndex]!];
|
||||||
|
return indexCueOrder(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertCueInOrder = (cues: Cue[], cue: Cue) => {
|
||||||
|
const sorted = sortCuesByOrder(cues);
|
||||||
|
const existingIndex = sorted.findIndex((entry) => entry.id === cue.id);
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
sorted.splice(existingIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted.splice(Math.max(0, Math.min(cue.orderIndex, sorted.length)), 0, cue);
|
||||||
|
return indexCueOrder(sorted);
|
||||||
|
};
|
||||||
@ -57,6 +57,9 @@ export interface Submission {
|
|||||||
submittedAt: string;
|
submittedAt: string;
|
||||||
status: SubmissionStatus;
|
status: SubmissionStatus;
|
||||||
consentId: string;
|
consentId: string;
|
||||||
|
contributorName?: string;
|
||||||
|
lovedOneName?: string;
|
||||||
|
/** Legacy/operator label retained for existing runtime data and older clients. */
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
promptAnswer?: string;
|
promptAnswer?: string;
|
||||||
@ -344,6 +347,8 @@ export interface SessionEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SubmissionPayload {
|
export interface SubmissionPayload {
|
||||||
|
contributorName?: string;
|
||||||
|
lovedOneName?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
promptAnswer?: string;
|
promptAnswer?: string;
|
||||||
@ -355,6 +360,8 @@ export interface SubmissionPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SubmissionUpdatePayload {
|
export interface SubmissionUpdatePayload {
|
||||||
|
contributorName?: string;
|
||||||
|
lovedOneName?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
promptAnswer?: string;
|
promptAnswer?: string;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./entities";
|
export * from "./entities";
|
||||||
|
export * from "./cue-order";
|
||||||
export * from "./events";
|
export * from "./events";
|
||||||
export * from "./mock";
|
export * from "./mock";
|
||||||
export * from "./scene-params";
|
export * from "./scene-params";
|
||||||
|
|||||||
@ -174,8 +174,12 @@ const createSubmissionFromMultipart = async (
|
|||||||
|
|
||||||
const storedAssetId = crypto.randomUUID();
|
const storedAssetId = crypto.randomUUID();
|
||||||
const originalKey = await storeUploadedFile(filePart, storedAssetId);
|
const originalKey = await storeUploadedFile(filePart, storedAssetId);
|
||||||
|
const contributorName = fields.contributorName || defaults.contributorName || undefined;
|
||||||
|
const lovedOneName = fields.lovedOneName || defaults.lovedOneName || undefined;
|
||||||
const payload: SubmissionPayload = {
|
const payload: SubmissionPayload = {
|
||||||
displayName: fields.displayName || defaults.displayName || undefined,
|
contributorName,
|
||||||
|
lovedOneName,
|
||||||
|
displayName: fields.displayName || defaults.displayName || lovedOneName || contributorName || undefined,
|
||||||
caption: fields.caption || defaults.caption || undefined,
|
caption: fields.caption || defaults.caption || undefined,
|
||||||
promptAnswer: fields.promptAnswer || defaults.promptAnswer || undefined,
|
promptAnswer: fields.promptAnswer || defaults.promptAnswer || undefined,
|
||||||
allowArchive: defaults.allowArchive ?? coerceBoolean(fields.allowArchive),
|
allowArchive: defaults.allowArchive ?? coerceBoolean(fields.allowArchive),
|
||||||
|
|||||||
@ -10,7 +10,9 @@ import {
|
|||||||
defaultShowConfig,
|
defaultShowConfig,
|
||||||
defaultTags,
|
defaultTags,
|
||||||
flattenSceneParams,
|
flattenSceneParams,
|
||||||
|
indexCueOrder,
|
||||||
mergeSceneParams,
|
mergeSceneParams,
|
||||||
|
sortCuesByOrder,
|
||||||
setSceneParamValue,
|
setSceneParamValue,
|
||||||
type ContributorConsent,
|
type ContributorConsent,
|
||||||
type Cue,
|
type Cue,
|
||||||
@ -174,6 +176,8 @@ const mergeImportedSubmission = (existing: Submission | undefined, imported: Sub
|
|||||||
return {
|
return {
|
||||||
...imported,
|
...imported,
|
||||||
...existing,
|
...existing,
|
||||||
|
contributorName: existing.contributorName ?? imported.contributorName,
|
||||||
|
lovedOneName: existing.lovedOneName ?? imported.lovedOneName,
|
||||||
displayName: existing.displayName ?? imported.displayName,
|
displayName: existing.displayName ?? imported.displayName,
|
||||||
caption: existing.caption ?? imported.caption,
|
caption: existing.caption ?? imported.caption,
|
||||||
promptAnswer: existing.promptAnswer ?? imported.promptAnswer,
|
promptAnswer: existing.promptAnswer ?? imported.promptAnswer,
|
||||||
@ -181,13 +185,7 @@ const mergeImportedSubmission = (existing: Submission | undefined, imported: Sub
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeCueOrder = (cues: Cue[]) =>
|
const normalizeCueOrder = sortCuesByOrder;
|
||||||
[...cues]
|
|
||||||
.sort((left, right) => left.orderIndex - right.orderIndex)
|
|
||||||
.map((cue, index) => ({
|
|
||||||
...cue,
|
|
||||||
orderIndex: index
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ensureSafeCue = (cues: Cue[]) => {
|
const ensureSafeCue = (cues: Cue[]) => {
|
||||||
const safeCue = defaultCueStack.find((cue) => cue.id === defaultShowConfig.safeSceneCueId);
|
const safeCue = defaultCueStack.find((cue) => cue.id === defaultShowConfig.safeSceneCueId);
|
||||||
@ -346,7 +344,10 @@ const buildGeneratedCueDraft = (state: RepositoryState, payload: CueGeneratePayl
|
|||||||
|
|
||||||
const anchorSubmission = state.submissions.find((submission) => submission.id === selectedAssets[0]?.submissionId);
|
const anchorSubmission = state.submissions.find((submission) => submission.id === selectedAssets[0]?.submissionId);
|
||||||
const anchorLabel =
|
const anchorLabel =
|
||||||
anchorSubmission?.caption?.trim() || anchorSubmission?.promptAnswer?.trim() || anchorSubmission?.displayName?.trim();
|
anchorSubmission?.caption?.trim() ||
|
||||||
|
anchorSubmission?.promptAnswer?.trim() ||
|
||||||
|
anchorSubmission?.lovedOneName?.trim() ||
|
||||||
|
anchorSubmission?.displayName?.trim();
|
||||||
const transitionOptions =
|
const transitionOptions =
|
||||||
scene.sceneFamily === "arrival"
|
scene.sceneFamily === "arrival"
|
||||||
? (["shutter_reveal", "mist_reveal", "dissolve"] as const)
|
? (["shutter_reveal", "mist_reveal", "dissolve"] as const)
|
||||||
@ -444,7 +445,13 @@ export class StateStore {
|
|||||||
submittedAt: now,
|
submittedAt: now,
|
||||||
status: "processing",
|
status: "processing",
|
||||||
consentId,
|
consentId,
|
||||||
displayName: input.displayName,
|
contributorName: normalizeEditableText(input.contributorName) || undefined,
|
||||||
|
lovedOneName: normalizeEditableText(input.lovedOneName) || undefined,
|
||||||
|
displayName:
|
||||||
|
normalizeEditableText(input.displayName) ||
|
||||||
|
normalizeEditableText(input.lovedOneName) ||
|
||||||
|
normalizeEditableText(input.contributorName) ||
|
||||||
|
undefined,
|
||||||
caption: input.caption,
|
caption: input.caption,
|
||||||
promptAnswer: input.promptAnswer
|
promptAnswer: input.promptAnswer
|
||||||
};
|
};
|
||||||
@ -489,6 +496,12 @@ export class StateStore {
|
|||||||
throw new Error("Submission not found.");
|
throw new Error("Submission not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, "contributorName")) {
|
||||||
|
submission.contributorName = normalizeEditableText(payload.contributorName);
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, "lovedOneName")) {
|
||||||
|
submission.lovedOneName = normalizeEditableText(payload.lovedOneName);
|
||||||
|
}
|
||||||
if (Object.prototype.hasOwnProperty.call(payload, "displayName")) {
|
if (Object.prototype.hasOwnProperty.call(payload, "displayName")) {
|
||||||
submission.displayName = normalizeEditableText(payload.displayName);
|
submission.displayName = normalizeEditableText(payload.displayName);
|
||||||
}
|
}
|
||||||
@ -671,7 +684,7 @@ export class StateStore {
|
|||||||
sorted.splice(targetIndex, 0, baseCue);
|
sorted.splice(targetIndex, 0, baseCue);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.cues = normalizeCueOrder(sorted);
|
state.cues = indexCueOrder(sorted);
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -698,7 +711,7 @@ export class StateStore {
|
|||||||
const current = sorted[currentIndex]!;
|
const current = sorted[currentIndex]!;
|
||||||
sorted[currentIndex] = sorted[swapIndex]!;
|
sorted[currentIndex] = sorted[swapIndex]!;
|
||||||
sorted[swapIndex] = current;
|
sorted[swapIndex] = current;
|
||||||
state.cues = normalizeCueOrder(sorted);
|
state.cues = indexCueOrder(sorted);
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user