diff --git a/lib/MusicBrainz/Server/Controller/Admin/Attributes.pm b/lib/MusicBrainz/Server/Controller/Attributes.pm similarity index 63% rename from lib/MusicBrainz/Server/Controller/Admin/Attributes.pm rename to lib/MusicBrainz/Server/Controller/Attributes.pm index 66a522a4946..e423cea50f2 100644 --- a/lib/MusicBrainz/Server/Controller/Admin/Attributes.pm +++ b/lib/MusicBrainz/Server/Controller/Attributes.pm @@ -1,4 +1,4 @@ -package MusicBrainz::Server::Controller::Admin::Attributes; +package MusicBrainz::Server::Controller::Attributes; use Moose; use namespace::autoclean; use utf8; @@ -8,57 +8,81 @@ use MusicBrainz::Server::Entity::Util::JSON qw( to_json_array ); BEGIN { extends 'MusicBrainz::Server::Controller' } -my @models = qw( +my @entity_type_models = qw( AreaType ArtistType CollectionType - CoverArtType EventType - Gender InstrumentType LabelType - Language - MediumFormat PlaceType ReleaseGroupType ReleaseGroupSecondaryType + SeriesType + WorkType +); + +my @alias_type_models = qw( + AreaAliasType + ArtistAliasType + EventAliasType + GenreAliasType + InstrumentAliasType + LabelAliasType + PlaceAliasType + RecordingAliasType + ReleaseAliasType + ReleaseGroupAliasType + SeriesAliasType + WorkAliasType +); + +my @other_models = qw( + CoverArtType + Gender + Language + MediumFormat ReleaseStatus ReleasePackaging Script - SeriesType - WorkType WorkAttributeType ); -# Missing: Alias types, WorkAttributeTypeAllowedValue -sub index : Path('/admin/attributes') Args(0) RequireAuth(account_admin) { +my @all_models = (@entity_type_models, @alias_type_models, @other_models); +# Missing: WorkAttributeTypeAllowedValue + +sub index : Path('/attributes') Args(0) { my ($self, $c) = @_; $c->stash( current_view => 'Node', - component_path => 'admin/attributes/Index', - component_props => {models => \@models}, + component_path => 'attributes/AttributesList', + component_props => { + aliasTypeModels => \@alias_type_models, + entityTypeModels => \@entity_type_models, + otherModels => \@other_models, + }, ); } -sub attribute_base : Chained('/') PathPart('admin/attributes') CaptureArgs(1) RequireAuth(account_admin) { +sub attribute_base : Chained('/') PathPart('attributes') CaptureArgs(1) { my ($self, $c, $model) = @_; - $c->detach('/error_404') unless contains_string(\@models, $model); + $c->detach('/error_404') unless contains_string(\@all_models, $model); $c->stash->{model} = $model; } -sub attribute_index : Chained('attribute_base') PathPart('') RequireAuth(account_admin) { +sub attribute_index : Chained('attribute_base') PathPart('') { my ($self, $c) = @_; my $model = $c->stash->{model}; my @attr = $c->model($model)->get_all(); my %component_paths = ( - Language => 'admin/attributes/Language', - Script => 'admin/attributes/Script', + Language => 'attributes/Language', + Script => 'attributes/Script', ); - my $component_path = $component_paths{$model} // 'admin/attributes/Attribute'; + my $component_path = $component_paths{$model} // 'attributes/Attribute'; $c->stash( current_view => 'Node', @@ -70,15 +94,15 @@ sub attribute_index : Chained('attribute_base') PathPart('') RequireAuth(account ); } -sub create : Chained('attribute_base') RequireAuth(account_admin) SecureForm { +sub create : Chained('attribute_base') RequireAuth(relationship_editor) SecureForm { my ($self, $c) = @_; my $model = $c->stash->{model}; my %forms = ( - Language => 'Admin::Attributes::Language', - Script => 'Admin::Attributes::Script', + Language => 'Attributes::Language', + Script => 'Attributes::Script', ); - my $form_name = $forms{$model} // 'Admin::Attributes'; + my $form_name = $forms{$model} // 'Attributes::Generic'; my $form = $c->form( form => $form_name ); if ($c->form_posted_and_valid($form)) { @@ -86,21 +110,21 @@ sub create : Chained('attribute_base') RequireAuth(account_admin) SecureForm { $c->model($model)->insert({ map { $_->name => $_->value } $form->edit_fields }); }); - $c->response->redirect($c->uri_for('/admin/attributes', $model)); + $c->response->redirect($c->uri_for('/attributes', $model)); $c->detach; } } -sub edit : Chained('attribute_base') Args(1) RequireAuth(account_admin) SecureForm { +sub edit : Chained('attribute_base') Args(1) RequireAuth(relationship_editor) SecureForm { my ($self, $c, $id) = @_; my $model = $c->stash->{model}; my $attr = $c->model($model)->get_by_id($id); my %forms = ( - Language => 'Admin::Attributes::Language', - Script => 'Admin::Attributes::Script', + Language => 'Attributes::Language', + Script => 'Attributes::Script', ); - my $form_name = $forms{$model} // 'Admin::Attributes'; + my $form_name = $forms{$model} // 'Attributes::Generic'; my $form = $c->form( form => $form_name, init_object => $attr ); if ($c->form_posted_and_valid($form)) { @@ -108,12 +132,12 @@ sub edit : Chained('attribute_base') Args(1) RequireAuth(account_admin) SecureFo $c->model($model)->update($id, { map { $_->name => $_->value } $form->edit_fields }); }); - $c->response->redirect($c->uri_for('/admin/attributes', $model)); + $c->response->redirect($c->uri_for('/attributes', $model)); $c->detach; } } -sub delete : Chained('attribute_base') Args(1) RequireAuth(account_admin) SecureForm { +sub delete : Chained('attribute_base') Args(1) RequireAuth(relationship_editor) SecureForm { my ($self, $c, $id) = @_; my $model = $c->stash->{model}; my $attr = $c->model($model)->get_by_id($id) @@ -130,7 +154,7 @@ sub delete : Chained('attribute_base') Args(1) RequireAuth(account_admin) Secure $c->stash( current_view => 'Node', - component_path => 'admin/attributes/CannotRemoveAttribute', + component_path => 'attributes/CannotRemoveAttribute', component_props => {message => $error_message}, ); @@ -143,7 +167,7 @@ sub delete : Chained('attribute_base') Args(1) RequireAuth(account_admin) Secure $c->stash( current_view => 'Node', - component_path => 'admin/attributes/CannotRemoveAttribute', + component_path => 'attributes/CannotRemoveAttribute', component_props => {message => $error_message}, ); @@ -151,7 +175,7 @@ sub delete : Chained('attribute_base') Args(1) RequireAuth(account_admin) Secure } $c->stash( - component_path => 'admin/attributes/DeleteAttribute', + component_path => 'attributes/DeleteAttribute', component_props => { attribute => $attr->TO_JSON, form => $form->TO_JSON, @@ -168,7 +192,7 @@ sub delete : Chained('attribute_base') Args(1) RequireAuth(account_admin) Secure }); } - $c->response->redirect($c->uri_for('/admin/attributes', $model)); + $c->response->redirect($c->uri_for('/attributes', $model)); $c->detach; } } diff --git a/lib/MusicBrainz/Server/Data/Role/AliasType.pm b/lib/MusicBrainz/Server/Data/Role/AliasType.pm index f0cb05c9427..84fb73e927d 100644 --- a/lib/MusicBrainz/Server/Data/Role/AliasType.pm +++ b/lib/MusicBrainz/Server/Data/Role/AliasType.pm @@ -5,9 +5,8 @@ use namespace::autoclean; use MusicBrainz::Server::Entity::AliasType; use MusicBrainz::Server::Data::Utils qw( load_subobjects ); -with 'MusicBrainz::Server::Data::Role::OptionsTree'; - -sub _columns { 'id, gid, name, parent AS parent_id, child_order, description' } +with 'MusicBrainz::Server::Data::Role::OptionsTree', + 'MusicBrainz::Server::Data::Role::Attribute'; sub load { my ($self, @objs) = @_; @@ -15,6 +14,15 @@ sub load { load_subobjects($self, 'type', @objs); } +sub in_use { + my ($self, $id) = @_; + # We can get the alias table by just dropping "_type" from the type table + my $alias_table = $self->_table =~ s/_type$//r; + return $self->sql->select_single_value( + "SELECT 1 FROM $alias_table WHERE type = ? LIMIT 1", + $id); +} + 1; =head1 COPYRIGHT AND LICENSE diff --git a/lib/MusicBrainz/Server/Form/Admin/Attributes.pm b/lib/MusicBrainz/Server/Form/Attributes/Generic.pm similarity index 96% rename from lib/MusicBrainz/Server/Form/Admin/Attributes.pm rename to lib/MusicBrainz/Server/Form/Attributes/Generic.pm index 019e9181328..1299f2ca128 100644 --- a/lib/MusicBrainz/Server/Form/Admin/Attributes.pm +++ b/lib/MusicBrainz/Server/Form/Attributes/Generic.pm @@ -1,4 +1,4 @@ -package MusicBrainz::Server::Form::Admin::Attributes; +package MusicBrainz::Server::Form::Attributes::Generic; use strict; use warnings; diff --git a/lib/MusicBrainz/Server/Form/Admin/Attributes/Language.pm b/lib/MusicBrainz/Server/Form/Attributes/Language.pm similarity index 95% rename from lib/MusicBrainz/Server/Form/Admin/Attributes/Language.pm rename to lib/MusicBrainz/Server/Form/Attributes/Language.pm index 6d15d66f062..fa712b12ebd 100644 --- a/lib/MusicBrainz/Server/Form/Admin/Attributes/Language.pm +++ b/lib/MusicBrainz/Server/Form/Attributes/Language.pm @@ -1,4 +1,4 @@ -package MusicBrainz::Server::Form::Admin::Attributes::Language; +package MusicBrainz::Server::Form::Attributes::Language; use strict; use warnings; diff --git a/lib/MusicBrainz/Server/Form/Admin/Attributes/Script.pm b/lib/MusicBrainz/Server/Form/Attributes/Script.pm similarity index 94% rename from lib/MusicBrainz/Server/Form/Admin/Attributes/Script.pm rename to lib/MusicBrainz/Server/Form/Attributes/Script.pm index 986a945668c..b975eed8b94 100644 --- a/lib/MusicBrainz/Server/Form/Admin/Attributes/Script.pm +++ b/lib/MusicBrainz/Server/Form/Attributes/Script.pm @@ -1,4 +1,4 @@ -package MusicBrainz::Server::Form::Admin::Attributes::Script; +package MusicBrainz::Server::Form::Attributes::Script; use strict; use warnings; diff --git a/root/admin/attributes/Attribute.js b/root/admin/attributes/Attribute.js deleted file mode 100644 index dbe392d1e2f..00000000000 --- a/root/admin/attributes/Attribute.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * @flow strict - * Copyright (C) 2019 Anirudh Jain - * Copyright (C) 2014 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 {compare} from '../../static/scripts/common/i18n.js'; -import yesNo from '../../static/scripts/common/utility/yesNo.js'; -import loopParity from '../../utility/loopParity.js'; - -import {type AttributeT} from './types.js'; - -type Props = { - +attributes: Array, - +model: string, -}; - -const renderAttributesHeaderAccordingToModel = (model: string) => { - switch (model) { - case 'MediumFormat': { - return ( - <> - {'Year'} - {'Disc IDs allowed'} - - ); - } - case 'SeriesType': - case 'CollectionType': { - return {'Entity type'}; - } - case 'WorkAttributeType': { - return {'Free text'}; - } - default: return null; - } -}; - -const renderAttributes = (attribute: AttributeT) => { - switch (attribute.entityType) { - case 'medium_format': { - return ( - <> - {attribute.year} - {yesNo(attribute.has_discids)} - - ); - } - case 'series_type': - case 'collection_type': { - return {attribute.item_entity_type}; - } - case 'work_attribute_type': { - return {yesNo(attribute.free_text)}; - } - default: return null; - } -}; - -const Attribute = ({ - attributes, - model, -}: Props): React$Element => ( - -

- {'Attributes'} - {' / ' + model} -

- - - - - - - - - - {renderAttributesHeaderAccordingToModel(model)} - - - - - {attributes ? attributes - .sort((a, b) => compare(a.name, b.name)) - .map((attribute, index) => ( - - - - - - - - {renderAttributes(attribute)} - - - )) : null} - -
{'ID'}{'Name'}{'Description'}{'MBID'}{'Child order'}{'Parent ID'}{'Actions'}
{attribute.id}{attribute.name}{exp.l_admin(attribute.description)}{attribute.gid}{attribute.child_order}{attribute.parent_id} - - {'Edit'} - - {' | '} - - {'Remove'} - -
-

- - - {'Add new attribute'} - - -

-
-); - -export default Attribute; diff --git a/root/admin/attributes/Index.js b/root/admin/attributes/Index.js deleted file mode 100644 index 1790175fa29..00000000000 --- a/root/admin/attributes/Index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * @flow strict - * Copyright (C) 2019 Anirudh Jain - * Copyright (C) 2014 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'; - -type Props = { - +models: Array, -}; - -const Attributes = ({models}: Props): React$Element => ( - -

