UX: improves local-dates form (#7268)

This commit is contained in:
Joffrey JAFFEUX 2019-03-28 16:34:56 +01:00 committed by GitHub
parent 6e2e095b53
commit 9a56b398a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 370 additions and 207 deletions

View File

@ -1,11 +1,11 @@
import computed from "ember-addons/ember-computed-decorators"; import { default as computed } from "ember-addons/ember-computed-decorators";
import { observes } from "ember-addons/ember-computed-decorators"; import { cookAsync } from "discourse/lib/text";
import debounce from "discourse/lib/debounce";
export default Ember.Component.extend({ export default Ember.Component.extend({
timeFormat: "HH:mm:ss", timeFormat: "HH:mm:ss",
dateFormat: "YYYY-MM-DD", dateFormat: "YYYY-MM-DD",
dateTimeFormat: "YYYY-MM-DD HH:mm:ss", dateTimeFormat: "YYYY-MM-DD HH:mm:ss",
config: null,
date: null, date: null,
toDate: null, toDate: null,
time: null, time: null,
@ -15,23 +15,152 @@ export default Ember.Component.extend({
recurring: null, recurring: null,
advancedMode: false, advancedMode: false,
isValid: true, isValid: true,
timezone: null,
timezones: null,
init() { init() {
this._super(); this._super(...arguments);
this.set("date", moment().format(this.dateFormat)); this.setProperties({
this.set("timezones", []); timezones: [],
this.set( formats: (this.siteSettings.discourse_local_dates_default_formats || "")
"formats",
(this.siteSettings.discourse_local_dates_default_formats || "")
.split("|") .split("|")
.filter(f => f) .filter(f => f),
); timezone: moment.tz.guess(),
date: moment().format(this.dateFormat)
});
}, },
@observes("date", "time", "toDate", "toTime") didInsertElement() {
_resetFormValidity() { this._super(...arguments);
this.set("isValid", true);
this._renderPreview();
},
_renderPreview: debounce(function() {
const markup = this.get("markup");
if (markup) {
cookAsync(markup).then(result => {
this.set("currentPreview", result);
Ember.run.schedule("afterRender", () =>
this.$(".preview .discourse-local-date").applyLocalDates()
);
});
}
}, 250).observes("markup"),
@computed("date", "toDate", "toTime")
isRange(date, toDate, toTime) {
return date && (toDate || toTime);
},
@computed("computedConfig", "isRange")
isValid(config, isRange) {
const fromConfig = config.from;
if (!config.from.dateTime || !config.from.dateTime.isValid()) {
return false;
}
if (isRange) {
const toConfig = config.to;
if (
!toConfig.dateTime ||
!toConfig.dateTime.isValid() ||
toConfig.dateTime.diff(fromConfig.dateTime) < 0
) {
return false;
}
}
return true;
},
@computed("date", "time", "isRange", "options.{format,timezone}")
fromConfig(date, time, isRange, options = {}) {
const timeInferred = time ? false : true;
let dateTime;
if (!timeInferred) {
dateTime = moment.tz(`${date} ${time}`, options.timezone);
} else {
dateTime = moment.tz(date, options.timezone);
}
if (!timeInferred) {
time = dateTime.format(this.timeFormat);
}
let format = options.format;
if (timeInferred && this.get("formats").includes(format)) {
format = "LL";
}
return Ember.Object.create({
date: dateTime.format(this.dateFormat),
time,
dateTime,
format,
range: isRange ? "start" : false
});
},
@computed("toDate", "toTime", "isRange", "options.{timezone,format}")
toConfig(date, time, isRange, options = {}) {
const timeInferred = time ? false : true;
if (time && !date) {
date = moment().format(this.dateFormat);
}
let dateTime;
if (!timeInferred) {
dateTime = moment.tz(`${date} ${time}`, options.timezone);
} else {
dateTime = moment.tz(date, options.timezone).endOf("day");
}
if (!timeInferred) {
time = dateTime.format(this.timeFormat);
}
let format = options.format;
if (timeInferred && this.get("formats").includes(format)) {
format = "LL";
}
return Ember.Object.create({
date: dateTime.format(this.dateFormat),
time,
dateTime,
format,
range: isRange ? "end" : false
});
},
@computed("recurring", "timezones", "timezone", "format")
options(recurring, timezones, timezone, format) {
return Ember.Object.create({
recurring,
timezones,
timezone,
format
});
},
@computed(
"fromConfig.{date}",
"toConfig.{date}",
"options.{recurring,timezones,timezone,format}"
)
computedConfig(fromConfig, toConfig, options) {
return Ember.Object.create({
from: fromConfig,
to: toConfig,
options
});
}, },
@computed @computed
@ -51,15 +180,41 @@ export default Ember.Component.extend({
@computed @computed
recurringOptions() { recurringOptions() {
const key = "discourse_local_dates.create.form.recurring";
return [ return [
{ name: "Every day", id: "1.days" }, {
{ name: "Every week", id: "1.weeks" }, name: I18n.t(`${key}.every_day`),
{ name: "Every two weeks", id: "2.weeks" }, id: "1.days"
{ name: "Every month", id: "1.months" }, },
{ name: "Every two months", id: "2.months" }, {
{ name: "Every three months", id: "3.months" }, name: I18n.t(`${key}.every_week`),
{ name: "Every six months", id: "6.months" }, id: "1.weeks"
{ name: "Every year", id: "1.years" } },
{
name: I18n.t(`${key}.every_two_weeks`),
id: "2.weeks"
},
{
name: I18n.t(`${key}.every_month`),
id: "1.months"
},
{
name: I18n.t(`${key}.every_two_months`),
id: "2.months"
},
{
name: I18n.t(`${key}.every_three_months`),
id: "3.months"
},
{
name: I18n.t(`${key}.every_six_months`),
id: "6.months"
},
{
name: I18n.t(`${key}.every_year`),
id: "1.years"
}
]; ];
}, },
@ -74,77 +229,27 @@ export default Ember.Component.extend({
return moment.tz.names(); return moment.tz.names();
}, },
getConfig(range) { _generateDateMarkup(config, options, isRange) {
const endOfRange = range && range === "end";
const time = endOfRange ? this.get("toTime") : this.get("time");
let date = endOfRange ? this.get("toDate") : this.get("date");
if (endOfRange && time && !date) {
date = moment().format(this.dateFormat);
}
const recurring = this.get("recurring");
const format = this.get("format");
const timezones = this.get("timezones");
const timeInferred = time ? false : true;
const timezone = this.get("currentUserTimezone");
let dateTime;
if (!timeInferred) {
dateTime = moment.tz(`${date} ${time}`, timezone);
} else {
if (endOfRange) {
dateTime = moment.tz(date, timezone).endOf("day");
} else {
dateTime = moment.tz(date, timezone);
}
}
let config = {
date: dateTime.format(this.dateFormat),
dateTime,
recurring,
format,
timezones,
timezone
};
if (!timeInferred) {
config.time = dateTime.format(this.timeFormat);
}
if (timeInferred) {
config.displayedTimezone = this.get("currentUserTimezone");
}
if (timeInferred && this.get("formats").includes(format)) {
config.format = "LL";
}
return config;
},
_generateDateMarkup(config) {
let text = `[date=${config.date}`; let text = `[date=${config.date}`;
if (config.time) { if (config.time) {
text += ` time=${config.time} `; text += ` time=${config.time}`;
} }
if (config.format && config.format.length) { if (config.format && config.format.length) {
text += ` format="${config.format}" `; text += ` format="${config.format}"`;
} }
if (config.timezone) { if (options.timezone) {
text += ` timezone="${config.timezone}"`; text += ` timezone="${options.timezone}"`;
} }
if (config.timezones && config.timezones.length) { if (options.timezones && options.timezones.length) {
text += ` timezones="${config.timezones.join("|")}"`; text += ` timezones="${options.timezones.join("|")}"`;
} }
if (config.recurring) { if (options.recurring && !isRange) {
text += ` recurring="${config.recurring}"`; text += ` recurring="${options.recurring}"`;
} }
text += `]`; text += `]`;
@ -152,31 +257,6 @@ export default Ember.Component.extend({
return text; return text;
}, },
valid(isRange) {
const fromConfig = this.getConfig(isRange ? "start" : null);
if (!fromConfig.dateTime || !fromConfig.dateTime.isValid()) {
this.set("isValid", false);
return false;
}
if (isRange) {
const toConfig = this.getConfig("end");
if (
!toConfig.dateTime ||
!toConfig.dateTime.isValid() ||
toConfig.dateTime.diff(fromConfig.dateTime) < 0
) {
this.set("isValid", false);
return false;
}
}
this.set("isValid", true);
return true;
},
@computed("advancedMode") @computed("advancedMode")
toggleModeBtnLabel(advancedMode) { toggleModeBtnLabel(advancedMode) {
return advancedMode return advancedMode
@ -184,35 +264,36 @@ export default Ember.Component.extend({
: "discourse_local_dates.create.form.advanced_mode"; : "discourse_local_dates.create.form.advanced_mode";
}, },
@computed("computedConfig.{from,to,options}", "options", "isValid", "isRange")
markup(config, options, isValid, isRange) {
let text;
if (isValid && config.from) {
text = this._generateDateMarkup(config.from, options, isRange);
if (config.to && config.to.range) {
text += ` → `;
text += this._generateDateMarkup(config.to, options, isRange);
}
}
return text;
},
actions: { actions: {
advancedMode() { advancedMode() {
this.toggleProperty("advancedMode"); this.toggleProperty("advancedMode");
}, },
save() { save() {
const isRange = const markup = this.get("markup");
this.get("date") && (this.get("toDate") || this.get("toTime"));
if (this.valid(isRange)) { if (markup) {
this._closeModal(); this._closeModal();
this.get("toolbarEvent").addText(markup);
let text = this._generateDateMarkup(
this.getConfig(isRange ? "start" : null)
);
if (isRange) {
text += ` → `;
text += this._generateDateMarkup(this.getConfig("end"));
}
this.get("toolbarEvent").addText(text);
} }
}, },
fillFormat(format) {
this.set("format", format);
},
cancel() { cancel() {
this._closeModal(); this._closeModal();
} }

View File

@ -5,6 +5,19 @@
style="overflow: auto"}} style="overflow: auto"}}
<div class="form"> <div class="form">
{{#unless isValid}}
<div class="alert alert-error">
{{i18n "discourse_local_dates.create.form.invalid_date"}}
</div>
{{else}}
<div class="preview alert alert-info">
<b>{{i18n "discourse_local_dates.create.form.preview_for" timezone=currentUserTimezone}}</b>
<span>{{currentPreview}}</span>
</div>
{{/unless}}
{{computeDate}}
<div class="date-time-configuration"> <div class="date-time-configuration">
<div class="range"> <div class="range">
<div class="from"> <div class="from">
@ -13,7 +26,11 @@
{{i18n "discourse_local_dates.create.form.date_title"}} {{i18n "discourse_local_dates.create.form.date_title"}}
</label> </label>
<div class="controls"> <div class="controls">
{{date-picker class="date-input" value=date defaultDate="DD-MM-YYYY"}} {{date-picker
onSelect=(action (mut date))
class="date-input"
value=date
defaultDate="DD-MM-YYYY"}}
</div> </div>
</div> </div>
@ -22,12 +39,14 @@
{{i18n "discourse_local_dates.create.form.time_title"}} {{i18n "discourse_local_dates.create.form.time_title"}}
</label> </label>
<div class="controls"> <div class="controls">
{{input type="time" value=time class="time-input"}} {{input input=(mut time) type="time" value=time class="time-input"}}
</div> </div>
</div> </div>
</div> </div>
<span class="to-indicator"><span>{{i18n "discourse_local_dates.create.form.to"}}</span></span> <div class="to-indicator">
{{if site.mobileView "↓" "→"}}
</div>
<div class="to"> <div class="to">
<div class="control-group date"> <div class="control-group date">
@ -35,7 +54,11 @@
{{i18n "discourse_local_dates.create.form.date_title"}} {{i18n "discourse_local_dates.create.form.date_title"}}
</label> </label>
<div class="controls"> <div class="controls">
{{date-picker class="date-input" value=toDate defaultDate="DD-MM-YYYY"}} {{date-picker
onSelect=(action (mut toDate))
class="date-input"
value=toDate
defaultDate="DD-MM-YYYY"}}
</div> </div>
</div> </div>
@ -44,33 +67,49 @@
{{i18n "discourse_local_dates.create.form.time_title"}} {{i18n "discourse_local_dates.create.form.time_title"}}
</label> </label>
<div class="controls"> <div class="controls">
{{input type="time" value=toTime class="time-input"}} {{input input=(mut toTime) type="time" value=toTime class="time-input"}}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<span class="preview">{{currentUserTimezone}}</span> <div class="timezone">
</div> <div class="control-group time">
<label class="control-label">
{{i18n "discourse_local_dates.create.form.timezone"}}
</label>
<div class="controls">
{{combo-box
class="timezone-input"
allowAny=false
content=allTimezones
value=timezone
onSelect=(action (mut timezone))}}
</div>
</div>
{{#unless isValid}}
<span class="validation-error">{{i18n "discourse_local_dates.create.form.invalid_date"}}</span>
{{/unless}}
<div class="control-group recurrence">
<label class="control-label">
{{i18n "discourse_local_dates.create.form.recurring_title"}}
</label>
{{#if advancedMode}}
<p>{{{i18n "discourse_local_dates.create.form.recurring_description"}}}</p>
{{/if}}
<div class="controls">
{{combo-box content=recurringOptions class="recurrence-input" value=recurring none="discourse_local_dates.create.form.recurring_none"}}
</div> </div>
</div> </div>
{{#if advancedMode}} {{#if advancedMode}}
<div class="advanced-options"> <div class="advanced-options">
{{#unless isRange}}
<div class="control-group recurrence">
<label class="control-label">
{{i18n "discourse_local_dates.create.form.recurring_title"}}
</label>
<p>{{{i18n "discourse_local_dates.create.form.recurring_description"}}}</p>
<div class="controls">
{{combo-box
content=recurringOptions
class="recurrence-input"
value=recurring
onSelect=(action (mut recurring))
none="discourse_local_dates.create.form.recurring_none"}}
</div>
</div>
{{/unless}}
<div class="control-group format"> <div class="control-group format">
<label>{{i18n "discourse_local_dates.create.form.format_title"}}</label> <label>{{i18n "discourse_local_dates.create.form.format_title"}}</label>
<p> <p>
@ -87,7 +126,7 @@
<ul class="formats"> <ul class="formats">
{{#each previewedFormats as |previewedFormat|}} {{#each previewedFormats as |previewedFormat|}}
<li class="format"> <li class="format">
<a class="moment-format" href {{action "fillFormat" previewedFormat.format}}> <a class="moment-format" href {{action (mut format) previewedFormat.format}}>
{{previewedFormat.format}} {{previewedFormat.format}}
</a> </a>
<span class="previewed-format"> <span class="previewed-format">

View File

@ -57,7 +57,7 @@
} }
.discourse-local-dates-create-modal { .discourse-local-dates-create-modal {
min-height: 300px; min-height: 200px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -70,72 +70,69 @@
.date-time-configuration { .date-time-configuration {
display: flex; display: flex;
align-items: center;
flex-direction: row;
.range { .range,
.timezone {
display: flex;
justify-content: flex-start;
flex: 1;
align-items: center;
.from { .from {
flex-direction: row;
display: flex; display: flex;
flex-direction: row;
justify-content: space-between;
} }
.to { .to {
flex-direction: row;
display: flex; display: flex;
flex-direction: row;
justify-content: space-between;
} }
.to-indicator { .to-indicator {
display: flex; display: flex;
justify-content: center; flex-direction: row;
margin: 0.5em 0; padding-top: 0.5em;
margin: 0 1em;
font-size: $font-up-2;
} }
} }
.date { .date {
margin-right: 0.5em;
.date-input { .date-input {
margin-right: 1em; width: 100px;
text-align: center;
.date-picker { .date-picker {
padding-top: 5px; padding: 0;
bottom: 5px;
margin: 0; margin: 0;
width: 120px; width: 100%;
text-align: left;
} }
} }
} }
.timezone {
margin-left: 1em;
.timezone-input {
width: 180px;
}
}
.time { .time {
.time-input { .time-input {
margin: 0 0.5em 0 0; width: 80px;
width: 120px; margin: 0;
padding: 3.5px 10px; padding: 0;
text-align: center;
} }
} }
}
.preview { .preview {
flex: 1 0 0px; text-align: center;
margin-top: 16px; margin-top: 0;
text-align: center; margin-bottom: 1em;
}
@include breakpoint(medium) {
flex-direction: column;
align-items: flex-start;
.range .from,
.range .to {
flex-direction: column;
}
.date .date-input .date-picker {
width: 200px;
}
.time .time-input {
width: 200px;
}
}
} }
.validation-error { .validation-error {
@ -176,3 +173,38 @@
width: 99%; width: 99%;
} }
} }
@media (max-width: 700px) {
.discourse-local-dates-create-modal {
.form {
.date-time-configuration {
flex-direction: column;
.range,
.timezone {
display: flex;
flex: 1;
flex-direction: column;
.controls,
.control-group {
width: 100%;
}
.to-indicator {
margin: 0.5em 1em;
}
}
.timezone {
margin: 0.5em 0 0 0;
padding: 0.5em 0 0 0;
border-top: 1px solid $primary-low;
.timezone-input {
width: 100%;
}
}
}
}
}
}

View File

@ -10,7 +10,6 @@ en:
modal_title: Insert date modal_title: Insert date
modal_subtitle: "We will automatically convert the date and time to the viewers local time zone." modal_subtitle: "We will automatically convert the date and time to the viewers local time zone."
form: form:
to: "to"
insert: Insert insert: Insert
advanced_mode: Advanced mode advanced_mode: Advanced mode
simple_mode: Simple mode simple_mode: Simple mode
@ -24,3 +23,14 @@ en:
date_title: Date date_title: Date
time_title: Time time_title: Time
format_title: Date format format_title: Date format
timezone: Timezone
preview_for: Preview for %{timezone}
recurring:
every_day: "Every day"
every_week: "Every week"
every_two_weeks: "Every two weeks"
every_month: "Every month"
every_two_months: "Every two months"
every_three_months: "Every three months"
every_six_months: "Every six months"
every_year: "Every year"

View File

@ -4,38 +4,39 @@
# author: Joffrey Jaffeux # author: Joffrey Jaffeux
hide_plugin if self.respond_to?(:hide_plugin) hide_plugin if self.respond_to?(:hide_plugin)
register_asset "javascripts/discourse-local-dates.js.no-module.es6" register_asset 'javascripts/discourse-local-dates.js.no-module.es6'
register_asset "stylesheets/common/discourse-local-dates.scss" register_asset 'stylesheets/common/discourse-local-dates.scss'
register_asset "moment.js", :vendored_core_pretty_text register_asset 'moment.js', :vendored_core_pretty_text
register_asset "moment-timezone.js", :vendored_core_pretty_text register_asset 'moment-timezone.js', :vendored_core_pretty_text
enabled_site_setting :discourse_local_dates_enabled enabled_site_setting :discourse_local_dates_enabled
after_initialize do after_initialize do
module ::DiscourseLocalDates module ::DiscourseLocalDates
PLUGIN_NAME ||= "discourse-local-dates".freeze PLUGIN_NAME ||= 'discourse-local-dates'.freeze
POST_CUSTOM_FIELD ||= "local_dates".freeze POST_CUSTOM_FIELD ||= 'local_dates'.freeze
end end
[ %w[../lib/discourse_local_dates/engine.rb].each do |path|
"../lib/discourse_local_dates/engine.rb", load File.expand_path(path, __FILE__)
].each { |path| load File.expand_path(path, __FILE__) } end
register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json) register_post_custom_field_type(DiscourseLocalDates::POST_CUSTOM_FIELD, :json)
on(:before_post_process_cooked) do |doc, post| on(:before_post_process_cooked) do |doc, post|
dates = doc.css('span.discourse-local-date').map do |cooked_date| dates =
date = {} doc.css('span.discourse-local-date').map do |cooked_date|
cooked_date.attributes.values.each do |attribute| date = {}
data_name = attribute.name&.gsub('data-', '') cooked_date.attributes.values.each do |attribute|
if data_name && ['date', 'time', 'timezone', 'recurring'].include?(data_name) data_name = attribute.name&.gsub('data-', '')
unless attribute.value == 'undefined' if data_name && %w[date time timezone recurring].include?(data_name)
date[data_name] = CGI.escapeHTML(attribute.value || "") unless attribute.value == 'undefined'
date[data_name] = CGI.escapeHTML(attribute.value || '')
end
end end
end end
date
end end
date
end
if dates.present? if dates.present?
post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates post.custom_fields[DiscourseLocalDates::POST_CUSTOM_FIELD] = dates
@ -51,9 +52,9 @@ after_initialize do
end end
on(:reduce_cooked) do |fragment| on(:reduce_cooked) do |fragment|
fragment.css(".discourse-local-date").each do |container| fragment.css('.discourse-local-date').each do |container|
if container.attributes["data-email-preview"] if container.attributes['data-email-preview']
preview = container.attributes["data-email-preview"].value preview = container.attributes['data-email-preview'].value
container.content = preview container.content = preview
end end
end end