add job skill edit sidebar
This commit is contained in:
parent
acd784fe2d
commit
adf38c0c28
3 changed files with 242 additions and 1 deletions
216
src/lib/features/database/jobs/JobSkillEditPane.svelte
Normal file
216
src/lib/features/database/jobs/JobSkillEditPane.svelte
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import type { JobSkill } from '$lib/types/api/entities'
|
||||
import type { JobSkillPayload } from '$lib/api/adapters/job.adapter'
|
||||
import { jobAdapter } from '$lib/api/adapters/job.adapter'
|
||||
import { useQueryClient } from '@tanstack/svelte-query'
|
||||
import { jobKeys } from '$lib/api/queries/job.queries'
|
||||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import { untrack } from 'svelte'
|
||||
import DetailsSection from '$lib/components/sidebar/details/DetailsSection.svelte'
|
||||
import DetailItem from '$lib/components/ui/DetailItem.svelte'
|
||||
import Button from '$lib/components/ui/Button.svelte'
|
||||
|
||||
interface Props {
|
||||
/** The job's granblue_id */
|
||||
jobId: string
|
||||
/** Existing skill to edit, or undefined for creating new */
|
||||
skill?: JobSkill
|
||||
/** Callback when skill is saved successfully */
|
||||
onSaved?: () => void
|
||||
}
|
||||
|
||||
let { jobId, skill, onSaved }: Props = $props()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// Form state
|
||||
let nameEn = $state(skill?.name?.en ?? '')
|
||||
let nameJp = $state(skill?.name?.ja ?? '')
|
||||
let color = $state(skill?.color ?? 0)
|
||||
let order = $state(skill?.order ?? 0)
|
||||
let imageId = $state(skill?.imageId ?? '')
|
||||
let actionId = $state(skill?.actionId ?? 0)
|
||||
let isSaving = $state(false)
|
||||
let isDownloading = $state(false)
|
||||
|
||||
// Skill type - only one can be true
|
||||
let skillType = $state<'main' | 'sub' | 'emp' | 'base'>(
|
||||
skill?.main ? 'main' : skill?.sub ? 'sub' : skill?.emp ? 'emp' : skill?.base ? 'base' : 'main'
|
||||
)
|
||||
|
||||
const skillTypeOptions = [
|
||||
{ value: 'main', label: 'Main' },
|
||||
{ value: 'sub', label: 'Subskill' },
|
||||
{ value: 'emp', label: 'EMP' },
|
||||
{ value: 'base', label: 'Base' }
|
||||
]
|
||||
|
||||
// Auto-generate slug from English name
|
||||
function generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim()
|
||||
}
|
||||
|
||||
// Derive slug from English name
|
||||
const slug = $derived(generateSlug(nameEn))
|
||||
|
||||
// Derive current payload
|
||||
const currentPayload = $derived<JobSkillPayload>({
|
||||
name_en: nameEn,
|
||||
name_jp: nameJp || undefined,
|
||||
slug: slug,
|
||||
color: color,
|
||||
main: skillType === 'main',
|
||||
sub: skillType === 'sub',
|
||||
emp: skillType === 'emp',
|
||||
base: skillType === 'base',
|
||||
order: order,
|
||||
image_id: imageId || undefined,
|
||||
action_id: actionId || undefined
|
||||
})
|
||||
|
||||
// Derive validity
|
||||
const isValid = $derived(nameEn.trim().length > 0)
|
||||
|
||||
// Handle save
|
||||
async function handleSave() {
|
||||
if (!isValid || isSaving) return
|
||||
|
||||
isSaving = true
|
||||
try {
|
||||
if (skill) {
|
||||
await jobAdapter.updateSkill(jobId, skill.id, currentPayload)
|
||||
} else {
|
||||
await jobAdapter.createSkill(jobId, currentPayload)
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: jobKeys.skills(jobId) })
|
||||
onSaved?.()
|
||||
sidebar.close()
|
||||
} catch (error) {
|
||||
console.error('Failed to save skill:', error)
|
||||
// TODO: Show error toast
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle image download
|
||||
async function handleDownloadImage() {
|
||||
if (!skill || !imageId || isDownloading) return
|
||||
|
||||
isDownloading = true
|
||||
try {
|
||||
const result = await jobAdapter.downloadSkillImage(jobId, skill.id)
|
||||
if (result.success) {
|
||||
console.log('Image downloaded:', result.filename)
|
||||
// TODO: Show success toast
|
||||
} else {
|
||||
console.error('Failed to download image')
|
||||
// TODO: Show error toast
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to download image:', error)
|
||||
// TODO: Show error toast
|
||||
} finally {
|
||||
isDownloading = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update sidebar action button when form validity changes
|
||||
$effect(() => {
|
||||
// Capture dependencies
|
||||
const valid = isValid
|
||||
const saving = isSaving
|
||||
|
||||
// Use untrack to prevent setAction from triggering re-runs
|
||||
untrack(() => {
|
||||
sidebar.setAction(valid && !saving ? handleSave : undefined, saving ? 'Saving...' : 'Save', undefined, true)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="job-skill-edit-pane">
|
||||
<DetailsSection title="Basic Info">
|
||||
<DetailItem label="Name (EN)" bind:value={nameEn} editable={true} placeholder="English name" />
|
||||
<DetailItem
|
||||
label="Name (JP)"
|
||||
bind:value={nameJp}
|
||||
editable={true}
|
||||
placeholder="Japanese name (optional)"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Slug"
|
||||
value={slug || '(auto-generated from name)'}
|
||||
editable={false}
|
||||
/>
|
||||
</DetailsSection>
|
||||
|
||||
<DetailsSection title="Classification">
|
||||
<DetailItem
|
||||
label="Skill Type"
|
||||
bind:value={skillType}
|
||||
editable={true}
|
||||
type="select"
|
||||
options={skillTypeOptions}
|
||||
/>
|
||||
<DetailItem label="Color" bind:value={color} editable={true} type="number" placeholder="0" />
|
||||
<DetailItem label="Order" bind:value={order} editable={true} type="number" placeholder="0" />
|
||||
</DetailsSection>
|
||||
|
||||
<DetailsSection title="Game Data">
|
||||
<DetailItem
|
||||
label="Image ID"
|
||||
bind:value={imageId}
|
||||
editable={true}
|
||||
placeholder="e.g. 2710_3"
|
||||
/>
|
||||
<DetailItem
|
||||
label="Action ID"
|
||||
bind:value={actionId}
|
||||
editable={true}
|
||||
type="number"
|
||||
placeholder="e.g. 203921"
|
||||
/>
|
||||
</DetailsSection>
|
||||
|
||||
{#if skill && imageId}
|
||||
<div class="download-section">
|
||||
<Button onclick={handleDownloadImage} disabled={isDownloading} variant="secondary">
|
||||
{isDownloading ? 'Downloading...' : 'Download Image'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
|
||||
.job-skill-edit-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: spacing.$unit-3x;
|
||||
padding: spacing.$unit-2x 0;
|
||||
}
|
||||
|
||||
// Override DetailsSection styles for this pane
|
||||
.job-skill-edit-pane :global(.details-section) {
|
||||
padding: 0 spacing.$unit-2x;
|
||||
}
|
||||
|
||||
.job-skill-edit-pane :global(.details-section h3) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
padding: 0 spacing.$unit-2x;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
||||
25
src/lib/features/database/jobs/openJobSkillEditSidebar.ts
Normal file
25
src/lib/features/database/jobs/openJobSkillEditSidebar.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { sidebar } from '$lib/stores/sidebar.svelte'
|
||||
import JobSkillEditPane from './JobSkillEditPane.svelte'
|
||||
import type { JobSkill } from '$lib/types/api/entities'
|
||||
|
||||
interface OpenJobSkillEditOptions {
|
||||
jobId: string
|
||||
skill?: JobSkill
|
||||
onSaved?: () => void
|
||||
}
|
||||
|
||||
export function openJobSkillEditSidebar(options: OpenJobSkillEditOptions) {
|
||||
const { jobId, skill, onSaved } = options
|
||||
const title = skill ? 'Edit Skill' : 'New Skill'
|
||||
|
||||
sidebar.openWithComponent(
|
||||
title,
|
||||
JobSkillEditPane,
|
||||
{
|
||||
jobId,
|
||||
skill,
|
||||
onSaved
|
||||
},
|
||||
{ scrollable: true }
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
<JobFeaturesSection {job} />
|
||||
</section>
|
||||
{:else if currentTab === 'skills'}
|
||||
<JobSkillsTab {job} />
|
||||
<JobSkillsTab {job} {canEdit} />
|
||||
{:else if currentTab === 'images'}
|
||||
<JobImagesTab {job} />
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Reference in a new issue