Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/scaffold-festival-days/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function getLinkedPlacesFromEvent(event: MBEvent): MBPlace[] {
gid: placeGid,
name: place.name,
disambiguation: typeof place.disambiguation === 'string' ? place.disambiguation : undefined,
creditName: relation['target-credit'],
});
}

Expand Down
2 changes: 1 addition & 1 deletion scripts/scaffold-festival-days/src/matrix-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function MatrixDialog(props: {
checked={isColumnChecked()}
onChange={event => setPlaceSelected(place.gid, event.currentTarget.checked)}
/>
<span>{place.name}</span>
<span>{place.creditName ?? place.name}</span>
</label>
</th>
);
Expand Down
4 changes: 2 additions & 2 deletions scripts/scaffold-festival-days/src/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export async function scaffoldFestivalDays(params: {
}

for (const place of selectedPlaces) {
const venueName = `${event.name}: ${place.name}`;
const venueName = `${event.name}: ${place.creditName ?? place.name}`;
const venueEventGid = await createSubEvent({
name: venueName,
begin: singleDate,
Expand Down Expand Up @@ -165,7 +165,7 @@ export async function scaffoldFestivalDays(params: {
continue;
}

const venueName = `${event.name}, ${dayWord} ${dayEvent.date.dayNumber}: ${place.name}`;
const venueName = `${event.name}, ${dayWord} ${dayEvent.date.dayNumber}: ${place.creditName ?? place.name}`;
const venueEventGid = await createSubEvent({
name: venueName,
begin: dayEvent.date,
Expand Down
1 change: 1 addition & 0 deletions scripts/scaffold-festival-days/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface MBPlace {
gid: string;
name: string;
disambiguation?: string;
creditName?: string;
}

export interface DateParts {
Expand Down
34 changes: 32 additions & 2 deletions scripts/scaffold-festival-days/src/ui.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,44 @@
}

.searchResultRow {
display: flex;
display: grid;
grid-template-columns: minmax(0, 1fr) 10em auto;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}

.searchResultPlaceName {
min-width: 0;
}

.creditNameInput {
box-sizing: border-box;
min-width: 0;
}

.addPlaceButton {
width: 4.5em;
margin-left: 0 !important;
justify-self: start;
}

.placeOption {
display: flex;
align-items: center;
gap: 0.35rem;
}

.actionsRow {
display: flex;
align-items: center;
gap: 0.75rem;
}

.dayWordControl {
display: flex;
align-items: center;
}

.actionsButtons {
margin-left: auto;
}
154 changes: 90 additions & 64 deletions scripts/scaffold-festival-days/src/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const CUSTOM_SENTINEL = '__custom__';
function ScaffoldFestivalUI(props: {event: MBEvent; places: MBPlace[]; dayWord: string}) {
const [availablePlaces, setAvailablePlaces] = createSignal<MBPlace[]>(props.places);
const [searchResults, setSearchResults] = createSignal<MBPlace[]>([]);
const [draftCreditNames, setDraftCreditNames] = createSignal<Record<string, string>>({});
const [placeInput, setPlaceInput] = createSignal('');
const [selectedPlaces, setSelectedPlaces] = createSignal<Set<string>>(new Set(props.places.map(place => place.gid)));
const [isCreating, setIsCreating] = createSignal(false);
Expand Down Expand Up @@ -84,9 +85,8 @@ function ScaffoldFestivalUI(props: {event: MBEvent; places: MBPlace[]; dayWord:
if (placeGid) {
const place = await fetchPlaceByGid(placeGid);
if (place) {
addAndSelectPlace(place);
setSearchResults([]);
setStatus({message: `Added place: ${place.name}`, kind: 'info'});
setSearchResults([place]);
setStatus({message: `Found: ${place.name}. Optionally set a credit name, then click Add.`, kind: 'info'});
} else {
setStatus({message: 'Could not load place from provided link/MBID.', kind: 'error'});
}
Expand Down Expand Up @@ -157,47 +157,6 @@ function ScaffoldFestivalUI(props: {event: MBEvent; places: MBPlace[]; dayWord:
? 'Select places to create direct per-place sub-events for this single-day festival.'
: 'Select places to also create per-venue sub-events (optional).'}
</p>
<Show when={!singleDayMode()}>
<div>
<label>
{'Day word: '}
<select
value={isCustomDayWord() ? CUSTOM_SENTINEL : dayWord()}
onChange={e => {
const value = (e.target as HTMLSelectElement).value;
if (value === CUSTOM_SENTINEL) {
setIsCustomDayWord(true);
return;
}

setIsCustomDayWord(false);
setDayWord(value);
GM.setValue(DAY_WORD_STORAGE_KEY, value).catch(console.error);
}}
disabled={isCreating()}
>
<For each={DAY_WORD_PRESETS}>
{preset => <option value={preset.word}>{`${preset.language} (${preset.word})`}</option>}
</For>
<option value={CUSTOM_SENTINEL}>Custom…</option>
</select>
</label>
<Show when={isCustomDayWord()}>
<input
type="text"
value={dayWord()}
onInput={e => {
const value = (e.target as HTMLInputElement).value;
setIsCustomDayWord(true);
setDayWord(value);
GM.setValue(DAY_WORD_STORAGE_KEY, value).catch(console.error);
}}
disabled={isCreating()}
style={{'margin-left': '4px', width: '6em'}}
/>
</Show>
</div>
</Show>
<div class={classes.placeSearchBox}>
<input
class={classes.placeSearchInput}
Expand All @@ -220,13 +179,36 @@ function ScaffoldFestivalUI(props: {event: MBEvent; places: MBPlace[]; dayWord:
<Show when={searchResults().length > 0}>
<div class={classes.searchResults}>
<For each={searchResults()}>
{place => (
{(place, index) => (
<div class={classes.searchResultRow}>
<span>
{place.name}
<Show when={place.disambiguation}>{disambiguation => <span>{` (${disambiguation()})`}</span>}</Show>
<span class={classes.searchResultPlaceName}>
<a href={`/place/${place.gid}`}>
{place.name}
<Show when={place.disambiguation}>{disambiguation => <span>{` (${disambiguation()})`}</span>}</Show>
</a>
</span>
<Button class="button" onClick={() => addAndSelectPlace(place)} disabled={isCreating()}>
<input
class={classes.creditNameInput}
type="text"
placeholder="Credited as"
value={draftCreditNames()[place.gid] ?? ''}
onInput={e => {
const value = (e.currentTarget as HTMLInputElement).value;
setDraftCreditNames(prev => ({...prev, [place.gid]: value}));
}}
disabled={isCreating()}
/>
<Button
class={`button ${classes.addPlaceButton}`}
onClick={() => {
const draft = draftCreditNames()[place.gid] ?? '';
const creditName = draft.trim() || undefined;
addAndSelectPlace({...place, creditName});
setSearchResults(prev => prev.filter((_, i) => i !== index()));
setStatus({message: `Added place: ${creditName ?? place.name}`, kind: 'info'});
}}
disabled={isCreating()}
>
Add
</Button>
</div>
Expand All @@ -247,31 +229,75 @@ function ScaffoldFestivalUI(props: {event: MBEvent; places: MBPlace[]; dayWord:
<div class={classes.placesList}>
<For each={availablePlaces()}>
{place => (
<label class={classes.placeOption}>
<div class={classes.placeOption}>
<input
type="checkbox"
aria-label={place.creditName ?? place.name}
checked={selectedPlaces().has(place.gid)}
onChange={() => togglePlace(place.gid)}
disabled={isCreating()}
/>
{place.name}
</label>
<a href={`/place/${place.gid}`}>{place.creditName ?? place.name}</a>
</div>
)}
</For>
</div>
</Show>
<div class="buttons">
<Button
class="button"
onClick={() => void handleScaffold()}
disabled={isCreating() || (singleDayMode() && selectedPlaceIds().length === 0)}
>
{isCreating()
? 'Creating...'
: singleDayMode()
? 'Create Festival Place Sub-events'
: 'Create Festival Day Sub-events'}
</Button>
<div class={classes.actionsRow}>
<Show when={!singleDayMode()}>
<div class={classes.dayWordControl}>
<label>
{'Day word: '}
<select
value={isCustomDayWord() ? CUSTOM_SENTINEL : dayWord()}
onChange={e => {
const value = (e.target as HTMLSelectElement).value;
if (value === CUSTOM_SENTINEL) {
setIsCustomDayWord(true);
return;
}

setIsCustomDayWord(false);
setDayWord(value);
GM.setValue(DAY_WORD_STORAGE_KEY, value).catch(console.error);
}}
disabled={isCreating()}
>
<For each={DAY_WORD_PRESETS}>
{preset => <option value={preset.word}>{`${preset.language} (${preset.word})`}</option>}
</For>
<option value={CUSTOM_SENTINEL}>Custom…</option>
</select>
</label>
<Show when={isCustomDayWord()}>
<input
type="text"
value={dayWord()}
onInput={e => {
const value = (e.target as HTMLInputElement).value;
setIsCustomDayWord(true);
setDayWord(value);
GM.setValue(DAY_WORD_STORAGE_KEY, value).catch(console.error);
}}
disabled={isCreating()}
style={{'margin-left': '4px', width: '6em'}}
/>
</Show>
</div>
</Show>
<div class={`buttons ${classes.actionsButtons}`}>
<Button
class="button"
onClick={() => void handleScaffold()}
disabled={isCreating() || (singleDayMode() && selectedPlaceIds().length === 0)}
>
{isCreating()
? 'Creating...'
: singleDayMode()
? 'Create Festival Place Sub-events'
: 'Create Festival Day Sub-events'}
</Button>
</div>
</div>
<MatrixDialog
open={isMatrixDialogOpen()}
Expand Down
102 changes: 102 additions & 0 deletions scripts/scaffold-festival-days/tests/scaffold.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,4 +686,106 @@ test.describe('scaffold festival days', () => {

await page.unrouteAll();
});

test('uses place credit name when creating venue sub-events for multi-day festival', async ({
page,
userscriptPage,
musicbrainzPage,
testFestivalEvent,
testPlaces,
}) => {
const placeIds = testPlaces.getAll();
const placeCreditNames = ['Credit Name 1', 'Credit Name 2'] as const;
const routeState = await setupScaffoldRoutes({
page,
userscriptPage,
testFestivalEvent,
testPlaces,
relations: placeIds.map((id, index) => ({
'target-type': 'place',
'target-credit': placeCreditNames[index] ?? TEST_PLACE_NAMES[index],
place: {
id,
gid: id,
name: TEST_PLACE_NAMES[index],
},
})),
});
await musicbrainzPage.userscriptPage.goto(`/event/${testFestivalEvent.gid}`);

await expect(page.getByRole('group', {name: 'dvirtz MusicBrainz scripts'})).toBeAttached();

const checkboxes = page.getByRole('checkbox');
const count = await checkboxes.count();
for (let i = 0; i < count; i++) {
await checkboxes.nth(i).check();
}

await confirmScaffoldCreation(page);
await expect(page.getByText('Festival days scaffolding complete!')).toBeAttached();

const dayCount = testFestivalEvent.getDates().length;
const venueEvents = routeState.createdEvents.filter(event => event.placeId !== null);

expect(venueEvents).toHaveLength(dayCount * placeIds.length);

for (let dayNumber = 1; dayNumber <= dayCount; dayNumber += 1) {
for (let i = 0; i < placeIds.length; i++) {
const creditName = placeCreditNames[i]!;
const placeId = placeIds[i]!;
const expectedName = `${TEST_FESTIVAL_NAME}, Day ${dayNumber}: ${creditName}`;
const match = venueEvents.find(event => event.name === expectedName && event.placeId === placeId);
expect(match).toBeDefined();
}
}

await page.unrouteAll();
});

test('uses place credit name when creating per-place sub-events for single-day festival', async ({
page,
userscriptPage,
musicbrainzPage,
testFestivalEvent,
testPlaces,
}) => {
const placeIds = testPlaces.getAll();
const placeCreditNames = ['Credit Name 1', 'Credit Name 2'] as const;
const routeState = await setupScaffoldRoutes({
page,
userscriptPage,
testFestivalEvent,
testPlaces,
endDate: testFestivalEvent.getBeginDate(),
relations: placeIds.map((id, index) => ({
'target-type': 'place',
'target-credit': placeCreditNames[index] ?? TEST_PLACE_NAMES[index],
place: {
id,
gid: id,
name: TEST_PLACE_NAMES[index],
},
})),
});
await musicbrainzPage.userscriptPage.goto(`/event/${testFestivalEvent.gid}`);

await expect(page.getByRole('group', {name: 'dvirtz MusicBrainz scripts'})).toBeAttached();

await confirmScaffoldCreation(page, false);
await expect(page.getByText('Festival days scaffolding complete!')).toBeAttached();

const venueEvents = routeState.createdEvents.filter(event => event.placeId !== null);

expect(venueEvents).toHaveLength(placeIds.length);

for (let i = 0; i < placeIds.length; i++) {
const creditName = placeCreditNames[i]!;
const placeId = placeIds[i]!;
const expectedName = `${TEST_FESTIVAL_NAME}: ${creditName}`;
const match = venueEvents.find(event => event.name === expectedName && event.placeId === placeId);
expect(match).toBeDefined();
}

await page.unrouteAll();
});
});
Loading