From aca728b5579f3172645a4b87ab5268a8ef2d06e4 Mon Sep 17 00:00:00 2001
From: Josh Wright
Date: Sat, 17 Oct 2026 19:42:05 -0400
Subject: [PATCH] Link prior moderation actions to case decisions
---
docs/guides/moderation.md | 9 +
src/lib/moderation.ts | 12 +
src/lib/server/db/migrations.ts | 13 +-
src/lib/server/moderation/actions.ts | 62 +++-
src/lib/server/moderation/case-actions.ts | 92 +++++
src/lib/server/moderation/cases.ts | 13 +-
src/lib/server/moderation/schema.ts | 16 +
.../moderation/LinkCaseActionDialog.svelte | 161 +++++++++
.../moderation/ModerationDialog.svelte | 51 ++-
src/lib/ui/moderation.ts | 20 ++
src/routes/moderation/cases/[id]/+page.svelte | 66 +++-
tests/client/moderation-case-page.test.ts | 119 ++++++-
tests/unit/moderation-case-actions.test.ts | 331 ++++++++++++++++++
13 files changed, 948 insertions(+), 17 deletions(-)
create mode 100644 src/lib/server/moderation/case-actions.ts
create mode 100644 src/lib/ui/components/moderation/LinkCaseActionDialog.svelte
create mode 100644 tests/unit/moderation-case-actions.test.ts
diff --git a/docs/guides/moderation.md b/docs/guides/moderation.md
index efe2d5b..9191bb0 100644
--- a/docs/guides/moderation.md
+++ b/docs/guides/moderation.md
@@ -149,6 +149,15 @@ the required permission and reason, including after a case closes. Reopening a
case also leaves its restrictions in place. Check enforcement coverage after
applying or revoking a restriction.
+Use **Link existing action** to associate a warning, kick, private message, ban, or
+mute already recorded for this player with the case. This preserves the original
+action, author, time, delivery state, and expiry; it does not issue another action
+or extend a restriction. The same action can support more than one related case.
+Linking requires the current case revision and a reason, and appears in its timeline.
+When closing with **Warning issued** or **Restriction applied**, optionally reference
+one of the linked records. Warning references require a confirmed warning; failed
+or uncertain delivery remains visible without being presented as a confirmed warning.
+
Staff notes are explicitly labeled as notes. In match chat, select up to 100
messages and choose **Attach to case** to open a case or add them to an existing
case for the selected subject. Select at least one message from that subject;
diff --git a/src/lib/moderation.ts b/src/lib/moderation.ts
index 652d5fd..da70f49 100644
--- a/src/lib/moderation.ts
+++ b/src/lib/moderation.ts
@@ -125,11 +125,23 @@ export interface ModerationCaseDetail {
case: ModerationCase;
player: ModerationPlayer;
sanctions: ModerationSanction[];
+ linkedActions?: ModerationCaseAction[];
+ availableActions?: ModerationCaseAction[];
events: ModerationEvent[];
coverage: ModerationCoverage[];
assignees: { id: string; username: string }[];
checkedAt: string;
}
+export interface ModerationCaseAction {
+ id: string;
+ type: 'restriction' | 'action';
+ kind: string;
+ reason: string;
+ actor: string;
+ createdAt: string;
+ state: string;
+ expiresAt: string | null;
+}
export interface ModerationEvent {
id: number;
game: ModerationGame | null;
diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts
index ac519d4..2abffa2 100644
--- a/src/lib/server/db/migrations.ts
+++ b/src/lib/server/db/migrations.ts
@@ -28,7 +28,11 @@ import { migrateBank } from './bank.js';
import { migrateAfk } from './afk.js';
import { migrateBankCredentials } from './bank-credentials.js';
import { migrateTemperatures } from '../hosts/temperatures.js';
-import { migrateModeration, migrateModerationCases } from '../moderation/schema.js';
+import {
+ migrateModeration,
+ migrateModerationCases,
+ migrateModerationCaseActions
+} from '../moderation/schema.js';
import type { Migration } from './migration-runner.js';
// Append only: never renumber entries or change an applied migration to add an
@@ -170,7 +174,12 @@ export const migrations: readonly Migration[] = [
mode: 'foreign-keys-off',
run: migrateModerationCases
},
- { version: 30, name: 'persistent AFK reserves and gameplay rewards', run: migrateAfk }
+ { version: 30, name: 'persistent AFK reserves and gameplay rewards', run: migrateAfk },
+ {
+ version: 31,
+ name: 'moderation case links to existing actions',
+ run: migrateModerationCaseActions
+ }
];
export const databaseVersion = migrations.length;
diff --git a/src/lib/server/moderation/actions.ts b/src/lib/server/moderation/actions.ts
index 302a7d4..8f25a7d 100644
--- a/src/lib/server/moderation/actions.ts
+++ b/src/lib/server/moderation/actions.ts
@@ -1,6 +1,7 @@
import { randomUUID } from 'node:crypto';
import { db } from '../db/index.js';
import { archivedEvidenceSnapshots } from './evidence.js';
+import { caseActionReference, recordedCaseAction, isCaseActionLinked } from './case-actions.js';
import { AuthError } from '../auth/index.js';
import { moderatorMaximumRestrictionMs, type AuthUser } from '../../auth.js';
import {
@@ -220,7 +221,13 @@ function revokeSanction(actor: AuthUser, input: Record, now: st
.run(now, id);
advancePolicy();
moderationEvent(actor, 'restriction.revoked', row, id, reason, now);
- if (row.case_id) touchCase(row.case_id, now);
+ db()
+ .prepare(
+ `UPDATE moderation_cases SET updated_at=?,revision=revision+1
+ WHERE id=? OR id IN (SELECT case_id FROM moderation_case_action_links
+ WHERE action_type='restriction' AND action_id=?)`
+ )
+ .run(now, row.case_id, id);
return { id };
}
function liveAction(actor: AuthUser, input: Record, now: string) {
@@ -465,6 +472,27 @@ export function mutateModeration(actorId: string, input: Record
if (input.op === 'action') return liveAction(actor, input, now);
if (input.op === 'create-case') return createCase(actor, input, now);
if (input.op === 'update-case') return updateCase(actor, input, now);
+ if (input.op === 'link-case-action') {
+ const row = caseRow(input.id);
+ checkCaseRevision(row, input.revision);
+ const reference = caseActionReference({ type: input.actionType, id: input.actionId });
+ const reason = textField(input.reason, 'Link reason', 4000);
+ recordedCaseAction(row, reference, now);
+ if (isCaseActionLinked(row.id, reference)) return { id: row.id };
+ db()
+ .prepare('INSERT INTO moderation_case_action_links VALUES(?,?,?,?,?)')
+ .run(row.id, reference.type, reference.id, actor.username, now);
+ touchCase(row.id, now);
+ moderationEvent(
+ actor,
+ 'case.action_linked',
+ row,
+ row.id,
+ JSON.stringify({ actionReference: reference, reason }),
+ now
+ );
+ return { id: row.id };
+ }
if (input.op === 'case-note') {
const row = caseRow(input.id);
if (input.revision !== undefined) checkCaseRevision(row, input.revision);
@@ -481,13 +509,43 @@ export function mutateModeration(actorId: string, input: Record
const outcome = moderationCaseOutcomes.find((status) => status === input.status);
if (!outcome) throw new AuthError('Choose a case outcome.');
const reason = textField(input.reason, 'Decision reason', 4000);
+ const reference =
+ input.actionReference === undefined || input.actionReference === null
+ ? null
+ : caseActionReference(input.actionReference);
+ if (reference) {
+ const action = recordedCaseAction(row, reference, now);
+ if (!isCaseActionLinked(row.id, reference))
+ throw new AuthError(
+ 'Link the recorded action to this case before citing it.',
+ 409
+ );
+ if (
+ outcome === 'warning_issued' &&
+ (action.type !== 'action' ||
+ action.kind !== 'warn' ||
+ action.state !== 'confirmed')
+ )
+ throw new AuthError('Warning issued must reference a confirmed warning.');
+ if (outcome === 'restriction_applied' && action.type !== 'restriction')
+ throw new AuthError(
+ 'Restriction applied must reference a recorded restriction.'
+ );
+ }
evidence(actor, row.id, reason, moderationCaseStatusLabels[outcome], null, now, true);
db()
.prepare(
'UPDATE moderation_cases SET status=?,updated_at=?,revision=revision+1 WHERE id=?'
)
.run(outcome, now, row.id);
- moderationEvent(actor, `case.${outcome}`, row, row.id, reason, now);
+ moderationEvent(
+ actor,
+ `case.${outcome}`,
+ row,
+ row.id,
+ reference ? JSON.stringify({ reason, actionReference: reference }) : reason,
+ now
+ );
return { id: row.id };
}
if (input.op === 'reopen-case') {
diff --git a/src/lib/server/moderation/case-actions.ts b/src/lib/server/moderation/case-actions.ts
new file mode 100644
index 0000000..7b0eb38
--- /dev/null
+++ b/src/lib/server/moderation/case-actions.ts
@@ -0,0 +1,92 @@
+import { db } from '../db/index.js';
+import { AuthError } from '../auth/index.js';
+import { textField } from './core.js';
+import type { ModerationCaseAction, ModerationGame } from '../../moderation.js';
+
+type Subject = { game: ModerationGame; guid: string };
+export type CaseActionReference = Pick;
+
+export function caseActionReference(value: unknown): CaseActionReference {
+ if (!value || typeof value !== 'object') throw new AuthError('Choose a recorded action.');
+ const reference = value as Record;
+ if (reference.type !== 'action' && reference.type !== 'restriction')
+ throw new AuthError('Choose a recorded warning, action, or restriction.');
+ return { type: reference.type, id: textField(reference.id, 'Action ID', 128) };
+}
+
+const restrictionColumns = `id,'restriction' AS type,kind,reason,actor,
+ created_at AS createdAt,expires_at AS expiresAt,
+ CASE WHEN revoked_at IS NOT NULL THEN 'revoked'
+ WHEN expires_at IS NOT NULL AND expires_at<=? THEN 'expired' ELSE 'active' END AS state`;
+const actionColumns = `id,'action' AS type,kind,reason,actor,
+ created_at AS createdAt,expires_at AS expiresAt,state`;
+
+/** Only actual records for this account can become case evidence. */
+export function recordedCaseAction(
+ subject: Subject,
+ reference: CaseActionReference,
+ now: string
+): ModerationCaseAction {
+ const handle = db();
+ const action =
+ reference.type === 'restriction'
+ ? handle
+ .prepare(
+ `SELECT ${restrictionColumns} FROM moderation_sanctions
+ WHERE id=? AND guid=? AND (game=? OR scope='fleet')`
+ )
+ .get(now, reference.id, subject.guid, subject.game)
+ : handle
+ .prepare(
+ `SELECT ${actionColumns} FROM moderation_actions
+ WHERE id=? AND guid=? AND game=?`
+ )
+ .get(reference.id, subject.guid, subject.game);
+ if (!action)
+ throw new AuthError('The recorded action is unavailable for this case’s player.', 404);
+ return action as ModerationCaseAction;
+}
+
+export function isCaseActionLinked(caseId: string, reference: CaseActionReference): boolean {
+ return Boolean(
+ db()
+ .prepare(
+ `SELECT 1 FROM moderation_case_action_links
+ WHERE case_id=? AND action_type=? AND action_id=?
+ UNION ALL SELECT 1 FROM moderation_sanctions
+ WHERE ?='restriction' AND id=? AND case_id=? LIMIT 1`
+ )
+ .get(caseId, reference.type, reference.id, reference.type, reference.id, caseId)
+ );
+}
+
+export function caseActionLists(caseId: string, subject: Subject, now: string) {
+ const handle = db();
+ const records = handle
+ .prepare(
+ `SELECT ${restrictionColumns},
+ (case_id=? OR EXISTS (SELECT 1 FROM moderation_case_action_links l
+ WHERE l.case_id=? AND l.action_type='restriction' AND l.action_id=moderation_sanctions.id)) AS linked
+ FROM moderation_sanctions WHERE guid=? AND (game=? OR scope='fleet')
+ UNION ALL SELECT ${actionColumns},
+ EXISTS (SELECT 1 FROM moderation_case_action_links l
+ WHERE l.case_id=? AND l.action_type='action' AND l.action_id=moderation_actions.id) AS linked
+ FROM moderation_actions WHERE guid=? AND game=?
+ ORDER BY createdAt DESC,id`
+ )
+ .all(
+ now,
+ caseId,
+ caseId,
+ subject.guid,
+ subject.game,
+ caseId,
+ subject.guid,
+ subject.game
+ ) as Array;
+ const linkedActions: ModerationCaseAction[] = [];
+ const availableActions: ModerationCaseAction[] = [];
+ for (const { linked, ...record } of records)
+ (linked ? linkedActions : availableActions).push(record);
+ return { linkedActions, availableActions };
+}
diff --git a/src/lib/server/moderation/cases.ts b/src/lib/server/moderation/cases.ts
index f8b39f5..4841f42 100644
--- a/src/lib/server/moderation/cases.ts
+++ b/src/lib/server/moderation/cases.ts
@@ -2,6 +2,7 @@ import { db } from '../db/index.js';
import { AuthError } from '../auth/index.js';
import { moderationActor, textField } from './core.js';
import { moderationOverview } from './overview.js';
+import { caseActionLists } from './case-actions.js';
import type {
ModerationCase,
ModerationCaseDetail,
@@ -42,9 +43,11 @@ export function moderationCaseDetail(actorId: string, id: string): ModerationCas
.prepare(
`SELECT id,case_id AS caseId,game,guid,kind,scope,server,reason,note,
created_at AS createdAt,expires_at AS expiresAt,revoked_at AS revokedAt,actor_id AS actorId,actor,revision
- FROM moderation_sanctions WHERE case_id=? ORDER BY created_at DESC,id`
+ FROM moderation_sanctions WHERE case_id=? OR id IN
+ (SELECT action_id FROM moderation_case_action_links WHERE case_id=? AND action_type='restriction')
+ ORDER BY created_at DESC,id`
)
- .all(caseId) as Omit[]
+ .all(caseId, caseId) as Omit[]
).map((restriction) => ({
...restriction,
active:
@@ -55,9 +58,10 @@ export function moderationCaseDetail(actorId: string, id: string): ModerationCas
.prepare(
`SELECT id,game,guid,action,subject_id AS subjectId,actor,detail,created_at AS createdAt
FROM moderation_events WHERE subject_id=? OR subject_id IN
- (SELECT id FROM moderation_sanctions WHERE case_id=?) ORDER BY id DESC`
+ (SELECT id FROM moderation_sanctions WHERE case_id=?) OR subject_id IN
+ (SELECT action_id FROM moderation_case_action_links WHERE case_id=?) ORDER BY id DESC`
)
- .all(caseId, caseId) as ModerationEvent[];
+ .all(caseId, caseId, caseId) as ModerationEvent[];
const assignees = handle
.prepare(
`SELECT id,username FROM auth_users WHERE role IN ('admin','moderator')
@@ -77,6 +81,7 @@ export function moderationCaseDetail(actorId: string, id: string): ModerationCas
lastSeen: null
},
sanctions,
+ ...caseActionLists(caseId, row, overview.checkedAt),
events,
coverage: overview.coverage,
assignees,
diff --git a/src/lib/server/moderation/schema.ts b/src/lib/server/moderation/schema.ts
index 9259434..d753171 100644
--- a/src/lib/server/moderation/schema.ts
+++ b/src/lib/server/moderation/schema.ts
@@ -149,3 +149,19 @@ export function migrateModerationCases(handle: Database.Database): void {
CREATE INDEX IF NOT EXISTS moderation_events_subject_id ON moderation_events(subject_id,id);
`);
}
+
+/** Links record prior enforcement without changing its original case or issuing it again. */
+export function migrateModerationCaseActions(handle: Database.Database): void {
+ handle.exec(`
+ CREATE TABLE IF NOT EXISTS moderation_case_action_links (
+ case_id TEXT NOT NULL REFERENCES moderation_cases(id),
+ action_type TEXT NOT NULL CHECK(action_type IN ('action','restriction')),
+ action_id TEXT NOT NULL,
+ actor TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ PRIMARY KEY(case_id,action_type,action_id)
+ );
+ CREATE INDEX IF NOT EXISTS moderation_case_action_source
+ ON moderation_case_action_links(action_type,action_id);
+ `);
+}
diff --git a/src/lib/ui/components/moderation/LinkCaseActionDialog.svelte b/src/lib/ui/components/moderation/LinkCaseActionDialog.svelte
new file mode 100644
index 0000000..791845b
--- /dev/null
+++ b/src/lib/ui/components/moderation/LinkCaseActionDialog.svelte
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
diff --git a/src/lib/ui/components/moderation/ModerationDialog.svelte b/src/lib/ui/components/moderation/ModerationDialog.svelte
index 59d5427..e8921bc 100644
--- a/src/lib/ui/components/moderation/ModerationDialog.svelte
+++ b/src/lib/ui/components/moderation/ModerationDialog.svelte
@@ -7,9 +7,11 @@
type ModerationCoverage,
type ModerationGame,
type ModerationScope,
- type ModerationCase
+ type ModerationCase,
+ type ModerationCaseAction
} from '$lib/moderation.js';
- import { moderationGameName } from '$ui/moderation.js';
+ import { moderationActionName, moderationGameName } from '$ui/moderation.js';
+ import { timeFormat } from '$ui/stores/time-zone.js';
import { serverTitle } from '$ui/server-labels.js';
import { confirmAction } from '$ui/confirm.js';
import Dialog from '../primitives/Dialog.svelte';
@@ -26,6 +28,7 @@
admin,
seed = {},
assignees = [],
+ linkedActions = [],
onsubmitted
}: {
open?: boolean;
@@ -43,6 +46,7 @@
coverage: ModerationCoverage[];
admin: boolean;
assignees?: { id: string; username: string }[];
+ linkedActions?: ModerationCaseAction[];
seed?: {
game?: ModerationGame;
guid?: string;
@@ -81,6 +85,21 @@
const reasonBytes = $derived(new TextEncoder().encode(reason.trim()).length);
const gameText = $derived(mode === 'restriction' || mode === 'action');
let resolution = $state('resolved');
+ let actionReference = $state('');
+ const referenceActions = $derived(
+ linkedActions.filter((item) =>
+ resolution === 'warning_issued'
+ ? item.type === 'action' && item.kind === 'warn' && item.state === 'confirmed'
+ : resolution === 'restriction_applied' && item.type === 'restriction'
+ )
+ );
+ const referencedAction = $derived(
+ referenceActions.find((item) => `${item.type}:${item.id}` === actionReference)
+ );
+ $effect(() => {
+ void resolution;
+ actionReference = '';
+ });
let busy = $state(false);
let error = $state('');
let uncertain = $state(false);
@@ -203,7 +222,10 @@
id: seed.id,
revision: seed.revision,
status: resolution,
- reason: reason.trim()
+ reason: reason.trim(),
+ ...(referencedAction
+ ? { actionReference: { type: referencedAction.type, id: referencedAction.id } }
+ : {})
};
}
@@ -468,6 +490,29 @@
separately, and send warnings through Live action. Choosing an outcome does not
change enforcement or send a warning.
{/if}
+ {#if mode === 'resolve' && ['warning_issued', 'restriction_applied'].includes(resolution)}
+
+ ({
+ value: `${item.type}:${item.id}`,
+ label: `${moderationActionName(item.kind)} · ${$timeFormat.dateTime(item.createdAt)} · ${item.reason}`
+ }))
+ ]}
+ />
+
+ {#if !referenceActions.length}
+ {resolution === 'warning_issued'
+ ? 'No confirmed warnings are linked to this case.'
+ : 'No restrictions are linked to this case.'} Use “Link existing action” on
+ the case to add a prior action.
+
{/if}
+ {/if}
{#if mode === 'reopen'}
Reopening returns the case to Open and preserves its evidence, restrictions, and
earlier decisions.
diff --git a/src/lib/ui/moderation.ts b/src/lib/ui/moderation.ts
index 23f2efa..e48f8ed 100644
--- a/src/lib/ui/moderation.ts
+++ b/src/lib/ui/moderation.ts
@@ -70,10 +70,18 @@ export const moderationActionLabels: Record = {
'case.updated': 'Case updated',
'case.evidence_attached': 'Evidence attached',
'case.restriction': 'Restriction linked to case',
+ 'case.action_linked': 'Existing action linked to case',
+ 'link-case-action': 'Existing action linked to case',
'player.note': 'Private player note',
'runtime.configure': 'Integration configuration requested'
};
+export function moderationActionName(kind: string): string {
+ return (
+ moderationActionLabels[kind] ?? (kind === 'ban' ? 'Ban' : kind === 'mute' ? 'Mute' : kind)
+ );
+}
+
export function moderationEventDetail(detail: string, action: string): string {
if (action.startsWith('case.')) {
try {
@@ -110,6 +118,18 @@ export function moderationEventDetail(detail: string, action: string): string {
return `${value.count} archived chat ${value.count === 1 ? 'entry' : 'entries'} attached from match #${value.matchId}.`;
if (action === 'case.restriction' && typeof value.reason === 'string')
return `${value.kind === 'ban' ? 'Ban' : 'Mute'} · ${display(value.scope)} scope\n${value.reason}`;
+ if (
+ typeof value.reason === 'string' &&
+ value.actionReference &&
+ typeof value.actionReference === 'object'
+ ) {
+ const reference = value.actionReference as { type?: unknown; id?: unknown };
+ if (
+ (reference.type === 'restriction' || reference.type === 'action') &&
+ typeof reference.id === 'string'
+ )
+ return `${value.reason}\nReferenced ${reference.type === 'restriction' ? 'restriction' : 'action'}: ${reference.id}`;
+ }
} catch {
// Older case events and decision reasons are plain staff-authored text.
}
diff --git a/src/routes/moderation/cases/[id]/+page.svelte b/src/routes/moderation/cases/[id]/+page.svelte
index cf2bc04..14136e7 100644
--- a/src/routes/moderation/cases/[id]/+page.svelte
+++ b/src/routes/moderation/cases/[id]/+page.svelte
@@ -12,6 +12,7 @@
import { contextHref } from '$lib/navigation-context.js';
import {
moderationActionLabels,
+ moderationActionName,
moderationEventDetail,
moderationGameName,
moderationHref
@@ -29,6 +30,7 @@
import ServerIdentity from '$ui/components/ServerIdentity.svelte';
import ModerationDialog from '$ui/components/moderation/ModerationDialog.svelte';
import EvidenceMessages from '$ui/components/moderation/EvidenceMessages.svelte';
+ import LinkCaseActionDialog from '$ui/components/moderation/LinkCaseActionDialog.svelte';
import { preservedConversation } from '$ui/moderation-evidence.js';
let { data }: { data: { moderationCase: ModerationCaseDetail } } = $props();
@@ -55,6 +57,7 @@
let generation = 0;
let controller: AbortController | undefined;
let dialogOpen = $state(false);
+ let linkActionOpen = $state(false);
let dialogKey = $state(0);
let mode = $state['mode']>('case-note');
let seed = $state['seed']>>({});
@@ -67,6 +70,7 @@
refreshError = '';
notice = '';
dialogOpen = false;
+ linkActionOpen = false;
updatedAt = Date.parse(initial.checkedAt);
});
});
@@ -149,6 +153,12 @@
cancelRefresh();
void refresh();
}
+ function actionLinked() {
+ notice =
+ 'Existing action linked to this case. Its original date and outcome are preserved.';
+ cancelRefresh();
+ void refresh();
+ }
function archiveContext(raw: string | null | undefined) {
if (!raw) return null;
@@ -218,8 +228,8 @@
{#if !permitted}
- Read-only moderation access. You can inspect this case, its evidence, and linked
- restrictions.
+ Read-only moderation access. You can inspect this case, its evidence, and linked actions
+ and restrictions.
{/if}
{#if notice}{notice}
{/if}
{#if refreshError}
@@ -400,6 +410,45 @@
>
+
+
+
Linked actions
+ {#if permitted} (linkActionOpen = true)}>Link existing action {/if}
+
+ {#if report.linkedActions?.length}
+ {#each report.linkedActions as action (`${action.type}:${action.id}`)}
+
+ {moderationActionName(action.kind)}
+
+ {action.reason}
+
+ {action.actor} ·
+
+
+ {/each}
+ {:else}
+ No existing actions are linked to this case.
+
{/if}
+ {#if permitted}
+ {report.availableActions?.length
+ ? `${report.availableActions.length} previous actions available for this player.`
+ : 'No additional recorded actions are available for this player.'}
+
{/if}
+
+
Linked restrictions
@@ -466,9 +515,7 @@
})}>Revoke restriction{/if}
{/each}
- {:else}
- No restrictions have been applied from this case.
-
{/if}
+ {:else}
No restrictions are linked to this case.
{/if}
Applying or revoking a restriction does not change the case outcome.
@@ -506,9 +553,18 @@
{admin}
coverage={report.coverage}
assignees={report.assignees}
+ linkedActions={report.linkedActions}
onsubmitted={submitted}
/>{/key}{/if}
+{#if permitted && linkActionOpen}
{/if}
+