This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import {
|
||||
isModerationCaseOpen,
|
||||
moderationCaseOutcomes,
|
||||
moderationCaseStatuses,
|
||||
moderationCaseStatusLabels,
|
||||
type ModerationCoverage,
|
||||
type ModerationGame,
|
||||
type ModerationScope,
|
||||
type ModerationCase,
|
||||
type ModerationCaseAction
|
||||
type ModerationCase
|
||||
} from '$lib/moderation.js';
|
||||
import { moderationActionName, moderationGameName } from '$ui/moderation.js';
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { moderationGameName } from '$ui/moderation.js';
|
||||
import { serverTitle } from '$ui/server-labels.js';
|
||||
import { confirmAction } from '$ui/confirm.js';
|
||||
import Dialog from '../primitives/Dialog.svelte';
|
||||
@@ -29,7 +26,6 @@
|
||||
admin,
|
||||
seed = {},
|
||||
assignees = [],
|
||||
linkedActions = [],
|
||||
onsubmitted
|
||||
}: {
|
||||
open?: boolean;
|
||||
@@ -40,14 +36,12 @@
|
||||
| 'note'
|
||||
| 'revoke'
|
||||
| 'case-note'
|
||||
| 'resolve'
|
||||
| 'reopen'
|
||||
| 'case-status'
|
||||
| 'edit-case'
|
||||
| 'evidence';
|
||||
coverage: ModerationCoverage[];
|
||||
admin: boolean;
|
||||
assignees?: { id: string; username: string }[];
|
||||
linkedActions?: ModerationCaseAction[];
|
||||
seed?: {
|
||||
game?: ModerationGame;
|
||||
guid?: string;
|
||||
@@ -81,26 +75,10 @@
|
||||
let status = $state(untrack(() => seed.status ?? 'open'));
|
||||
let matchId = $state<number | undefined>(untrack(() => seed.matchId));
|
||||
let message = $state(untrack(() => seed.message ?? ''));
|
||||
const statusUnchanged = $derived(mode === 'case-status' && status === seed.status);
|
||||
const lockedTarget = $derived(mode === 'restriction' && !!seed.caseId);
|
||||
const stateChanged = $derived(mode === 'edit-case' && status !== seed.status);
|
||||
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);
|
||||
@@ -113,8 +91,7 @@
|
||||
note: 'Add private note',
|
||||
revoke: 'Revoke restriction',
|
||||
'case-note': 'Add case note',
|
||||
resolve: 'Review case',
|
||||
reopen: 'Reopen case',
|
||||
'case-status': 'Change status',
|
||||
'edit-case': 'Edit case',
|
||||
evidence: 'Attach archived evidence'
|
||||
}[mode]
|
||||
@@ -184,13 +161,6 @@
|
||||
if (mode === 'note') return { op: 'note', game, guid: guid.trim(), text: note.trim() };
|
||||
if (mode === 'case-note')
|
||||
return { op: 'case-note', id: seed.id, revision: seed.revision, text: note.trim() };
|
||||
if (mode === 'reopen')
|
||||
return {
|
||||
op: 'reopen-case',
|
||||
id: seed.id,
|
||||
revision: seed.revision,
|
||||
reason: reason.trim()
|
||||
};
|
||||
if (mode === 'edit-case')
|
||||
return {
|
||||
op: 'update-case',
|
||||
@@ -201,7 +171,6 @@
|
||||
...(assigneeId !== (seed.assigneeId ?? '')
|
||||
? { assigneeId: assigneeId || null }
|
||||
: {}),
|
||||
...(isModerationCaseOpen(status) ? { status } : {}),
|
||||
reason: reason.trim()
|
||||
};
|
||||
if (mode === 'evidence')
|
||||
@@ -219,14 +188,11 @@
|
||||
reason: reason.trim()
|
||||
};
|
||||
return {
|
||||
op: 'resolve-case',
|
||||
op: 'update-case',
|
||||
id: seed.id,
|
||||
revision: seed.revision,
|
||||
status: resolution,
|
||||
reason: reason.trim(),
|
||||
...(referencedAction
|
||||
? { actionReference: { type: referencedAction.type, id: referencedAction.id } }
|
||||
: {})
|
||||
status,
|
||||
reason: reason.trim()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -392,7 +358,7 @@
|
||||
</p>{/if}
|
||||
{#if lockedTarget}<p class="hint">
|
||||
This restriction will be linked to the case and its recorded player identity.
|
||||
Applying it does not close the case; record the outcome when your review is
|
||||
Applying it does not close the case; change its status when your work is
|
||||
complete.
|
||||
</p>{/if}
|
||||
{#if mode === 'case' || mode === 'edit-case'}
|
||||
@@ -447,26 +413,8 @@
|
||||
/></Field
|
||||
>
|
||||
</div>
|
||||
{#if isModerationCaseOpen(seed.status ?? 'open')}
|
||||
<Field label="Workflow status"
|
||||
><Select
|
||||
bind:value={status}
|
||||
options={[
|
||||
{ value: 'open', label: moderationCaseStatusLabels.open },
|
||||
{ value: 'in_review', label: moderationCaseStatusLabels.in_review },
|
||||
{ value: 'escalated', label: moderationCaseStatusLabels.escalated }
|
||||
]}
|
||||
/></Field
|
||||
>
|
||||
{:else}<p class="hint">
|
||||
The recorded outcome is preserved. Reopen the case to resume review.
|
||||
</p>{/if}
|
||||
<Field
|
||||
label="Change reason"
|
||||
required={stateChanged}
|
||||
hint="Required when changing workflow status."
|
||||
>
|
||||
<Textarea bind:value={reason} required={stateChanged} rows={3} />
|
||||
<Field label="Change reason" hint="Optional note about these edits.">
|
||||
<Textarea bind:value={reason} rows={3} />
|
||||
</Field>
|
||||
{/if}
|
||||
{#if mode === 'evidence'}
|
||||
@@ -481,55 +429,29 @@
|
||||
><Textarea bind:value={message} required rows={4} maxlength={4096} /></Field
|
||||
>
|
||||
{/if}
|
||||
{#if mode === 'resolve'}<Field label="Outcome"
|
||||
{#if mode === 'case-status'}
|
||||
<Field label="Status"
|
||||
><Select
|
||||
bind:value={resolution}
|
||||
options={moderationCaseOutcomes.map((value) => ({
|
||||
bind:value={status}
|
||||
options={moderationCaseStatuses.map((value) => ({
|
||||
value,
|
||||
label: moderationCaseStatusLabels[value]
|
||||
}))}
|
||||
/></Field
|
||||
>{/if}
|
||||
{#if mode === 'resolve'}<p class="hint">
|
||||
This records the decision and closes the case. Apply or revoke restrictions
|
||||
separately, and send warnings through Live action. Choosing an outcome does not
|
||||
change enforcement or send a warning.
|
||||
</p>{/if}
|
||||
{#if mode === 'resolve' && ['warning_issued', 'restriction_applied'].includes(resolution)}
|
||||
<Field
|
||||
label="Supporting action"
|
||||
hint="Optional. Reference an existing linked action as the basis for this outcome."
|
||||
>
|
||||
<Select
|
||||
bind:value={actionReference}
|
||||
options={[
|
||||
{ value: '', label: 'No action reference' },
|
||||
...referenceActions.map((item) => ({
|
||||
value: `${item.type}:${item.id}`,
|
||||
label: `${moderationActionName(item.kind)} · ${$timeFormat.dateTime(item.createdAt)} · ${item.reason}`
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
{#if !referenceActions.length}<p class="hint">
|
||||
{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.
|
||||
</p>{/if}
|
||||
<p class="hint">
|
||||
Track whether this case is open, in progress, or closed. Findings and recorded
|
||||
actions stay in the case history.
|
||||
</p>
|
||||
{/if}
|
||||
{#if mode === 'reopen'}<p class="hint">
|
||||
Reopening returns the case to Open and preserves its evidence, restrictions, and
|
||||
earlier decisions.
|
||||
</p>{/if}
|
||||
{#if ['restriction', 'action', 'revoke', 'resolve', 'reopen'].includes(mode)}
|
||||
{#if ['restriction', 'action', 'revoke', 'case-status'].includes(mode)}
|
||||
<Field
|
||||
label={mode === 'action' && ['message', 'announce'].includes(action)
|
||||
? 'Message'
|
||||
: mode === 'restriction' || mode === 'action'
|
||||
? 'Player-visible reason'
|
||||
: mode === 'reopen'
|
||||
? 'Reopening reason'
|
||||
: mode === 'case-status'
|
||||
? 'Change reason'
|
||||
: 'Review reason'}
|
||||
hint={gameText
|
||||
? `${reasonBytes} / 240 bytes. Some characters use more than one byte.`
|
||||
@@ -561,7 +483,7 @@
|
||||
<div class="actions">
|
||||
<Button disabled={busy} onclick={() => (open = false)}>Cancel</Button><Button
|
||||
type="submit"
|
||||
disabled={!uncertain && actionUnavailable}
|
||||
disabled={!uncertain && (actionUnavailable || statusUnchanged)}
|
||||
variant={mode === 'restriction' || mode === 'revoke' ? 'danger' : 'primary'}
|
||||
loading={busy}>{uncertain ? 'Retry same request' : 'Save'}</Button
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user