[% date_range_fieldset(r, 'label', l('This label has ended.')) %]
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/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/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 42e79601b16..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'),
@@ -530,6 +532,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..5a7c785ee74 100644
--- a/root/static/scripts/area/edit.js
+++ b/root/static/scripts/area/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-area\\.type_id]';
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/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.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/edit/components/AddButton.js b/root/static/scripts/edit/components/AddButton.js
new file mode 100644
index 00000000000..7b0004455cf
--- /dev/null
+++ b/root/static/scripts/edit/components/AddButton.js
@@ -0,0 +1,38 @@
+/*
+ * @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(
+ id: string,
+ onClick: (event: SyntheticEvent) => void,
+ label?: string,
+) {
+ if (label == null) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default AddButton;
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/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/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
new file mode 100644
index 00000000000..e8bb1e0f77d
--- /dev/null
+++ b/root/static/scripts/edit/components/FormRowTextList.js
@@ -0,0 +1,182 @@
+/*
+ * @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 mutate from 'mutate-cow';
+import React from 'react';
+
+import {pushField} from '../utility/pushField.js';
+
+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 StateT = RepeatableFieldT>;
+
+type ActionT =
+ | {+type: 'add-row'}
+ | {+fieldId: number, +type: 'remove-row'}
+ | {+fieldId: number, +type: 'update-row', +value: string};
+
+component TextListRow(
+ dispatch: (ActionT) => void,
+ fieldId: number,
+ name: string,
+ onFocus?: (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 (
+