feat: Beute L3–L5 (3D-Szene, Umbauen, Durchsicht, offline) und Diktat (T7 L1)
Beute L3 Runden 2+3 und Review-Fixes: - Glas-Holo ohne warmes Leuchten, Wanderboden-Fuss, Fokusflug mit Geist-Ansicht, Inhalte im Fokus, Rähmchen mit Art-Farbe beim Anheben - Hinweis-Geometrie je Zarge nur einmal je Layout gebaut; Drehen mit Pointer-Capture, endet auch bei Loslassen ausserhalb (rig-pointer.ts) - Tiefenunschärfe ab 55 fps, Leistungsprobe bei jeder Bewegungsstufe, Context-Loss-Listener aufgeräumt, Trefferboxen als skalierte Einheitsbox Beute L4 Umbauen (/colonies/[id]/hive): Entwurf mit Basis-Schnappschuss beim ersten Schritt, Rückgängig/Wiederholen, Hinweis bei Serverstand- Änderung, Konfliktblatt. Beute L5: - Durchsicht-Abschnitt „Beute“: Aufbau-Änderungen in der Durchsicht, Blatt-Formulare stoppen das Submit-Bubbling durch Portale (sonst speicherte „Speichern“ im Blatt die ganze Durchsicht), Fokus nach Rückgängig, Folgevorschläge beim Speichern - Offline: Beute im Offline-Paket, Warteschlange pending-hive-changes (DB v5), Abarbeiten in Reihenfolge, keine Online-Änderung überholt wartende, Hinweis bei abgelehnter Änderung mit „Warteschlange öffnen“ T7 L1 Diktat: Spracherkennungs-Kapsel (Web Speech API), Einwilligung standardmässig aus, Diktat-Knopf an Freitextfeldern, Seite „Sprache & Datenschutz“; axe-core als exakte devDependency. Backend: NotificationsService liest Empfänger und schreibt in einer Transaktion mit FOR KEY SHARE (paralleles Löschen eines Benutzers führte zu P2003 und verzögerten Vorschlägen; Ursache der wackeligen Endtests). Zwei e2e-Prüfungen gegen geteilten DB-Zustand entschärft. Gates: Frontend lint, typecheck, 2740 Tests, build; Backend lint, typecheck, 4266 Unit-Tests, build; parallele e2e 3x grün (978). Browser-Prüfung 1280/390 px, Tastatur, offline, reduced motion. Refs #19 #20 #21 #22 #27 #32 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
ab45d6e24e
commit
1a57de8573
123 files changed
+7639
-355
No files matched your search
@@ -1,11 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ApiError, apiFetch } from "@/lib/auth/api-client";
|
||||
import { listPacks } from "@/lib/apiaries/offline-pack";
|
||||
import { offlinePack, packColony, storedPack } from "@/lib/apiaries/offline-pack.fixtures";
|
||||
import { setupView, systemDto } from "@/components/hive/test-fixtures";
|
||||
import {
|
||||
createHive,
|
||||
fetchHive,
|
||||
fetchHiveAt,
|
||||
fetchHiveHistory,
|
||||
fetchHiveSystems,
|
||||
fetchHiveSystemsWithFallback,
|
||||
fetchHiveTemplate,
|
||||
hiveConflictOf,
|
||||
hiveErrorCode,
|
||||
@@ -18,6 +22,17 @@ vi.mock("@/lib/auth/api-client", async (importOriginal) => {
|
||||
return { ...original, apiFetch: vi.fn() };
|
||||
});
|
||||
|
||||
vi.mock("@/lib/apiaries/offline-pack", () => ({ listPacks: vi.fn() }));
|
||||
|
||||
const NETWORK_DOWN = new TypeError("Failed to fetch");
|
||||
const packWithHive = storedPack(
|
||||
offlinePack({
|
||||
colonies: [packColony("colony-1", "V-1"), packColony("colony-2", "V-2")],
|
||||
hives: { "colony-1": setupView() },
|
||||
hiveCatalog: [systemDto()],
|
||||
}),
|
||||
);
|
||||
|
||||
describe("hive api", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(apiFetch).mockReset();
|
||||
@@ -66,4 +81,49 @@ describe("hive api", () => {
|
||||
expect(hiveErrorCode(new ApiError(404, "nf", { code: "HIVE_NOT_FOUND" }))).toBe("HIVE_NOT_FOUND");
|
||||
expect(hiveErrorCode(new ApiError(500, "x"))).toBeNull();
|
||||
});
|
||||
|
||||
describe("offline reading", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(listPacks).mockReset().mockResolvedValue([packWithHive]);
|
||||
vi.mocked(apiFetch).mockRejectedValue(NETWORK_DOWN);
|
||||
});
|
||||
|
||||
it("answers the hive from the pack when the network fails", async () => {
|
||||
await expect(fetchHive("colony-1", "user-1")).resolves.toEqual(setupView());
|
||||
expect(listPacks).toHaveBeenCalledWith("user-1");
|
||||
});
|
||||
|
||||
it("answers HIVE_NOT_FOUND for a colony the pack knows has no setup", async () => {
|
||||
const error = await fetchHive("colony-2", "user-1").catch((caught: unknown) => caught);
|
||||
expect(error).toBeInstanceOf(ApiError);
|
||||
expect(hiveErrorCode(error)).toBe("HIVE_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("passes the network error on when the pack cannot tell (older pack, unknown colony)", async () => {
|
||||
vi.mocked(listPacks).mockResolvedValue([storedPack(offlinePack())]);
|
||||
await expect(fetchHive("colony-1", "user-1")).rejects.toBe(NETWORK_DOWN);
|
||||
vi.mocked(listPacks).mockResolvedValue([packWithHive]);
|
||||
await expect(fetchHive("unknown", "user-1")).rejects.toBe(NETWORK_DOWN);
|
||||
});
|
||||
|
||||
it("never covers up a server answer with the pack", async () => {
|
||||
vi.mocked(apiFetch).mockRejectedValue(new ApiError(404, "nf", { code: "HIVE_NOT_FOUND" }));
|
||||
await expect(fetchHive("colony-1", "user-1")).rejects.toBeInstanceOf(ApiError);
|
||||
expect(listPacks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reads the server only without a user", async () => {
|
||||
await expect(fetchHive("colony-1")).rejects.toBe(NETWORK_DOWN);
|
||||
expect(listPacks).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the catalog of the pack for the systems", async () => {
|
||||
await expect(fetchHiveSystemsWithFallback("user-1")).resolves.toEqual([systemDto()]);
|
||||
});
|
||||
|
||||
it("passes the network error on when no pack carries a catalog", async () => {
|
||||
vi.mocked(listPacks).mockResolvedValue([storedPack(offlinePack())]);
|
||||
await expect(fetchHiveSystemsWithFallback("user-1")).rejects.toBe(NETWORK_DOWN);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApiError, apiFetch } from "@/lib/auth/api-client";
|
||||
import { idempotencyHeaders } from "@/lib/offline/idempotency";
|
||||
import { withPackFallback } from "@/lib/offline/pack-fallback";
|
||||
import { hiveCatalogFromPacks, hiveFromPacks } from "./pack";
|
||||
import type {
|
||||
CreateHiveInput,
|
||||
HiveAtView,
|
||||
@@ -19,13 +21,41 @@ export const hiveKeys = {
|
||||
|
||||
const colonyPath = (colonyId: string) => `/colonies/${encodeURIComponent(colonyId)}/hive`;
|
||||
|
||||
/** Every system with its part types (usable as a plain queryFn). */
|
||||
export function fetchHiveSystems(): Promise<HiveSystemDto[]> {
|
||||
return apiFetch<HiveSystemDto[]>("/hive-systems");
|
||||
}
|
||||
|
||||
/** 404 HIVE_NOT_FOUND when the colony has no setup yet. */
|
||||
export function fetchHive(colonyId: string): Promise<HiveSetupView> {
|
||||
return apiFetch<HiveSetupView>(colonyPath(colonyId));
|
||||
/**
|
||||
* Like fetchHiveSystems; when the network fails, the catalog of the user's
|
||||
* offline packs answers (spec 10).
|
||||
*/
|
||||
export function fetchHiveSystemsWithFallback(
|
||||
ownerUserId: string | null,
|
||||
fromServer: () => Promise<HiveSystemDto[]> = fetchHiveSystems,
|
||||
): Promise<HiveSystemDto[]> {
|
||||
if (!ownerUserId) return fromServer();
|
||||
return withPackFallback(fromServer, hiveCatalogFromPacks, ownerUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 404 HIVE_NOT_FOUND when the colony has no setup yet. Only when the network
|
||||
* fails, the colony's offline pack answers - also with that 404 when the pack
|
||||
* knows the colony has no setup.
|
||||
*/
|
||||
export function fetchHive(colonyId: string, ownerUserId: string | null = null): Promise<HiveSetupView> {
|
||||
const fromServer = () => apiFetch<HiveSetupView>(colonyPath(colonyId));
|
||||
if (!ownerUserId) return fromServer();
|
||||
return withPackFallback(
|
||||
fromServer,
|
||||
(packs) => {
|
||||
const packed = hiveFromPacks(packs, colonyId);
|
||||
if (!packed) return null;
|
||||
if (!packed.setup) throw new ApiError(404, "Hive not found", { code: "HIVE_NOT_FOUND" });
|
||||
return packed.setup;
|
||||
},
|
||||
ownerUserId,
|
||||
);
|
||||
}
|
||||
|
||||
/** Template of a system with fresh ids; 409 TEMPLATE_NOT_SET while it is empty. */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { apiFetch } from "@/lib/auth/api-client";
|
||||
import type { HiveBoxDepth, HivePartKind, HivePartTypeDto, HiveSystemDto, StateField } from "./types";
|
||||
import type { HiveBoxDepth, HiveTemplate, HivePartKind, HivePartTypeDto, HiveSystemDto, StateField } from "./types";
|
||||
|
||||
// Admin writes of the hive catalog (hive-systems:manage). Reads stay in api.ts
|
||||
// (`fetchHiveSystems`); callers refresh them via `hiveKeys.systems`.
|
||||
@@ -12,8 +12,8 @@ export interface HiveSystemInput {
|
||||
footprintWMm: number;
|
||||
}
|
||||
|
||||
/** Template editing is not part of this screen (it is saved from the editor). */
|
||||
export type HiveSystemPatch = Partial<HiveSystemInput> & { active?: boolean };
|
||||
/** `template` is saved from the hive editor („Als Vorlage speichern“), not from the catalog screen. */
|
||||
export type HiveSystemPatch = Partial<HiveSystemInput> & { active?: boolean; template?: HiveTemplate };
|
||||
|
||||
export interface HivePartTypeInput {
|
||||
kind: HivePartKind;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { offlinePack, packColony, storedPack } from "@/lib/apiaries/offline-pack.fixtures";
|
||||
import { setupView, systemDto } from "@/components/hive/test-fixtures";
|
||||
import { hiveCatalogFromPacks, hiveFromPacks } from "./pack";
|
||||
|
||||
const withHive = storedPack(
|
||||
offlinePack({
|
||||
apiaryId: "apiary-1",
|
||||
colonies: [packColony("c1", "V-1"), packColony("c2", "V-2")],
|
||||
hives: { c1: setupView(undefined, "c1") },
|
||||
hiveCatalog: [systemDto()],
|
||||
}),
|
||||
{ savedAt: "2026-10-13T10:00:00.000Z" },
|
||||
);
|
||||
const oldPack = storedPack(offlinePack({ apiaryId: "apiary-2", colonies: [packColony("c9", "V-9")] }), {
|
||||
savedAt: "2026-10-01T10:00:00.000Z",
|
||||
});
|
||||
|
||||
describe("hive pack readers", () => {
|
||||
it("returns the setup of a colony in the pack", () => {
|
||||
expect(hiveFromPacks([withHive, oldPack], "c1")?.setup?.colonyId).toBe("c1");
|
||||
});
|
||||
|
||||
it("tells a colony without a hive from a pack that cannot tell", () => {
|
||||
expect(hiveFromPacks([withHive], "c2")).toEqual({ setup: null });
|
||||
expect(hiveFromPacks([oldPack], "c9")).toBeNull();
|
||||
expect(hiveFromPacks([withHive], "unknown")).toBeNull();
|
||||
});
|
||||
|
||||
it("prefers the newest pack that lists the colony", () => {
|
||||
const newer = storedPack(
|
||||
offlinePack({
|
||||
apiaryId: "apiary-3",
|
||||
colonies: [packColony("c1", "V-1")],
|
||||
hives: { c1: setupView(undefined, "c1", 9) },
|
||||
}),
|
||||
{ savedAt: "2026-10-14T10:00:00.000Z" },
|
||||
);
|
||||
expect(hiveFromPacks([withHive, newer], "c1")?.setup?.version).toBe(9);
|
||||
});
|
||||
|
||||
it("takes the catalog from the newest pack that has one", () => {
|
||||
expect(hiveCatalogFromPacks([oldPack, withHive])?.[0].id).toBe("sys");
|
||||
expect(hiveCatalogFromPacks([oldPack])).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { StoredPack } from "@/lib/offline/db";
|
||||
import type { HiveSetupView, HiveSystemDto } from "./types";
|
||||
|
||||
// What the offline packs say about hives (spec 10). Both fields are optional:
|
||||
// a pack from before the hive card says nothing, and "nothing" is not "no hive".
|
||||
|
||||
function newestFirst(packs: readonly StoredPack[]): StoredPack[] {
|
||||
return [...packs].sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* The colony's hive per its newest pack. `null`: no pack lists the colony or
|
||||
* the pack predates hives, so it cannot tell. `{ setup: null }`: the pack
|
||||
* knows the colony and the colony has no hive yet.
|
||||
*/
|
||||
export function hiveFromPacks(
|
||||
packs: readonly StoredPack[],
|
||||
colonyId: string,
|
||||
): { setup: HiveSetupView | null } | null {
|
||||
const holding = newestFirst(packs).find((stored) =>
|
||||
stored.pack.colonies.some((colony) => colony.id === colonyId),
|
||||
);
|
||||
if (!holding?.pack.hives) return null;
|
||||
return { setup: holding.pack.hives[colonyId] ?? null };
|
||||
}
|
||||
|
||||
/** The hive systems of the newest pack that carries them; null when none does. */
|
||||
export function hiveCatalogFromPacks(packs: readonly StoredPack[]): HiveSystemDto[] | null {
|
||||
return newestFirst(packs).find((stored) => stored.pack.hiveCatalog)?.pack.hiveCatalog ?? null;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { frames, inlineCatalog, part } from "@/lib/hive/test-utils";
|
||||
import type { HiveOp, HiveSetupView, HiveState } from "@/lib/hive/types";
|
||||
import { deriveHiveFacts } from "./facts";
|
||||
|
||||
type DbModule = typeof import("@/lib/offline/db");
|
||||
type PendingModule = typeof import("./pending");
|
||||
|
||||
const catalog = inlineCatalog();
|
||||
const COLONY = "colony-1";
|
||||
const day = "2026-10-15";
|
||||
|
||||
const state: HiveState = {
|
||||
schemaVersion: 1,
|
||||
stack: [part("f", "floor"), part("b", "box", { frames: frames(10) }), part("r", "roof")],
|
||||
};
|
||||
const setup: HiveSetupView = {
|
||||
colonyId: COLONY,
|
||||
systemId: "sys",
|
||||
version: 1,
|
||||
state,
|
||||
facts: deriveHiveFacts(state, catalog),
|
||||
updatedAt: "2026-10-15T10:00:00.000Z",
|
||||
};
|
||||
const label = (value: string): HiveOp => ({ op: "setLabel", id: "b", label: value });
|
||||
|
||||
let db: DbModule;
|
||||
let pending: PendingModule;
|
||||
|
||||
async function hiveChange(ops: HiveOp[], owner = "user-1", colonyId = COLONY) {
|
||||
await db.queueHiveChange({ colonyId, occurredOn: day, source: "CARD", ops }, `k-${Math.random()}`, owner);
|
||||
}
|
||||
|
||||
const inspection = (colonyId = COLONY, hive?: HiveOp[]) => ({
|
||||
colonyId,
|
||||
date: "2026-10-15T00:00:00.000Z",
|
||||
...(hive ? { hiveChange: { ops: hive } } : {}),
|
||||
});
|
||||
|
||||
/** Entries queued in the same millisecond keep their order only by the clock moving. */
|
||||
async function tick(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3));
|
||||
}
|
||||
|
||||
describe("pending hive changes", () => {
|
||||
beforeEach(async () => {
|
||||
globalThis.indexedDB = new IDBFactory();
|
||||
vi.resetModules();
|
||||
db = await import("@/lib/offline/db");
|
||||
pending = await import("./pending");
|
||||
});
|
||||
|
||||
it("merges both queues in queuedAt order", async () => {
|
||||
await hiveChange([label("eins")]);
|
||||
await tick();
|
||||
await db.queueInspection(inspection(COLONY, [label("zwei")]), "k-i", "user-1");
|
||||
await tick();
|
||||
await hiveChange([label("drei")]);
|
||||
|
||||
const ops = await pending.pendingHiveOps(COLONY, "user-1");
|
||||
|
||||
expect(ops).toEqual([label("eins"), label("zwei"), label("drei")]);
|
||||
});
|
||||
|
||||
it("ignores other colonies, other owners, inspections without hive change and rejected entries", async () => {
|
||||
await hiveChange([label("andere Wabe")], "user-1", "colony-2");
|
||||
await hiveChange([label("fremd")], "user-2");
|
||||
await db.queueInspection(inspection(COLONY), "k-plain", "user-1");
|
||||
await hiveChange([label("abgelehnt")]);
|
||||
const rejected = (await db.getPendingEntries("pending-hive-changes")).find(
|
||||
(entry) => JSON.stringify(entry.input.ops).includes("abgelehnt"),
|
||||
);
|
||||
await db.markFailed("pending-hive-changes", rejected?.id ?? -1, { status: 409, message: "x", at: day });
|
||||
|
||||
expect(await pending.pendingHiveOps(COLONY, "user-1")).toEqual([]);
|
||||
expect(await pending.pendingHiveChanges(COLONY, "user-1")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("classifies an entry as either waiting or rejected, never both", async () => {
|
||||
await hiveChange([label("a")]);
|
||||
expect(await pending.hasRejectedHiveChange(COLONY, "user-1")).toBe(false);
|
||||
const [entry] = await db.getPendingEntries("pending-hive-changes");
|
||||
await db.markFailed("pending-hive-changes", entry.id, { status: 409, message: "x", at: day });
|
||||
expect(await pending.hasRejectedHiveChange(COLONY, "user-1")).toBe(true);
|
||||
expect(await pending.pendingHiveChanges(COLONY, "user-1")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("counts the waiting changes, not their ops", async () => {
|
||||
await hiveChange([label("a"), label("b")]);
|
||||
expect(await pending.pendingHiveChanges(COLONY, "user-1")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("applies waiting ops to the loaded setup", () => {
|
||||
const shown = pending.withPendingOps(setup, [label("Brut")], catalog);
|
||||
|
||||
expect(shown.state.stack[1].label).toBe("Brut");
|
||||
expect(shown.version).toBe(1);
|
||||
});
|
||||
|
||||
it("shows the setup unchanged when the ops do not fit it", () => {
|
||||
const bad: HiveOp = { op: "removePart", id: "gone" };
|
||||
expect(pending.withPendingOps(setup, [bad], catalog)).toBe(setup);
|
||||
expect(pending.withPendingOps(setup, [], catalog)).toBe(setup);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { getPendingEntries, hasHiveChange, isForeignEntry } from "@/lib/offline/db";
|
||||
import type { PendingEntry } from "@/lib/offline/db";
|
||||
import { deriveHiveFacts } from "./facts";
|
||||
import { applyHiveOps } from "./ops";
|
||||
import type { HiveCatalog, HiveOp, HiveSetupView } from "./types";
|
||||
|
||||
/** One waiting change of a colony's hive: a card change, or the hive part of a queued inspection. */
|
||||
export interface PendingHiveChange {
|
||||
ops: HiveOp[];
|
||||
queuedAt: string;
|
||||
}
|
||||
|
||||
function opsOfInspection(input: unknown): HiveOp[] {
|
||||
return (input as { hiveChange?: { ops?: HiveOp[] } }).hiveChange?.ops ?? [];
|
||||
}
|
||||
|
||||
function isWaiting(entry: PendingEntry<unknown>, ownerUserId: string): boolean {
|
||||
// A rejected entry is not waiting for network: the queue sheet shows it.
|
||||
return !isForeignEntry(entry, ownerUserId) && !entry.lastError;
|
||||
}
|
||||
|
||||
/** True when a queued hive change of the colony was refused by the server and still waits for the user. */
|
||||
export async function hasRejectedHiveChange(colonyId: string, ownerUserId: string): Promise<boolean> {
|
||||
const [hive, inspections] = await Promise.all([
|
||||
getPendingEntries("pending-hive-changes"),
|
||||
getPendingEntries("pending-inspections"),
|
||||
]);
|
||||
const rejected = (entry: PendingEntry<{ colonyId: string }>) =>
|
||||
!isForeignEntry(entry, ownerUserId) && entry.input.colonyId === colonyId && !!entry.lastError;
|
||||
return hive.some(rejected) || inspections.some((entry) => rejected(entry) && hasHiveChange(entry.input));
|
||||
}
|
||||
|
||||
/**
|
||||
* True while any hive change of the colony is queued for the user, rejected
|
||||
* ones included: a new change must queue behind it, not overtake it.
|
||||
*/
|
||||
export async function hasQueuedHiveChange(colonyId: string, ownerUserId: string): Promise<boolean> {
|
||||
const [hive, inspections] = await Promise.all([
|
||||
getPendingEntries("pending-hive-changes"),
|
||||
getPendingEntries("pending-inspections"),
|
||||
]);
|
||||
const mine = (entry: PendingEntry<{ colonyId: string }>) =>
|
||||
!isForeignEntry(entry, ownerUserId) && entry.input.colonyId === colonyId;
|
||||
return hive.some(mine) || inspections.some((entry) => mine(entry) && hasHiveChange(entry.input));
|
||||
}
|
||||
|
||||
/**
|
||||
* The colony's waiting hive changes of the user, oldest first, from both
|
||||
* sources: the queue `pending-hive-changes` and the `hiveChange` of queued
|
||||
* inspections.
|
||||
*/
|
||||
export async function pendingHiveChanges(
|
||||
colonyId: string,
|
||||
ownerUserId: string,
|
||||
): Promise<PendingHiveChange[]> {
|
||||
const [hive, inspections] = await Promise.all([
|
||||
getPendingEntries("pending-hive-changes"),
|
||||
getPendingEntries("pending-inspections"),
|
||||
]);
|
||||
const fromHive = hive
|
||||
.filter((entry) => isWaiting(entry, ownerUserId) && entry.input.colonyId === colonyId)
|
||||
.map((entry) => ({ ops: entry.input.ops, queuedAt: entry.queuedAt }));
|
||||
const fromInspections = inspections
|
||||
.filter(
|
||||
(entry) =>
|
||||
isWaiting(entry, ownerUserId) &&
|
||||
entry.input.colonyId === colonyId &&
|
||||
hasHiveChange(entry.input),
|
||||
)
|
||||
.map((entry) => ({ ops: opsOfInspection(entry.input), queuedAt: entry.queuedAt }));
|
||||
return [...fromHive, ...fromInspections].sort((a, b) => a.queuedAt.localeCompare(b.queuedAt));
|
||||
}
|
||||
|
||||
/** All ops of the colony's waiting hive changes, in the order they will be sent. */
|
||||
export async function pendingHiveOps(colonyId: string, ownerUserId: string): Promise<HiveOp[]> {
|
||||
return (await pendingHiveChanges(colonyId, ownerUserId)).flatMap((change) => change.ops);
|
||||
}
|
||||
|
||||
/**
|
||||
* The setup with the waiting ops applied, for the card. When they do not fit
|
||||
* the loaded state (it moved on), the setup is shown as it is - the queue
|
||||
* reports the conflict when it is sent.
|
||||
*/
|
||||
export function withPendingOps(
|
||||
setup: HiveSetupView,
|
||||
ops: readonly HiveOp[],
|
||||
catalog: HiveCatalog,
|
||||
): HiveSetupView {
|
||||
if (ops.length === 0) return setup;
|
||||
const applied = applyHiveOps(setup.state, [...ops], catalog);
|
||||
if (!applied.ok) return setup;
|
||||
return { ...setup, state: applied.state, facts: deriveHiveFacts(applied.state, catalog) };
|
||||
}
|
||||
@@ -66,6 +66,14 @@ export interface HivePart {
|
||||
slot?: number;
|
||||
}
|
||||
|
||||
/** A stored template part: no ids (the server stamps fresh ones when a hive starts from it). */
|
||||
export type HiveTemplatePart = Omit<HivePart, "id" | "inserts"> & { inserts?: HiveTemplatePart[] };
|
||||
|
||||
export interface HiveTemplate {
|
||||
schemaVersion: 1;
|
||||
stack: HiveTemplatePart[];
|
||||
}
|
||||
|
||||
export interface HiveState {
|
||||
schemaVersion: 1;
|
||||
stack: HivePart[];
|
||||
|
||||
Reference in new issue
Block a user