Skip to content

Commit

Permalink
MBS-11778: Convert edit header to React
Browse files Browse the repository at this point in the history
  • Loading branch information
reosarevok committed Jul 8, 2021
1 parent 8980f8c commit 434c7c7
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 131 deletions.
28 changes: 5 additions & 23 deletions lib/MusicBrainz/Server/Edit.pm
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use MusicBrainz::Server::Data::Utils qw( boolean_to_json datetime_to_iso8601 );
use MusicBrainz::Server::Edit::Exceptions;
use MusicBrainz::Server::Edit::Utils qw( edit_status_name );
use MusicBrainz::Server::Entity::Types;
use MusicBrainz::Server::Entity::Util::JSON qw( to_json_array );
use MusicBrainz::Server::Entity::Util::JSON qw( add_linked_entity to_json_array );
use MusicBrainz::Server::Constants qw(
:edit_status
:expire_action
Expand Down Expand Up @@ -139,19 +139,6 @@ sub no_votes {
scalar shift->_grep_votes(sub { $_->vote == $VOTE_NO && !$_->superseded });
}

sub votes_for_editor
{
my ($self, $editor_id) = @_;
$self->_grep_votes(sub { $_->editor_id == $editor_id });
}

sub latest_vote_for_editor
{
my ($self, $editor_id) = @_;
my @votes = $self->votes_for_editor($editor_id) or return;
return $votes[-1];
}

sub is_open
{
return shift->status == $STATUS_OPEN;
Expand Down Expand Up @@ -233,15 +220,6 @@ sub editor_may_cancel {
&& $self->editor_id == $editor->id;
}

sub was_approved
{
my $self = shift;

return 0 if $self->is_open;

return scalar $self->_grep_votes(sub { $_->vote == $VOTE_APPROVE })
}

sub approval_requires_comment {
my ($self, $editor) = @_;

Expand Down Expand Up @@ -319,9 +297,12 @@ sub initialize
sub TO_JSON {
my ($self) = @_;

add_linked_entity('editor', $self->editor_id, $self->editor);

my $can_preview = $self->does('MusicBrainz::Server::Edit::Role::Preview');
my $conditions = $self->edit_conditions;
return {
auto_edit => boolean_to_json($self->auto_edit),
close_time => datetime_to_iso8601($self->close_time),
conditions => {
duration => $conditions->{duration} + 0,
Expand All @@ -333,6 +314,7 @@ sub TO_JSON {
display_data => $self->display_data,
data => $self->data,
edit_kind => $self->edit_kind,
edit_name => $self->edit_name,
edit_notes => to_json_array($self->edit_notes),
edit_type => $self->edit_type + 0,
editor_id => $self->editor_id + 0,
Expand Down
11 changes: 3 additions & 8 deletions root/components/common-macros.tt
Original file line number Diff line number Diff line change
Expand Up @@ -494,11 +494,6 @@ END -%]
INCLUDE _link_other_entity content=text action=action type='user' default_content=editor.name avatar=image_url image_size=size;
END -%]

[%~ MACRO editor_type_info(editor) BLOCK; -%]
[% IF editor.is_limited; '<span class="editor-class">(<span class="tooltip" title="' _ l('This user is new to MusicBrainz.') _ '">' _ l('beginner') _ '</span>)</span>'; END %]
[% IF editor.is_bot; '<span class="editor-class">(<span class="tooltip" title="' _ l('This user is automated.') _ '">' _ l('bot') _ '</span>)</span>'; END %]
[%- END -%]

[%~ MACRO link_edit(edit, action, text) BLOCK; # Converted to React at root/static/scripts/common/components/EditLink.js
INCLUDE _link_other_entity content=text action=action type='edit' default_content=edit.id;
END -%]
Expand Down Expand Up @@ -611,7 +606,7 @@ END -%]
CASE 2; l('High');
END -%]

[%~ MACRO edit_status_class(edit) BLOCK ~%]
[%~ MACRO edit_status_class(edit) BLOCK # Converted to React as getEditStatusClass at root/utility/edit.js ~%]
[%~ IF edit.status == 1 ~%]
[%- " open" -%]
[%~ ELSIF edit.status == 2 ~%]
Expand All @@ -625,15 +620,15 @@ END -%]
[%~ END ~%]
[%~ END ~%]

[%~ MACRO vote_tally(edit) BLOCK -%]
[%~ MACRO vote_tally(edit) BLOCK # Converted to React at root/edit/components/VoteTally.js -%]
[%- IF edit.auto_edit; '<strong>' _ l('automatically applied') _ '</strong>';
ELSE; l('{yes} yes : {no} no',
{ yes => '<strong>' _ edit.yes_votes _ '</strong>',
no => '<strong>' _ edit.no_votes _ '</strong>' });
END -%]
[%- END -%]

[%~ MACRO css_class_name(name) BLOCK;
[%~ MACRO css_class_name(name) BLOCK; # React equivalent is kebabCase at root/static/scripts/common/utility/strings.js
name | lower | replace('[^a-z]+', '-') | remove('^-|-$');
END -%]

Expand Down
227 changes: 227 additions & 0 deletions root/edit/components/EditHeader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* @flow strict-local
* Copyright (C) 2021 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 {EDIT_VOTE_APPROVE} from '../../constants';
import RequestLogin from '../../components/RequestLogin';
import VotingPeriod from '../../components/VotingPeriod';
import linkedEntities from '../../static/scripts/common/linkedEntities';
import EditLink from '../../static/scripts/common/components/EditLink';
import EditorLink from '../../static/scripts/common/components/EditorLink';
import bracketed from '../../static/scripts/common/utility/bracketed';
import {isBot} from '../../static/scripts/common/utility/privileges';
import {kebabCase} from '../../static/scripts/common/utility/strings';
import getVoteName from '../../static/scripts/edit/utility/getVoteName';
import {
editorMayApprove,
editorMayCancel,
getEditStatusClass,
getLatestVoteForEditor,
} from '../../utility/edit';
import formatUserDate from '../../utility/formatUserDate';
import {returnToCurrentPage} from '../../utility/returnUri';

import VoteTally from './VoteTally';

type Props = {
+$c: CatalystContextT,
+edit: {...EditT, +id: number},
+isSummary?: boolean,
+voter?: UnsanitizedEditorT,
};

const EditorTypeInfo = ({editor}: {editor: EditorT}) => (
editor.is_limited ? (
<span className="editor-class">
{bracketed(
<span
className="tooltip"
title={l('This user is new to MusicBrainz.')}
>
{l('beginner')}
</span>,
)}
</span>
) : isBot(editor) ? (
<span className="editor-class">
{bracketed(
<span className="tooltip" title={l('This user is automated.')}>
{l('bot')}
</span>,
)}
</span>
) : null
);

const EditHeader = ({
$c,
edit,
isSummary = false,
voter,
}: Props): React.Element<'div'> => {
const user = $c.user;
const mayApprove = editorMayApprove(edit, user);
const mayCancel = editorMayCancel(edit, user);
const editTitle = texp.l(
'Edit #{id} - {name}',
{id: edit.id, name: l(edit.edit_name)},
);
const editEditor = linkedEntities.editor[edit.editor_id];
const isEditEditor = user ? user.id === edit.editor_id : false;
const isVoter = user && voter && user.id === voter.id;
const latestVoteForEditor = user
? getLatestVoteForEditor(edit, user)
: null;
const latestVoteForEditorName = latestVoteForEditor
? getVoteName(latestVoteForEditor.vote)
: null;
const latestVoteForVoter = voter
? getLatestVoteForEditor(edit, voter)
: null;
const latestVoteForVoterName = latestVoteForVoter
? getVoteName(latestVoteForVoter.vote)
: null;
const editWasApproved = !edit.is_open && edit.votes.some(
(vote) => vote.vote === EDIT_VOTE_APPROVE,
);
const showVoteTally = latestVoteForEditor || isEditEditor || !edit.is_open;

return (
<div
className={
'edit-header' + ' ' +
getEditStatusClass(edit) + ' ' +
'edit-' + edit.edit_kind + ' ' +
kebabCase(edit.edit_name)}
>
{isSummary ? (
<>
<div className="edit-description">
<table>
<tr>
<td>
{voter && isVoter === false ? (
<div className="my-vote">
<strong>{l('Their vote: ')}</strong>
{nonEmpty(latestVoteForVoterName)
? lp(latestVoteForVoterName, 'vote')
: null}
</div>
) : user ? (
<div className="my-vote">
<strong>{l('My vote: ')}</strong>
{nonEmpty(latestVoteForEditorName) ? (
lp(latestVoteForEditorName, 'vote')
) : isEditEditor ? (
l('N/A')
) : l('None')}
</div>
) : null}
</td>
<td className="vote-count">
{showVoteTally ? (
<div>
{user ? null : (
<>
<strong>{addColon(l('Vote tally'))}</strong>
{' '}
</>
)}
<VoteTally edit={edit} />
</div>
) : null}
</td>
</tr>
<tr>
<td className="edit-expiration" colSpan="2">
{edit.is_open ? (
<>
<strong>{addColon(l('Voting'))}</strong>
{' '}
<VotingPeriod
$c={$c}
closingDate={edit.expires_time}
/>
</>
) : editWasApproved ? (
<>
<strong>{addColon(l('Approved'))}</strong>
{' '}
{formatUserDate($c, edit.close_time)}
</>
) : (
<>
<strong>{addColon(l('Closed'))}</strong>
{' '}
{formatUserDate($c, edit.close_time)}
</>
)}
</td>
</tr>
</table>
</div>
<h2>
<EditLink content={editTitle} edit={edit} />
</h2>
</>
) : (
<>
{user && (mayApprove || mayCancel) ? (
<div className="cancel-edit buttons">
{mayApprove ? (
<a
className="positive"
href={`/edit/${edit.id}/approve?${returnToCurrentPage($c)}`}
>
{l('Approve edit')}
</a>
) : null}
{mayCancel ? (
<a
className="negative"
href={`/edit/${edit.id}/cancel?${returnToCurrentPage($c)}`}
>
{l('Cancel edit')}
</a>
) : null}
</div>
) : null}
<h1>{editTitle}</h1>
</>
)}

<p className="subheader">
<span className="prefix">{'~'}</span>
{user ? (
<>
{exp.l(
'Edit by {editor}',
{editor: <EditorLink editor={editEditor} />},
)}
<EditorTypeInfo editor={editEditor} />
</>
) : (
<>
{l('Editor hidden')}
{' '}
{/* Show editor type since knowing it's, say, a bot is useful */}
<EditorTypeInfo editor={editEditor} />
{' '}
{bracketed(
<RequestLogin $c={$c} text={l('log in to see who')} />,
)}
</>
)}
</p>
</div>
);
};

export default EditHeader;
43 changes: 43 additions & 0 deletions root/edit/components/VoteTally.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* @flow strict
* Copyright (C) 2021 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 {
EDIT_VOTE_YES,
EDIT_VOTE_NO,
} from '../../constants';

function countVotes(votes, voteValue): number {
return votes.reduce(
(count, vote) => {
return count + ((vote.vote === voteValue && !vote.superseded) ? 1 : 0);
},
0,
);
}

const VoteTally = ({edit}: {edit: EditT}): Expand2ReactOutput => {
if (edit.auto_edit) {
return <strong>{l('automatically applied')}</strong>;
}

const yesVotes = countVotes(edit.votes, EDIT_VOTE_YES);
const noVotes = countVotes(edit.votes, EDIT_VOTE_NO);

return (
exp.l(
'{yes} yes : {no} no',
{
no: <strong>{noVotes}</strong>,
yes: <strong>{yesVotes}</strong>,
},
)
);
};

export default VoteTally;

0 comments on commit 434c7c7

Please sign in to comment.