{'Attributes'}

-
    - {models.sort().map((item) => ( -
  • - {item} -
  • - ))} -
-
-); - -export default Attributes; diff --git a/root/admin/attributes/Language.js b/root/admin/attributes/Language.js deleted file mode 100644 index 17b7a264848..00000000000 --- a/root/admin/attributes/Language.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * @flow strict - * Copyright (C) 2019 Anirudh Jain - * Copyright (C) 2014 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 {compare} from '../../static/scripts/common/i18n.js'; -import loopParity from '../../utility/loopParity.js'; - -const frequencyLabels = { - [0]: 'Hidden', - [1]: 'Other', - [2]: 'Frequently used', -}; - -type Props = { - +attributes: Array, - +model: string, -}; - -const Language = ({ - model, - attributes, -}: Props): React$Element => { - return ( - -

- {'Attributes'} - {' / Language'} -

- - - - - - - - - - - - - - {attributes - .sort((a, b) => ( - (b.frequency - a.frequency) || compare(a.name, b.name) - )) - .map((attr, index) => ( - - - - - - - - - - - ))} -
{'ID'}{'Name'}{'ISO 639-1'}{'ISO 639-2/B'}{'ISO 639-2/T'}{'ISO 639-3'}{'Frequency'}{'Actions'}
{attr.id}{attr.name}{attr.iso_code_1}{attr.iso_code_2b}{attr.iso_code_2t}{attr.iso_code_3}{frequencyLabels[attr.frequency]} - - {'Edit'} - - {' | '} - - {'Remove'} - -
-

- - - {'Add new attribute'} - - -

-
- ); -}; - -export default Language; diff --git a/root/admin/attributes/Script.js b/root/admin/attributes/Script.js deleted file mode 100644 index e286ed736c1..00000000000 --- a/root/admin/attributes/Script.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * @flow strict - * Copyright (C) 2019 Anirudh Jain - * Copyright (C) 2014 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 {compare} from '../../static/scripts/common/i18n.js'; -import loopParity from '../../utility/loopParity.js'; - -const frequencyLabels = { - [1]: 'Hidden', - [2]: 'Other (uncommon)', - [3]: 'Other', - [4]: 'Frequently used', -}; - -type Props = { - +attributes: Array, - +model: string, -}; - -const Script = ({ - model, - attributes, -}: Props): React$Element => ( - -

- {'Attributes'} - {' / Script'} -

- - - - - - - - - - - - - {attributes - .sort((a, b) => ( - (b.frequency - a.frequency) || compare(a.name, b.name) - )) - .map((attr, index) => ( - - - - - - - - - ))} -
{'ID'}{'Name'}{'ISO code'}{'ISO number'}{'Frequency'}{'Actions'}
{attr.id}{attr.name}{attr.iso_code}{attr.iso_number}{frequencyLabels[attr.frequency]} - - {'Edit'} - - {' | '} - - {'Remove'} - -
- -

- - - {'Add new attribute'} - - -

-
-); - -export default Script; diff --git a/root/attributes/Attribute.js b/root/attributes/Attribute.js new file mode 100644 index 00000000000..6917dfe8022 --- /dev/null +++ b/root/attributes/Attribute.js @@ -0,0 +1,135 @@ +/* + * @flow strict + * Copyright (C) 2019 Anirudh Jain + * Copyright (C) 2014 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 * as React from 'react'; + +import {CatalystContext} from '../context.mjs'; +import {compare} from '../static/scripts/common/i18n.js'; +import {l_admin} from '../static/scripts/common/i18n/admin.js'; +import expand2react from '../static/scripts/common/i18n/expand2react.js'; +import {isRelationshipEditor} + from '../static/scripts/common/utility/privileges.js'; +import yesNo from '../static/scripts/common/utility/yesNo.js'; +import loopParity from '../utility/loopParity.js'; + +import AttributeLayout from './AttributeLayout.js'; +import {type AttributeT} from './types.js'; + +type Props = { + +attributes: Array, + +model: string, +}; + +const extraHeaders = (model: string) => { + switch (model) { + case 'MediumFormat': { + return ( + <> + {l('Year')} + {l('Disc IDs allowed')} + + ); + } + case 'SeriesType': + case 'CollectionType': { + return {l('Entity type')}; + } + case 'WorkAttributeType': { + return {l('Free text')}; + } + default: return null; + } +}; + +const extraColumns = (attribute: AttributeT) => { + switch (attribute.entityType) { + case 'medium_format': { + return ( + <> + {attribute.year} + {yesNo(attribute.has_discids)} + + ); + } + case 'series_type': + case 'collection_type': { + return {attribute.item_entity_type}; + } + case 'work_attribute_type': { + return {yesNo(attribute.free_text)}; + } + default: return null; + } +}; + +const Attribute = ({ + attributes, + model, +}: Props): React$Element => { + const $c = React.useContext(CatalystContext); + const showEditSections = isRelationshipEditor($c.user); + + return ( + + + + + + + + + {showEditSections ? ( + <> + + + + ) : null} + {extraHeaders(model)} + {showEditSections ? ( + + ) : null} + + + + {attributes ? attributes + .sort((a, b) => compare(a.name, b.name)) + .map((attribute, index) => ( + + + + + + {showEditSections ? ( + <> + + + + ) : null} + {extraColumns(attribute)} + {showEditSections ? ( + + ) : null} + + )) : null} + +
{l('ID')}{l('Name')}{l('Description')}{l('MBID')}{l_admin('Child order')}{l_admin('Parent ID')}{l_admin('Actions')}
{attribute.id}{attribute.name}{expand2react(attribute.description)}{attribute.gid}{attribute.child_order}{attribute.parent_id} + + {l_admin('Edit')} + + {' | '} + + {l_admin('Remove')} + +
+
+ ); +}; + +export default Attribute; diff --git a/root/attributes/AttributeLayout.js b/root/attributes/AttributeLayout.js new file mode 100644 index 00000000000..66941fbe425 --- /dev/null +++ b/root/attributes/AttributeLayout.js @@ -0,0 +1,46 @@ +/* + * @flow strict + * Copyright (C) 2022 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 {l_admin} from '../static/scripts/common/i18n/admin.js'; +import attributeModelName + from '../static/scripts/common/utility/attributeModelName.js'; + +type Props = { + +children: React$Node, + +model: string, + +showEditSections: boolean, +}; + +const AttributeLayout = ({ + children, + model, + showEditSections, +}: Props): React$Element => ( + +

+ {l('Attributes')} + {' / ' + attributeModelName(model)} +

+ + {children} + + {showEditSections ? ( +

+ + + {l_admin('Add new attribute')} + + +

+ ) : null} +
+); + +export default AttributeLayout; diff --git a/root/attributes/AttributesList.js b/root/attributes/AttributesList.js new file mode 100644 index 00000000000..1e6efec2d94 --- /dev/null +++ b/root/attributes/AttributesList.js @@ -0,0 +1,58 @@ +/* + * @flow strict + * Copyright (C) 2019 Anirudh Jain + * Copyright (C) 2014 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 {sortByString} from '../static/scripts/common/utility/arrays.js'; +import attributeModelName + from '../static/scripts/common/utility/attributeModelName.js'; + +const AttributeList = ({modelList}: {modelList: Array}) => { + const sortedModels = sortByString( + modelList, + model => attributeModelName(model), + ); + + return ( + + ); +}; + +type Props = { + +aliasTypeModels: Array, + +entityTypeModels: Array, + +otherModels: Array, +}; + +const AttributesList = ({ + aliasTypeModels, + entityTypeModels, + otherModels, +}: Props): React$Element => ( + +

{l('Attributes')}

+ +

{l('Entity types')}

+ + +

{l('Alias types')}

+ + +

{l('Other attributes')}

+ +
+); + +export default AttributesList; diff --git a/root/admin/attributes/CannotRemoveAttribute.js b/root/attributes/CannotRemoveAttribute.js similarity index 92% rename from root/admin/attributes/CannotRemoveAttribute.js rename to root/attributes/CannotRemoveAttribute.js index 75c981a3ecc..f12caa211d2 100644 --- a/root/admin/attributes/CannotRemoveAttribute.js +++ b/root/attributes/CannotRemoveAttribute.js @@ -7,7 +7,7 @@ * later version: http://www.gnu.org/licenses/gpl-2.0.txt */ -import Layout from '../../layout/index.js'; +import Layout from '../layout/index.js'; type Props = { +message: string, diff --git a/root/admin/attributes/DeleteAttribute.js b/root/attributes/DeleteAttribute.js similarity index 92% rename from root/admin/attributes/DeleteAttribute.js rename to root/attributes/DeleteAttribute.js index 0824be57a4b..3bab1743838 100644 --- a/root/admin/attributes/DeleteAttribute.js +++ b/root/attributes/DeleteAttribute.js @@ -7,7 +7,7 @@ * later version: http://www.gnu.org/licenses/gpl-2.0.txt */ -import ConfirmLayout from '../../components/ConfirmLayout.js'; +import ConfirmLayout from '../components/ConfirmLayout.js'; import {type AttributeT} from './types.js'; diff --git a/root/attributes/Language.js b/root/attributes/Language.js new file mode 100644 index 00000000000..551135dbc61 --- /dev/null +++ b/root/attributes/Language.js @@ -0,0 +1,88 @@ +/* + * @flow strict + * Copyright (C) 2019 Anirudh Jain + * Copyright (C) 2014 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 * as React from 'react'; + +import {CatalystContext} from '../context.mjs'; +import {compare} from '../static/scripts/common/i18n.js'; +import {l_admin} from '../static/scripts/common/i18n/admin.js'; +import {isRelationshipEditor} + from '../static/scripts/common/utility/privileges.js'; +import loopParity from '../utility/loopParity.js'; + +import AttributeLayout from './AttributeLayout.js'; + +const frequencyLabels = { + [0]: N_lp('Hidden', 'language optgroup'), + [1]: N_lp('Other', 'language optgroup'), + [2]: N_lp('Frequently used', 'language optgroup'), +}; + +type Props = { + +attributes: Array, + +model: string, +}; + +const Language = ({ + model, + attributes, +}: Props): React$Element => { + const $c = React.useContext(CatalystContext); + const showEditSections = isRelationshipEditor($c.user); + + return ( + + + + + + + + + + + + {showEditSections ? ( + + ) : null} + + + {attributes + .sort((a, b) => ( + (b.frequency - a.frequency) || compare(a.name, b.name) + )) + .map((attr, index) => ( + + + + + + + + + {showEditSections ? ( + + ) : null} + + ))} +
{l('ID')}{l('Name')}{l('ISO 639-1')}{l('ISO 639-2/B')}{l('ISO 639-2/T')}{l('ISO 639-3')}{l('Frequency')}{l_admin('Actions')}
{attr.id}{attr.name}{attr.iso_code_1}{attr.iso_code_2b}{attr.iso_code_2t}{attr.iso_code_3}{frequencyLabels[attr.frequency]()} + + {l_admin('Edit')} + + {' | '} + + {l_admin('Remove')} + +
+
+ ); +}; + +export default Language; diff --git a/root/attributes/Script.js b/root/attributes/Script.js new file mode 100644 index 00000000000..2572d4fa163 --- /dev/null +++ b/root/attributes/Script.js @@ -0,0 +1,85 @@ +/* + * @flow strict + * Copyright (C) 2019 Anirudh Jain + * Copyright (C) 2014 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 * as React from 'react'; + +import {CatalystContext} from '../context.mjs'; +import {compare} from '../static/scripts/common/i18n.js'; +import {l_admin} from '../static/scripts/common/i18n/admin.js'; +import {isRelationshipEditor} + from '../static/scripts/common/utility/privileges.js'; +import loopParity from '../utility/loopParity.js'; + +import AttributeLayout from './AttributeLayout.js'; + +const frequencyLabels = { + [1]: N_lp('Hidden', 'script frequency'), + [2]: N_lp('Other (uncommon)', 'script frequency'), + [3]: N_lp('Other', 'script frequency'), + [4]: N_lp('Frequently used', 'script frequency'), +}; + +type Props = { + +attributes: Array, + +model: string, +}; + +const Script = ({ + model, + attributes, +}: Props): React$Element => { + const $c = React.useContext(CatalystContext); + const showEditSections = isRelationshipEditor($c.user); + + return ( + + + + + + + + + + {showEditSections ? ( + + ) : null} + + + {attributes + .sort((a, b) => ( + (b.frequency - a.frequency) || compare(a.name, b.name) + )) + .map((attr, index) => ( + + + + + + + {showEditSections ? ( + + ) : null} + + ))} +
{l('ID')}{l('Name')}{l('ISO code')}{l('ISO number')}{l('Frequency')}{l_admin('Actions')}
{attr.id}{attr.name}{attr.iso_code}{attr.iso_number}{frequencyLabels[attr.frequency]()} + + {l_admin('Edit')} + + {' | '} + + {l_admin('Remove')} + +
+
+ ); +}; + +export default Script; diff --git a/root/admin/attributes/create.tt b/root/attributes/create.tt similarity index 74% rename from root/admin/attributes/create.tt rename to root/attributes/create.tt index 199e7931151..95a3c883116 100644 --- a/root/admin/attributes/create.tt +++ b/root/attributes/create.tt @@ -1,6 +1,6 @@ [% WRAPPER "layout.tt" title="New attribute" full_width=1 %]

[% "New attribute" %]

- [% INCLUDE "admin/attributes/form.tt" %] + [% INCLUDE "attributes/form.tt" %]
[% END %] diff --git a/root/admin/attributes/edit.tt b/root/attributes/edit.tt similarity index 74% rename from root/admin/attributes/edit.tt rename to root/attributes/edit.tt index a917ffff287..0130a8f02a5 100644 --- a/root/admin/attributes/edit.tt +++ b/root/attributes/edit.tt @@ -1,6 +1,6 @@ [% WRAPPER "layout.tt" title="Edit attribute" full_width=1 %]

[% "Edit attribute" %]

- [% INCLUDE "admin/attributes/form.tt" %] + [% INCLUDE "attributes/form.tt" %]
[% END %] diff --git a/root/admin/attributes/form.tt b/root/attributes/form.tt similarity index 100% rename from root/admin/attributes/form.tt rename to root/attributes/form.tt diff --git a/root/admin/attributes/types.js b/root/attributes/types.js similarity index 100% rename from root/admin/attributes/types.js rename to root/attributes/types.js diff --git a/root/layout/components/BottomMenu.js b/root/layout/components/BottomMenu.js index e05ebd2abd7..435ac0eb65a 100644 --- a/root/layout/components/BottomMenu.js +++ b/root/layout/components/BottomMenu.js @@ -319,6 +319,9 @@ const DocumentationMenu = () => (
  • {l('Relationship types')}
  • +
  • + {l('Entity attributes')} +
  • {l('Instrument list')}
  • diff --git a/root/layout/components/TopMenu.js b/root/layout/components/TopMenu.js index c9f0f571c41..c4c64de828c 100644 --- a/root/layout/components/TopMenu.js +++ b/root/layout/components/TopMenu.js @@ -163,7 +163,7 @@ const AdminMenu = ({user}: UserProp) => ( {isAccountAdmin(user) ? ( <>
  • - {l_admin('Edit attributes')} + {l_admin('Edit attributes')}
  • diff --git a/root/server/components.mjs b/root/server/components.mjs index 1a1d1d57471..a7413a30f46 100644 --- a/root/server/components.mjs +++ b/root/server/components.mjs @@ -41,12 +41,6 @@ export default { 'account/ResetPasswordStatus': (): Promise => import('../account/ResetPasswordStatus.js'), 'account/sso/DiscourseRegistered': (): Promise => import('../account/sso/DiscourseRegistered.js'), 'account/sso/DiscourseUnconfirmedEmailAddress': (): Promise => import('../account/sso/DiscourseUnconfirmedEmailAddress.js'), - 'admin/attributes/Attribute': (): Promise => import('../admin/attributes/Attribute.js'), - 'admin/attributes/CannotRemoveAttribute': (): Promise => import('../admin/attributes/CannotRemoveAttribute.js'), - 'admin/attributes/DeleteAttribute': (): Promise => import('../admin/attributes/DeleteAttribute.js'), - 'admin/attributes/Index': (): Promise => import('../admin/attributes/Index.js'), - 'admin/attributes/Language': (): Promise => import('../admin/attributes/Language.js'), - 'admin/attributes/Script': (): Promise => import('../admin/attributes/Script.js'), 'admin/DeleteUser': (): Promise => import('../admin/DeleteUser.js'), 'admin/EditBanner': (): Promise => import('../admin/EditBanner.js'), 'admin/EditUser': (): Promise => import('../admin/EditUser.js'), @@ -89,6 +83,12 @@ export default { 'artist/SpecialPurpose': (): Promise => import('../artist/SpecialPurpose.js'), 'artist_credit/ArtistCreditIndex': (): Promise => import('../artist_credit/ArtistCreditIndex.js'), 'artist_credit/EntityList': (): Promise => import('../artist_credit/EntityList.js'), + 'attributes/Attribute': (): Promise => import('../attributes/Attribute.js'), + 'attributes/AttributesList': (): Promise => import('../attributes/AttributesList.js'), + 'attributes/CannotRemoveAttribute': (): Promise => import('../attributes/CannotRemoveAttribute.js'), + 'attributes/DeleteAttribute': (): Promise => import('../attributes/DeleteAttribute.js'), + 'attributes/Language': (): Promise => import('../attributes/Language.js'), + 'attributes/Script': (): Promise => import('../attributes/Script.js'), 'cdstub/BrowseCDStubs': (): Promise => import('../cdstub/BrowseCDStubs.js'), 'cdstub/CDStubIndex': (): Promise => import('../cdstub/CDStubIndex.js'), 'cdstub/CDStubNotFound': (): Promise => import('../cdstub/CDStubNotFound.js'), diff --git a/root/static/scripts/common/utility/attributeModelName.js b/root/static/scripts/common/utility/attributeModelName.js new file mode 100644 index 00000000000..6187cb88da7 --- /dev/null +++ b/root/static/scripts/common/utility/attributeModelName.js @@ -0,0 +1,79 @@ +/* + * @flow strict + * Copyright (C) 2022 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 + */ + +function attributeModelName(model: string): string { + switch (model) { + case 'AreaType': + return l('Area types'); + case 'AreaAliasType': + return l('Area alias types'); + case 'ArtistType': + return l('Artist types'); + case 'ArtistAliasType': + return l('Artist alias types'); + case 'CollectionType': + return l('Collection types'); + case 'CoverArtType': + return l('Cover art types'); + case 'EventType': + return l('Event types'); + case 'EventAliasType': + return l('Event alias types'); + case 'Gender': + return l('Genders'); + case 'GenreAliasType': + return l('Genre alias types'); + case 'InstrumentType': + return l('Instrument types'); + case 'InstrumentAliasType': + return l('Instrument alias types'); + case 'LabelType': + return l('Label types'); + case 'LabelAliasType': + return l('Label alias types'); + case 'Language': + return l('Languages'); + case 'MediumFormat': + return l('Medium formats'); + case 'PlaceType': + return l('Place types'); + case 'PlaceAliasType': + return l('Place alias types'); + case 'RecordingAliasType': + return l('Recording alias types'); + case 'ReleaseAliasType': + return l('Release alias types'); + case 'ReleaseGroupSecondaryType': + return l('Release group secondary types'); + case 'ReleaseGroupType': + return l('Release group primary types'); + case 'ReleaseGroupAliasType': + return l('Release group alias types'); + case 'ReleasePackaging': + return l('Release packagings'); + case 'ReleaseStatus': + return l('Release statuses'); + case 'Script': + return l('Scripts'); + case 'SeriesType': + return l('Series types'); + case 'SeriesAliasType': + return l('Series alias types'); + case 'WorkAttributeType': + return l('Work attribute types'); + case 'WorkType': + return l('Work types'); + case 'WorkAliasType': + return l('Work alias types'); + default: + return model; + } +} + +export default attributeModelName; diff --git a/t/lib/t/MusicBrainz/Server/Controller/Admin/Attributes/Delete.pm b/t/lib/t/MusicBrainz/Server/Controller/Attributes/Delete.pm similarity index 78% rename from t/lib/t/MusicBrainz/Server/Controller/Admin/Attributes/Delete.pm rename to t/lib/t/MusicBrainz/Server/Controller/Attributes/Delete.pm index 420fb3cf1a3..cf9596b45cc 100644 --- a/t/lib/t/MusicBrainz/Server/Controller/Admin/Attributes/Delete.pm +++ b/t/lib/t/MusicBrainz/Server/Controller/Attributes/Delete.pm @@ -1,4 +1,4 @@ -package t::MusicBrainz::Server::Controller::Admin::Attributes::Delete; +package t::MusicBrainz::Server::Controller::Attributes::Delete; use utf8; use strict; use warnings; @@ -21,7 +21,7 @@ test 'Delete standard attribute (series type)' => sub { with_fields => { username => 'editor', password => 'password' }, ); - $mech->get('/admin/attributes/SeriesType/delete/1'); + $mech->get('/attributes/SeriesType/delete/1'); is( $mech->status, HTTP_FORBIDDEN, @@ -31,24 +31,27 @@ test 'Delete standard attribute (series type)' => sub { $test->mech->get('/logout'); $test->mech->get('/login'); $test->mech->submit_form( - with_fields => { username => 'admin', password => 'password' }, + with_fields => { + username => 'relationship_editor', + password => 'password', + }, ); - $mech->get('/admin/attributes/SeriesType/delete/1'); + $mech->get('/attributes/SeriesType/delete/1'); html_ok($mech->content); $mech->text_contains( 'because it is the parent of other attributes.', 'Series type with children attributes cannot be deleted', ); - $mech->get('/admin/attributes/SeriesType/delete/2'); + $mech->get('/attributes/SeriesType/delete/2'); html_ok($mech->content); $mech->text_contains( 'You cannot remove the attribute “Release series” because it is still in use.', 'Series type in use on a series cannot be deleted', ); - $mech->get_ok('/admin/attributes/SeriesType/delete/47'); + $mech->get_ok('/attributes/SeriesType/delete/47'); html_ok($mech->content); $mech->text_contains( 'Are you sure you wish to remove the Release group award attribute?', @@ -59,13 +62,13 @@ test 'Delete standard attribute (series type)' => sub { $mech->form_with_fields('confirm.submit'); $mech->click('confirm.submit'); - $mech->get_ok('/admin/attributes/SeriesType'); + $mech->get_ok('/attributes/SeriesType'); $mech->text_lacks( 'Release group award', 'The series type has been deleted (no longer shows on the types list)', ); - $mech->get('/admin/attributes/SeriesType/delete/1'); + $mech->get('/attributes/SeriesType/delete/1'); html_ok($mech->content); $mech->text_contains( 'Are you sure you wish to remove the Release group series attribute?', @@ -84,7 +87,7 @@ test 'Delete language' => sub { with_fields => { username => 'editor', password => 'password' }, ); - $mech->get('/admin/attributes/Language/delete/120'); + $mech->get('/attributes/Language/delete/120'); is( $mech->status, HTTP_FORBIDDEN, @@ -94,31 +97,34 @@ test 'Delete language' => sub { $test->mech->get('/logout'); $test->mech->get('/login'); $test->mech->submit_form( - with_fields => { username => 'admin', password => 'password' }, + with_fields => { + username => 'relationship_editor', + password => 'password', + }, ); - $mech->get_ok('/admin/attributes/Language/delete/120'); + $mech->get_ok('/attributes/Language/delete/120'); html_ok($mech->content); $mech->text_contains( 'You cannot remove the attribute “English” because it is still in use.', 'Language in use on a release cannot be deleted', ); - $mech->get_ok('/admin/attributes/Language/delete/27'); + $mech->get_ok('/attributes/Language/delete/27'); html_ok($mech->content); $mech->text_contains( 'You cannot remove the attribute “Asturian” because it is still in use.', 'Language in use on a work cannot be deleted', ); - $mech->get_ok('/admin/attributes/Language/delete/123'); + $mech->get_ok('/attributes/Language/delete/123'); html_ok($mech->content); $mech->text_contains( 'You cannot remove the attribute “Estonian” because it is still in use.', 'Language in use on an editor cannot be deleted', ); - $mech->get_ok('/admin/attributes/Language/delete/113'); + $mech->get_ok('/attributes/Language/delete/113'); html_ok($mech->content); $mech->text_contains( 'Are you sure you wish to remove the Dutch attribute?', @@ -129,7 +135,7 @@ test 'Delete language' => sub { $mech->form_with_fields('confirm.submit'); $mech->click('confirm.submit'); - $mech->get_ok('/admin/attributes/Language'); + $mech->get_ok('/attributes/Language'); $mech->text_lacks( 'Dutch', 'The language has been deleted (no longer shows on the languages list)', @@ -147,7 +153,7 @@ test 'Delete script' => sub { with_fields => { username => 'editor', password => 'password' }, ); - $mech->get('/admin/attributes/Script/delete/28'); + $mech->get('/attributes/Script/delete/28'); is( $mech->status, HTTP_FORBIDDEN, @@ -157,17 +163,20 @@ test 'Delete script' => sub { $test->mech->get('/logout'); $test->mech->get('/login'); $test->mech->submit_form( - with_fields => { username => 'admin', password => 'password' }, + with_fields => { + username => 'relationship_editor', + password => 'password', + }, ); - $mech->get_ok('/admin/attributes/Script/delete/28'); + $mech->get_ok('/attributes/Script/delete/28'); html_ok($mech->content); $mech->text_contains( 'You cannot remove the attribute “Latin” because it is still in use.', 'Script in use on a release cannot be deleted', ); - $mech->get_ok('/admin/attributes/Script/delete/85'); + $mech->get_ok('/attributes/Script/delete/85'); html_ok($mech->content); $mech->text_contains( 'Are you sure you wish to remove the Japanese attribute?', @@ -178,7 +187,7 @@ test 'Delete script' => sub { $mech->form_with_fields('confirm.submit'); $mech->click('confirm.submit'); - $mech->get_ok('/admin/attributes/Script'); + $mech->get_ok('/attributes/Script'); $mech->text_lacks( 'Japanese', 'The script has been deleted (no longer shows on the scripts list)', diff --git a/t/lib/t/MusicBrainz/Server/Controller/UnconfirmedEmailAddresses.pm b/t/lib/t/MusicBrainz/Server/Controller/UnconfirmedEmailAddresses.pm index 6f13cb94c74..b78fd6c0a5c 100644 --- a/t/lib/t/MusicBrainz/Server/Controller/UnconfirmedEmailAddresses.pm +++ b/t/lib/t/MusicBrainz/Server/Controller/UnconfirmedEmailAddresses.pm @@ -73,9 +73,9 @@ test 'Paths that allow browsing without a confirmed email address' => sub { 'Controller::Account::reset_password', 'Controller::Account::revoke_application_access', 'Controller::Account::verify_email', - 'Controller::Admin::Attributes::attribute_base', - 'Controller::Admin::Attributes::attribute_index', - 'Controller::Admin::Attributes::index', + 'Controller::Attributes::attribute_base', + 'Controller::Attributes::attribute_index', + 'Controller::Attributes::index', 'Controller::Admin::StatisticsEvent::create', 'Controller::Admin::StatisticsEvent::delete', 'Controller::Admin::StatisticsEvent::edit', diff --git a/t/sql/attributes.sql b/t/sql/attributes.sql index ebdf4022999..76cc2e0b0e1 100644 --- a/t/sql/attributes.sql +++ b/t/sql/attributes.sql @@ -11,8 +11,8 @@ INSERT INTO editor ( id, name, password, ha1, email, email_confirm_date, privs) VALUES ( - 2, 'admin', '{CLEARTEXT}password', '3a115bc4f05ea9856bd4611b75c80bca', - 'foo@example.com', now(), 128); + 2, 'relationship_editor', '{CLEARTEXT}password', + '3a115bc4f05ea9856bd4611b75c80bca', 'foo@example.com', now(), 8); -- Release for language and script usage INSERT INTO artist (