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 @@
+
\ 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.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