Skip to content

Commit

Permalink
feat: support URL pasting
Browse files Browse the repository at this point in the history
use html form to trigger import

fixes #33
  • Loading branch information
dvirtz committed Dec 21, 2024
1 parent 892c941 commit 810f657
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 120 deletions.
4 changes: 2 additions & 2 deletions src/acum-work-import/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ To import a whole medium:

1. Open the release relationship editor.
2. Find the album ID, this will be in the end of the album URL in ACUM, e.g. https://nocs.acum.org.il/acumsitesearchdb/album?albumid=011820.
3. Insert the ACUM album ID in the input box.
3. Insert the ACUM url or album ID in the input box.
4. Select the recordings whose works you want to import.
5. Click the import button.
6. New works (green background links) will be created and exitsting works (yellow background) will be updated with links to the selected recordings and any writer and arranger as well as ISWCs and ACUM ID work attribute.
Expand All @@ -27,7 +27,7 @@ To import a single work:

1. Open a work editor.
2. Find the work ID, this will be in the end of the work URL in ACUM, e.g. https://nocs.acum.org.il/acumsitesearchdb/work?workid=1005566.
3. Insert the ACUM work ID in the input box.
3. Insert the ACUM url or work ID in the input box.
4. Click the import button.
5. The work will be updated with links writers, ISWCs and ACUM ID work attribute.
6. Review the changes.
Expand Down
14 changes: 14 additions & 0 deletions src/acum-work-import/acum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,17 @@ export enum WorkLanguage {
export function workLanguage(track: WorkVersion): WorkLanguage {
return stringToEnum(track.workLanguage, WorkLanguage);
}

export function replaceUrlWith(field: string): (input: string) => string {
return (input: string) => {
try {
const url = new URL(input);
if (url.hostname === 'nocs.acum.org.il' && url.searchParams.has(field)) {
return url.searchParams.get(field)!;
}
} catch (e) {
console.debug('failed to parse URL', input, e);
}
return input;
};
}
54 changes: 54 additions & 0 deletions src/acum-work-import/ui/import-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {Button} from '@kobalte/core/button';
import {TextField} from '@kobalte/core/text-field';
import {createEffect, createSignal, ParentProps} from 'solid-js';
import {replaceUrlWith} from '../acum';

export function ImportForm(props: ParentProps & {field: string; onSubmit: (id: string) => Promise<void>}) {
const [id, setId] = createSignal('');
const [importing, setImporting] = createSignal(false);

let submitButton: HTMLButtonElement;

createEffect((prevTitle?: string) => {
const button = submitButton!;
button.disabled = importing();
button.title = importing() ? 'Importing...' : (prevTitle ?? button.title);
return button.title;
});

const onSubmit = (ev: SubmitEvent) => {
ev.preventDefault();
setImporting(true);
props
.onSubmit(id())
.catch(console.error)
.finally(() => setImporting(false));
};

return (
<form onSubmit={onSubmit}>
<div class="buttons" style={{display: 'flex'}}>
<Button type="submit" ref={submitButton!}>
<img
src="https://nocs.acum.org.il/acumsitesearchdb/resources/images/faviconSite.svg"
alt="ACUM logo"
style={{width: '16px', height: '16px', margin: '2px'}}
></img>
<span>Import works from ACUM</span>
</Button>
<TextField
required={true}
value={id()}
onChange={value => setId(replaceUrlWith(`${props.field}id`)(value))}
style={{'margin': '0 7px 0 0'}}
>
<TextField.Input
pattern="\d{1,14}"
placeholder={`${props.field.charAt(0).toUpperCase()}${props.field.slice(1)} ID`}
/>
</TextField>
{props.children}
</div>
</form>
);
}
72 changes: 23 additions & 49 deletions src/acum-work-import/ui/release-editor-ui.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,52 @@
import {Button} from '@kobalte/core/button';
import {createEffect, createMemo, createSignal, on} from 'solid-js';
import {createEffect, createMemo, createSignal} from 'solid-js';
import {render} from 'solid-js/web';
import {Toolbox} from 'src/common/musicbrainz/toolbox';
import {importAlbum as tryImportWorks} from '../import-album';
import {submitWorks as trySubmitWorks} from '../submit';
import {SelectionStatus, validateNumericId, validateSelection} from '../validate';
import {SelectionStatus, validateSelection} from '../validate';
import {ImportForm} from './import-form';
import {ProgressBar} from './progressbar';
import {useWarnings, WarningsProvider} from './warnings';

void validateNumericId;

function AcumImporter(props: {recordingCheckboxes: NodeListOf<HTMLInputElement>}) {
const [albumId, setAlbumId] = createSignal('Album ID');
const [selectedRecordings, setSelectedRecordings] = createSignal(MB.relationshipEditor.state.selectedRecordings);
props.recordingCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => setSelectedRecordings(MB.relationshipEditor.state.selectedRecordings));
});
const selectionStatus = createMemo(() => validateSelection(selectedRecordings()));
const [albumIdValid, setAlbumIdValid] = createSignal(false);
const inputValid = createMemo(() => albumIdValid() && selectionStatus() == SelectionStatus.VALID);
const {addWarning, clearWarnings} = useWarnings();
const [worksPending, setWorksPending] = createSignal(false);
const [submitting, setSubmitting] = createSignal(false);
const [progress, setProgress] = createSignal<readonly [number, string]>([0, '']);
const submissionDisabled = createMemo(() => !worksPending() || submitting());

