diff --git a/app/assets/javascripts/admin/components/admin-report.js b/app/assets/javascripts/admin/components/admin-report.js index 178bef98f28..97db7c9df72 100644 --- a/app/assets/javascripts/admin/components/admin-report.js +++ b/app/assets/javascripts/admin/components/admin-report.js @@ -1,7 +1,7 @@ import discourseComputed from "discourse-common/utils/decorators"; import { makeArray } from "discourse-common/lib/helpers"; -import { alias, or, and, reads, equal, notEmpty } from "@ember/object/computed"; -import EmberObject from "@ember/object"; +import { alias, or, and, equal, notEmpty } from "@ember/object/computed"; +import EmberObject, { computed, action } from "@ember/object"; import { next } from "@ember/runloop"; import Component from "@ember/component"; import ReportLoader from "discourse/lib/reports-loader"; @@ -9,6 +9,7 @@ import { exportEntity } from "discourse/lib/export-csv"; import { outputExportResult } from "discourse/lib/export-result"; import Report, { SCHEMA_VERSION } from "admin/models/report"; import ENV from "discourse-common/config/environment"; +import { isPresent } from "@ember/utils"; const TABLE_OPTIONS = { perPage: 8, @@ -69,8 +70,21 @@ export default Component.extend({ this._reports = []; }, - startDate: reads("filters.startDate"), - endDate: reads("filters.endDate"), + startDate: computed("filters.startDate", function() { + if (this.filters && isPresent(this.filters.startDate)) { + return moment(this.filters.startDate, "YYYY-MM-DD"); + } else { + return moment(); + } + }), + + endDate: computed("filters.endDate", function() { + if (this.filters && isPresent(this.filters.endDate)) { + return moment(this.filters.endDate, "YYYY-MM-DD"); + } else { + return moment(); + } + }), didReceiveAttrs() { this._super(...arguments); @@ -126,39 +140,18 @@ export default Component.extend({ return `admin-report-${currentMode.replace(/_/g, "-")}`; }, - @discourseComputed("startDate") - normalizedStartDate(startDate) { - return startDate && typeof startDate.isValid === "function" - ? moment - .utc(startDate.toISOString()) - .locale("en") - .format("YYYYMMDD") - : moment(startDate) - .locale("en") - .format("YYYYMMDD"); - }, - - @discourseComputed("endDate") - normalizedEndDate(endDate) { - return endDate && typeof endDate.isValid === "function" - ? moment - .utc(endDate.toISOString()) - .locale("en") - .format("YYYYMMDD") - : moment(endDate) - .locale("en") - .format("YYYYMMDD"); - }, - @discourseComputed( "dataSourceName", - "normalizedStartDate", - "normalizedEndDate", + "startDate", + "endDate", "filters.customFilters" ) reportKey(dataSourceName, startDate, endDate, customFilters) { if (!dataSourceName || !startDate || !endDate) return null; + startDate = startDate.toISOString(true).split("T")[0]; + endDate = endDate.toISOString(true).split("T")[0]; + let reportKey = "reports:"; reportKey += [ dataSourceName, @@ -179,74 +172,61 @@ export default Component.extend({ return reportKey; }, - actions: { - onChangeEndDate(date) { - const startDate = moment(this.startDate); - const newEndDate = moment(date).endOf("day"); + @action + onChangeDateRange(range) { + this.send("refreshReport", { + startDate: range.from, + endDate: range.to + }); + }, - if (newEndDate.isSameOrAfter(startDate)) { - this.set("endDate", newEndDate.format("YYYY-MM-DD")); - } else { - this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD")); - } + @action + applyFilter(id, value) { + let customFilters = this.get("filters.customFilters") || {}; - this.send("refreshReport"); - }, - - onChangeStartDate(date) { - const endDate = moment(this.endDate); - const newStartDate = moment(date).startOf("day"); - - if (newStartDate.isSameOrBefore(endDate)) { - this.set("startDate", newStartDate.format("YYYY-MM-DD")); - } else { - this.set("startDate", endDate.startOf("day").format("YYYY-MM-DD")); - } - - this.send("refreshReport"); - }, - - applyFilter(id, value) { - let customFilters = this.get("filters.customFilters") || {}; - - if (typeof value === "undefined") { - delete customFilters[id]; - } else { - customFilters[id] = value; - } - - this.attrs.onRefresh({ - type: this.get("model.type"), - startDate: this.startDate, - endDate: this.endDate, - filters: customFilters - }); - }, - - refreshReport() { - this.attrs.onRefresh({ - type: this.get("model.type"), - startDate: this.startDate, - endDate: this.endDate, - filters: this.get("filters.customFilters") - }); - }, - - exportCsv() { - const customFilters = this.get("filters.customFilters") || {}; - - exportEntity("report", { - name: this.get("model.type"), - start_date: this.startDate, - end_date: this.endDate, - category_id: customFilters.category, - group_id: customFilters.group - }).then(outputExportResult); - }, - - changeMode(mode) { - this.set("currentMode", mode); + if (typeof value === "undefined") { + delete customFilters[id]; + } else { + customFilters[id] = value; } + + this.send("refreshReport", { + filters: customFilters + }); + }, + + @action + refreshReport(options = {}) { + this.attrs.onRefresh({ + type: this.get("model.type"), + startDate: + typeof options.startDate === "undefined" + ? this.startDate + : options.startDate, + endDate: + typeof options.endDate === "undefined" ? this.endDate : options.endDate, + filters: + typeof options.filters === "undefined" + ? this.get("filters.customFilters") + : options.filters + }); + }, + + @action + exportCsv() { + const customFilters = this.get("filters.customFilters") || {}; + exportEntity("report", { + name: this.get("model.type"), + start_date: this.startDate.toISOString(true).split("T")[0], + end_date: this.endDate.toISOString(true).split("T")[0], + category_id: customFilters.category, + group_id: customFilters.group + }).then(outputExportResult); + }, + + @action + changeMode(mode) { + this.set("currentMode", mode); }, _computeReport() { @@ -276,10 +256,8 @@ export default Component.extend({ if (!this.startDate || !this.endDate) { report = sort(filteredReports)[0]; } else { - const reportKey = this.reportKey; - report = sort( - filteredReports.filter(r => r.report_key.includes(reportKey)) + filteredReports.filter(r => r.report_key.includes(this.reportKey)) )[0]; if (!report) return; @@ -339,15 +317,15 @@ export default Component.extend({ let payload = { data: { cache: true, facets } }; if (this.startDate) { - payload.data.start_date = moment - .utc(this.startDate, "YYYY-MM-DD") - .toISOString(); + payload.data.start_date = moment(this.startDate) + .toISOString(true) + .split("T")[0]; } if (this.endDate) { - payload.data.end_date = moment - .utc(this.endDate, "YYYY-MM-DD") - .toISOString(); + payload.data.end_date = moment(this.endDate) + .toISOString(true) + .split("T")[0]; } if (this.get("reportOptions.table.limit")) { diff --git a/app/assets/javascripts/admin/routes/admin-reports-show.js b/app/assets/javascripts/admin/routes/admin-reports-show.js index 1fa8bc739d1..3a8a5430a9b 100644 --- a/app/assets/javascripts/admin/routes/admin-reports-show.js +++ b/app/assets/javascripts/admin/routes/admin-reports-show.js @@ -13,8 +13,7 @@ export default DiscourseRoute.extend({ params.startDate = params.start_date || - moment - .utc() + moment() .subtract(1, "day") .subtract(1, "month") .startOf("day") @@ -23,8 +22,7 @@ export default DiscourseRoute.extend({ params.endDate = params.end_date || - moment - .utc() + moment() .endOf("day") .format("YYYY-MM-DD"); delete params.end_date; @@ -56,9 +54,13 @@ export default DiscourseRoute.extend({ onParamsChange(params) { const queryParams = { type: params.type, - start_date: params.startDate, + start_date: params.startDate + ? params.startDate.toISOString(true).split("T")[0] + : null, filters: params.filters, end_date: params.endDate + ? params.endDate.toISOString(true).split("T")[0] + : null }; this.transitionTo("adminReports.show", { queryParams }); diff --git a/app/assets/javascripts/admin/templates/components/admin-report.hbs b/app/assets/javascripts/admin/templates/components/admin-report.hbs index ab0c1a63980..ee375ac6ff6 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report.hbs @@ -132,26 +132,16 @@ {{#if showDatesOptions}}
- {{i18n "admin.dashboard.reports.start_date"}} + {{i18n "admin.dashboard.reports.dates"}}
- {{date-input - date=startDate - onChange=(action "onChangeStartDate") - }} -
-
- -
- - {{i18n "admin.dashboard.reports.end_date"}} - - -
- {{date-input - date=endDate - onChange=(action "onChangeEndDate") + {{date-time-input-range + from=startDate + to=endDate + onChange=(action "onChangeDateRange") + showFromTime=false + showToTime=false }}
diff --git a/app/assets/javascripts/discourse/components/date-input.js b/app/assets/javascripts/discourse/components/date-input.js index b44640bcd42..d4ee63ee9b0 100644 --- a/app/assets/javascripts/discourse/components/date-input.js +++ b/app/assets/javascripts/discourse/components/date-input.js @@ -1,4 +1,5 @@ import { schedule } from "@ember/runloop"; +import { action } from "@ember/object"; import Component from "@ember/component"; /* global Pikaday:true */ import loadScript from "discourse/lib/load-script"; @@ -25,8 +26,8 @@ export default Component.extend({ this._loadPikadayPicker(container); } - if (this.date && this._picker) { - this._picker.setDate(this.date, true); + if (this._picker && this.date) { + this._picker.setDate(moment(this.date).toDate(), true); } }); }, @@ -34,9 +35,12 @@ export default Component.extend({ didUpdateAttrs() { this._super(...arguments); - if (this._picker && typeof this.date === "string") { - const [year, month, day] = this.date.split("-").map(x => parseInt(x, 10)); - this._picker.setDate(new Date(year, month - 1, day), true); + if (this._picker && this.date) { + this._picker.setDate(moment(this.date).toDate(), true); + } + + if (this._picker && this.relativeDate) { + this._picker.setMinDate(moment(this.relativeDate).toDate(), true); } if (this._picker && !this.date) { @@ -46,13 +50,12 @@ export default Component.extend({ _loadPikadayPicker(container) { loadScript("/javascripts/pikaday.js").then(() => { - const defaultOptions = { + let defaultOptions = { field: this.element.querySelector(".date-picker"), container: container || this.element.querySelector(".picker-container"), bound: container === null, format: "LL", firstDay: 1, - trigger: this.element, i18n: { previousMonth: I18n.t("dates.previous_month"), nextMonth: I18n.t("dates.next_month"), @@ -63,8 +66,16 @@ export default Component.extend({ onSelect: date => this._handleSelection(date) }; - this._picker = new Pikaday(Object.assign(defaultOptions, this._opts())); - this._picker.setDate(this.date, true); + if (this.relativeDate) { + defaultOptions = Object.assign({}, defaultOptions, { + minDate: moment(this.relativeDate).toDate() + }); + } + + this._picker = new Pikaday( + Object.assign({}, defaultOptions, this._opts()) + ); + this._picker.setDate(moment(this.date).toDate(), true); }); }, @@ -79,18 +90,23 @@ export default Component.extend({ /* do nothing for native */ }; picker.setDate = date => { - picker.value = date; + picker.value = moment(date).format("YYYY-MM-DD"); + }; + picker.setMinDate = date => { + picker.min = date; }; this._picker = picker; + + if (this.date) { + picker.setDate(this.date); + } }, _handleSelection(value) { if (!this.element || this.isDestroying || this.isDestroyed) return; - this._picker && this._picker.hide(); - if (this.onChange) { - this.onChange(value); + this.onChange(value ? moment(value) : null); } }, @@ -98,8 +114,8 @@ export default Component.extend({ _destroy() { if (this._picker) { this._picker.destroy(); + this._picker = null; } - this._picker = null; }, @discourseComputed() @@ -111,9 +127,8 @@ export default Component.extend({ return null; }, - actions: { - onInput(event) { - this._picker && this._picker.setDate(event.target.value, true); - } + @action + onChangeDate(event) { + this._handleSelection(event.target.value); } }); diff --git a/app/assets/javascripts/discourse/components/date-time-input-range.js b/app/assets/javascripts/discourse/components/date-time-input-range.js index 52438ffd949..cb30e162358 100644 --- a/app/assets/javascripts/discourse/components/date-time-input-range.js +++ b/app/assets/javascripts/discourse/components/date-time-input-range.js @@ -1,53 +1,46 @@ -import { equal } from "@ember/object/computed"; import Component from "@ember/component"; +import { action } from "@ember/object"; + export default Component.extend({ classNames: ["d-date-time-input-range"], - from: null, to: null, onChangeTo: null, onChangeFrom: null, - currentPanel: "from", - showFromTime: true, + toTimeFirst: false, showToTime: true, - error: null, + showFromTime: true, + clearable: false, - fromPanelActive: equal("currentPanel", "from"), - toPanelActive: equal("currentPanel", "to"), + @action + onChangeRanges(options, value) { + if (this.onChange) { + const state = { + from: this.from, + to: this.to + }; - _valid(state) { - if (state.to && state.from && state.to < state.from) { - return I18n.t("date_time_picker.errors.to_before_from"); - } + const diff = {}; - return true; - }, - - actions: { - _onChange(options, value) { - if (this.onChange) { - const state = { - from: this.from, - to: this.to - }; - - const diff = {}; - diff[options.prop] = value; - - const newState = Object.assign(state, diff); - - const validation = this._valid(newState); - if (validation === true) { - this.set("error", null); - this.onChange(newState); + if (options.prop === "from") { + if (value && value.isAfter(this.to)) { + diff[options.prop] = value; + diff["to"] = value.clone().add(1, "hour"); } else { - this.set("error", validation); + diff[options.prop] = value; } } - }, - onChangePanel(panel) { - this.set("currentPanel", panel); + if (options.prop === "to") { + if (value && value.isBefore(this.from)) { + diff[options.prop] = this.from.clone().add(1, "hour"); + } else { + diff[options.prop] = value; + } + } + + const newState = Object.assign({}, state, diff); + this.onChange(newState); } } }); diff --git a/app/assets/javascripts/discourse/components/date-time-input.js b/app/assets/javascripts/discourse/components/date-time-input.js index eff8b99edc4..01fbddaf1c3 100644 --- a/app/assets/javascripts/discourse/components/date-time-input.js +++ b/app/assets/javascripts/discourse/components/date-time-input.js @@ -1,44 +1,63 @@ import Component from "@ember/component"; -import { computed } from "@ember/object"; +import { computed, action } from "@ember/object"; export default Component.extend({ classNames: ["d-date-time-input"], date: null, + relativeDate: null, showTime: true, clearable: false, - _hours: computed("date", function() { - return this.date && this.showTime ? new Date(this.date).getHours() : null; + hours: computed("date", "showTime", function() { + return this.date && this.get("showTime") ? this.date.hours() : null; }), - _minutes: computed("date", function() { - return this.date && this.showTime ? new Date(this.date).getMinutes() : null; + minutes: computed("date", "showTime", function() { + return this.date && this.get("showTime") ? this.date.minutes() : null; }), - actions: { - onClear() { - this.onChange(null); - }, + @action + onClear() { + this.onChange(null); + }, - onChangeTime(time) { - if (this.onChange) { - const date = new Date(this.date); - const year = date.getFullYear(); - const month = date.getMonth(); - const day = date.getDate(); - this.onChange(new Date(year, month, day, time.hours, time.minutes)); - } - }, + @action + onChangeTime(time) { + if (this.onChange) { + const date = this.date + ? this.date + : this.relativeDate + ? this.relativeDate + : moment(); - onChangeDate(date) { - if (this.onChange) { - const year = date.getFullYear(); - const month = date.getMonth(); - const day = date.getDate(); - this.onChange( - new Date(year, month, day, this._hours || 0, this._minutes || 0) - ); - } + this.onChange( + moment({ + year: date.year(), + month: date.month(), + day: date.date(), + hours: time.hours, + minutes: time.minutes + }) + ); } + }, + + @action + onChangeDate(date) { + if (!date) { + this.onClear(); + return; + } + + this.onChange && + this.onChange( + moment({ + year: date.year(), + month: date.month(), + day: date.date(), + hours: this.hours || 0, + minutes: this.minutes || 0 + }) + ); } }); diff --git a/app/assets/javascripts/discourse/components/time-input.js b/app/assets/javascripts/discourse/components/time-input.js index e636380a770..2e28f6974e7 100644 --- a/app/assets/javascripts/discourse/components/time-input.js +++ b/app/assets/javascripts/discourse/components/time-input.js @@ -1,76 +1,171 @@ -import { oneWay, or } from "@ember/object/computed"; -import { schedule } from "@ember/runloop"; +import { isPresent } from "@ember/utils"; +import { computed, action } from "@ember/object"; import Component from "@ember/component"; -import { isNumeric } from "discourse/lib/utilities"; + +function convertMinutes(num) { + return { hours: Math.floor(num / 60), minutes: num % 60 }; +} + +function convertMinutesToString(n) { + const hoursAndMinutes = convertMinutes(n); + return `${hoursAndMinutes.hours + .toString() + .padStart(2, "0")}:${hoursAndMinutes.minutes.toString().padStart(2, "0")}`; +} + +function convertMinutesToDurationString(n) { + const hoursAndMinutes = convertMinutes(n); + + let output = ""; + + if (hoursAndMinutes.hours) { + output = `${hoursAndMinutes.hours}h`; + + if (hoursAndMinutes.minutes > 0) { + output = `${output} ${hoursAndMinutes.minutes} min`; + } + } else { + output = `${hoursAndMinutes.minutes} min`; + } + + return output; +} export default Component.extend({ classNames: ["d-time-input"], + hours: null, + minutes: null, - _hours: oneWay("hours"), - _minutes: oneWay("minutes"), - isSafari: oneWay("capabilities.isSafari"), - isMobile: oneWay("site.mobileView"), - nativePicker: or("isSafari", "isMobile"), - actions: { - onInput(options, event) { - event.preventDefault(); + relativeDate: null, - if (this.onChange) { - let value = event.target.value; + didReceiveAttrs() { + this._super(...arguments); - if (!isNumeric(value)) { - value = 0; - } else { - value = parseInt(value, 10); - } + if (isPresent(this.date)) { + this.setProperties({ + hours: this.date.hours(), + minutes: this.date.minutes() + }); + } - if (options.prop === "hours") { - value = Math.max(0, Math.min(value, 23)) - .toString() - .padStart(2, "0"); - this._processHoursChange(value); - } else { - value = Math.max(0, Math.min(value, 59)) - .toString() - .padStart(2, "0"); - this._processMinutesChange(value); - } - - schedule("afterRender", () => (event.target.value = value)); - } - }, - - onFocusIn(value, event) { - if (value && event.target) { - event.target.select(); - } - }, - - onChangeTime(event) { - const time = event.target.value; - - if (time && this.onChange) { - this.onChange({ - hours: time.split(":")[0], - minutes: time.split(":")[1] - }); - } + if ( + !isPresent(this.date) && + !isPresent(this.attrs.hours) && + !isPresent(this.attrs.minutes) + ) { + this.setProperties({ + hours: null, + minutes: null + }); } }, - _processHoursChange(hours) { - this.onChange({ - hours, - minutes: this._minutes || "00" + minimumTime: computed("relativeDate", "date", function() { + if (this.relativeDate) { + if (this.date) { + if (this.date.diff(this.relativeDate, "minutes") > 1440) { + return 0; + } else { + return this.relativeDate.hours() * 60 + this.relativeDate.minutes(); + } + } else { + return this.relativeDate.hours() * 60 + this.relativeDate.minutes(); + } + } + }), + + timeOptions: computed("minimumTime", "hours", "minutes", function() { + let options = []; + + const start = this.minimumTime + ? this.minimumTime > this.time + ? this.time + : this.minimumTime + : 0; + + // theres 1440 minutes in a day + // and 1440 / 15 = 96 + let i = 0; + while (i < 96) { + // while diff with minimumTime is less than one hour + // use 15 minutes steps and then 30 minutes + const minutes = this.minimumTime ? (i <= 4 ? 15 : 30) : 15; + const option = start + i * minutes; + + // when start is higher than 0 we will reach 1440 minutes + // before the 96 iterations + if (option > 1440) { + break; + } + + options.push(option); + + i++; + } + + if (this.time && !options.includes(this.time)) { + options = [this.time].concat(options); + } + + options = options.sort((a, b) => a - b); + + return options.map(option => { + let name = convertMinutesToString(option); + let label; + + if (this.minimumTime) { + const diff = option - this.minimumTime; + label = `${name} (${convertMinutesToDurationString( + diff + )})`.htmlSafe(); + } + + return { + id: option, + name, + label, + title: name + }; }); + }), + + time: computed("minimumTime", "hours", "minutes", function() { + if (isPresent(this.hours) && isPresent(this.minutes)) { + return parseInt(this.hours, 10) * 60 + parseInt(this.minutes, 10); + } + }), + + @action + onFocusIn(value, event) { + if (value && event.target) { + event.target.select(); + } }, - _processMinutesChange(minutes) { - this.onChange({ - hours: this._hours || "00", - minutes - }); + @action + onChangeTime(time) { + if (isPresent(time) && this.onChange) { + if (typeof time === "string" && time.length) { + let [hours, minutes] = time.split(":"); + if (hours && minutes) { + if (hours < 0) hours = 0; + if (hours > 23) hours = 23; + if (minutes < 0) minutes = 0; + if (minutes > 59) minutes = 59; + + this.onChange({ + hours: parseInt(hours, 10), + minutes: parseInt(minutes, 10) + }); + } + } else { + this.onChange({ + hours: convertMinutes(time).hours, + minutes: convertMinutes(time).minutes + }); + } + } } }); diff --git a/app/assets/javascripts/discourse/controllers/review-index.js b/app/assets/javascripts/discourse/controllers/review-index.js index aeb61d7d61e..4d319ee8bf3 100644 --- a/app/assets/javascripts/discourse/controllers/review-index.js +++ b/app/assets/javascripts/discourse/controllers/review-index.js @@ -1,5 +1,6 @@ import discourseComputed from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; +import { isPresent } from "@ember/utils"; export default Controller.extend({ queryParams: [ @@ -86,12 +87,7 @@ export default Controller.extend({ }, setRange(range) { - if (range.from) { - this.set("from", new Date(range.from).toISOString().split("T")[0]); - } - if (range.to) { - this.set("to", new Date(range.to).toISOString().split("T")[0]); - } + this.setProperties(range); }, actions: { @@ -118,8 +114,12 @@ export default Controller.extend({ status: this.filterStatus, category_id: this.filterCategoryId, username: this.filterUsername, - from_date: this.filterFromDate, - to_date: this.filterToDate, + from_date: isPresent(this.filterFromDate) + ? this.filterFromDate.toISOString(true).split("T")[0] + : null, + to_date: isPresent(this.filterToDate) + ? this.filterToDate.toISOString(true).split("T")[0] + : null, sort_order: this.filterSortOrder, additional_filters: JSON.stringify(this.additionalFilters) }); diff --git a/app/assets/javascripts/discourse/routes/review-index.js b/app/assets/javascripts/discourse/routes/review-index.js index 3f2a8abc4a3..4edd43a3343 100644 --- a/app/assets/javascripts/discourse/routes/review-index.js +++ b/app/assets/javascripts/discourse/routes/review-index.js @@ -1,4 +1,5 @@ import DiscourseRoute from "discourse/routes/discourse"; +import { isPresent } from "@ember/utils"; export default DiscourseRoute.extend({ model(params) { @@ -23,8 +24,8 @@ export default DiscourseRoute.extend({ filterPriority: meta.priority, reviewableTypes: meta.reviewable_types, filterUsername: meta.username, - filterFromDate: meta.from_date, - filterToDate: meta.to_date, + filterFromDate: isPresent(meta.from_date) ? moment(meta.from_date) : null, + filterToDate: isPresent(meta.to_date) ? moment(meta.to_date) : null, filterSortOrder: meta.sort_order, additionalFilters: meta.additional_filters || {} }); diff --git a/app/assets/javascripts/discourse/templates/components/date-input.hbs b/app/assets/javascripts/discourse/templates/components/date-input.hbs index ad48e3e25de..b86c4604419 100644 --- a/app/assets/javascripts/discourse/templates/components/date-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/date-input.hbs @@ -2,9 +2,8 @@ type=inputType class="date-picker" placeholder=placeholder - value=value - input=(action "onInput") - readonly=true + value=(readonly value) + input=(action "onChangeDate") }}
diff --git a/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs b/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs index faa96a1eca4..a8e1786acec 100644 --- a/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs +++ b/app/assets/javascripts/discourse/templates/components/date-time-input-range.hbs @@ -1,35 +1,16 @@ - +{{date-time-input + date=from + onChange=(action "onChangeRanges" (hash prop="from")) + showTime=showFromTime + class="from" +}} -{{#if error}} -
{{error}}
-{{/if}} - -
- {{date-time-input - date=from - onChange=(action "_onChange" (hash prop="from")) - showTime=showFromTime - }} -
- -
- {{date-time-input - date=to - onChange=(action "_onChange" (hash prop="to")) - showTime=showToTime - clearable=true - }} -
+{{date-time-input + date=to + relativeDate=from + onChange=(action "onChangeRanges" (hash prop="to")) + timeFirst=toTimeFirst + showTime=showToTime + clearable=clearable + class="to" +}} diff --git a/app/assets/javascripts/discourse/templates/components/date-time-input.hbs b/app/assets/javascripts/discourse/templates/components/date-time-input.hbs index 0b243e74a48..0ec475f5714 100644 --- a/app/assets/javascripts/discourse/templates/components/date-time-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/date-time-input.hbs @@ -1,13 +1,31 @@ -{{date-input date=date onChange=(action "onChangeDate")}} +{{#unless timeFirst}} + {{date-input + date=date + relativeDate=relativeDate + onChange=(action "onChangeDate") + }} +{{/unless}} {{#if showTime}} {{time-input - hours=_hours - minutes=_minutes + date=date + relativeDate=relativeDate onChange=(action "onChangeTime") }} - - {{#if clearable}} - {{d-button icon="times" action=(action "onClear")}} - {{/if}} +{{/if}} + +{{#if timeFirst}} + {{date-input + date=date + relativeDate=relativeDate + onChange=(action "onChangeDate") + }} +{{/if}} + +{{#if clearable}} + {{d-button + class="clear-date-time" + icon="times" + action=(action "onClear") + }} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/time-input.hbs b/app/assets/javascripts/discourse/templates/components/time-input.hbs index 51ada91bf64..8dd13ff06b3 100644 --- a/app/assets/javascripts/discourse/templates/components/time-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/time-input.hbs @@ -1,40 +1,12 @@ -
- {{#if nativePicker}} - {{input - class="field time" - type="time" - value=(concat _hours ":" _minutes) - change=(action "onChangeTime") - }} - {{else}} - {{input - class="field hours" - type="number" - title="Hours" - minlength=2 - maxlength=2 - max="23" - min="0" - placeholder="00" - value=_hours - input=(action "onInput" (hash prop="hours")) - focus-in=(action "onFocusIn") - }} - -
:
- - {{input - class="field minutes" - title="Minutes" - type="number" - minlength=2 - maxlength=2 - max="59" - min="0" - placeholder="00" - value=_minutes - input=(action "onInput" (hash prop="minutes")) - focus-in=(action "onFocusIn") - }} - {{/if}} -
+{{combo-box + value=time + content=timeOptions + onChange=(action "onChangeTime") + options=(hash + translatedNone="--:--" + allowAny=true + filterable=false + autoInsertNoneItem=false + translatedFilterPlaceholder="--:--" + ) +}} diff --git a/app/assets/javascripts/discourse/templates/review-index.hbs b/app/assets/javascripts/discourse/templates/review-index.hbs index 7b603f9fa08..c5a0fad8891 100644 --- a/app/assets/javascripts/discourse/templates/review-index.hbs +++ b/app/assets/javascripts/discourse/templates/review-index.hbs @@ -77,7 +77,13 @@ {{/if}}
- {{date-time-input-range showFromTime=false showToTime=false from=filterFromDate to=filterToDate onChange=setRange}} + {{date-time-input-range + from=filterFromDate + to=filterToDate + onChange=setRange + showFromTime=false + showToTime=false + }}
diff --git a/app/assets/javascripts/select-kit/components/select-kit.js b/app/assets/javascripts/select-kit/components/select-kit.js index 360dd65738d..4c0b095c1b7 100644 --- a/app/assets/javascripts/select-kit/components/select-kit.js +++ b/app/assets/javascripts/select-kit/components/select-kit.js @@ -482,10 +482,7 @@ export default Component.extend( this.selectKit.options.allowAny && !this.selectKit.isExpanded ) { - return this.defaultItem( - null, - I18n.t("select_kit.filter_placeholder_with_any") - ); + return null; } let item; @@ -754,8 +751,6 @@ export default Component.extend( }, _onCloseWrapper(event) { - this._focusFilter(this.multiSelect); - this.set("selectKit.highlighted", null); let boundaryAction = this._boundaryActionHandler("onClose"); diff --git a/app/assets/stylesheets/common/admin/admin_report.scss b/app/assets/stylesheets/common/admin/admin_report.scss index 3bf74813193..82bef5d1be5 100644 --- a/app/assets/stylesheets/common/admin/admin_report.scss +++ b/app/assets/stylesheets/common/admin/admin_report.scss @@ -161,13 +161,9 @@ width: 100%; } - .date-picker-wrapper { + .d-date-time-input-range { + flex-direction: column; width: 100%; - .date-picker { - box-sizing: border-box; - width: 100%; - margin: 0; - } } } } diff --git a/app/assets/stylesheets/common/base/reviewables.scss b/app/assets/stylesheets/common/base/reviewables.scss index 2b85d211052..8c2bc4f91d2 100644 --- a/app/assets/stylesheets/common/base/reviewables.scss +++ b/app/assets/stylesheets/common/base/reviewables.scss @@ -126,6 +126,7 @@ width: inherit; border: none; padding: 0; + flex-direction: column; .d-date-input { flex: 1 1 auto; diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index e62f24f2921..895a4727b6f 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -116,11 +116,9 @@ flex-direction: column; #search-min-post-count, - .date-picker, .combo-box, .ac-wrap, .control-group, - .date-picker-wrapper, .search-advanced-category-chooser { box-sizing: border-box; width: 100%; @@ -131,15 +129,9 @@ } } - .date-picker-wrapper { + .d-date-input { margin-top: 0.5em; - } - - .date-picker { - box-sizing: border-box; - text-align: left; - padding: 4px; - margin-bottom: 0; + width: 100%; } .search-advanced-title { diff --git a/app/assets/stylesheets/common/components/date-input.scss b/app/assets/stylesheets/common/components/date-input.scss index b24ad8ea4eb..c0f453d4230 100644 --- a/app/assets/stylesheets/common/components/date-input.scss +++ b/app/assets/stylesheets/common/components/date-input.scss @@ -1,19 +1,58 @@ .d-date-input { - display: flex; - flex: 1; + display: inline-flex; cursor: pointer; flex-direction: column; + min-width: 140px; .date-picker { cursor: pointer; margin: 0; text-align: left; width: 100%; - outline: none; box-shadow: none !important; + + &::-webkit-input-placeholder { + font-size: $font-0; + color: $primary-medium; + } + + &::-ms-input-placeholder { + font-size: $font-0; + color: $primary-medium; + } + + &::placeholder { + font-size: $font-0; + color: $primary-medium; + } + + &:focus { + outline: 1px solid $tertiary; + outline-offset: 0; + } } .pika-single { margin-left: -1px; + margin-top: 1px; + + .pika-row td { + .pika-button.pika-day { + box-shadow: none; + border-radius: 0; + } + } } } + +.d-date-input + .d-time-input { + margin-left: 1px; +} + +.d-time-input + .d-date-input { + margin-left: 1px; +} + +.d-date-input + .clear-date-time { + margin-left: 1px; +} diff --git a/app/assets/stylesheets/common/components/date-time-input-range.scss b/app/assets/stylesheets/common/components/date-time-input-range.scss index e46dd6f0b67..47704fcd647 100644 --- a/app/assets/stylesheets/common/components/date-time-input-range.scss +++ b/app/assets/stylesheets/common/components/date-time-input-range.scss @@ -1,41 +1,5 @@ .d-date-time-input-range { - padding: 0.5em; border: 1px solid $primary-low; - width: 300px; - display: flex; - flex-direction: column; - - .panels { - display: inline-flex; - list-style: none; - margin: 0 0 0.5em 0; - flex: 1; - - &.from { - .from-panel { - background: $danger; - color: $secondary; - } - } - - &.to { - .to-panel { - background: $danger; - color: $secondary; - } - } - - .btn { - margin-right: 0.5em; - } - } - - .panel { - display: none; - flex: 1; - - &.visible { - display: flex; - } - } + display: inline-flex; + box-sizing: border-box; } diff --git a/app/assets/stylesheets/common/components/date-time-input.scss b/app/assets/stylesheets/common/components/date-time-input.scss index 1ebe50f9f95..6d113b31307 100644 --- a/app/assets/stylesheets/common/components/date-time-input.scss +++ b/app/assets/stylesheets/common/components/date-time-input.scss @@ -1,10 +1,8 @@ .d-date-time-input { display: flex; - align-items: center; border: 1px solid $primary-low; box-sizing: border-box; position: relative; - flex: 1; .date-picker, .fields { @@ -13,5 +11,22 @@ .d-date-time-input { margin-left: auto; + flex: 1 1 auto; + } + + .d-time-input { + .select-kit.combo-box { + .select-kit-header { + border: none; + } + } + } + + .d-date-input + .d-time-input { + margin-left: 1px; + } + + .d-time-input + .d-date-input { + margin-right: 1px; } } diff --git a/app/assets/stylesheets/common/components/time-input.scss b/app/assets/stylesheets/common/components/time-input.scss index d11e2b7c7d9..73d5ef692cd 100644 --- a/app/assets/stylesheets/common/components/time-input.scss +++ b/app/assets/stylesheets/common/components/time-input.scss @@ -1,39 +1,43 @@ +.modal-inner-container .d-time-input, .d-time-input { - box-sizing: border-box; + display: inline-flex; - .fields { - display: flex; - align-items: center; - border: 1px solid $primary-low; + .combo-box { + width: 65px; + min-width: auto; - .field { - text-align: center; + &:not(.has-selection) { + .select-kit-selected-name .name { + color: $primary-medium; + } + } + + .select-kit-header { + .d-icon { + padding-left: 0; + } + + &:focus { + outline: 1px solid $tertiary; + } + } + + .select-kit-collection { + min-width: auto; + } + + .select-kit-filter { width: auto; - margin: 0; - border: none; - outline: none; - box-shadow: none; - width: 32px; - - &.time { - width: 100%; - text-align: left; + overflow: hidden; + .filter-input { + min-width: 0; } + } - &.hours, - &.minutes { - text-align: center; - width: 45px; - } - - &.hours { - padding-right: 0; - } - - &.minutes { - padding-left: 10px; - width: 55px; - } + .selected-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 94a3b84af2b..af4c1549b1d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -304,7 +304,6 @@ en: unbookmark: "Click to remove all bookmarks in this topic" unbookmark_with_reminder: "Click to remove all bookmarks and reminders in this topic. You have a reminder set %{reminder_at} for this topic." - bookmarks: created: "you've bookmarked this post" not_bookmarked: "bookmark this post" @@ -3393,8 +3392,7 @@ en: view_table: "table" view_graph: "graph" refresh_report: "Refresh Report" - start_date: "Start Date (UTC)" - end_date: "End Date (UTC)" + dates: "Dates (UTC)" groups: "All groups" disabled: "This report is disabled" totals_for_sample: "Totals for sample" diff --git a/test/javascripts/acceptance/dashboard-test.js b/test/javascripts/acceptance/dashboard-test.js index affe6c0a9f5..09c8d0a0b71 100644 --- a/test/javascripts/acceptance/dashboard-test.js +++ b/test/javascripts/acceptance/dashboard-test.js @@ -102,7 +102,7 @@ QUnit.test("reports tab", async assert => { QUnit.test("report filters", async assert => { await visit( - '/admin/reports/signups?end_date=2018-07-16&filters=%7B"group"%3A88%7D&start_date=2018-06-16' + '/admin/reports/signups_with_groups?end_date=2018-07-16&filters=%7B"group"%3A88%7D&start_date=2018-06-16' ); const groupFilter = selectKit(".group-filter .combo-box"); diff --git a/test/javascripts/components/date-input-test.js b/test/javascripts/components/date-input-test.js index 64686aba06d..29f01b4f50e 100644 --- a/test/javascripts/components/date-input-test.js +++ b/test/javascripts/components/date-input-test.js @@ -18,7 +18,7 @@ async function pika(year, month, day) { function noop() {} -const DEFAULT_DATE = new Date(2019, 0, 29); +const DEFAULT_DATE = moment("2019-01-29"); componentTest("default", { template: `{{date-input date=date}}`, @@ -44,7 +44,7 @@ componentTest("prevents mutations", { await click(dateInput()); await pika(2019, 0, 2); - assert.ok(this.date.getTime() === DEFAULT_DATE.getTime()); + assert.ok(this.date.isSame(DEFAULT_DATE)); } }); @@ -60,6 +60,6 @@ componentTest("allows mutations through actions", { await click(dateInput()); await pika(2019, 0, 2); - assert.ok(this.date.getTime() === new Date(2019, 0, 2).getTime()); + assert.ok(this.date.isSame(moment("2019-01-02"))); } }); diff --git a/test/javascripts/components/date-time-input-range-test.js b/test/javascripts/components/date-time-input-range-test.js index 69ff0a9d458..9ffa7e6ef66 100644 --- a/test/javascripts/components/date-time-input-range-test.js +++ b/test/javascripts/components/date-time-input-range-test.js @@ -3,40 +3,22 @@ import componentTest from "helpers/component-test"; moduleForComponent("date-time-input-range", { integration: true }); function fromDateInput() { - return find(".from .date-picker"); + return find(".from.d-date-time-input .date-picker")[0]; } -function fromHoursInput() { - return find(".from .field.hours"); -} - -function fromMinutesInput() { - return find(".from .field.minutes"); +function fromTimeInput() { + return find(".from.d-date-time-input .d-time-input .combo-box-header")[0]; } function toDateInput() { - return find(".to .date-picker"); + return find(".to.d-date-time-input .date-picker")[0]; } -function toHoursInput() { - return find(".to .field.hours"); +function toTimeInput() { + return find(".to.d-date-time-input .d-time-input .combo-box-header")[0]; } -function toMinutesInput() { - return find(".to .field.minutes"); -} - -function setDates(dates) { - this.setProperties(dates); -} - -async function pika(year, month, day) { - await click( - `.pika-button.pika-day[data-pika-year="${year}"][data-pika-month="${month}"][data-pika-day="${day}"]` - ); -} - -const DEFAULT_DATE_TIME = new Date(2019, 0, 29, 14, 45); +const DEFAULT_DATE_TIME = moment("2019-01-29 14:45"); componentTest("default", { template: `{{date-time-input-range from=from to=to}}`, @@ -46,60 +28,9 @@ componentTest("default", { }, test(assert) { - assert.equal(fromDateInput().val(), "January 29, 2019"); - assert.equal(fromHoursInput().val(), "14"); - assert.equal(fromMinutesInput().val(), "45"); - - assert.equal(toDateInput().val(), ""); - assert.equal(toHoursInput().val(), ""); - assert.equal(toMinutesInput().val(), ""); - } -}); - -componentTest("can switch panels", { - template: `{{date-time-input-range}}`, - - async test(assert) { - assert.ok(exists(".panel.from.visible")); - assert.notOk(exists(".panel.to.visible")); - - await click(".panels button.to-panel"); - - assert.ok(exists(".panel.to.visible")); - assert.notOk(exists(".panel.from.visible")); - } -}); - -componentTest("prevents toDate to be before fromDate", { - template: `{{date-time-input-range from=from to=to onChange=onChange}}`, - - beforeEach() { - this.setProperties({ - from: DEFAULT_DATE_TIME, - to: DEFAULT_DATE_TIME, - onChange: setDates - }); - }, - - async test(assert) { - assert.notOk(exists(".error"), "it begins with no error"); - - await click(".panels button.to-panel"); - await click(toDateInput()); - await pika(2019, 0, 1); - - assert.ok(exists(".error"), "it shows an error"); - assert.deepEqual(this.to, DEFAULT_DATE_TIME, "it didnt trigger a mutation"); - - await click(".panels button.to-panel"); - await click(toDateInput()); - await pika(2019, 0, 30); - - assert.notOk(exists(".error"), "it removes the error"); - assert.deepEqual( - this.to, - new Date(2019, 0, 30, 14, 45), - "it has changed the date" - ); + assert.equal(fromDateInput().value, "January 29, 2019"); + assert.equal(fromTimeInput().dataset.name, "14:45"); + assert.equal(toDateInput().value, ""); + assert.equal(toTimeInput().dataset.name, "--:--"); } }); diff --git a/test/javascripts/components/date-time-input-test.js b/test/javascripts/components/date-time-input-test.js index 2dd4ac8fcf4..e56e3e09130 100644 --- a/test/javascripts/components/date-time-input-test.js +++ b/test/javascripts/components/date-time-input-test.js @@ -3,15 +3,11 @@ import componentTest from "helpers/component-test"; moduleForComponent("date-time-input", { integration: true }); function dateInput() { - return find(".date-picker"); + return find(".date-picker")[0]; } -function hoursInput() { - return find(".field.hours"); -} - -function minutesInput() { - return find(".field.minutes"); +function timeInput() { + return find(".d-time-input .combo-box-header")[0]; } function setDate(date) { @@ -24,7 +20,7 @@ async function pika(year, month, day) { ); } -const DEFAULT_DATE_TIME = new Date(2019, 0, 29, 14, 45); +const DEFAULT_DATE_TIME = moment("2019-01-29 14:45"); componentTest("default", { template: `{{date-time-input date=date}}`, @@ -34,9 +30,8 @@ componentTest("default", { }, test(assert) { - assert.equal(dateInput().val(), "January 29, 2019"); - assert.equal(hoursInput().val(), "14"); - assert.equal(minutesInput().val(), "45"); + assert.equal(dateInput().value, "January 29, 2019"); + assert.equal(timeInput().dataset.name, "14:45"); } }); @@ -51,7 +46,7 @@ componentTest("prevents mutations", { await click(dateInput()); await pika(2019, 0, 2); - assert.ok(this.date.getTime() === DEFAULT_DATE_TIME.getTime()); + assert.ok(this.date.isSame(DEFAULT_DATE_TIME)); } }); @@ -67,7 +62,7 @@ componentTest("allows mutations through actions", { await click(dateInput()); await pika(2019, 0, 2); - assert.ok(this.date.getTime() === new Date(2019, 0, 2, 14, 45).getTime()); + assert.ok(this.date.isSame(moment("2019-01-02 14:45"))); } }); @@ -79,6 +74,6 @@ componentTest("can hide time", { }, async test(assert) { - assert.notOk(exists(hoursInput())); + assert.notOk(exists(timeInput())); } }); diff --git a/test/javascripts/components/time-input-test.js b/test/javascripts/components/time-input-test.js index edbe7574c82..a6ef6606c99 100644 --- a/test/javascripts/components/time-input-test.js +++ b/test/javascripts/components/time-input-test.js @@ -1,21 +1,18 @@ +import selectKit from "helpers/select-kit-helper"; import componentTest from "helpers/component-test"; -moduleForComponent("time-input", { integration: true }); +moduleForComponent("time-input", { + integration: true, -function hoursInput() { - return find(".field.hours"); -} - -function minutesInput() { - return find(".field.minutes"); -} + beforeEach() { + this.set("subject", selectKit()); + } +}); function setTime(time) { this.setProperties(time); } -function noop() {} - componentTest("default", { template: `{{time-input hours=hours minutes=minutes}}`, @@ -24,8 +21,7 @@ componentTest("default", { }, test(assert) { - assert.equal(hoursInput().val(), "14"); - assert.equal(minutesInput().val(), "58"); + assert.equal(this.subject.header().name(), "14:58"); } }); @@ -37,11 +33,9 @@ componentTest("prevents mutations", { }, async test(assert) { - await fillIn(hoursInput(), "12"); - assert.ok(this.hours === "14"); - - await fillIn(minutesInput(), "36"); - assert.ok(this.minutes === "58"); + await this.subject.expand(); + await this.subject.selectRowByIndex(3); + assert.equal(this.subject.header().name(), "14:58"); } }); @@ -54,44 +48,8 @@ componentTest("allows mutations through actions", { }, async test(assert) { - await fillIn(hoursInput(), "12"); - assert.ok(this.hours === "12"); - - await fillIn(minutesInput(), "36"); - assert.ok(this.minutes === "36"); - } -}); - -componentTest("hours and minutes have boundaries", { - template: `{{time-input hours=14 minutes=58 onChange=onChange}}`, - - beforeEach() { - this.set("onChange", noop); - }, - - async test(assert) { - await fillIn(hoursInput(), "2"); - assert.equal(hoursInput().val(), "02"); - - await fillIn(hoursInput(), "@"); - assert.equal(hoursInput().val(), "00"); - - await fillIn(hoursInput(), "24"); - assert.equal(hoursInput().val(), "23"); - - await fillIn(hoursInput(), "-1"); - assert.equal(hoursInput().val(), "00"); - - await fillIn(minutesInput(), "@"); - assert.equal(minutesInput().val(), "00"); - - await fillIn(minutesInput(), "2"); - assert.equal(minutesInput().val(), "02"); - - await fillIn(minutesInput(), "60"); - assert.equal(minutesInput().val(), "59"); - - await fillIn(minutesInput(), "-1"); - assert.equal(minutesInput().val(), "00"); + await this.subject.expand(); + await this.subject.selectRowByIndex(3); + assert.equal(this.subject.header().name(), "00:45"); } }); diff --git a/test/javascripts/fixtures/reports_bulk.js b/test/javascripts/fixtures/reports_bulk.js index a41cf345836..100648b3f4f 100644 --- a/test/javascripts/fixtures/reports_bulk.js +++ b/test/javascripts/fixtures/reports_bulk.js @@ -59,7 +59,7 @@ let signups = { prev_end_date: "2018-06-17T00:00:00Z", prev30Days: null, dates_filtering: true, - report_key: 'reports:signups:start:end:[:prev_period]:50:{"group":"88"}:4', + report_key: "reports:signups:start:end:[:prev_period]:4", available_filters: [ { id: "group", type: "group", allow_any: false, choices: [], default: "88" } ], @@ -77,18 +77,29 @@ let signups = { let signups_fixture = JSON.parse(JSON.stringify(signups)); signups_fixture.type = "signups_exception"; signups_fixture.error = "exception"; +signups_fixture.report_key = + "reports:signups_exception:start:end:[:prev_period]:4"; const signups_exception = signups_fixture; signups_fixture = JSON.parse(JSON.stringify(signups)); signups_fixture.type = "signups_timeout"; signups_fixture.error = "timeout"; +signups_fixture.report_key = + "reports:signups_timeout:start:end:[:prev_period]:4"; const signups_timeout = signups_fixture; signups_fixture = JSON.parse(JSON.stringify(signups)); signups_fixture.type = "not_found"; signups_fixture.error = "not_found"; +signups_fixture.report_key = "reports:not_found:start:end:[:prev_period]:4"; const signups_not_found = signups_fixture; +signups_fixture = JSON.parse(JSON.stringify(signups)); +signups_fixture.type = "signups_with_groups"; +signups_fixture.report_key = + 'reports:signups_with_groups:start:end:[:prev_period]:50:{"group":"88"}:4'; +const signups_with_group = signups_fixture; + const startDate = moment() .locale("en") .utc() @@ -180,6 +191,7 @@ export default { "/admin/reports/bulk": { reports: [ signups, + signups_with_group, signups_not_found, signups_exception, signups_timeout,