From 2493c5162267bc7a61d75b75b04c2cb772c318f4 Mon Sep 17 00:00:00 2001 From: yvanzo Date: Thu, 13 Sep 2018 19:09:59 +0200 Subject: [PATCH] Rewrite Preferences page to React/JSX as part of MBS-9699, plus: - Remove unused checkbox for not yet implemented feature MBS-3285 ('Notify me by email on new releases to my watched artists') which should be added to branch metabrainz/mbs-3285 instead; - Reorder timezone and date/time format fields; - Update date/time format options on timezone change; - Add helper button to guess user timezone. --- lib/MusicBrainz/Server/Controller/Account.pm | 18 ++ root/account/Preferences.js | 35 +++ root/account/preferences.tt | 54 ---- root/static/gulpfile.js | 4 + root/static/images/attribution.txt | 2 + root/static/images/icons/magnet.png | Bin 0 -> 426 bytes .../account/components/PreferencesForm.js | 257 ++++++++++++++++++ root/static/scripts/account/preferences.js | 10 + root/static/styles/forms.less | 6 + 9 files changed, 332 insertions(+), 54 deletions(-) create mode 100644 root/account/Preferences.js delete mode 100644 root/account/preferences.tt create mode 100644 root/static/images/icons/magnet.png create mode 100644 root/static/scripts/account/components/PreferencesForm.js create mode 100644 root/static/scripts/account/preferences.js diff --git a/lib/MusicBrainz/Server/Controller/Account.pm b/lib/MusicBrainz/Server/Controller/Account.pm index b66ec8ea9cd..4d2cdb2e481 100644 --- a/lib/MusicBrainz/Server/Controller/Account.pm +++ b/lib/MusicBrainz/Server/Controller/Account.pm @@ -4,7 +4,9 @@ BEGIN { extends 'MusicBrainz::Server::Controller' } use namespace::autoclean; use Digest::SHA qw(sha1_base64); +use List::MoreUtils qw( uniq ); use MusicBrainz::Server::ControllerUtils::JSON qw( serialize_pager ); +use MusicBrainz::Server::Data::Utils qw( boolean_to_json ); use MusicBrainz::Server::Translation qw( l ); use MusicBrainz::Server::Validation qw( encode_entities is_positive_integer ); use Try::Tiny; @@ -473,6 +475,22 @@ sub preferences : Path('/account/preferences') RequireAuth DenyWhenReadonly $c->response->redirect($c->uri_for_action('/account/preferences', { ok => 1 })); $c->detach; + } else { + $c->stash( + current_view => 'Node', + component_path => 'account/Preferences', + component_props => { + form => $form, + timezone_options => { + grouped => boolean_to_json(0), + options => [ map { { + value => $_, + label => $_ + } } uniq values @{ $form->options_timezone } ], + }, + }, + ); + $c->detach; } } diff --git a/root/account/Preferences.js b/root/account/Preferences.js new file mode 100644 index 00000000000..9acf5754d9e --- /dev/null +++ b/root/account/Preferences.js @@ -0,0 +1,35 @@ +/* + * @flow + * Copyright (C) 2018 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 UserAccountLayout from '../components/UserAccountLayout'; +import {withCatalystContext} from '../context'; +import PreferencesForm from '../static/scripts/account/components/PreferencesForm'; +import type {PreferencesFormPropsT} from '../static/scripts/account/components/PreferencesForm'; +import {l} from '../static/scripts/common/i18n'; +import * as manifest from '../static/manifest'; + +type Props = {| + +$c: CatalystContextT, + ...PreferencesFormPropsT, +|}; + +const Preferences = withCatalystContext(({$c, ...props}) => ( + + + {manifest.js('account/preferences')} + +)); + +export default Preferences; diff --git a/root/account/preferences.tt b/root/account/preferences.tt deleted file mode 100644 index 09be8665136..00000000000 --- a/root/account/preferences.tt +++ /dev/null @@ -1,54 +0,0 @@ -[% USE dt = Class('DateTime') %] -[% WRAPPER "user/profile/layout.tt" page="preferences" title=l("Preferences") %] - -
- - [%- USE r = FormRenderer(form) -%] - [%- USE w = FormRenderer(watch_prefs) -%] - -
- [% l('Regional settings') %] - [% WRAPPER form_row %] - [% r.label('datetime_format', l('Date/time format:')) %] - [% r.select('datetime_format') %] - [% field_errors(r.form, 'datetime_format') %] - [% END %] - [% form_row_select(r, 'timezone', l('Timezone:')) %] -
- -
- [% l('Privacy') %] - [% form_row_checkbox(r, 'public_subscriptions', l('Allow other users to see my subscriptions')) %] - [% form_row_checkbox(r, 'public_tags', l('Allow other users to see my tags')) %] - [% form_row_checkbox(r, 'public_ratings', l('Allow other users to see my ratings')) %] - [% form_row_checkbox(r, 'show_gravatar', l('Show my Gravatar')) %] -
- -
- [% l('Email') %] - [% form_row_checkbox(r, 'email_on_no_vote', l('Mail me when one of my edits gets a "no" vote. - (Note: the email is only sent for the first "no" vote, not each one)')) %] - [% form_row_checkbox(r, 'email_on_notes', l('When I add a note to an edit, mail me all - future notes for that edit.')) %] - [% form_row_checkbox(r, 'email_on_vote', l('When I vote on an edit, mail me all future notes - for that edit.')) %] - [% form_row_checkbox(w, 'notify_via_email', l('Notify me by email on new releases to my watched artists')) %] - [% form_row_select(r, 'subscriptions_email_period', l('Send me mails with edits to my subscriptions:')) %] -
- -
- [% l('Editing') %] - [% form_row_checkbox(r, 'subscribe_to_created_artists', - l('Automatically subscribe me to artists I create.')) %] - [% form_row_checkbox(r, 'subscribe_to_created_labels', - l('Automatically subscribe me to labels I create.')) %] - [% form_row_checkbox(r, 'subscribe_to_created_series', - l('Automatically subscribe me to series I create.')) %] -
- -
- [% form_submit(l('Save')) %] -
-
- -[% END %] diff --git a/root/static/gulpfile.js b/root/static/gulpfile.js index 22bf9f15248..68806428e8e 100644 --- a/root/static/gulpfile.js +++ b/root/static/gulpfile.js @@ -261,6 +261,10 @@ runYarb('account/applications/register.js', function (b) { b.external(commonBundle); }); +runYarb('account/preferences.js', function (b) { + b.external(commonBundle); +}); + runYarb('area/index.js', function (b) { b.external(commonBundle); }); diff --git a/root/static/images/attribution.txt b/root/static/images/attribution.txt index 9633f623e6c..34673b15e68 100644 --- a/root/static/images/attribution.txt +++ b/root/static/images/attribution.txt @@ -129,10 +129,12 @@ Image set: Android Author: http://www.androidicons.com License: CC-BY (see download page) Download: http://findicons.com/icon/80718/monitor (original, cropped in MusicBrainz) + http://findicons.com/icon/80717/magnet (original, cropped in MusicBrainz) Images: icons/video.png + icons/magnet.png ---------------------------------------------------------------------------- diff --git a/root/static/images/icons/magnet.png b/root/static/images/icons/magnet.png new file mode 100644 index 0000000000000000000000000000000000000000..1bd7d60572d20ea817186dba6bb498593ca09797 GIT binary patch literal 426 zcmV;b0agBqP)``^Erv<*#Nosdp zeQ7q<(7#(Ds9lUYk zEh3wF, + +email_on_no_vote: FieldT, + +email_on_notes: FieldT, + +email_on_vote: FieldT, + +notify_via_email: FieldT, + +public_ratings: FieldT, + +public_subscriptions: FieldT, + +public_tags: FieldT, + +show_gravatar: FieldT, + +subscribe_to_created_artists: FieldT, + +subscribe_to_created_labels: FieldT, + +subscribe_to_created_series: FieldT, + +subscriptions_email_period: FieldT, + +timezone: FieldT, +|}>; + +type Props = {| + +form: PreferencesFormT, + +timezone_options: MaybeGroupedOptionsT, +|}; + +type State = {| + form: PreferencesFormT, + timezoneOptions: MaybeGroupedOptionsT, +|}; + +const allowedDateTimeFormats = [ + '%Y-%m-%d %H:%M %Z', + '%c', + '%x %X', + '%X %x', + '%A %B %e %Y, %H:%M', + '%d %B %Y %H:%M', + '%a %b %e %Y, %H:%M', + '%d %b %Y %H:%M', + '%d/%m/%Y %H:%M', + '%m/%d/%Y %H:%M', + '%d.%m.%Y %H:%M', + '%m.%d.%Y %H:%M', +]; + +function buildDateTimeFormatOptions(timezone) { + const hereAndNow = moment.tz(timezone); + return { + grouped: false, + options: allowedDateTimeFormats.map(a => ({ + label: hereAndNow.strftime(a), + value: a, + })), + }; +} + +const subscriptionsEmailPeriodOptions = { + grouped: false, + options: [ + {label: N_l('Daily'), value: 'daily'}, + {label: N_l('Weekly'), value: 'weekly'}, + {label: N_l('Never'), value: 'never'}, + ], +}; + +const timezoneFieldLens: Lens = + compose3(prop('field'), prop('timezone'), prop('value')); + +const dateTimeFormatFieldLens: Lens = + compose3(prop('field'), prop('datetime_format'), prop('value')); + +const subscriptionsEmailPeriodFieldLens: Lens = + compose3(prop('field'), prop('subscriptions_email_period'), prop('value')); + +class PreferencesForm extends React.Component { + constructor(props: Props) { + super(props); + this.state = {form: props.form, timezoneOptions: props.timezone_options}; + this.handleTimezoneChange = this.handleTimezoneChange.bind(this); + this.handleTimezoneGuess = this.handleTimezoneGuess.bind(this); + this.handleDateTimeFormatChange = + this.handleDateTimeFormatChange.bind(this); + this.handleSubscriptionsEmailPeriodChange = + this.handleSubscriptionsEmailPeriodChange.bind(this); + } + + handleTimezoneChange: (e: SyntheticEvent) => void; + + handleTimezoneChange(e: SyntheticEvent) { + const selectedTimezone = e.currentTarget.value; + this.setState(prevState => ({ + form: set( + timezoneFieldLens, + selectedTimezone, + prevState.form, + ), + })); + } + + handleTimezoneGuess: (e: SyntheticEvent) => void; + + handleTimezoneGuess(e: SyntheticEvent) { + const guess = moment.tz.guess(); + // $FlowFixMe - $ReadOnlyArray is incompatible with array type + if (_.some(this.state.timezoneOptions.options, {value: guess})) { + this.setState(prevState => ({ + form: set( + timezoneFieldLens, + moment.tz.guess(), + prevState.form, + ), + })); + } + } + + handleDateTimeFormatChange: (e: SyntheticEvent) => void; + + handleDateTimeFormatChange(e: SyntheticEvent) { + const selectedDateTimeFormat = e.currentTarget.value; + this.setState(prevState => ({ + form: set( + dateTimeFormatFieldLens, + selectedDateTimeFormat, + prevState.form, + ), + })); + } + + handleSubscriptionsEmailPeriodChange: (e: SyntheticEvent) + => void; + + handleSubscriptionsEmailPeriodChange(e: SyntheticEvent) { + const selectedSubscriptionsEmailPeriod = e.currentTarget.value; + this.setState(prevState => ({ + form: set( + subscriptionsEmailPeriodFieldLens, + selectedSubscriptionsEmailPeriod, + prevState.form, + ), + })); + } + + render() { + const field = this.state.form.field; + return ( +
+
+ {l('Regional settings')} + + {' '} +
+
+ {l('Privacy')} + + + + +
+
+ {l('Email')} + + + + +
+
+ {l('Editing')} + + + +
+ + + +
+ ); + } +} + +export type PreferencesFormPropsT = Props; +export default hydrate('preferences-form', PreferencesForm); diff --git a/root/static/scripts/account/preferences.js b/root/static/scripts/account/preferences.js new file mode 100644 index 00000000000..ab0765fc80f --- /dev/null +++ b/root/static/scripts/account/preferences.js @@ -0,0 +1,10 @@ +/* + * @flow + * Copyright (C) 2018 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 './components/PreferencesForm'; diff --git a/root/static/styles/forms.less b/root/static/styles/forms.less index c7c1d2756d8..ae294564d56 100644 --- a/root/static/styles/forms.less +++ b/root/static/styles/forms.less @@ -722,3 +722,9 @@ div.rel-editor-dialog { border-bottom: 1px @dark-grey dotted; } } + +/* Guess user timezone */ + +button.guess-timezone { + background-image: data-uri('../images/icons/magnet.png'); +}