import type { ConcordReplayPlugin } from "@ternent/concord"; import { createRuntimePrivacyService } from "../runtime/privacy"; import type { SystemPermissionsState } from "../system/permissions"; export type TaskAudienceType = "everyone" | "user" | "permission"; export type TaskRecord = { id: string; title: string; columnId: string; audienceType: TaskAudienceType; audienceId: string | null; createdBy: string; }; export type TasksState = { byId: Record; order: string[]; }; export function selectTaskById(state: TasksState, taskId: string): TaskRecord | null { return state.byId[taskId] ?? null; } export function selectVisibleTasks(input: { tasks: TasksState; permissions: SystemPermissionsState; viewerIdentityKey: string | null; }): TaskRecord[] { return input.tasks.order .map((taskId) => input.tasks.byId[taskId]) .filter((task): task is NonNullable => Boolean(task)) .filter((task) => { if (task.audienceType === "everyone") { return true; } if (!input.viewerIdentityKey) { return false; } if (task.audienceType === "user") { return task.audienceId === input.viewerIdentityKey; } if (!task.audienceId) { return false; } return Boolean( input.permissions.byId[task.audienceId]?.memberIdentityKeys.includes(input.viewerIdentityKey), ); }); } const privacy = createRuntimePrivacyService(); export function createTasksPlugin(): ConcordReplayPlugin { return { id: "tasks", initialState: () => ({ byId: {}, order: [] }), commands: { "task.create": async (_ctx, input: TaskRecord) => { const protection = privacy.resolveAudience( { audienceType: input.audienceType, audienceId: input.audienceId, }, { byId: {}, order: [] }, ); return { kind: "task.create", payload: input, protection, }; }, "task.move": async (_ctx, input: { taskId: string; columnId: string }) => ({ kind: "task.move", payload: input, }), "task.rename": async (_ctx, input: { taskId: string; title: string }) => ({ kind: "task.rename", payload: input, }), }, applyEntry(entry, ctx) { const state = ctx.getState(); if (entry.kind === "task.create" && entry.payload.type === "plain") { const task = entry.payload.data as TaskRecord; ctx.setState({ byId: { ...state.byId, [task.id]: task, }, order: state.order.includes(task.id) ? state.order : [...state.order, task.id], }); } if (entry.kind === "task.move" && entry.payload.type === "plain") { const payload = entry.payload.data as { taskId: string; columnId: string }; const current = state.byId[payload.taskId]; if (!current) { return; } ctx.setState({ byId: { ...state.byId, [payload.taskId]: { ...current, columnId: payload.columnId, }, }, order: [...state.order], }); } if (entry.kind === "task.rename" && entry.payload.type === "plain") { const payload = entry.payload.data as { taskId: string; title: string }; const current = state.byId[payload.taskId]; if (!current) { return; } ctx.setState({ byId: { ...state.byId, [payload.taskId]: { ...current, title: payload.title, }, }, order: [...state.order], }); } }, }; }