batch import: use individual inputs instead of comma-separated

- start with 3 inputs, add/remove as needed
- avoids issues with item names containing commas
This commit is contained in:
Justin Edmund 2025-12-14 12:41:05 -08:00
parent dd1591d5b3
commit 42f7722e50
3 changed files with 249 additions and 66 deletions

View file

@ -18,6 +18,7 @@
import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte' import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import Input from '$lib/components/ui/Input.svelte' import Input from '$lib/components/ui/Input.svelte'
import Icon from '$lib/components/Icon.svelte'
import TagInput from '$lib/components/ui/TagInput.svelte' import TagInput from '$lib/components/ui/TagInput.svelte'
import type { PageData } from './$types' import type { PageData } from './$types'
@ -35,7 +36,7 @@
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
// Input phase // Input phase
let wikiPagesInput = $state('') let wikiPagesInputs = $state<string[]>(['', '', ''])
let isFetching = $state(false) let isFetching = $state(false)
let fetchError = $state<string | null>(null) let fetchError = $state<string | null>(null)
@ -135,10 +136,20 @@
} }
} }
// Add/remove input fields
function addInput() {
wikiPagesInputs = [...wikiPagesInputs, '']
}
function removeInput(index: number) {
if (wikiPagesInputs.length > 1) {
wikiPagesInputs = wikiPagesInputs.filter((_, i) => i !== index)
}
}
// Fetch wiki data for entered pages // Fetch wiki data for entered pages
async function fetchWikiData() { async function fetchWikiData() {
const pages = wikiPagesInput const pages = wikiPagesInputs
.split(',')
.map((p) => p.trim()) .map((p) => p.trim())
.filter((p) => p.length > 0) .filter((p) => p.length > 0)
.slice(0, 10) .slice(0, 10)
@ -349,24 +360,41 @@
<!-- Input phase --> <!-- Input phase -->
{#if entities.size === 0} {#if entities.size === 0}
<div class="input-phase"> <div class="input-phase">
<DetailsContainer title="Enter Wiki Pages"> <p class="hint">Enter up to 10 wiki page names to import data</p>
<div class="wiki-input"> <div class="wiki-inputs">
<Input {#each wikiPagesInputs as _, index}
bind:value={wikiPagesInput} <div class="input-row">
placeholder="Narmaya_(Summer), Zeta_(Summer), Beatrix_(Summer)" <Input
contained bind:value={wikiPagesInputs[index]}
/> placeholder="Narmaya_(Summer)"
<p class="hint">Enter wiki page names separated by commas (up to 10)</p> contained
</div> fullWidth
{#if fetchError} />
<p class="error">{fetchError}</p> {#if wikiPagesInputs.length > 1}
{/if} <button
<div class="fetch-button"> type="button"
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}> class="remove-button"
{isFetching ? 'Fetching...' : 'Fetch Wiki Data'} onclick={() => removeInput(index)}
</Button> aria-label="Remove input"
</div> >
</DetailsContainer> <Icon name="close" size={16} />
</button>
{/if}
</div>
{/each}
<Button variant="ghost" onclick={addInput}>
<Icon name="plus" size={16} />
Add another
</Button>
</div>
{#if fetchError}
<p class="error">{fetchError}</p>
{/if}
<div class="fetch-button">
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}>
{isFetching ? 'Fetching...' : 'Fetch data'}
</Button>
</div>
</div> </div>
{:else} {:else}
<!-- Entity selector --> <!-- Entity selector -->
@ -586,15 +614,48 @@
} }
.input-phase { .input-phase {
display: flex;
flex-direction: column;
gap: spacing.$unit-2x;
padding: spacing.$unit-2x; padding: spacing.$unit-2x;
} }
.wiki-input { .wiki-inputs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: spacing.$unit; gap: spacing.$unit;
} }
.input-row {
display: flex;
gap: spacing.$unit;
align-items: center;
}
.remove-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
border-radius: layout.$input-corner;
color: colors.$grey-50;
cursor: pointer;
flex-shrink: 0;
&:hover {
background: colors.$grey-90;
color: colors.$grey-30;
}
:global(svg) {
fill: currentColor;
}
}
.hint { .hint {
font-size: typography.$font-small; font-size: typography.$font-small;
color: colors.$grey-50; color: colors.$grey-50;

View file

@ -18,6 +18,7 @@
import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte' import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import Input from '$lib/components/ui/Input.svelte' import Input from '$lib/components/ui/Input.svelte'
import Icon from '$lib/components/Icon.svelte'
import TagInput from '$lib/components/ui/TagInput.svelte' import TagInput from '$lib/components/ui/TagInput.svelte'
import type { PageData } from './$types' import type { PageData } from './$types'
@ -34,7 +35,7 @@
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
// Input phase // Input phase
let wikiPagesInput = $state('') let wikiPagesInputs = $state<string[]>(['', '', ''])
let isFetching = $state(false) let isFetching = $state(false)
let fetchError = $state<string | null>(null) let fetchError = $state<string | null>(null)
@ -125,10 +126,20 @@
} }
} }
// Add/remove input fields
function addInput() {
wikiPagesInputs = [...wikiPagesInputs, '']
}
function removeInput(index: number) {
if (wikiPagesInputs.length > 1) {
wikiPagesInputs = wikiPagesInputs.filter((_, i) => i !== index)
}
}
// Fetch wiki data for entered pages // Fetch wiki data for entered pages
async function fetchWikiData() { async function fetchWikiData() {
const pages = wikiPagesInput const pages = wikiPagesInputs
.split(',')
.map((p) => p.trim()) .map((p) => p.trim())
.filter((p) => p.length > 0) .filter((p) => p.length > 0)
.slice(0, 10) .slice(0, 10)
@ -326,24 +337,41 @@
<!-- Input phase --> <!-- Input phase -->
{#if entities.size === 0} {#if entities.size === 0}
<div class="input-phase"> <div class="input-phase">
<DetailsContainer title="Enter Wiki Pages"> <p class="hint">Enter up to 10 wiki page names to import data</p>
<div class="wiki-input"> <div class="wiki-inputs">
<Input {#each wikiPagesInputs as _, index}
bind:value={wikiPagesInput} <div class="input-row">
placeholder="Bahamut, Lucifer, Zeus" <Input
contained bind:value={wikiPagesInputs[index]}
/> placeholder="Bahamut"
<p class="hint">Enter wiki page names separated by commas (up to 10)</p> contained
</div> fullWidth
{#if fetchError} />
<p class="error">{fetchError}</p> {#if wikiPagesInputs.length > 1}
{/if} <button
<div class="fetch-button"> type="button"
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}> class="remove-button"
{isFetching ? 'Fetching...' : 'Fetch Wiki Data'} onclick={() => removeInput(index)}
</Button> aria-label="Remove input"
</div> >
</DetailsContainer> <Icon name="close" size={16} />
</button>
{/if}
</div>
{/each}
<Button variant="ghost" onclick={addInput}>
<Icon name="plus" size={16} />
Add another
</Button>
</div>
{#if fetchError}
<p class="error">{fetchError}</p>
{/if}
<div class="fetch-button">
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}>
{isFetching ? 'Fetching...' : 'Fetch data'}
</Button>
</div>
</div> </div>
{:else} {:else}
<!-- Entity selector --> <!-- Entity selector -->
@ -564,15 +592,48 @@
} }
.input-phase { .input-phase {
display: flex;
flex-direction: column;
gap: spacing.$unit-2x;
padding: spacing.$unit-2x; padding: spacing.$unit-2x;
} }
.wiki-input { .wiki-inputs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: spacing.$unit; gap: spacing.$unit;
} }
.input-row {
display: flex;
gap: spacing.$unit;
align-items: center;
}
.remove-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
border-radius: layout.$input-corner;
color: colors.$grey-50;
cursor: pointer;
flex-shrink: 0;
&:hover {
background: colors.$grey-90;
color: colors.$grey-30;
}
:global(svg) {
fill: currentColor;
}
}
.hint { .hint {
font-size: typography.$font-small; font-size: typography.$font-small;
color: colors.$grey-50; color: colors.$grey-50;

View file

@ -18,6 +18,7 @@
import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte' import SidebarHeader from '$lib/components/ui/SidebarHeader.svelte'
import Button from '$lib/components/ui/Button.svelte' import Button from '$lib/components/ui/Button.svelte'
import Input from '$lib/components/ui/Input.svelte' import Input from '$lib/components/ui/Input.svelte'
import Icon from '$lib/components/Icon.svelte'
import TagInput from '$lib/components/ui/TagInput.svelte' import TagInput from '$lib/components/ui/TagInput.svelte'
import CharacterTypeahead from '$lib/components/ui/CharacterTypeahead.svelte' import CharacterTypeahead from '$lib/components/ui/CharacterTypeahead.svelte'
@ -35,7 +36,7 @@
let { data }: { data: PageData } = $props() let { data }: { data: PageData } = $props()
// Input phase // Input phase
let wikiPagesInput = $state('') let wikiPagesInputs = $state<string[]>(['', '', ''])
let isFetching = $state(false) let isFetching = $state(false)
let fetchError = $state<string | null>(null) let fetchError = $state<string | null>(null)
@ -127,10 +128,20 @@
} }
} }
// Add/remove input fields
function addInput() {
wikiPagesInputs = [...wikiPagesInputs, '']
}
function removeInput(index: number) {
if (wikiPagesInputs.length > 1) {
wikiPagesInputs = wikiPagesInputs.filter((_, i) => i !== index)
}
}
// Fetch wiki data for entered pages // Fetch wiki data for entered pages
async function fetchWikiData() { async function fetchWikiData() {
const pages = wikiPagesInput const pages = wikiPagesInputs
.split(',')
.map((p) => p.trim()) .map((p) => p.trim())
.filter((p) => p.length > 0) .filter((p) => p.length > 0)
.slice(0, 10) .slice(0, 10)
@ -331,24 +342,41 @@
<!-- Input phase --> <!-- Input phase -->
{#if entities.size === 0} {#if entities.size === 0}
<div class="input-phase"> <div class="input-phase">
<DetailsContainer title="Enter Wiki Pages"> <p class="hint">Enter up to 10 wiki page names to import data</p>
<div class="wiki-input"> <div class="wiki-inputs">
<Input {#each wikiPagesInputs as _, index}
bind:value={wikiPagesInput} <div class="input-row">
placeholder="Ixaba, Benedia, Sky Ace" <Input
contained bind:value={wikiPagesInputs[index]}
/> placeholder="Ixaba"
<p class="hint">Enter wiki page names separated by commas (up to 10)</p> contained
</div> fullWidth
{#if fetchError} />
<p class="error">{fetchError}</p> {#if wikiPagesInputs.length > 1}
{/if} <button
<div class="fetch-button"> type="button"
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}> class="remove-button"
{isFetching ? 'Fetching...' : 'Fetch Wiki Data'} onclick={() => removeInput(index)}
</Button> aria-label="Remove input"
</div> >
</DetailsContainer> <Icon name="close" size={16} />
</button>
{/if}
</div>
{/each}
<Button variant="ghost" onclick={addInput}>
<Icon name="plus" size={16} />
Add another
</Button>
</div>
{#if fetchError}
<p class="error">{fetchError}</p>
{/if}
<div class="fetch-button">
<Button variant="primary" onclick={fetchWikiData} disabled={isFetching}>
{isFetching ? 'Fetching...' : 'Fetch data'}
</Button>
</div>
</div> </div>
{:else} {:else}
<!-- Entity selector --> <!-- Entity selector -->
@ -567,15 +595,48 @@
} }
.input-phase { .input-phase {
display: flex;
flex-direction: column;
gap: spacing.$unit-2x;
padding: spacing.$unit-2x; padding: spacing.$unit-2x;
} }
.wiki-input { .wiki-inputs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: spacing.$unit; gap: spacing.$unit;
} }
.input-row {
display: flex;
gap: spacing.$unit;
align-items: center;
}
.remove-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: none;
border-radius: layout.$input-corner;
color: colors.$grey-50;
cursor: pointer;
flex-shrink: 0;
&:hover {
background: colors.$grey-90;
color: colors.$grey-30;
}
:global(svg) {
fill: currentColor;
}
}
.hint { .hint {
font-size: typography.$font-small; font-size: typography.$font-small;
color: colors.$grey-50; color: colors.$grey-50;