// need dependencies explicit to avoid infinite recursion
// otherwise, the warning actions will trigger the effect again
createEffect(
on([albumIdValid, selectionStatus], () => {
if (albumIdValid()) {
switch (selectionStatus()) {
case SelectionStatus.VALID:
clearWarnings(/select .*/);
break;
case SelectionStatus.NO_RECORDINGS:
addWarning('select at least one recording');
break;
case SelectionStatus.MULTIPLE_MEDIA:
addWarning('select recordings only from a single medium');
break;
}
}
})
);
createEffect((prevTitle?: string) => {
const submitButton = document.querySelector('button.submit') as HTMLButtonElement;
submitButton.disabled = worksPending();
submitButton.title = worksPending() ? 'Submit works or cancel first' : (prevTitle ?? submitButton.title);
return submitButton.title;
});

function importWorks() {
async function importWorks(albumId: string) {
clearWarnings();
tryImportWorks(albumId(), addWarning, clearWarnings, setProgress)
.then(() => setWorksPending(true))
.catch(err => {
console.error(err);
addWarning(`Import failed: ${err}`);
});
switch (selectionStatus()) {
case SelectionStatus.VALID:
break;
case SelectionStatus.NO_RECORDINGS:
addWarning('select at least one recording');
return;
case SelectionStatus.MULTIPLE_MEDIA:
addWarning('select recordings only from a single medium');
return;
}
try {
await tryImportWorks(albumId, addWarning, clearWarnings, setProgress);
setWorksPending(true);
} catch (err) {
console.error(err);
addWarning(`Import failed: ${String(err)}`);
}
}

function submitWorks() {
Expand All @@ -85,21 +73,7 @@ function AcumImporter(props: {recordingCheckboxes: NodeListOf<HTMLInputElement>}

return (
<>
<div class="buttons" style={{display: 'flex'}}>
<Button disabled={!inputValid()} onclick={importWorks}>
<img
src="https://nocs.acum.org.il/acumsitesearchdb/resources/images/faviconSite.svg"
alt="ACUM logo"
style={{width: '16px', height: '16px', margin: '2px'}}
></img>
<span>Import works from ACUM</span>
</Button>
<input
type="text"
placeholder={'Album ID'}
use:validateNumericId={[[albumId, setAlbumId], setAlbumIdValid]}
style={{'margin': '0 7px 0 0'}}
></input>
<ImportForm field="album" onSubmit={importWorks}>
<Button id="acum-work-submit" class="worksubmit" disabled={submissionDisabled()} onclick={submitWorks}>
<span>Submit works</span>
</Button>
Expand All @@ -113,7 +87,7 @@ function AcumImporter(props: {recordingCheckboxes: NodeListOf<HTMLInputElement>}
maxValue={1}
style={{'flex-grow': 1, padding: '5px 10px 5px 7px'}}
/>
</div>
</ImportForm>

<div>
<p>This will add a new work for each checked recording that has no work already</p>
Expand Down
52 changes: 9 additions & 43 deletions src/acum-work-import/ui/work-editor-ui.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,23 @@
import {Button} from '@kobalte/core/button';
import {createEffect, createSignal} from 'solid-js';
import {render} from 'solid-js/web';
import {Toolbox} from 'src/common/musicbrainz/toolbox';
import {importWork as tryImportWork} from '../import-work';
import {validateNumericId} from '../validate';
import {ImportForm} from './import-form';
import {useWarnings, WarningsProvider} from './warnings';

void validateNumericId;

function AcumImporter(props: {form: HTMLFormElement}) {
const [workId, setWorkId] = createSignal('');
const [workIdValid, setWorkIdValid] = createSignal(false);
const {addWarning, clearWarnings} = useWarnings();
const [importing, setImporting] = createSignal(false);

function importWork() {
setImporting(true);
async function importWork(workId: string) {
clearWarnings();
tryImportWork(workId(), props.form, addWarning)
.catch(err => {
console.error(err);
addWarning(`Import failed: ${err}`);
})
.finally(() => setImporting(false));
try {
return await tryImportWork(workId, props.form, addWarning);
} catch (err) {
console.error(err);
addWarning(`Import failed: ${String(err)}`);
}
}

createEffect((prevTitle?: string) => {
const submitButton = document.querySelector('button.submit') as HTMLButtonElement;
submitButton.disabled = importing();
submitButton.title = importing() ? 'Importing...' : (prevTitle ?? submitButton.title);
return submitButton.title;
});

return (
<>
<div class="buttons" style={{display: 'flex'}}>
<Button disabled={!workIdValid() || importing()} type="button" onClick={importWork}>
<img
src="https://nocs.acum.org.il/acumsitesearchdb/resources/images/faviconSite.svg"
alt="ACUM logo"
style={{width: '16px', height: '16px', margin: '2px'}}
></img>
<span>Import works from ACUM</span>
</Button>
<input
type="text"
placeholder={'Work ID'}
use:validateNumericId={[[workId, setWorkId], setWorkIdValid]}
style={{'margin': '0 7px 0 0'}}
></input>
</div>
</>
);
return <ImportForm field="work" onSubmit={importWork} />;
}

const releaseEditorContainerId = 'acum-work-import-container';
Expand Down
26 changes: 0 additions & 26 deletions src/acum-work-import/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {Accessor, Setter, Signal} from 'solid-js';
import * as tree from 'weight-balanced-tree';

export enum SelectionStatus {
Expand All @@ -7,31 +6,6 @@ export enum SelectionStatus {
MULTIPLE_MEDIA,
}

// https://docs.solidjs.com/configuration/typescript#custom-directives
declare module 'solid-js' {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace JSX {
interface Directives {
validateNumericId: [Signal<string>, Setter<boolean>]; // Corresponds to `use:validateAlbumId`
}
}
}

export function validateNumericId(input: HTMLInputElement, accessor: Accessor<[Signal<string>, Setter<boolean>]>) {
const [[id, setId], setIdValid] = accessor();
input.oninput = () => {
setId(input.value);
if (/^\d+$/.test(id())) {
setIdValid(true);
input.setCustomValidity('');
} else {
setIdValid(false);
input.setCustomValidity('ID must be a number');
}
input.reportValidity();
};
}

export function validateSelection(selectedRecordings: tree.ImmutableTree<RecordingT>): SelectionStatus {
if (!selectedRecordings || selectedRecordings.size == 0) {
return SelectionStatus.NO_RECORDINGS;
Expand Down

0 comments on commit 810f657

Please sign in to comment.