From ab52b84ca0f0868c21198693d912af28fe863435 Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Thu, 16 May 2024 01:11:32 -0400 Subject: [PATCH 01/18] Convert form_row_text_list to React --- root/components/forms.tt | 3 +- .../scripts/edit/components/AddButton.js | 26 ++++ .../edit/components/FormRowTextList.js | 141 ++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 root/static/scripts/edit/components/AddButton.js create mode 100644 root/static/scripts/edit/components/FormRowTextList.js diff --git a/root/components/forms.tt b/root/components/forms.tt index 9fb99031880..e39b1383b10 100644 --- a/root/components/forms.tt +++ b/root/components/forms.tt @@ -133,7 +133,8 @@ [% END %] [%- END -%] -[%- MACRO form_row_text_list(r, field_name, label, item_name) BLOCK -%] +[%- MACRO form_row_text_list(r, field_name, label, item_name) BLOCK # Converted to React at root/static/scripts/edit/components/FormTowTextList.js +-%] [% WRAPPER form_row %]
diff --git a/root/static/scripts/edit/components/AddButton.js b/root/static/scripts/edit/components/AddButton.js new file mode 100644 index 00000000000..1e073626672 --- /dev/null +++ b/root/static/scripts/edit/components/AddButton.js @@ -0,0 +1,26 @@ +/* + * @flow strict + * Copyright (C) 2024 MetaBrainz Foundation + * + * This file is part of MusicBrainz, the open internet music database, + * and is licensed under the GPL version 2, or (at your option) any + * later version: http://www.gnu.org/licenses/gpl-2.0.txt + */ + +component AddButton( + onClick: (event: SyntheticEvent) => void, + label?: string, +) { + if (label == null) { + return + ); +}; + +export default AddButton; + \ No newline at end of file diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js new file mode 100644 index 00000000000..bdd1cb53699 --- /dev/null +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -0,0 +1,141 @@ +/* + * @flow strict + * Copyright (C) 2024 MetaBrainz Foundation + * + * This file is part of MusicBrainz, the open internet music database, + * and is licensed under the GPL version 2, or (at your option) any + * later version: http://www.gnu.org/licenses/gpl-2.0.txt + */ + +import React, {useState} from 'react'; + +import AddButton from './AddButton.js'; +import FieldErrors from './FieldErrors.js'; +import FormLabel from './FormLabel.js'; +import FormRow from './FormRow.js'; +import RemoveButton from './RemoveButton.js'; + +type TextListRowProps = { + +name: string, + +value?: string, + +template?: boolean, + +onChange?: (event: SyntheticEvent) => void, + +onRemove?: (event: SyntheticEvent) => void, + +index?: number, +}; + +component TextListRow(...{ + name, + value = "", + template = false, + onChange = () => {}, + onRemove = () => {}, + index = 0 +}: TextListRowProps) { + if (template) { + return ( +
+ + +
+ ); + } + + return ( +
+ + +
+ ); +} + +const initialRows = (repeatable: RepeatableFieldT>) => { + if (repeatable.field.length === 0) { + return [{name: repeatable.html_name + '.0', value: ''}]; + } + + return repeatable.field.map((field, index) => ({ + name: repeatable.html_name + '.' + index, + value: field.value ?? '', + })); +}; + +component FormRowTextList( + repeatable: RepeatableFieldT>, + label: string, + itemName: string, + required: boolean = false, +) { + const newRow = (name: string, value: string, index: number) => { + return {name: name + '.' + index, value}; + }; + + const [rows, setRows] = useState(initialRows(repeatable)); + + const add = () => { + const index = rows.length; + + setRows([...rows, newRow(repeatable.html_name, '', index)]); + }; + + const change = (index: number, value: string) => { + const newRows = [...rows]; + newRows[index] = newRow(repeatable.html_name, value, index); + setRows(newRows); + }; + + const removeRow = (index: number) => { + if (rows.length === 1) { + setRows([newRow(repeatable.html_name, '', 0)]); + return; + } + + setRows(rows.filter((_, i) => i !== index)); + }; + + return ( + + + +
+ {}} + onRemove={() => {}} + template + /> + + {rows.map((field, index) => ( + change(index, event.currentTarget.value)} + onRemove={() => removeRow(index)} + value={field.value} + /> + ))} + +
+ +
+
+ + +
+ ); +} + +export default FormRowTextList; From 99825eda3ff6cafaa1d66cb03af5d8eb1fb63834 Mon Sep 17 00:00:00 2001 From: Serial <69764315+Serial-ATA@users.noreply.github.com> Date: Thu, 16 May 2024 02:04:06 -0400 Subject: [PATCH 02/18] Fix ESLint warnings --- root/static/scripts/edit/components/AddButton.js | 11 +++++------ .../scripts/edit/components/FormRowTextList.js | 14 ++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/root/static/scripts/edit/components/AddButton.js b/root/static/scripts/edit/components/AddButton.js index 1e073626672..cdef659bdee 100644 --- a/root/static/scripts/edit/components/AddButton.js +++ b/root/static/scripts/edit/components/AddButton.js @@ -12,15 +12,14 @@ component AddButton( label?: string, ) { if (label == null) { - return ); -}; - +} + export default AddButton; - \ No newline at end of file diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index bdd1cb53699..b9be1b56ed0 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -16,21 +16,21 @@ import FormRow from './FormRow.js'; import RemoveButton from './RemoveButton.js'; type TextListRowProps = { + +index?: number, +name: string, - +value?: string, - +template?: boolean, +onChange?: (event: SyntheticEvent) => void, +onRemove?: (event: SyntheticEvent) => void, - +index?: number, + +template?: boolean, + +value?: string, }; component TextListRow(...{ + index = 0, name, - value = "", - template = false, onChange = () => {}, onRemove = () => {}, - index = 0 + template = false, + value = '', }: TextListRowProps) { if (template) { return ( @@ -113,8 +113,6 @@ component FormRowTextList(
{}} - onRemove={() => {}} template /> From 3d1b6615c4781744218ec4cc7b05bb5fdc08f692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Thu, 5 Sep 2024 17:30:34 +0200 Subject: [PATCH 03/18] Support hydrated FormRowTextList This is needed to be able to use the component in our TT forms until all forms are converted (and makes it easier to test without having to fully convert the entity editors to React). --- root/server/components.mjs | 1 + root/static/scripts/area/edit.js | 1 + .../static/scripts/edit/components/FormRow.js | 4 +++- .../edit/components/FormRowTextList.js | 21 +++++++++++++++++-- webpack/client.config.mjs | 1 + 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/root/server/components.mjs b/root/server/components.mjs index 42e79601b16..dc18e851636 100644 --- a/root/server/components.mjs +++ b/root/server/components.mjs @@ -530,6 +530,7 @@ export default { 'static/scripts/common/components/WarningIcon': (): Promise => import('../static/scripts/common/components/WarningIcon.js'), 'static/scripts/edit/components/AddIcon': (): Promise => import('../static/scripts/edit/components/AddIcon.js'), 'static/scripts/edit/components/FormRowNameWithGuessCase': (): Promise => import('../static/scripts/edit/components/FormRowNameWithGuessCase.js'), + 'static/scripts/edit/components/FormRowTextList': (): Promise => import('../static/scripts/edit/components/FormRowTextList.js'), 'static/scripts/edit/components/GuessCaseIcon': (): Promise => import('../static/scripts/edit/components/GuessCaseIcon.js'), 'static/scripts/edit/components/InformationIcon': (): Promise => import('../static/scripts/edit/components/InformationIcon.js'), 'static/scripts/recording/RecordingName': (): Promise => import('../static/scripts/recording/RecordingName.js'), diff --git a/root/static/scripts/area/edit.js b/root/static/scripts/area/edit.js index 1f779dd6898..02b9aab0a60 100644 --- a/root/static/scripts/area/edit.js +++ b/root/static/scripts/area/edit.js @@ -8,6 +8,7 @@ */ import typeBubble from '../edit/typeBubble.js'; +import '../edit/components/FormRowTextList.js'; const typeIdField = 'select[name=edit-area\\.type_id]'; typeBubble(typeIdField); diff --git a/root/static/scripts/edit/components/FormRow.js b/root/static/scripts/edit/components/FormRow.js index f9c1b3a6240..853e98b86f2 100644 --- a/root/static/scripts/edit/components/FormRow.js +++ b/root/static/scripts/edit/components/FormRow.js @@ -9,6 +9,7 @@ component FormRow( children: React.Node, + className as passedClassname?: string, hasNoLabel: boolean = false, hasNoMargin: boolean = false, rowRef?: {-current: HTMLDivElement | null}, @@ -18,7 +19,8 @@ component FormRow( className={ 'row' + (hasNoLabel ? ' no-label' : '') + - (hasNoMargin ? ' no-margin' : '') + (hasNoMargin ? ' no-margin' : '') + + (nonEmpty(passedClassname) ? ' ' + passedClassname : '') } ref={rowRef} > diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index b9be1b56ed0..64e5332d5cb 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -107,7 +107,7 @@ component FormRowTextList( }; return ( - + <>
@@ -132,8 +132,25 @@ component FormRowTextList(
+ + ); +} + +export component NonHydratedFormRowTextList( + ...props: React.PropsOf +) { + return ( + + ); } -export default FormRowTextList; +/* + * Hydration must be moved higher up in the component hierarchy once + * more of the page is converted to React. + */ +export default (hydrate>( + 'div.row.form-row-text-list-container', + FormRowTextList, +): React.AbstractComponent, void>); diff --git a/webpack/client.config.mjs b/webpack/client.config.mjs index 853c851a5d8..4760aacc968 100644 --- a/webpack/client.config.mjs +++ b/webpack/client.config.mjs @@ -60,6 +60,7 @@ const entries = [ 'common/components/WorkArtists', 'confirm-seed', 'edit', + 'edit/components/FormRowTextList', 'edit/components/NewNotesAlertCheckbox', 'event/components/EventEditForm', 'event/edit', From 4cebb2600d8ef24b768f9e5cf20377c71ef42561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Thu, 5 Sep 2024 17:35:31 +0200 Subject: [PATCH 04/18] Use FormRowTextList in area/edit_form This allows testing the code. We eventually will want to use it in all TT forms and drop the TT version. --- root/area/edit_form.tt | 18 +++++++++++++++--- root/static/scripts/area/edit.js | 3 ++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/root/area/edit_form.tt b/root/area/edit_form.tt index af8a57fe99d..d030bcebee7 100644 --- a/root/area/edit_form.tt +++ b/root/area/edit_form.tt @@ -9,9 +9,21 @@ [%- form_row_name_with_guesscase(r) -%] [%- form_row_text_long(r, 'comment', 'Disambiguation:') -%] [%- form_row_select(r, 'type_id', 'Type:') -%] - [%- form_row_text_list(r, 'iso_3166_1', 'ISO 3166-1:', 'ISO 3166-1') -%] - [%- form_row_text_list(r, 'iso_3166_2', 'ISO 3166-2:', 'ISO 3166-2') -%] - [%- form_row_text_list(r, 'iso_3166_3', 'ISO 3166-3:', 'ISO 3166-3') -%] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('iso_3166_1')), + label => 'ISO 3166-1:', + itemName => 'ISO 3166-1' + }) %] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('iso_3166_2')), + label => 'ISO 3166-2:', + itemName => 'ISO 3166-2' + }) %] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('iso_3166_3')), + label => 'ISO 3166-3:', + itemName => 'ISO 3166-3' + }) %] [% date_range_fieldset(r, 'area', 'This area has ended.') %] diff --git a/root/static/scripts/area/edit.js b/root/static/scripts/area/edit.js index 02b9aab0a60..5a7c785ee74 100644 --- a/root/static/scripts/area/edit.js +++ b/root/static/scripts/area/edit.js @@ -7,8 +7,9 @@ * later version: http://www.gnu.org/licenses/gpl-2.0.txt */ -import typeBubble from '../edit/typeBubble.js'; import '../edit/components/FormRowTextList.js'; +import typeBubble from '../edit/typeBubble.js'; + const typeIdField = 'select[name=edit-area\\.type_id]'; typeBubble(typeIdField); From 9b8d190ed393f3f5808082a171605b98bd0ed668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Fri, 6 Sep 2024 09:06:10 +0200 Subject: [PATCH 05/18] Pass full labels to FormRowTextList This is a lot better for translators than "Add {item}" (and needed for Estonian at least to actually be translated correctly in a simple way). For area, we don't want translations though since it's an admin form. --- root/area/edit_form.tt | 9 +++-- .../scripts/edit/components/AddButton.js | 2 +- .../edit/components/FormRowTextList.js | 39 ++++++++----------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/root/area/edit_form.tt b/root/area/edit_form.tt index d030bcebee7..06d170794ce 100644 --- a/root/area/edit_form.tt +++ b/root/area/edit_form.tt @@ -12,17 +12,20 @@ [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { repeatable => form_to_json(form.field('iso_3166_1')), label => 'ISO 3166-1:', - itemName => 'ISO 3166-1' + addButtonLabel => 'Add ISO 3166-1', + removeButtonLabel => 'Remove ISO 3166-1', }) %] [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { repeatable => form_to_json(form.field('iso_3166_2')), label => 'ISO 3166-2:', - itemName => 'ISO 3166-2' + addButtonLabel => 'Add ISO 3166-2', + removeButtonLabel => 'Remove ISO 3166-2', }) %] [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { repeatable => form_to_json(form.field('iso_3166_3')), label => 'ISO 3166-3:', - itemName => 'ISO 3166-3' + addButtonLabel => 'Add ISO 3166-3', + removeButtonLabel => 'Remove ISO 3166-3', }) %] diff --git a/root/static/scripts/edit/components/AddButton.js b/root/static/scripts/edit/components/AddButton.js index cdef659bdee..8d27b2870c9 100644 --- a/root/static/scripts/edit/components/AddButton.js +++ b/root/static/scripts/edit/components/AddButton.js @@ -17,7 +17,7 @@ component AddButton( return ( ); } diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 64e5332d5cb..fca66d7ce27 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -15,23 +15,15 @@ import FormLabel from './FormLabel.js'; import FormRow from './FormRow.js'; import RemoveButton from './RemoveButton.js'; -type TextListRowProps = { - +index?: number, - +name: string, - +onChange?: (event: SyntheticEvent) => void, - +onRemove?: (event: SyntheticEvent) => void, - +template?: boolean, - +value?: string, -}; - -component TextListRow(...{ - index = 0, - name, - onChange = () => {}, - onRemove = () => {}, - template = false, - value = '', -}: TextListRowProps) { +component TextListRow( + index: number = 0, + name: string, + onChange: (event: SyntheticEvent) => void = () => {}, + onRemove: (event: SyntheticEvent) => void = () => {}, + removeButtonLabel: string, + template: boolean = false, + value: string = '', +) { if (template) { return (
); @@ -57,7 +49,7 @@ component TextListRow(...{ type="text" value={value} /> - +
); } @@ -74,9 +66,10 @@ const initialRows = (repeatable: RepeatableFieldT>) => { }; component FormRowTextList( - repeatable: RepeatableFieldT>, + addButtonLabel: string, label: string, - itemName: string, + removeButtonLabel: string, + repeatable: RepeatableFieldT>, required: boolean = false, ) { const newRow = (name: string, value: string, index: number) => { @@ -113,6 +106,7 @@ component FormRowTextList(
@@ -122,12 +116,13 @@ component FormRowTextList( name={field.name} onChange={(event) => change(index, event.currentTarget.value)} onRemove={() => removeRow(index)} + removeButtonLabel={removeButtonLabel} value={field.value} /> ))}
- +
From 87e3dde7d4d59d82a09c85179a319ab78f143e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Fri, 6 Sep 2024 10:38:53 +0200 Subject: [PATCH 06/18] Remove unneeded template hack This was a hack needed for JQuery to work on this, but with React it's AFAICT entirely useless. --- .../edit/components/FormRowTextList.js | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index fca66d7ce27..aa3d4e6e251 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -16,30 +16,12 @@ import FormRow from './FormRow.js'; import RemoveButton from './RemoveButton.js'; component TextListRow( - index: number = 0, name: string, - onChange: (event: SyntheticEvent) => void = () => {}, - onRemove: (event: SyntheticEvent) => void = () => {}, + onChange: (event: SyntheticEvent) => void, + onRemove: (event: SyntheticEvent) => void, removeButtonLabel: string, - template: boolean = false, - value: string = '', + value: string, ) { - if (template) { - return ( -
- - -
- ); - } - return (
- - {rows.map((field, index) => ( Date: Fri, 6 Sep 2024 11:06:43 +0200 Subject: [PATCH 07/18] Standardize state function names --- root/static/scripts/edit/components/FormRowTextList.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index aa3d4e6e251..71af505bf11 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -60,13 +60,13 @@ component FormRowTextList( const [rows, setRows] = useState(initialRows(repeatable)); - const add = () => { + const addRow = () => { const index = rows.length; setRows([...rows, newRow(repeatable.html_name, '', index)]); }; - const change = (index: number, value: string) => { + const changeRow = (index: number, value: string) => { const newRows = [...rows]; newRows[index] = newRow(repeatable.html_name, value, index); setRows(newRows); @@ -90,7 +90,7 @@ component FormRowTextList( change(index, event.currentTarget.value)} + onChange={(event) => changeRow(index, event.currentTarget.value)} onRemove={() => removeRow(index)} removeButtonLabel={removeButtonLabel} value={field.value} @@ -98,7 +98,7 @@ component FormRowTextList( ))}
- +
From d53c556f15804b5dfe02e58ef1d9b086e538d3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Fri, 6 Sep 2024 11:09:41 +0200 Subject: [PATCH 08/18] Use splice for RemoveRow This matches what we do elsewhere in the codebase. --- root/static/scripts/edit/components/FormRowTextList.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 71af505bf11..f5c41ebe170 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -78,7 +78,9 @@ component FormRowTextList( return; } - setRows(rows.filter((_, i) => i !== index)); + const newRows = [...rows]; + newRows.splice(index, 1); + setRows(newRows); }; return ( From 649270608ae5b40e65a3b0710c79c9324f77bd87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Fri, 6 Sep 2024 11:43:11 +0200 Subject: [PATCH 09/18] Reuse AddButton in FormRowSelectList It makes sense for this to be a reusable component, but then we should reuse it. --- root/area/edit_form.tt | 3 +++ .../scripts/edit/components/AddButton.js | 19 ++++++++++++++++--- .../edit/components/FormRowSelectList.js | 10 ++++------ .../edit/components/FormRowTextList.js | 7 ++++++- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/root/area/edit_form.tt b/root/area/edit_form.tt index 06d170794ce..c5d4b6e66c0 100644 --- a/root/area/edit_form.tt +++ b/root/area/edit_form.tt @@ -12,18 +12,21 @@ [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { repeatable => form_to_json(form.field('iso_3166_1')), label => 'ISO 3166-1:', + addButtonId => 'add-iso-3166-1', addButtonLabel => 'Add ISO 3166-1', removeButtonLabel => 'Remove ISO 3166-1', }) %] [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { repeatable => form_to_json(form.field('iso_3166_2')), label => 'ISO 3166-2:', + addButtonId => 'add-iso-3166-2', addButtonLabel => 'Add ISO 3166-2', removeButtonLabel => 'Remove ISO 3166-2', }) %] [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { repeatable => form_to_json(form.field('iso_3166_3')), label => 'ISO 3166-3:', + addButtonId => 'add-iso-3166-3', addButtonLabel => 'Add ISO 3166-3', removeButtonLabel => 'Remove ISO 3166-3', }) %] diff --git a/root/static/scripts/edit/components/AddButton.js b/root/static/scripts/edit/components/AddButton.js index 8d27b2870c9..7b0004455cf 100644 --- a/root/static/scripts/edit/components/AddButton.js +++ b/root/static/scripts/edit/components/AddButton.js @@ -8,15 +8,28 @@ */ component AddButton( - onClick: (event: SyntheticEvent) => void, + id: string, + onClick: (event: SyntheticEvent) => void, label?: string, ) { if (label == null) { - return ); diff --git a/root/static/scripts/edit/components/FormRowSelectList.js b/root/static/scripts/edit/components/FormRowSelectList.js index 0a2a5f98bf8..19b4b27307f 100644 --- a/root/static/scripts/edit/components/FormRowSelectList.js +++ b/root/static/scripts/edit/components/FormRowSelectList.js @@ -9,6 +9,7 @@ import SelectField from '../../common/components/SelectField.js'; +import AddButton from './AddButton.js'; import FieldErrors from './FieldErrors.js'; import FormRow from './FormRow.js'; @@ -49,14 +50,11 @@ component FormRowSelectList( ))} {hideAddButton ? null : (
- + />
)}
diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index f5c41ebe170..11f0b8fb373 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -49,6 +49,7 @@ const initialRows = (repeatable: RepeatableFieldT>) => { component FormRowTextList( addButtonLabel: string, + addButtonId: string, label: string, removeButtonLabel: string, repeatable: RepeatableFieldT>, @@ -100,7 +101,11 @@ component FormRowTextList( ))}
- +
From d4c4dd8a29d728af51ca4c37a5dc8d5cc948950f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Fri, 6 Sep 2024 12:26:40 +0200 Subject: [PATCH 10/18] Use FormRowTextList everywhere This allows us to remove the TextList jquery code that the TT version uses. --- root/artist/edit_form.tt | 16 ++++++- root/components/forms.tt | 34 -------------- root/label/edit_form.tt | 16 ++++++- root/recording/edit_form.tt | 8 +++- root/static/scripts/artist/edit.js | 1 + root/static/scripts/edit.js | 1 - root/static/scripts/edit/MB/TextList.js | 61 ------------------------- root/static/scripts/label/edit.js | 2 + root/static/scripts/recording/edit.js | 1 + root/static/scripts/work/edit.js | 2 + root/work/edit_form.tt | 8 +++- 11 files changed, 48 insertions(+), 102 deletions(-) delete mode 100644 root/static/scripts/edit/MB/TextList.js diff --git a/root/artist/edit_form.tt b/root/artist/edit_form.tt index 3506b6874a8..dabe29f0c35 100644 --- a/root/artist/edit_form.tt +++ b/root/artist/edit_form.tt @@ -26,8 +26,20 @@ [% field_errors(r.form, 'area.name') %] [% END %] - [%- form_row_text_list(r, 'ipi_codes', add_colon(l('IPI codes')), l('IPI code')) -%] - [%- form_row_text_list(r, 'isni_codes', add_colon(l('ISNI codes')), l('ISNI code')) -%] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('ipi_codes')), + label => add_colon(l('IPI codes')), + addButtonId => 'add-ipi-code', + addButtonLabel => lp('Add IPI code', 'interactive'), + removeButtonLabel => lp('Remove IPI code', 'interactive'), + }) %] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('isni_codes')), + label => add_colon(l('ISNI codes')), + addButtonId => 'add-isni-code', + addButtonLabel => lp('Add ISNI code', 'interactive'), + removeButtonLabel => lp('Remove ISNI code', 'interactive'), + }) %]
diff --git a/root/components/forms.tt b/root/components/forms.tt index e39b1383b10..a922e599441 100644 --- a/root/components/forms.tt +++ b/root/components/forms.tt @@ -133,40 +133,6 @@ [% END %] [%- END -%] -[%- MACRO form_row_text_list(r, field_name, label, item_name) BLOCK # Converted to React at root/static/scripts/edit/components/FormTowTextList.js --%] - [% WRAPPER form_row %] - -
- - [% FOR value=r.form.field(field_name).value -%] -
- - -
- [% END -%] -
- -
-
- - - [% field_errors(r.form, field_name) %] - [% END %] -[%- END -%] - [%- MACRO form_row_date(r, field_name, label) BLOCK # Converted to React at root/components/FormRowPartialDate.js -%] [% WRAPPER form_row %] diff --git a/root/label/edit_form.tt b/root/label/edit_form.tt index 5b4b33b0c79..d7fd8d1f165 100644 --- a/root/label/edit_form.tt +++ b/root/label/edit_form.tt @@ -29,8 +29,20 @@ LC- [% r.text('label_code', size => 5, class => "label-code", pattern => "[0-9]*") -%] [%- field_errors(form, 'label_code') -%] [% END %] - [%- form_row_text_list(r, 'ipi_codes', add_colon(l('IPI codes')), l('IPI code')) -%] - [%- form_row_text_list(r, 'isni_codes', add_colon(l('ISNI codes')), l('ISNI code')) -%] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('ipi_codes')), + label => add_colon(l('IPI codes')), + addButtonId => 'add-ipi-code', + addButtonLabel => lp('Add IPI code', 'interactive'), + removeButtonLabel => lp('Remove IPI code', 'interactive'), + }) %] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('isni_codes')), + label => add_colon(l('ISNI codes')), + addButtonId => 'add-isni-code', + addButtonLabel => lp('Add ISNI code', 'interactive'), + removeButtonLabel => lp('Remove ISNI code', 'interactive'), + }) %]
[% date_range_fieldset(r, 'label', l('This label has ended.')) %] diff --git a/root/recording/edit_form.tt b/root/recording/edit_form.tt index a703f5da200..d637a73a042 100644 --- a/root/recording/edit_form.tt +++ b/root/recording/edit_form.tt @@ -42,7 +42,13 @@ [%- END -%] [%- END -%] [%- form_row_checkbox(r, 'video', l('Video')) -%] - [%- form_row_text_list(r, 'isrcs', add_colon(l('ISRCs')), l('ISRC')) -%] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('isrcs')), + label => add_colon(l('ISRCs')), + addButtonId => 'add-isrc', + addButtonLabel => lp('Add ISRC', 'interactive'), + removeButtonLabel => lp('Remove ISRC', 'interactive'), + }) %] [% PROCESS 'forms/relationship-editor.tt' %] diff --git a/root/static/scripts/artist/edit.js b/root/static/scripts/artist/edit.js index 6ccf36351f8..fc67cfbad08 100644 --- a/root/static/scripts/artist/edit.js +++ b/root/static/scripts/artist/edit.js @@ -8,6 +8,7 @@ */ import './components/ArtistCreditRenamer.js'; +import '../edit/components/FormRowTextList.js'; import '../relationship-editor/components/RelationshipEditorWrapper.js'; import typeBubble from '../edit/typeBubble.js'; diff --git a/root/static/scripts/edit.js b/root/static/scripts/edit.js index ee090169761..59d17c8b5f8 100644 --- a/root/static/scripts/edit.js +++ b/root/static/scripts/edit.js @@ -21,7 +21,6 @@ require('./edit/MB/Control/Bubble.js'); require('./edit/URLCleanup.js'); require('./edit/MB/edit.js'); require('./edit/MB/reltypeslist.js'); -require('./edit/MB/TextList.js'); require('./edit/check-duplicates.js'); require('./guess-case/MB/Control/GuessCase.js'); diff --git a/root/static/scripts/edit/MB/TextList.js b/root/static/scripts/edit/MB/TextList.js deleted file mode 100644 index 79b6e0e0bac..00000000000 --- a/root/static/scripts/edit/MB/TextList.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2012 MetaBrainz Foundation - * - * This file is part of MusicBrainz, the open internet music database, - * and is licensed under the GPL version 2, or (at your option) any - * later version: http://www.gnu.org/licenses/gpl-2.0.txt - */ - -import $ from 'jquery'; - -import MB from '../../common/MB.js'; - -MB.Form = (MB.Form) ? MB.Form : {}; - -MB.Form.TextList = function (input) { - var template = input + '-template'; - var self = {}; - var $template = $('.' + template.replace(/\./g, '\\.')); - var counter = 0; - - self.removeEvent = function () { - $(this).closest('div.text-list-row').remove(); - }; - - self.init = function (maxIndex) { - counter = maxIndex; - $template - .parent() - .find('div.text-list-row input.value') - .siblings('button.remove-item') - .bind('click.mb', self.removeEvent); - - return self; - }; - - self.add = function (initValue) { - $template.clone() - .removeClass(template) - .insertAfter($template - .parent() - .find('div.text-list-row') - .last()) - .show() - .find('input.value') - .attr('name', input + '.' + counter) - .val(initValue) - .end() - .find('button.remove-item') - .bind('click.mb', self.removeEvent); - - counter++; - - return self; - }; - - $template.parent().find('button.add-item').bind('click.mb', function () { - self.add(''); - }); - - return self; -}; diff --git a/root/static/scripts/label/edit.js b/root/static/scripts/label/edit.js index bc875ef2aae..e793813c057 100644 --- a/root/static/scripts/label/edit.js +++ b/root/static/scripts/label/edit.js @@ -7,6 +7,8 @@ * later version: http://www.gnu.org/licenses/gpl-2.0.txt */ +import '../edit/components/FormRowTextList.js'; + import typeBubble from '../edit/typeBubble.js'; const typeIdField = 'select[name=edit-label\\.type_id]'; diff --git a/root/static/scripts/recording/edit.js b/root/static/scripts/recording/edit.js index a886a01f142..f0ef8d56882 100644 --- a/root/static/scripts/recording/edit.js +++ b/root/static/scripts/recording/edit.js @@ -8,3 +8,4 @@ */ import './RecordingName.js'; +import '../edit/components/FormRowTextList.js'; diff --git a/root/static/scripts/work/edit.js b/root/static/scripts/work/edit.js index 51a90c8d894..adfaa7dba72 100644 --- a/root/static/scripts/work/edit.js +++ b/root/static/scripts/work/edit.js @@ -17,6 +17,8 @@ import {flushSync} from 'react-dom'; import * as ReactDOMClient from 'react-dom/client'; import {legacy_createStore as createStore} from 'redux'; +import '../edit/components/FormRowTextList.js'; + import {LANGUAGE_MUL_ID, LANGUAGE_ZXX_ID} from '../common/constants.js'; import {groupBy} from '../common/utility/arrays.js'; import getScriptArgs from '../common/utility/getScriptArgs.js'; diff --git a/root/work/edit_form.tt b/root/work/edit_form.tt index c98ebfd8a6a..958c24d4421 100644 --- a/root/work/edit_form.tt +++ b/root/work/edit_form.tt @@ -13,7 +13,13 @@ [%- form_row_text_long(r, 'comment', add_colon(l('Disambiguation'))) -%] [%- form_row_select(r, 'type_id', add_colon(l('Type'))) -%]
- [%- form_row_text_list(r, 'iswcs', add_colon(l('ISWCs')), l('ISWC')) -%] + [% React.embed(c, 'static/scripts/edit/components/FormRowTextList', { + repeatable => form_to_json(form.field('iswcs')), + label => add_colon(l('ISWCs')), + addButtonId => 'add-iswc', + addButtonLabel => lp('Add ISWC', 'interactive'), + removeButtonLabel => lp('Remove ISWC', 'interactive'), + }) %]
From 13ef79e1584ed454e0261f19c45e0578a62622ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Tue, 10 Sep 2024 14:43:17 +0200 Subject: [PATCH 11/18] Keep repeatable field in state rather than reducing to rows --- .../edit/components/FormRowTextList.js | 61 +++++++++++-------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 11f0b8fb373..54775ff28ae 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -7,8 +7,11 @@ * later version: http://www.gnu.org/licenses/gpl-2.0.txt */ +import mutate from 'mutate-cow'; import React, {useState} from 'react'; +import {pushField} from '../utility/pushField.js'; + import AddButton from './AddButton.js'; import FieldErrors from './FieldErrors.js'; import FormLabel from './FormLabel.js'; @@ -36,15 +39,14 @@ component TextListRow( ); } -const initialRows = (repeatable: RepeatableFieldT>) => { - if (repeatable.field.length === 0) { - return [{name: repeatable.html_name + '.0', value: ''}]; +const createInitialState = (repeatable: RepeatableFieldT>) => { + let newField = {...repeatable}; + if (newField.last_index === -1) { + newField = mutate(newField).update((fieldCtx) => { + pushField(fieldCtx, ''); + }).final(); } - - return repeatable.field.map((field, index) => ({ - name: repeatable.html_name + '.' + index, - value: field.value ?? '', - })); + return newField; }; component FormRowTextList( @@ -55,33 +57,40 @@ component FormRowTextList( repeatable: RepeatableFieldT>, required: boolean = false, ) { - const newRow = (name: string, value: string, index: number) => { - return {name: name + '.' + index, value}; - }; - - const [rows, setRows] = useState(initialRows(repeatable)); + const [compoundField, setCompoundField] = + useState(createInitialState(repeatable)); const addRow = () => { - const index = rows.length; + const newField = mutate(compoundField).update((fieldCtx) => { + pushField(fieldCtx, ''); + }).final(); - setRows([...rows, newRow(repeatable.html_name, '', index)]); + setCompoundField(newField); }; const changeRow = (index: number, value: string) => { - const newRows = [...rows]; - newRows[index] = newRow(repeatable.html_name, value, index); - setRows(newRows); + const newField = mutate(compoundField) + .set('field', index, 'value', value) + .final(); + setCompoundField(newField); }; const removeRow = (index: number) => { - if (rows.length === 1) { - setRows([newRow(repeatable.html_name, '', 0)]); + if (compoundField.field.length === 1) { + const newField = mutate(compoundField) + .set('field', index, 'value', '') + .final(); + + setCompoundField(newField); + return; } - const newRows = [...rows]; - newRows.splice(index, 1); - setRows(newRows); + const newField = mutate(compoundField).update('field', (fieldCtx) => { + fieldCtx.write().splice(index, 1); + }).final(); + + setCompoundField(newField); }; return ( @@ -89,10 +98,10 @@ component FormRowTextList(
- {rows.map((field, index) => ( + {compoundField.field.map((field, index) => ( changeRow(index, event.currentTarget.value)} onRemove={() => removeRow(index)} removeButtonLabel={removeButtonLabel} From 83c21f4b114f7b65cd3cc388c9709fe734940fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Tue, 10 Sep 2024 19:02:21 +0200 Subject: [PATCH 12/18] Use field id rather than index for state changes --- .../edit/components/FormRowTextList.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 54775ff28ae..bb80816e166 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -68,14 +68,22 @@ component FormRowTextList( setCompoundField(newField); }; - const changeRow = (index: number, value: string) => { + const changeRow = (id: number, value: string) => { + const index = compoundField.field.findIndex( + (subfield) => subfield.id === id, + ); + const newField = mutate(compoundField) .set('field', index, 'value', value) .final(); setCompoundField(newField); }; - const removeRow = (index: number) => { + const removeRow = (id: number) => { + const index = compoundField.field.findIndex( + (subfield) => subfield.id === id, + ); + if (compoundField.field.length === 1) { const newField = mutate(compoundField) .set('field', index, 'value', '') @@ -98,12 +106,15 @@ component FormRowTextList(
- {compoundField.field.map((field, index) => ( + {compoundField.field.map((field) => ( changeRow(index, event.currentTarget.value)} - onRemove={() => removeRow(index)} + onChange={(event) => changeRow( + field.id, + event.currentTarget.value, + )} + onRemove={() => removeRow(field.id)} removeButtonLabel={removeButtonLabel} value={field.value} /> From 896455f5785af68851cac41865a130cb40afd44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Tue, 10 Sep 2024 19:48:15 +0200 Subject: [PATCH 13/18] Use dispatch/reducer This will be needed (or at least helpful) anyway when we use this in a larger React form so we might as well get it ready now. --- .../edit/components/FormRowTextList.js | 109 +++++++++++------- 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index bb80816e166..9a11d7d7b58 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -8,7 +8,7 @@ */ import mutate from 'mutate-cow'; -import React, {useState} from 'react'; +import React from 'react'; import {pushField} from '../utility/pushField.js'; @@ -18,6 +18,13 @@ import FormLabel from './FormLabel.js'; import FormRow from './FormRow.js'; import RemoveButton from './RemoveButton.js'; +type StateT = RepeatableFieldT>; + +type ActionT = + | {+type: 'add-row'} + | {+fieldId: number, +type: 'remove-row'} + | {+fieldId: number, +type: 'update-row', +value: string}; + component TextListRow( name: string, onChange: (event: SyntheticEvent) => void, @@ -49,6 +56,44 @@ const createInitialState = (repeatable: RepeatableFieldT>) => { return newField; }; +function reducer(state: StateT, action: ActionT): StateT { + const newStateCtx = mutate(state); + const fieldCtx = newStateCtx.get('field'); + + switch (action.type) { + case 'add-row': { + newStateCtx.update((fieldCtx) => { + pushField(fieldCtx, ''); + }); + break; + } + case 'remove-row': { + const index = fieldCtx.read().findIndex( + (subfield) => subfield.id === action.fieldId, + ); + + if (fieldCtx.read().length === 1) { + newStateCtx.set('field', index, 'value', ''); + break; + } + + newStateCtx.update('field', (fieldCtx) => { + fieldCtx.write().splice(index, 1); + }); + break; + } + case 'update-row': { + const index = fieldCtx.read().findIndex( + (subfield) => subfield.id === action.fieldId, + ); + + newStateCtx.set('field', index, 'value', action.value); + break; + } + } + return newStateCtx.final(); +} + component FormRowTextList( addButtonLabel: string, addButtonId: string, @@ -57,60 +102,40 @@ component FormRowTextList( repeatable: RepeatableFieldT>, required: boolean = false, ) { - const [compoundField, setCompoundField] = - useState(createInitialState(repeatable)); - - const addRow = () => { - const newField = mutate(compoundField).update((fieldCtx) => { - pushField(fieldCtx, ''); - }).final(); - - setCompoundField(newField); - }; - - const changeRow = (id: number, value: string) => { - const index = compoundField.field.findIndex( - (subfield) => subfield.id === id, + const [state, dispatch] = + React.useReducer>>( + reducer, + repeatable, + createInitialState, ); - const newField = mutate(compoundField) - .set('field', index, 'value', value) - .final(); - setCompoundField(newField); - }; - - const removeRow = (id: number) => { - const index = compoundField.field.findIndex( - (subfield) => subfield.id === id, - ); + const removeRow = React.useCallback(( + fieldId: number, + ): void => { + dispatch({fieldId, type: 'remove-row'}); + }, [dispatch]); - if (compoundField.field.length === 1) { - const newField = mutate(compoundField) - .set('field', index, 'value', '') - .final(); - - setCompoundField(newField); - - return; - } - - const newField = mutate(compoundField).update('field', (fieldCtx) => { - fieldCtx.write().splice(index, 1); - }).final(); + const addRow = React.useCallback(() => { + dispatch({type: 'add-row'}); + }, [dispatch]); - setCompoundField(newField); - }; + const updateRow = React.useCallback(( + fieldId: number, + value: string, + ) => { + dispatch({fieldId, type: 'update-row', value}); + }, [dispatch]); return ( <>
- {compoundField.field.map((field) => ( + {state.field.map((field) => ( changeRow( + onChange={(event) => updateRow( field.id, event.currentTarget.value, )} From 29ab7969dc8194853b34d5b3e4f0e55b8fc11d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Tue, 10 Sep 2024 20:05:07 +0200 Subject: [PATCH 14/18] Move update/removeRow down to TextListRow --- .../edit/components/FormRowTextList.js | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 9a11d7d7b58..97166656e6f 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -26,22 +26,36 @@ type ActionT = | {+fieldId: number, +type: 'update-row', +value: string}; component TextListRow( + dispatch: (ActionT) => void, + fieldId: number, name: string, - onChange: (event: SyntheticEvent) => void, - onRemove: (event: SyntheticEvent) => void, removeButtonLabel: string, value: string, ) { + const removeRow = React.useCallback((): void => { + dispatch({fieldId, type: 'remove-row'}); + }, [dispatch, fieldId]); + + const updateRow = React.useCallback(( + event: SyntheticKeyboardEvent, + ) => { + const value = event.currentTarget.value; + dispatch({fieldId, type: 'update-row', value}); + }, [dispatch, fieldId]); + return (
- +
); } @@ -109,23 +123,10 @@ component FormRowTextList( createInitialState, ); - const removeRow = React.useCallback(( - fieldId: number, - ): void => { - dispatch({fieldId, type: 'remove-row'}); - }, [dispatch]); - const addRow = React.useCallback(() => { dispatch({type: 'add-row'}); }, [dispatch]); - const updateRow = React.useCallback(( - fieldId: number, - value: string, - ) => { - dispatch({fieldId, type: 'update-row', value}); - }, [dispatch]); - return ( <> @@ -133,13 +134,10 @@ component FormRowTextList(
{state.field.map((field) => ( updateRow( - field.id, - event.currentTarget.value, - )} - onRemove={() => removeRow(field.id)} removeButtonLabel={removeButtonLabel} value={field.value} /> From 845ccc494aff70678550d3884cd06318037c227b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Wed, 11 Sep 2024 12:37:45 +0200 Subject: [PATCH 15/18] Remove form label association This is not ideal, but it's the same as in production - and associating it with the repeatable field is invalid HTML. --- root/static/scripts/edit/components/FormRowTextList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 97166656e6f..2b4c2606ca8 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -129,7 +129,7 @@ component FormRowTextList( return ( <> - +
{state.field.map((field) => ( From eb5b5a8ad7b09403d419e7b23847572193a39ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Wed, 11 Sep 2024 15:05:49 +0200 Subject: [PATCH 16/18] Drop unneeded edit_form in event Role::Create --- lib/MusicBrainz/Server/Controller/Event.pm | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/MusicBrainz/Server/Controller/Event.pm b/lib/MusicBrainz/Server/Controller/Event.pm index 58dbc6424d6..33570649b89 100644 --- a/lib/MusicBrainz/Server/Controller/Event.pm +++ b/lib/MusicBrainz/Server/Controller/Event.pm @@ -128,7 +128,6 @@ with 'MusicBrainz::Server::Controller::Role::Merge' => { with 'MusicBrainz::Server::Controller::Role::Create' => { form => 'Event', edit_type => $EDIT_EVENT_CREATE, - dialog_template => 'event/edit_form.tt', }; with 'MusicBrainz::Server::Controller::Role::Edit' => { From 7cec36a13cb3704cb6d222fc000e231d2a7ce151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Mon, 16 Sep 2024 14:29:52 +0200 Subject: [PATCH 17/18] Remove activeUser param from AC editor This seems entirely unused. --- root/static/scripts/edit/components/ArtistCreditEditor.js | 1 - .../scripts/edit/components/ArtistCreditEditor/types.js | 1 - root/static/scripts/edit/components/forms.js | 1 - root/static/scripts/release-editor/bindingHandlers.js | 1 - root/static/scripts/tests/artist-credit-editor.js | 4 ---- 5 files changed, 8 deletions(-) diff --git a/root/static/scripts/edit/components/ArtistCreditEditor.js b/root/static/scripts/edit/components/ArtistCreditEditor.js index bfb11d74a34..ec80d35d14e 100644 --- a/root/static/scripts/edit/components/ArtistCreditEditor.js +++ b/root/static/scripts/edit/components/ArtistCreditEditor.js @@ -444,7 +444,6 @@ function createInitialNamesState( export function createInitialState( initialState: { - +activeUser: ActiveEditorT, +entity: ArtistCreditableT, +formName?: string, /* diff --git a/root/static/scripts/edit/components/ArtistCreditEditor/types.js b/root/static/scripts/edit/components/ArtistCreditEditor/types.js index 5f499b53d8b..144a8243f2c 100644 --- a/root/static/scripts/edit/components/ArtistCreditEditor/types.js +++ b/root/static/scripts/edit/components/ArtistCreditEditor/types.js @@ -29,7 +29,6 @@ export type ArtistCreditNameStateT = { }; export type StateT = { - +activeUser: ActiveEditorT | null, +artistCreditString: string, +changeMatchingTrackArtists?: boolean, +editsPending?: boolean, diff --git a/root/static/scripts/edit/components/forms.js b/root/static/scripts/edit/components/forms.js index 12bb1bed499..4d68f6c75f9 100644 --- a/root/static/scripts/edit/components/forms.js +++ b/root/static/scripts/edit/components/forms.js @@ -157,7 +157,6 @@ MB.initializeArtistCredit = function (form, initialArtistCredit) { }); const initialState = createArtistCreditEditorState({ - activeUser: window[GLOBAL_JS_NAMESPACE].$c.user, artistCredit: initialArtistCredit, entity: source, formName: form.name, diff --git a/root/static/scripts/release-editor/bindingHandlers.js b/root/static/scripts/release-editor/bindingHandlers.js index ca249bd1ac9..9d995d25bb1 100644 --- a/root/static/scripts/release-editor/bindingHandlers.js +++ b/root/static/scripts/release-editor/bindingHandlers.js @@ -105,7 +105,6 @@ ko.bindingHandlers.artistCreditEditor = { const artistCredit = entity.artistCredit(); const initialState = createArtistCreditEditorState({ - activeUser: window[GLOBAL_JS_NAMESPACE].$c.user, artistCredit, entity, id: entity.uniqueID, diff --git a/root/static/scripts/tests/artist-credit-editor.js b/root/static/scripts/tests/artist-credit-editor.js index 2117cc19bb7..56e57b43b12 100644 --- a/root/static/scripts/tests/artist-credit-editor.js +++ b/root/static/scripts/tests/artist-credit-editor.js @@ -9,22 +9,18 @@ import test from 'tape'; -import activeSanitizedEditor - from '../../../utility/activeSanitizedEditor.mjs'; import { createInitialState, reducer, } from '../edit/components/ArtistCreditEditor.js'; import { - genericEditor, genericRecording, } from './utility/constants.js'; test('MBS-13538: Removing all rows in the AC editor makes it disappear', function (t) { t.plan(2); const state = createInitialState({ - activeUser: activeSanitizedEditor(genericEditor), entity: genericRecording, id: '', }); From 7ee05733d2d965a096789f6185f2661b02d27fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Tamargo?= Date: Wed, 11 Sep 2024 16:16:41 +0200 Subject: [PATCH 18/18] [WIP] MBS-12761: Convert recording edit form to React --- eslint.config.mjs | 1 + .../Server/Controller/Recording.pm | 1 - .../Server/Controller/Role/Create.pm | 16 +- .../Server/Controller/Role/Edit.pm | 15 +- root/recording/CreateRecording.js | 30 ++ root/recording/EditRecording.js | 36 ++ root/recording/types.js | 19 + root/server/components.mjs | 2 + root/static/scripts/common/entity2.js | 4 + .../static/scripts/common/utility/catalyst.js | 12 +- .../edit/components/FormRowTextList.js | 7 +- .../recording/components/RecordingEditForm.js | 338 ++++++++++++++++++ root/static/scripts/relationship-editor.js | 2 +- root/types/formcomponents.js | 10 + webpack/client.config.mjs | 1 + 15 files changed, 480 insertions(+), 14 deletions(-) create mode 100644 root/recording/CreateRecording.js create mode 100644 root/recording/EditRecording.js create mode 100644 root/recording/types.js create mode 100644 root/static/scripts/recording/components/RecordingEditForm.js diff --git a/eslint.config.mjs b/eslint.config.mjs index 94f9c59776d..04ef3907f14 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -969,6 +969,7 @@ export default [ 'root/static/scripts/edit/components/UrlRelationshipCreditFieldset.js', 'root/static/scripts/edit/externalLinks.js', 'root/static/scripts/event/components/EventEditForm.js', + 'root/static/scripts/recording/components/RecordingEditForm.js', 'root/static/scripts/relationship-editor/components/DialogPreview.js', ], rules: { diff --git a/lib/MusicBrainz/Server/Controller/Recording.pm b/lib/MusicBrainz/Server/Controller/Recording.pm index 9e07c4a14e8..a59b5d2bd0f 100644 --- a/lib/MusicBrainz/Server/Controller/Recording.pm +++ b/lib/MusicBrainz/Server/Controller/Recording.pm @@ -198,7 +198,6 @@ with 'MusicBrainz::Server::Controller::Role::Create' => { $ret{form_args} = { used_by_tracks => 0 }; return %ret; }, - dialog_template => 'recording/edit_form.tt', }; sub _merge_load_entities { diff --git a/lib/MusicBrainz/Server/Controller/Role/Create.pm b/lib/MusicBrainz/Server/Controller/Role/Create.pm index f1789d88df3..c96e3fa62bc 100644 --- a/lib/MusicBrainz/Server/Controller/Role/Create.pm +++ b/lib/MusicBrainz/Server/Controller/Role/Create.pm @@ -60,10 +60,12 @@ role { my $model = $self->config->{model}; my $entity; my %props; + my %edit_arguments = $params->edit_arguments->($self, $c); - if ($model eq 'Event' || $model eq 'Genre') { + if ($model eq 'Event' || $model eq 'Genre' || $model eq 'Recording') { my $type = model_to_type($model); - my $form = $c->form( form => $params->form ); + my %form_args = %{ $edit_arguments{form_args} || {}}; + my $form = $c->form( form => $params->form, ctx => $c, %form_args ); %props = ( form => $form->TO_JSON ); $c->stash( @@ -98,8 +100,9 @@ role { delete $c->flash->{message}; }, pre_validation => sub { + my $form = shift; + if ($model eq 'Event') { - my $form = shift; my %event_descriptions = map { $_->id => $_->l_description } $c->model('EventType')->get_all(); @@ -107,6 +110,11 @@ role { $props{eventTypes} = $form->options_type_id; $props{eventDescriptions} = \%event_descriptions; } + + if ($model eq 'Recording') { + $props{usedByTracks} = $form->used_by_tracks; + } + }, redirect => sub { $c->response->redirect($c->uri_for_action( @@ -114,7 +122,7 @@ role { }, no_redirect => $args{within_dialog}, edit_rels => 1, - $params->edit_arguments->($self, $c), + %edit_arguments, ); }; }; diff --git a/lib/MusicBrainz/Server/Controller/Role/Edit.pm b/lib/MusicBrainz/Server/Controller/Role/Edit.pm index 62591ffa727..531586c92ac 100644 --- a/lib/MusicBrainz/Server/Controller/Role/Edit.pm +++ b/lib/MusicBrainz/Server/Controller/Role/Edit.pm @@ -34,18 +34,22 @@ role { method 'edit' => sub { my ($self, $c) = @_; - my @react_models = qw( Event Genre); + my @react_models = qw( Event Genre Recording ); my $entity_name = $self->{entity_name}; my $edit_entity = $c->stash->{ $entity_name }; my $model = $self->{model}; my %props; + my %edit_arguments = $params->edit_arguments->($self, $c, $edit_entity); if (any { $_ eq $model } @react_models) { my $type = model_to_type($model); + my %form_args = %{ $edit_arguments{form_args} || {}}; my $form = $c->form( form => $params->form, + ctx => $c, init_object => $edit_entity, + %form_args, ); %props = ( @@ -69,8 +73,9 @@ role { edit_args => { to_edit => $edit_entity }, edit_rels => 1, pre_validation => sub { + my $form = shift; + if ($model eq 'Event') { - my $form = shift; my %event_descriptions = map { $_->id => $_->l_description } $c->model('EventType')->get_all(); @@ -78,12 +83,16 @@ role { $props{eventTypes} = $form->options_type_id; $props{eventDescriptions} = \%event_descriptions; } + + if ($model eq 'Recording') { + $props{usedByTracks} = $form->used_by_tracks; + } }, redirect => sub { $c->response->redirect( $c->uri_for_action($self->action_for('show'), [ $edit_entity->gid ])); }, - $params->edit_arguments->($self, $c, $edit_entity), + %edit_arguments, ); }; }; diff --git a/root/recording/CreateRecording.js b/root/recording/CreateRecording.js new file mode 100644 index 00000000000..68e71f3a219 --- /dev/null +++ b/root/recording/CreateRecording.js @@ -0,0 +1,30 @@ +/* + * @flow strict-local + * Copyright (C) 2024 MetaBrainz Foundation + * + * This file is part of MusicBrainz, the open internet music database, + * and is licensed under the GPL version 2, or (at your option) any + * later version: http://www.gnu.org/licenses/gpl-2.0.txt + */ + +import Layout from '../layout/index.js'; +import manifest from '../static/manifest.mjs'; +import RecordingEditForm + from '../static/scripts/recording/components/RecordingEditForm.js'; + +import type {RecordingFormT} from './types.js'; + +component CreateRecording(form: RecordingFormT, usedByTracks: boolean) { + return ( + +
+

{lp('Add recording', 'header')}

+ +
+ {manifest('recording/components/RecordingEditForm', {async: 'async'})} + {manifest('relationship-editor', {async: 'async'})} +
+ ); +} + +export default CreateRecording; diff --git a/root/recording/EditRecording.js b/root/recording/EditRecording.js new file mode 100644 index 00000000000..6bb6c0daeb8 --- /dev/null +++ b/root/recording/EditRecording.js @@ -0,0 +1,36 @@ +/* + * @flow strict-local + * Copyright (C) 2024 MetaBrainz Foundation + * + * This file is part of MusicBrainz, the open internet music database, + * and is licensed under the GPL version 2, or (at your option) any + * later version: http://www.gnu.org/licenses/gpl-2.0.txt + */ + +import manifest from '../static/manifest.mjs'; +import RecordingEditForm + from '../static/scripts/recording/components/RecordingEditForm.js'; + +import RecordingLayout from './RecordingLayout.js'; +import type {RecordingFormT} from './types.js'; + +component EditRecording( + entity: RecordingWithArtistCreditT, + form: RecordingFormT, + usedByTracks: boolean, +) { + return ( + + + {manifest('recording/components/RecordingEditForm', {async: 'async'})} + {manifest('relationship-editor', {async: 'async'})} + + ); +} + +export default EditRecording; diff --git a/root/recording/types.js b/root/recording/types.js new file mode 100644 index 00000000000..1917f80d46e --- /dev/null +++ b/root/recording/types.js @@ -0,0 +1,19 @@ +/* + * @flow strict + * Copyright (C) 2024 MetaBrainz Foundation + * + * This file is part of MusicBrainz, the open internet music database, + * and is licensed under the GPL version 2, or (at your option) any + * later version: http://www.gnu.org/licenses/gpl-2.0.txt + */ + +export type RecordingFormT = FormT<{ + +artist_credit: ArtistCreditFieldT, + +comment: FieldT, + +edit_note: FieldT, + +isrcs: RepeatableFieldT>, + +length: FieldT, + +make_votable: FieldT, + +name: FieldT, + +video: FieldT, + }>; diff --git a/root/server/components.mjs b/root/server/components.mjs index dc18e851636..a3a09f8e1d7 100644 --- a/root/server/components.mjs +++ b/root/server/components.mjs @@ -185,7 +185,9 @@ export default { 'place/PlaceMap': (): Promise => import('../place/PlaceMap.js'), 'place/PlaceMerge': (): Promise => import('../place/PlaceMerge.js'), 'place/PlacePerformances': (): Promise => import('../place/PlacePerformances.js'), + 'recording/CreateRecording': (): Promise => import('../recording/CreateRecording.js'), 'recording/DeleteRecording': (): Promise => import('../recording/DeleteRecording.js'), + 'recording/EditRecording': (): Promise => import('../recording/EditRecording.js'), 'recording/RecordingFingerprints': (): Promise => import('../recording/RecordingFingerprints.js'), 'recording/RecordingIndex': (): Promise => import('../recording/RecordingIndex.js'), 'recording/RecordingMerge': (): Promise => import('../recording/RecordingMerge.js'), diff --git a/root/static/scripts/common/entity2.js b/root/static/scripts/common/entity2.js index 5ad052fc502..5e1573c39f8 100644 --- a/root/static/scripts/common/entity2.js +++ b/root/static/scripts/common/entity2.js @@ -242,6 +242,10 @@ export function createRecordingObject( }>, ): RecordingT { return { + artist: '', + artistCredit: { + names: ([]: $ReadOnlyArray), + }, comment: '', editsPending: false, entityType: 'recording', diff --git a/root/static/scripts/common/utility/catalyst.js b/root/static/scripts/common/utility/catalyst.js index 2e5e726d9c4..f172f0b5b17 100644 --- a/root/static/scripts/common/utility/catalyst.js +++ b/root/static/scripts/common/utility/catalyst.js @@ -30,7 +30,9 @@ export function maybeGetCatalystContext(): ?SanitizedCatalystContextT { return globalThis[GLOBAL_JS_NAMESPACE]?.$c; } -export function getSourceEntityData(): +export function getSourceEntityData( + passedContext?: SanitizedCatalystContextT, +): | RelatableEntityT | { +entityType: RelatableEntityTypeT, @@ -39,12 +41,14 @@ export function getSourceEntityData(): +orderingTypeID?: number, } | null { - const $c = getCatalystContext(); + const $c = passedContext ?? getCatalystContext(); return $c.stash.source_entity ?? null; } -export function getSourceEntityDataForRelationshipEditor(): RelatableEntityT { - let source = getSourceEntityData(); +export function getSourceEntityDataForRelationshipEditor( + $c?: SanitizedCatalystContextT, +): RelatableEntityT { + let source = getSourceEntityData($c); invariant( source, 'Source entity data not found in global Catalyst stash', diff --git a/root/static/scripts/edit/components/FormRowTextList.js b/root/static/scripts/edit/components/FormRowTextList.js index 2b4c2606ca8..e8bb1e0f77d 100644 --- a/root/static/scripts/edit/components/FormRowTextList.js +++ b/root/static/scripts/edit/components/FormRowTextList.js @@ -29,6 +29,7 @@ component TextListRow( dispatch: (ActionT) => void, fieldId: number, name: string, + onFocus?: (event: SyntheticEvent) => void, removeButtonLabel: string, value: string, ) { @@ -49,6 +50,7 @@ component TextListRow( className="value with-button" name={name} onChange={updateRow} + onFocus={onFocus} type="text" value={value} /> @@ -112,6 +114,7 @@ component FormRowTextList( addButtonLabel: string, addButtonId: string, label: string, + onFocus?: (event: SyntheticEvent) => void, removeButtonLabel: string, repeatable: RepeatableFieldT>, required: boolean = false, @@ -138,6 +141,7 @@ component FormRowTextList( fieldId={field.id} key={field.id} name={field.html_name} + onFocus={onFocus} removeButtonLabel={removeButtonLabel} value={field.value} /> @@ -158,10 +162,11 @@ component FormRowTextList( } export component NonHydratedFormRowTextList( + rowRef?: {-current: HTMLDivElement | null}, ...props: React.PropsOf ) { return ( - + ); diff --git a/root/static/scripts/recording/components/RecordingEditForm.js b/root/static/scripts/recording/components/RecordingEditForm.js new file mode 100644 index 00000000000..5b427566ff5 --- /dev/null +++ b/root/static/scripts/recording/components/RecordingEditForm.js @@ -0,0 +1,338 @@ +/* + * @flow strict-local + * Copyright (C) 2024 MetaBrainz Foundation + * + * This file is part of MusicBrainz, the open internet music database, + * and is licensed under the GPL version 2, or (at your option) any + * later version: http://www.gnu.org/licenses/gpl-2.0.txt + */ + +import mutate from 'mutate-cow'; +import * as React from 'react'; + +import {SanitizedCatalystContext} from '../../../../context.mjs'; +import type {RecordingFormT} from '../../../../recording/types.js'; +import Bubble from '../../common/components/Bubble.js'; +import { + getSourceEntityDataForRelationshipEditor, +} from '../../common/utility/catalyst.js'; +import formatTrackLength + from '../../common/utility/formatTrackLength.js'; +import isBlank from '../../common/utility/isBlank.js'; +import ArtistCreditEditor, { + createInitialState as createArtistCreditState, + reducer as runArtistCreditReducer, +} from '../../edit/components/ArtistCreditEditor.js'; +import { + type ActionT as ArtistCreditActionT, + type StateT as ArtistCreditStateT, +} from '../../edit/components/ArtistCreditEditor/types.js'; +import EnterEdit from '../../edit/components/EnterEdit.js'; +import EnterEditNote from '../../edit/components/EnterEditNote.js'; +import FieldErrors from '../../edit/components/FieldErrors.js'; +import FormRow from '../../edit/components/FormRow.js'; +import FormRowCheckbox from '../../edit/components/FormRowCheckbox.js'; +import FormRowNameWithGuessCase, { + type ActionT as NameActionT, + runReducer as runNameReducer, +} from '../../edit/components/FormRowNameWithGuessCase.js'; +import {NonHydratedFormRowTextList as FormRowTextList} + from '../../edit/components/FormRowTextList.js'; +import FormRowTextLong from '../../edit/components/FormRowTextLong.js'; +import { + type StateT as GuessCaseOptionsStateT, + createInitialState as createGuessCaseOptionsState, +} from '../../edit/components/GuessCaseOptions.js'; +import { + _ExternalLinksEditor, + ExternalLinksEditor, + prepareExternalLinksHtmlFormSubmission, +} from '../../edit/externalLinks.js'; +import { + applyAllPendingErrors, + hasSubfieldErrors, +} from '../../edit/utility/subfieldErrors.js'; +import { + NonHydratedRelationshipEditorWrapper as RelationshipEditorWrapper, +} from '../../relationship-editor/components/RelationshipEditorWrapper.js'; + +/* eslint-disable ft-flow/sort-keys */ +type ActionT = + | {+type: 'show-all-pending-errors'} + | {+type: 'toggle-isrc-bubble'} + | {+type: 'update-name', +action: NameActionT} + | {+type: 'update-artist-credit', +action: ArtistCreditActionT}; +/* eslint-enable ft-flow/sort-keys */ + +type StateT = { + +artistCredit: ArtistCreditStateT, + +form: RecordingFormT, + +guessCaseOptions: GuessCaseOptionsStateT, + +isGuessCaseOptionsOpen: boolean, + +recording: RecordingT, + +showIsrcBubble: boolean, +}; + +function createInitialState( + form: RecordingFormT, + $c: SanitizedCatalystContextT, +) { + const recording = getSourceEntityDataForRelationshipEditor($c); + invariant(recording && recording.entityType === 'recording'); + + return { + artistCredit: createArtistCreditState({ + entity: recording, + formName: form.name, + id: 'artist-credit-editor', + }), + form, + guessCaseOptions: createGuessCaseOptionsState(), + isGuessCaseOptionsOpen: false, + recording, + showIsrcBubble: false, + }; +} + +function reducer(state: StateT, action: ActionT): StateT { + const newStateCtx = mutate(state); + + switch (action.type) { + case 'update-name': { + const nameStateCtx = mutate({ + field: state.form.field.name, + guessCaseOptions: state.guessCaseOptions, + isGuessCaseOptionsOpen: state.isGuessCaseOptionsOpen, + }); + runNameReducer(nameStateCtx, action.action); + + const nameState = nameStateCtx.read(); + newStateCtx + .update('form', 'field', 'name', (nameFieldCtx) => { + nameFieldCtx.set(nameState.field); + if (isBlank(nameState.field.value)) { + nameFieldCtx.set('has_errors', true); + nameFieldCtx.set('pendingErrors', [ + l('Required field.'), + ]); + } else { + nameFieldCtx.set('has_errors', false); + nameFieldCtx.set('pendingErrors', []); + nameFieldCtx.set('errors', []); + } + }) + .set('guessCaseOptions', nameState.guessCaseOptions) + .set('isGuessCaseOptionsOpen', nameState.isGuessCaseOptionsOpen); + break; + } + case 'toggle-isrc-bubble': { + newStateCtx.set('showIsrcBubble', true); + break; + } + case 'show-all-pending-errors': { + applyAllPendingErrors(newStateCtx.get('form')); + break; + } + case 'update-artist-credit': { + newStateCtx.set( + 'artistCredit', + runArtistCreditReducer(state.artistCredit, action.action), + ); + break; + } + default: { + /*:: exhaustive(action); */ + } + } + return newStateCtx.final(); +} + +component RecordingEditForm( + form as initialForm: RecordingFormT, + usedByTracks: boolean, +) { + const $c = React.useContext(SanitizedCatalystContext); + + const [state, dispatch] = React.useReducer( + reducer, + createInitialState(initialForm, $c), + ); + + const nameDispatch = React.useCallback((action: NameActionT) => { + dispatch({action, type: 'update-name'}); + }, [dispatch]); + + const artistCreditEditorDispatch = React.useCallback(( + action: ArtistCreditActionT, + ) => { + dispatch({action, type: 'update-artist-credit'}); + }, [dispatch]); + + function handleIsrcFocus() { + dispatch({type: 'toggle-isrc-bubble'}); + } + + const hasErrors = hasSubfieldErrors(state.form); + + const externalLinksEditorRef = React.createRef<_ExternalLinksEditor>(); + + // Ensure errors are shown if the user tries to submit with Enter + const handleKeyDown = (event: SyntheticKeyboardEvent) => { + if (event.key === 'Enter' && hasErrors) { + dispatch({type: 'show-all-pending-errors'}); + } + }; + + const handleSubmit = (event: SyntheticEvent) => { + if (hasErrors) { + dispatch({type: 'show-all-pending-errors'}); + event.preventDefault(); + } + invariant(externalLinksEditorRef.current); + prepareExternalLinksHtmlFormSubmission( + 'edit-recording', + externalLinksEditorRef.current, + ); + }; + + const isrcFieldRef = React.useRef(null); + + return ( +
+

+ {exp.l( + 'For more information, check the {doc_doc|documentation}.', + {doc_doc: {href: '/doc/Recording', target: '_blank'}}, + )} +

+ +
+
+ {l('Recording details')} + + + + + + + + {(!usedByTracks || state.form.field.length.has_errors) ? ( + + ) : ( + + + {exp.l( + `{recording_length} + ({length_info|derived} from the associated track lengths)`, + { + length_info: {href: '/doc/Recording', target: '_blank'}, + recording_length: formatTrackLength( + nonEmpty(state.form.field.length.value) + ? parseInt(state.form.field.length.value, 10) + : null, + ), + }, + )} + + + )} + + +
+ + + +
+ {l('External links')} + +
+ + + +
+ +
+ {state.showIsrcBubble ? ( + +

+ {exp.l(`You are about to add an ISRC to this recording. + The ISRC must be entered in standard + CCXXXYYNNNNN format:`)} +

+
    +
  • + {l(`"CC" is the appropriate for the registrant + two-character country code.`)} +
  • +
  • + {l(`"XXX" is a three character alphanumeric registrant code, + uniquely identifying the organisation + which registered the code.`)} +
  • +
  • + {l(`"YY" is the last two digits + of the year of registration.`)} +
  • +
  • + {l(`"NNNNN" is a unique 5-digit number identifying + the particular sound recording.`)} +
  • +
+
+ ) : null} +
+
+ ); +} + +export default (hydrate>( + 'div.recording-edit-form', + RecordingEditForm, +): React.AbstractComponent>); diff --git a/root/static/scripts/relationship-editor.js b/root/static/scripts/relationship-editor.js index fbd9fc2c2f3..64da631753a 100644 --- a/root/static/scripts/relationship-editor.js +++ b/root/static/scripts/relationship-editor.js @@ -16,7 +16,7 @@ import {createExternalLinksEditorForHtmlForm} from './edit/externalLinks.js'; const sourceData = getSourceEntityDataForRelationshipEditor(); const entityType = sourceData.entityType; -const reactEditors = ['event', 'genre']; +const reactEditors = ['event', 'genre', 'recording']; if ( /* diff --git a/root/types/formcomponents.js b/root/types/formcomponents.js index 4ccfc3593b2..44108eee631 100644 --- a/root/types/formcomponents.js +++ b/root/types/formcomponents.js @@ -19,6 +19,16 @@ declare type AreaFieldT = CompoundFieldT<{ +name: FieldT, }>; +declare type ArtistCreditFieldT = CompoundFieldT<{ + +names: RepeatableFieldT, +}>; + +declare type ArtistCreditNameFieldT = CompoundFieldT<{ + +artist: FieldT, + +join_phrase: FieldT, + +name: FieldT, +}>; + declare type CompoundFieldT<+F> = { +errors: $ReadOnlyArray, +field: F, diff --git a/webpack/client.config.mjs b/webpack/client.config.mjs index 4760aacc968..d43cb0122eb 100644 --- a/webpack/client.config.mjs +++ b/webpack/client.config.mjs @@ -75,6 +75,7 @@ const entries = [ 'place/edit', 'place/index', 'place/map', + 'recording/components/RecordingEditForm', 'recording/edit', 'relationship-editor', 'release/coverart',