From fb36af779913173aa5a4dc896378dd327f0f9671 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Fri, 11 Aug 2023 13:05:44 +1000 Subject: [PATCH] DEV: Move calendar date + time picker from local dates into core component (#23023) This commit moves the calendar date and time picker shown in the local dates modal into a core component that can be reused in other places. Also add system specs to make sure there isn't any breakages with this feature, and a section to the styleguide. --- .../components/calendar-date-time-input.hbs | 24 +++ .../components/calendar-date-time-input.js | 109 +++++++++++ .../stylesheets/common/components/_index.scss | 1 + .../components/calendar-date-time-input.scss | 50 +++++ .../discourse-local-dates-create-form.js | 175 +++++++----------- .../discourse-local-dates-create-form.hbs | 44 ++--- .../common/discourse-local-dates.scss | 58 +----- .../spec/system/local_dates_spec.rb | 82 ++++++++ .../page_objects/modals/insert_date_time.rb | 26 +++ .../acceptance/local-dates-composer-test.js | 2 +- .../sections/atoms/date-time-inputs.hbs | 4 +- .../styleguide/calendar-date-time-input.hbs | 34 ++++ .../styleguide/calendar-date-time-input.js | 24 +++ .../components/calendar_date_time_picker.rb | 30 +++ 14 files changed, 463 insertions(+), 200 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/calendar-date-time-input.hbs create mode 100644 app/assets/javascripts/discourse/app/components/calendar-date-time-input.js create mode 100644 app/assets/stylesheets/common/components/calendar-date-time-input.scss create mode 100644 plugins/discourse-local-dates/spec/system/page_objects/modals/insert_date_time.rb create mode 100644 plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.hbs create mode 100644 plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.js create mode 100644 spec/system/page_objects/components/calendar_date_time_picker.rb diff --git a/app/assets/javascripts/discourse/app/components/calendar-date-time-input.hbs b/app/assets/javascripts/discourse/app/components/calendar-date-time-input.hbs new file mode 100644 index 00000000000..800d5d2f2f8 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/calendar-date-time-input.hbs @@ -0,0 +1,24 @@ +
+ + +
+ +
+ {{d-icon "far-clock"}} + +
+
\ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/calendar-date-time-input.js b/app/assets/javascripts/discourse/app/components/calendar-date-time-input.js new file mode 100644 index 00000000000..104975b9037 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/calendar-date-time-input.js @@ -0,0 +1,109 @@ +/* global Pikaday:true */ +import { isEmpty } from "@ember/utils"; +import Component from "@glimmer/component"; +import I18n from "I18n"; +import loadScript from "discourse/lib/load-script"; +import { Promise } from "rsvp"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; + +export default class CalendarDateTimeInput extends Component { + _timeFormat = this.args.timeFormat || "HH:mm:ss"; + _dateFormat = this.args.dateFormat || "YYYY-MM-DD"; + _dateTimeFormat = this.args.dateTimeFormat || "YYYY-MM-DD HH:mm:ss"; + _picker = null; + + @tracked _time; + @tracked _date; + + @action + setupInternalDateTime() { + this._time = this.args.time; + this._date = this.args.date; + } + + @action + setupPikaday(element) { + this.#setupPicker(element).then((picker) => { + this._picker = picker; + }); + } + + @action + onChangeTime(event) { + this._time = event.target.value; + this.args.onChangeTime(this._time); + } + + @action + changeDate() { + if (moment(this.args.date, this._dateFormat).isValid()) { + this._date = this.args.date; + this._picker.setDate( + moment.utc(this._date).format(this._dateFormat), + true + ); + } else { + this._date = null; + this._picker.setDate(null); + } + } + + @action + changeTime() { + if (isEmpty(this.args.time)) { + this._time = null; + return; + } + + if (/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/.test(this.args.time)) { + this._time = this.args.time; + } + } + + @action + changeMinDate() { + if ( + this.args.minDate && + moment(this.args.minDate, this._dateFormat).isValid() + ) { + this._picker.setMinDate( + moment(this.args.minDate, this._dateFormat).toDate() + ); + } else { + this._picker.setMinDate(null); + } + } + + #setupPicker(element) { + return new Promise((resolve) => { + loadScript("/javascripts/pikaday.js").then(() => { + const options = { + field: element.querySelector(".fake-input"), + container: element.querySelector( + `#picker-container-${this.args.datePickerId}` + ), + bound: false, + format: "YYYY-MM-DD", + reposition: false, + firstDay: 1, + setDefaultDate: true, + keyboardInput: false, + i18n: { + previousMonth: I18n.t("dates.previous_month"), + nextMonth: I18n.t("dates.next_month"), + months: moment.months(), + weekdays: moment.weekdays(), + weekdaysShort: moment.weekdaysMin(), + }, + onSelect: (date) => { + const formattedDate = moment(date).format("YYYY-MM-DD"); + this.args.onChangeDate(formattedDate); + }, + }; + + resolve(new Pikaday(options)); + }); + }); + } +} diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index c33d212486c..a138ca93e84 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -6,6 +6,7 @@ @import "color-input"; @import "char-counter"; @import "conditional-loading-section"; +@import "calendar-date-time-input"; @import "convert-to-public-topic-modal"; @import "d-lightbox"; @import "d-tooltip"; diff --git a/app/assets/stylesheets/common/components/calendar-date-time-input.scss b/app/assets/stylesheets/common/components/calendar-date-time-input.scss new file mode 100644 index 00000000000..44935985630 --- /dev/null +++ b/app/assets/stylesheets/common/components/calendar-date-time-input.scss @@ -0,0 +1,50 @@ +.calendar-date-time-input { + .fake-input { + display: none; + } + + padding: 5px; + border: 1px solid var(--primary-low); + z-index: 1; + background: var(--secondary); + width: 200px; + box-sizing: border-box; + margin-left: 1em; + + .date-picker { + display: flex; + flex-direction: column; + width: auto; + box-sizing: border-box; + + .pika-single { + position: relative !important; + flex: 1; + display: flex; + border: 0; + } + } + + .time-pickers { + display: flex; + justify-content: center; + flex: 1; + margin-top: 1em; + align-items: center; + padding: 0.25em; + border-top: 1px solid var(--primary-low-mid); + box-sizing: border-box; + + .d-icon { + color: var(--primary-medium); + margin-right: 0.5em; + } + + .time-picker { + box-shadow: none; + margin: 0; + box-sizing: border-box; + width: 100%; + } + } +} diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js index 5db45c81c7b..ce019c9ae80 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js @@ -1,4 +1,3 @@ -/* global Pikaday:true */ import computed, { debounce, observes, @@ -7,10 +6,7 @@ import Component from "@ember/component"; import EmberObject, { action } from "@ember/object"; import I18n from "I18n"; import { INPUT_DELAY } from "discourse-common/config/environment"; -import { Promise } from "rsvp"; import { cookAsync } from "discourse/lib/text"; -import { isEmpty } from "@ember/utils"; -import loadScript from "discourse/lib/load-script"; import { notEmpty } from "@ember/object/computed"; import { propertyNotEqual } from "discourse/lib/computed"; import { schedule } from "@ember/runloop"; @@ -46,18 +42,14 @@ export default Component.extend({ formats: (this.siteSettings.discourse_local_dates_default_formats || "") .split("|") .filter((f) => f), - timezone: moment.tz.guess(), + timezone: this.currentUserTimezone, date: moment().format(this.dateFormat), }); }, didInsertElement() { this._super(...arguments); - - this._setupPicker().then((picker) => { - this._picker = picker; - this.send("focusFrom"); - }); + this.send("focusFrom"); }, @observes("computedConfig.{from,to,options}", "options", "isValid", "isRange") @@ -194,7 +186,7 @@ export default Component.extend({ @computed currentUserTimezone() { - return moment.tz.guess(); + return this.currentUser.user_option.timezone || moment.tz.guess(); }, @computed @@ -312,118 +304,79 @@ export default Component.extend({ this.set("format", format); }, - actions: { - setTime(event) { - this._setTimeIfValid(event.target.value, "time"); - }, + @computed("fromSelected", "toSelected") + selectedDate(fromSelected) { + return fromSelected ? this.date : this.toDate; + }, - setToTime(event) { - this._setTimeIfValid(event.target.value, "toTime"); - }, + @computed("fromSelected", "toSelected") + selectedTime(fromSelected) { + return fromSelected ? this.time : this.toTime; + }, - eraseToDateTime() { - this.setProperties({ toDate: null, toTime: null }); - this._setPickerDate(null); - }, + @action + changeSelectedDate(date) { + if (this.fromSelected) { + this.set("date", date); + } else { + this.set("toDate", date); + } + }, - focusFrom() { - this.setProperties({ fromSelected: true, toSelected: false }); - this._setPickerDate(this.get("fromConfig.date")); - this._setPickerMinDate(null); - }, + @action + changeSelectedTime(time) { + if (this.fromSelected) { + this.set("time", time); + } else { + this.set("toTime", time); + } + }, - focusTo() { - this.setProperties({ toSelected: true, fromSelected: false }); - this._setPickerDate(this.get("toConfig.date")); - this._setPickerMinDate(this.get("fromConfig.date")); - }, + @action + eraseToDateTime() { + this.setProperties({ + toDate: null, + toTime: null, + }); + this.focusFrom(); + }, - advancedMode() { - this.toggleProperty("advancedMode"); - }, + @action + focusFrom() { + this.setProperties({ + fromSelected: true, + toSelected: false, + minDate: null, + }); + }, - save() { - const markup = this.markup; + @action + focusTo() { + this.setProperties({ + toSelected: true, + fromSelected: false, + minDate: this.get("fromConfig.date"), + }); + }, - if (markup) { - this._closeModal(); - this.insertDate(markup); - } - }, + @action + toggleAdvancedMode() { + this.toggleProperty("advancedMode"); + }, - cancel() { + @action + save() { + const markup = this.markup; + + if (markup) { this._closeModal(); - }, - }, - - _setTimeIfValid(time, key) { - if (isEmpty(time)) { - this.set(key, null); - return; - } - - if (/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/.test(time)) { - this.set(key, time); + this.insertDate(markup); } }, - _setupPicker() { - return new Promise((resolve) => { - loadScript("/javascripts/pikaday.js").then(() => { - const options = { - field: this.element.querySelector(".fake-input"), - container: this.element.querySelector( - `#picker-container-${this.elementId}` - ), - bound: false, - format: "YYYY-MM-DD", - reposition: false, - firstDay: 1, - setDefaultDate: true, - keyboardInput: false, - i18n: { - previousMonth: I18n.t("dates.previous_month"), - nextMonth: I18n.t("dates.next_month"), - months: moment.months(), - weekdays: moment.weekdays(), - weekdaysShort: moment.weekdaysMin(), - }, - onSelect: (date) => { - const formattedDate = moment(date).format("YYYY-MM-DD"); - - if (this.fromSelected) { - this.set("date", formattedDate); - } - - if (this.toSelected) { - this.set("toDate", formattedDate); - } - }, - }; - - resolve(new Pikaday(options)); - }); - }); - }, - - _setPickerMinDate(date) { - schedule("afterRender", () => { - if (moment(date, this.dateFormat).isValid()) { - this._picker.setMinDate(moment(date, this.dateFormat).toDate()); - } else { - this._picker.setMinDate(null); - } - }); - }, - - _setPickerDate(date) { - schedule("afterRender", () => { - if (moment(date, this.dateFormat).isValid()) { - this._picker.setDate(moment.utc(date), true); - } else { - this._picker.setDate(null); - } - }); + @action + cancel() { + this._closeModal(); }, _closeModal() { diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs index 5b5aec0c706..53d838e9c7b 100644 --- a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs @@ -66,38 +66,16 @@
- -
- - {{#if this.fromSelected}} -
- {{d-icon "far-clock"}} - -
- {{/if}} - - {{#if this.toSelected}} - {{#if this.toDate}} -
- {{d-icon "far-clock"}} - -
- {{/if}} - {{/if}} +
{{#if this.site.mobileView}} @@ -210,7 +188,7 @@ diff --git a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss index 3f200f5ef8c..bd21f6f7616 100644 --- a/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss +++ b/plugins/discourse-local-dates/assets/stylesheets/common/discourse-local-dates.scss @@ -70,25 +70,6 @@ div[data-tippy-root] { flex-direction: row; padding: 0.5em; - .picker-panel { - padding: 5px; - border: 1px solid var(--primary-low); - } - - .date-picker { - display: flex; - flex-direction: column; - width: auto; - box-sizing: border-box; - - .pika-single { - position: relative !important; - flex: 1; - display: flex; - border: 0; - } - } - .form { flex: 1 0 0px; @@ -210,37 +191,6 @@ div[data-tippy-root] { .inputs-panel { flex: 1; } - - .picker-panel { - z-index: 1; - background: var(--secondary); - width: 200px; - box-sizing: border-box; - margin-left: 1em; - } - - .time-pickers { - display: flex; - justify-content: center; - flex: 1; - margin-top: 1em; - align-items: center; - padding: 0.25em; - border-top: 1px solid var(--primary-low-mid); - box-sizing: border-box; - - .d-icon { - color: var(--primary-medium); - margin-right: 0.5em; - } - - .time-picker { - box-shadow: none; - margin: 0; - box-sizing: border-box; - width: 100%; - } - } } .preview { @@ -318,17 +268,17 @@ html.mobile-view { flex-direction: column; } - .picker-panel { + .calendar-date-time-input { width: 100%; margin: 0 0 1em 0; .pika-single { justify-content: center; } - } - .time-picker { - padding-top: 6px; + .time-picker { + padding-top: 6px; + } } } } diff --git a/plugins/discourse-local-dates/spec/system/local_dates_spec.rb b/plugins/discourse-local-dates/spec/system/local_dates_spec.rb index ac8ad5080ad..08a892d7ea7 100644 --- a/plugins/discourse-local-dates/spec/system/local_dates_spec.rb +++ b/plugins/discourse-local-dates/spec/system/local_dates_spec.rb @@ -4,7 +4,10 @@ describe "Local dates", type: :system do fab!(:topic) { Fabricate(:topic) } fab!(:current_user) { Fabricate(:user) } let(:year) { Time.zone.now.year + 1 } + let(:month) { Time.zone.now.month } let(:bookmark_modal) { PageObjects::Modals::Bookmark.new } + let(:composer) { PageObjects::Components::Composer.new } + let(:insert_datetime_modal) { PageObjects::Modals::InsertDateTime.new } before do create_post(user: current_user, topic: topic, title: "Date range test post", raw: <<~RAW) @@ -69,6 +72,85 @@ describe "Local dates", type: :system do end end + describe "insert modal" do + let(:timezone) { "Australia/Brisbane" } + + before do + current_user.user_option.update!(timezone: timezone) + sign_in(current_user) + end + + it "allows selecting a date without a time and inserts into the post" do + topic_page.visit_topic_and_open_composer(topic) + expect(topic_page).to have_expanded_composer + composer.click_toolbar_button("local-dates") + expect(insert_datetime_modal).to be_open + insert_datetime_modal.calendar_date_time_picker.select_year(year) + insert_datetime_modal.calendar_date_time_picker.select_day(16) + insert_datetime_modal.click_primary_button + expect(composer.composer_input.value).to have_content( + "[date=#{Date.parse("#{year}-#{month}-16").strftime("%Y-%m-%d")} timezone=\"#{timezone}\"]", + ) + end + + it "allows selecting a date with a time and inserts into the post" do + topic_page.visit_topic_and_open_composer(topic) + expect(topic_page).to have_expanded_composer + composer.click_toolbar_button("local-dates") + expect(insert_datetime_modal).to be_open + insert_datetime_modal.calendar_date_time_picker.select_year(year) + insert_datetime_modal.calendar_date_time_picker.select_day(16) + insert_datetime_modal.calendar_date_time_picker.fill_time("11:45am") + insert_datetime_modal.click_primary_button + + expect(composer.composer_input.value).to have_content( + "[date=#{Date.parse("#{year}-#{month}-16").strftime("%Y-%m-%d")} time=11:45:00 timezone=\"#{timezone}\"]", + ) + end + + it "allows selecting a start date and time and an end date and time" do + topic_page.visit_topic_and_open_composer(topic) + expect(topic_page).to have_expanded_composer + composer.click_toolbar_button("local-dates") + expect(insert_datetime_modal).to be_open + insert_datetime_modal.calendar_date_time_picker.select_year(year) + insert_datetime_modal.calendar_date_time_picker.select_day(16) + insert_datetime_modal.calendar_date_time_picker.fill_time("11:45am") + insert_datetime_modal.select_to + + insert_datetime_modal.calendar_date_time_picker.select_year(year) + insert_datetime_modal.calendar_date_time_picker.select_day(23) + insert_datetime_modal.calendar_date_time_picker.fill_time("12:45pm") + + insert_datetime_modal.click_primary_button + expect(composer.composer_input.value).to have_content( + "[date-range from=#{Date.parse("#{year}-#{month}-16").strftime("%Y-%m-%d")}T11:45:00 to=#{Date.parse("#{year}-#{month}-23").strftime("%Y-%m-%d")}T12:45:00 timezone=\"#{timezone}\"]", + ) + end + + it "allows clearing the end date and time" do + topic_page.visit_topic_and_open_composer(topic) + expect(topic_page).to have_expanded_composer + composer.click_toolbar_button("local-dates") + expect(insert_datetime_modal).to be_open + + insert_datetime_modal.calendar_date_time_picker.select_year(year) + insert_datetime_modal.calendar_date_time_picker.select_day(16) + insert_datetime_modal.calendar_date_time_picker.fill_time("11:45am") + insert_datetime_modal.select_to + + insert_datetime_modal.calendar_date_time_picker.select_year(year) + insert_datetime_modal.calendar_date_time_picker.select_day(23) + insert_datetime_modal.calendar_date_time_picker.fill_time("12:45pm") + insert_datetime_modal.delete_to + + insert_datetime_modal.click_primary_button + expect(composer.composer_input.value).to have_content( + "[date=#{Date.parse("#{year}-#{month}-16").strftime("%Y-%m-%d")} time=11:45:00 timezone=\"#{timezone}\"]", + ) + end + end + describe "bookmarks" do before do current_user.user_option.update!(timezone: "Asia/Singapore") diff --git a/plugins/discourse-local-dates/spec/system/page_objects/modals/insert_date_time.rb b/plugins/discourse-local-dates/spec/system/page_objects/modals/insert_date_time.rb new file mode 100644 index 00000000000..71fbfae1fb8 --- /dev/null +++ b/plugins/discourse-local-dates/spec/system/page_objects/modals/insert_date_time.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module PageObjects + module Modals + class InsertDateTime < PageObjects::Modals::Base + MODAL_CSS_CLASS = ".discourse-local-dates-create-modal" + + def calendar_date_time_picker + @calendar_date_time_picker ||= + PageObjects::Components::CalendarDateTimePicker.new(MODAL_CSS_CLASS) + end + + def select_to + find(".date-time-control.to").click + end + + def select_from + find(".date-time-control.from").click + end + + def delete_to + find(".delete-to-date").click + end + end + end +end diff --git a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js index b0411398b89..63600c1e13a 100644 --- a/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js +++ b/plugins/discourse-local-dates/test/javascripts/acceptance/local-dates-composer-test.js @@ -130,7 +130,7 @@ acceptance("Local Dates - composer", function (needs) { await click(".delete-to-date"); assert.notOk( - query(".pika-table .is-selected"), + query(".date-time-control.to.is-selected"), "deleting selected TO date works" ); diff --git a/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/date-time-inputs.hbs b/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/date-time-inputs.hbs index 2e81a968ebb..64a848d0080 100644 --- a/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/date-time-inputs.hbs +++ b/plugins/styleguide/assets/javascripts/discourse/components/sections/atoms/date-time-inputs.hbs @@ -24,4 +24,6 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.hbs b/plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.hbs new file mode 100644 index 00000000000..a422014b50a --- /dev/null +++ b/plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.hbs @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.js b/plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.js new file mode 100644 index 00000000000..b2985cbfb4f --- /dev/null +++ b/plugins/styleguide/assets/javascripts/discourse/components/styleguide/calendar-date-time-input.js @@ -0,0 +1,24 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; + +export default class StyleguideCalendarDateTimeInput extends Component { + @service currentUser; + + @tracked dateFormat = "YYYY-MM-DD"; + @tracked timeFormat = "HH:mm:ss"; + @tracked date = null; + @tracked time = null; + @tracked minDate = null; + + @action + changeDate(date) { + this.date = date; + } + + @action + changeTime(time) { + this.time = time; + } +} diff --git a/spec/system/page_objects/components/calendar_date_time_picker.rb b/spec/system/page_objects/components/calendar_date_time_picker.rb new file mode 100644 index 00000000000..27b66d7a0b5 --- /dev/null +++ b/spec/system/page_objects/components/calendar_date_time_picker.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module PageObjects + module Components + class CalendarDateTimePicker < PageObjects::Components::Base + def initialize(context) + @context = context + end + + def component + find(@context) + end + + def select_day(day_number) + component.find("button.pika-button.pika-day[data-pika-day='#{day_number}']").click + end + + def select_year(year) + component + .find(".pika-select-year", visible: false) + .find("option[value='#{year}']") + .select_option + end + + def fill_time(time) + component.find(".time-picker").fill_in(with: time) + end + end + end +end