Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
68e6cea
Drop unneeded edit_form in event Role::Create
reosarevok Sep 11, 2024
775f59f
Remove activeUser param from AC editor
reosarevok Sep 16, 2024
7e63cae
Drop seemingly useless class
reosarevok Apr 24, 2025
027e12b
MBS-14180: Drop classical featuring guess feature
reosarevok Oct 17, 2025
2f19eb8
Update guessFeat to use Flow and add non-ko version
reosarevok Oct 17, 2025
947e860
MBS-12761: Convert recording edit form to React
reosarevok Sep 11, 2024
05f7b97
Use useEffect instead of manifest to load form tools
reosarevok Aug 14, 2025
eedca4b
Move formUnload code to hook
reosarevok Aug 14, 2025
a68f1ba
Use React validation for name and AC
reosarevok Aug 20, 2025
5cebf80
Use form artist credit for ArtistCreditEditor
reosarevok Aug 21, 2025
edf1636
Check for valid edit notes on RecordingEditForm
reosarevok Aug 21, 2025
3418212
Ignore invalid artist IDs in `createInitialNamesState`
mwiencek Sep 2, 2025
eff2da1
Move unformatTrackLength to its own file
reosarevok Sep 26, 2025
7852df6
Make unformatTrackLength work consistently with Perl
reosarevok Sep 30, 2025
66d249e
Check for valid recording length on RecordingEditForm
reosarevok Oct 8, 2025
e0e6fce
MBS-12761: Move ISRC validation and changing to React state
reosarevok Oct 8, 2025
9ddfdee
Also validate edit note on initial state
reosarevok Oct 10, 2025
b544601
Show external links bubble on focus
reosarevok Oct 16, 2025
a3b166f
Use the three-argument form of `useReducer`
reosarevok Oct 16, 2025
a5ec953
Ensure no accidental writes on nameStateCtx
reosarevok Oct 16, 2025
966fe5a
Use useLayoutEffect for Bubble
reosarevok Oct 16, 2025
84a23f2
Also validate length on initial state
reosarevok Oct 17, 2025
8f23591
Display doc bubble for AC field
reosarevok Nov 20, 2025
7d97f7c
MBS-12757: Convert instrument edit form to React
reosarevok Nov 11, 2025
00e426b
MBS-14190: Warn of unparseable HTML
reosarevok Nov 12, 2025
ef134de
MBS-12763 [wip]: Convert release group edit form to React
reosarevok Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,10 @@ 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/instrument/components/InstrumentEditForm.js',
'root/static/scripts/recording/components/RecordingEditForm.js',
'root/static/scripts/relationship-editor/components/DialogPreview.js',
'root/static/scripts/release_group/components/ReleaseGroupEditForm.js',
],
rules: {
'react/jsx-no-bind': 'off',
Expand Down
1 change: 0 additions & 1 deletion lib/MusicBrainz/Server/Controller/Event.pm
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,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' => {
Expand Down
1 change: 0 additions & 1 deletion lib/MusicBrainz/Server/Controller/Instrument.pm
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,6 @@ sub _merge_load_entities {
with 'MusicBrainz::Server::Controller::Role::Create' => {
form => 'Instrument',
edit_type => $EDIT_INSTRUMENT_CREATE,
dialog_template => 'instrument/edit_form.tt',
};

with 'MusicBrainz::Server::Controller::Role::Delete' => {
Expand Down
1 change: 0 additions & 1 deletion lib/MusicBrainz/Server/Controller/Recording.pm
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,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 {
Expand Down
1 change: 0 additions & 1 deletion lib/MusicBrainz/Server/Controller/ReleaseGroup.pm
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ with 'MusicBrainz::Server::Controller::Role::Create' => {
return ();
}
},
dialog_template => 'release_group/edit_form.tt',
};

with 'MusicBrainz::Server::Controller::Role::Edit' => {
Expand Down
29 changes: 26 additions & 3 deletions lib/MusicBrainz/Server/Controller/Role/Create.pm
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ role {
my $type = model_to_type($model);
my $entity;
my %props;
my %edit_arguments = $params->edit_arguments->($self, $c);

if ($model eq 'Event' || $model eq 'Genre') {
my $form = $c->form( form => $params->form );
if ($model eq 'Event' || $model eq 'Genre' || $model eq 'Instrument' || $model eq 'Recording' || $model eq 'ReleaseGroup') {
my $type = model_to_type($model);
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(
Expand Down Expand Up @@ -108,17 +111,37 @@ role {
$props{eventTypes} = $form->options_type_id;
$props{eventDescriptions} = \%event_descriptions;
}
if ($model eq 'Instrument') {
$props{instrumentTypes} = $form->options_type_id;
}
if ($self->does('MusicBrainz::Server::Controller::Role::IdentifierSet')) {
$self->munge_compound_text_fields($c, $form);
}
if ($model eq 'Recording') {
$props{usedByTracks} = $form->used_by_tracks;
}
if ($model eq 'ReleaseGroup') {
my %primary_type_descriptions = map {
$_->id => $_->l_description
} $c->model('ReleaseGroupType')->get_all();
my %secondary_type_descriptions = map {
$_->id => $_->l_description
} $c->model('ReleaseGroupSecondaryType')->get_all();

$props{primaryTypes} = $form->options_primary_type_id;
$props{primaryTypeDescriptions} = \%primary_type_descriptions;

$props{secondaryTypes} = $form->options_secondary_type_ids;
$props{secondaryTypeDescriptions} = \%secondary_type_descriptions;
}
},
redirect => sub {
$c->response->redirect($c->uri_for_action(
$self->action_for('show'), [ $entity->gid ]));
},
no_redirect => $args{within_dialog},
edit_rels => 1,
$params->edit_arguments->($self, $c),
%edit_arguments,
);

if ($ENTITIES{$type}{artist_credits}) {
Expand Down
30 changes: 28 additions & 2 deletions lib/MusicBrainz/Server/Controller/Role/Edit.pm
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,23 @@ role {
method 'edit' => sub {
my ($self, $c) = @_;

my @react_models = qw( Event Genre);
my @react_models = qw( Event Genre Instrument Recording ReleaseGroup );
my $entity_name = $self->{entity_name};
my $edit_entity = $c->stash->{ $entity_name };
my $model = $self->{model};
my $type = model_to_type($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 = (
Expand Down Expand Up @@ -78,16 +84,36 @@ role {
$props{eventTypes} = $form->options_type_id;
$props{eventDescriptions} = \%event_descriptions;
}
if ($model eq 'Instrument') {
$props{instrumentTypes} = $form->options_type_id;
}
if ($self->does('MusicBrainz::Server::Controller::Role::IdentifierSet')) {
$self->munge_compound_text_fields($c, $form);
$self->stash_current_identifier_values($c, $edit_entity->id);
}
if ($model eq 'Recording') {
$props{usedByTracks} = $form->used_by_tracks;
}
if ($model eq 'ReleaseGroup') {
my %primary_type_descriptions = map {
$_->id => $_->l_description
} $c->model('ReleaseGroupType')->get_all();
my %secondary_type_descriptions = map {
$_->id => $_->l_description
} $c->model('ReleaseGroupSecondaryType')->get_all();

$props{primaryTypes} = $form->options_primary_type_id;
$props{primaryTypeDescriptions} = \%primary_type_descriptions;

$props{secondaryTypes} = $form->options_secondary_type_ids;
$props{secondaryTypeDescriptions} = \%secondary_type_descriptions;
}
},
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,
);

if ($ENTITIES{$type}{artist_credits}) {
Expand Down
21 changes: 18 additions & 3 deletions lib/MusicBrainz/Server/Track.pm
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,40 @@ sub FormatTrackLength
sprintf($print_formats->{ms}, $minutes, $seconds);
}

# Keep in sync with static/scripts/common/utility/unformatTrackLength.js
sub UnformatTrackLength
{
my $length = shift;

if ($length =~ /^\s*(\d{1,3}):(\d{1,2}):(\d{1,2})\s*$/ && $2 < 60 && $3 < 60)
# ?:?? or just space are allowed to indicate unknown/empty
if ($length =~ /^\s*\?:\?\?\s*$/ || $length =~ /^\s*$/)
{
return undef;
}
# Check for HH:MM:SS
elsif ($length =~ /^\s*(\d{1,3}):(\d{1,2}):(\d{1,2})\s*$/ && $2 < 60 && $3 < 60)
{
return ($1 * 3600 + $2 * 60 + $3) * 1000;
}
# Check for MM:SS
elsif ($length =~ /^\s*(\d+):(\d{1,2})\s*$/ && $2 < 60)
{
return ($1 * 60 + $2) * 1000;
}
# Check for :SS
elsif ($length =~ /^\s*:(\d{1,2})\s*$/ && $1 < 60)
{
return ($1) * 1000;
}
# Check for XX ms
elsif ($length =~ /^\s*(\d+(\.\d+)?)?\s+ms\s*$/)
{
return int($1);
}
elsif ($length =~ /^\s*\?:\?\?\s*$/ || $length =~ /^\s*$/)
# Check for just a number of seconds
elsif ($length =~ /^\s*(\d+)\s*$/)
{
return undef;
return ($1) * 1000;
}
else {
confess("$length is not a valid track length");
Expand Down
33 changes: 33 additions & 0 deletions root/instrument/CreateInstrument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* @flow strict-local
* Copyright (C) 2025 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 InstrumentEditForm
from '../static/scripts/instrument/components/InstrumentEditForm.js';

import type {InstrumentFormT} from './types.js';

component CreateInstrument(
form: InstrumentFormT,
instrumentTypes: SelectOptionsT,
) {
return (
<Layout fullWidth title={lp('Add instrument', 'header')}>
<div id="content">
<h1>{lp('Add instrument', 'header')}</h1>
<InstrumentEditForm form={form} instrumentTypes={instrumentTypes} />
</div>
{manifest('instrument/components/InstrumentEditForm', {async: true})}
{manifest('relationship-editor', {async: true})}
</Layout>
);
}

export default CreateInstrument;
36 changes: 36 additions & 0 deletions root/instrument/EditInstrument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* @flow strict-local
* Copyright (C) 2025 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 InstrumentEditForm
from '../static/scripts/instrument/components/InstrumentEditForm.js';

import InstrumentLayout from './InstrumentLayout.js';
import type {InstrumentFormT} from './types.js';

component EditInstrument(
entity: InstrumentT,
form: InstrumentFormT,
instrumentTypes: SelectOptionsT,
) {
return (
<InstrumentLayout
entity={entity}
fullWidth
page="edit"
title={lp('Edit instrument', 'header')}
>
<InstrumentEditForm form={form} instrumentTypes={instrumentTypes} />
{manifest('instrument/components/InstrumentEditForm', {async: true})}
{manifest('relationship-editor', {async: true})}
</InstrumentLayout>
);
}

export default EditInstrument;
6 changes: 0 additions & 6 deletions root/instrument/create.tt

This file was deleted.

42 changes: 0 additions & 42 deletions root/instrument/edit_form.tt

This file was deleted.

17 changes: 17 additions & 0 deletions root/instrument/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* @flow strict
* Copyright (C) 2025 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 InstrumentFormT = FormT<{
+comment: FieldT<string>,
+description: FieldT<string>,
+edit_note: FieldT<string>,
+make_votable: FieldT<boolean>,
+name: FieldT<string | null>,
+type_id: FieldT<string>,
}>;
30 changes: 30 additions & 0 deletions root/recording/CreateRecording.js
Original file line number Diff line number Diff line change
@@ -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 (
<Layout fullWidth title={lp('Add recording', 'header')}>
<div id="content">
<h1>{lp('Add recording', 'header')}</h1>
<RecordingEditForm form={form} usedByTracks={usedByTracks} />
</div>
{manifest('recording/components/RecordingEditForm', {async: true})}
{manifest('relationship-editor', {async: true})}
</Layout>
);
}

export default CreateRecording;
Loading