Fix talent outcome validation feedback
This commit is contained in:
@@ -9,11 +9,34 @@
|
||||
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
|
||||
import MechanicProgramEditor from '$lib/components/isac/MechanicProgramEditor.svelte';
|
||||
import type { ConditionalMechanicDraftV1 } from '$lib/calculations/mechanic-editor';
|
||||
import { tick } from 'svelte';
|
||||
let { data, form } = $props();
|
||||
const categoryOptions = ['Weapon Talent 1', 'Weapon Talent 2', 'Backpack', 'Body Armor'].map((value) => ({ value, label: value }));
|
||||
let talent = $state<any>(null);
|
||||
let handledResultToken = $state('');
|
||||
let iconResetToken = $state(0);
|
||||
let validationSummary = $state<HTMLElement | null>(null);
|
||||
type TalentErrorArea = 'icon' | 'identity' | 'description' | 'mechanic' | 'outcomes';
|
||||
type TalentValidationError = { area: TalentErrorArea; message: string };
|
||||
const actionForm = $derived(form as Record<string, any> | null | undefined);
|
||||
const talentErrors = $derived(
|
||||
Array.isArray(actionForm?.talentErrors)
|
||||
? actionForm.talentErrors as TalentValidationError[]
|
||||
: actionForm?.talentError
|
||||
? [{ area: 'mechanic', message: String(actionForm.talentError) }] as TalentValidationError[]
|
||||
: []
|
||||
);
|
||||
const errorsFor = (area: TalentErrorArea) => talentErrors.filter((error) => error.area === area);
|
||||
const hasErrors = (areas: TalentErrorArea[]) => areas.some((area) => errorsFor(area).length > 0);
|
||||
const areaLabel = (area: TalentErrorArea) => ({
|
||||
icon: 'Talent icon',
|
||||
identity: 'Talent identity',
|
||||
description: 'Description template',
|
||||
mechanic: 'Mechanic program',
|
||||
outcomes: 'Mechanic outcomes'
|
||||
}[area]);
|
||||
const areaTarget = (area: TalentErrorArea) =>
|
||||
area === 'outcomes' ? 'talent-mechanic' : `talent-${area}`;
|
||||
const blankRule = (): ConditionalMechanicDraftV1 => ({
|
||||
version: 1,
|
||||
simulationReadiness: 'captured_only',
|
||||
@@ -81,6 +104,8 @@
|
||||
if (form.talentSaved) {
|
||||
if (form.talentMode === 'add') iconResetToken += 1;
|
||||
talent = form.talentMode === 'add' ? blankTalent() : null;
|
||||
} else if (talentErrors.length) {
|
||||
void tick().then(() => validationSummary?.focus());
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -95,21 +120,43 @@
|
||||
<div class="editor-title"><div><p class="eyebrow">{talent.id ? 'MODIFY TALENT' : 'NEW TALENT'}</p><h3>{talent.id ? talent.name : 'TALENT DEFINITION'}</h3></div><button aria-label="Close talent editor" onclick={() => talent = null}>×</button></div>
|
||||
<PendingForm action="?/saveTalent" enctype="multipart/form-data">
|
||||
<input type="hidden" name="id" value={talent.id}/><input type="hidden" name="conditionalRule" value={JSON.stringify(talent.conditionalRule)}/>
|
||||
<div class="talent-icon-identity"><IconUploadField currentUrl={talent.iconUrl} label="TALENT ICON" resetToken={iconResetToken}/><div class="talent-identity"><label class="field">NAME<input name="name" bind:value={talent.name} required/></label><SegmentedChoice legend="CATEGORY / SLOT" name="category" options={categoryOptions} bind:value={talent.category} required/></div></div>
|
||||
<label class="field template-field">SHARED DESCRIPTION TEMPLATE<textarea name="descriptionTemplate" bind:value={talent.descriptionTemplate} placeholder={'Hitting an enemy {1} times grants +{2} for {duration}...'} required></textarea><small>Use {'{1}'}, {'{2}'}, and so on for effect values. Use {'{duration}'} and {'{max_stacks}'} for defined mechanics. Cooldown is displayed separately.</small></label>
|
||||
{#if talentErrors.length}
|
||||
<div class="validation-summary" role="alert" tabindex="-1" bind:this={validationSummary}>
|
||||
<strong>CHECK {new Set(talentErrors.map((error) => error.area)).size} HIGHLIGHTED {new Set(talentErrors.map((error) => error.area)).size === 1 ? 'AREA' : 'AREAS'}</strong>
|
||||
<ul>
|
||||
{#each talentErrors as error}
|
||||
<li><a href={`#${areaTarget(error.area)}`}><span>{areaLabel(error.area)}</span>{error.message}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="talent-icon-identity">
|
||||
<div id="talent-icon" class:validation-area-error={hasErrors(['icon'])}>
|
||||
<IconUploadField currentUrl={talent.iconUrl} label="TALENT ICON" resetToken={iconResetToken}/>
|
||||
{#each errorsFor('icon') as error}<small class="field-error">{error.message}</small>{/each}
|
||||
</div>
|
||||
<div id="talent-identity" class="talent-identity" class:validation-area-error={hasErrors(['identity'])}>
|
||||
<label class="field">NAME<input name="name" bind:value={talent.name} required aria-invalid={hasErrors(['identity']) ? 'true' : undefined}/></label>
|
||||
<SegmentedChoice legend="CATEGORY / SLOT" name="category" options={categoryOptions} bind:value={talent.category} required/>
|
||||
{#each errorsFor('identity') as error}<small class="field-error">{error.message}</small>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<label id="talent-description" class="field template-field" class:validation-area-error={hasErrors(['description'])}>SHARED DESCRIPTION TEMPLATE<textarea name="descriptionTemplate" bind:value={talent.descriptionTemplate} placeholder={'Hitting an enemy {1} times grants +{2} for {duration}...'} required aria-invalid={hasErrors(['description']) ? 'true' : undefined}></textarea><small>Use {'{1}'}, {'{2}'}, and so on for effect values. Use {'{duration}'} and {'{max_stacks}'} for defined mechanics. Cooldown is displayed separately.</small>{#each errorsFor('description') as error}<small class="field-error">{error.message}</small>{/each}</label>
|
||||
<div class="tier-preview"><div><span>TIER I PREVIEW</span><p>{preview('I') || 'Description preview'}</p></div><div><span>TIER II PREVIEW</span><p>{preview('II') || 'Description preview'}</p></div></div>
|
||||
<MechanicProgramEditor
|
||||
attributes={data.stats}
|
||||
valueColumns={[{key:'tierI',variantKey:'I',label:'TIER I'},{key:'tierII',variantKey:'II',label:'TIER II'}]}
|
||||
idPrefix={`talent-${talent.id || 'new'}`}
|
||||
aiContext={{
|
||||
sourceKind: 'talent',
|
||||
sourceName: talent?.name ?? '',
|
||||
sourceDescription: talent?.descriptionTemplate ?? ''
|
||||
}}
|
||||
bind:rule={talent.conditionalRule}
|
||||
/>
|
||||
{#if form?.talentError}<p class="error">{form.talentError}</p>{/if}
|
||||
<div id="talent-mechanic" class:validation-area-error={hasErrors(['mechanic', 'outcomes'])}>
|
||||
<MechanicProgramEditor
|
||||
attributes={data.stats}
|
||||
valueColumns={[{key:'tierI',variantKey:'I',label:'TIER I'},{key:'tierII',variantKey:'II',label:'TIER II'}]}
|
||||
idPrefix={`talent-${talent.id || 'new'}`}
|
||||
aiContext={{
|
||||
sourceKind: 'talent',
|
||||
sourceName: talent?.name ?? '',
|
||||
sourceDescription: talent?.descriptionTemplate ?? ''
|
||||
}}
|
||||
bind:rule={talent.conditionalRule}
|
||||
/>
|
||||
{#each [...errorsFor('mechanic'), ...errorsFor('outcomes')] as error}<small class="field-error mechanic-field-error">{error.message}</small>{/each}
|
||||
</div>
|
||||
<div class="form-actions"><button type="button" class="secondary" onclick={() => talent = null}>CANCEL</button><button class="primary">SAVE TALENT →</button></div>
|
||||
</PendingForm>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user