diff --git a/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs b/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs new file mode 100644 index 00000000..e07a8320 --- /dev/null +++ b/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs @@ -0,0 +1,35 @@ +import { apiInitializer } from "discourse/lib/api"; +import GroupTimezones from "../components/group-timezones"; + +const GroupTimezonesShim = ; + +export default apiInitializer((api) => { + api.decorateCookedElement((element, helper) => { + element.querySelectorAll(".group-timezones").forEach((el) => { + const post = helper.getModel(); + + if (!post) { + return; + } + + const group = el.dataset.group; + if (!group) { + throw new Error( + "Group timezone element is missing 'data-group' attribute" + ); + } + + helper.renderGlimmer(el, GroupTimezonesShim, { + group, + members: (post.group_timezones || {})[group] || [], + size: el.dataset.size || "medium", + }); + }); + }); +}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones.js b/assets/javascripts/discourse/components/group-timezones/index.gjs similarity index 57% rename from assets/javascripts/discourse/widgets/discourse-group-timezones.js rename to assets/javascripts/discourse/components/group-timezones/index.gjs index 49f7ae23..98d8e75b 100644 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones.js +++ b/assets/javascripts/discourse/components/group-timezones/index.gjs @@ -1,38 +1,27 @@ -import hbs from "discourse/widgets/hbs-compiler"; -import { createWidget } from "discourse/widgets/widget"; -import roundTime from "../lib/round-time"; +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { fn } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { service } from "@ember/service"; +import { eq } from "truth-helpers"; +import { i18n } from "discourse-i18n"; +import roundTime from "../../lib/round-time"; +import NewDay from "./new-day"; +import TimeTraveller from "./time-traveller"; +import Timezone from "./timezone"; -export default createWidget("discourse-group-timezones", { - tagName: "div.group-timezones", +export default class GroupTimezones extends Component { + @service siteSettings; - buildKey: (attrs) => `group-timezones-${attrs.id}`, + @tracked filter = ""; + @tracked localTimeOffset = 0; - buildClasses(attrs) { - return attrs.size; - }, - - buildAttributes(attrs) { - return { - id: attrs.id, - }; - }, - - defaultState() { - return { - localTimeOffset: 0, - }; - }, - - onChangeCurrentUserTimeOffset(offset) { - this.state.localTimeOffset = offset; - }, - - transform(attrs, state) { - const members = attrs.members || []; + get groupedTimezones() { let groupedTimezones = []; - members.filterBy("timezone").forEach((member) => { - if (this._shouldAddMemberToGroup(this.state.filter, member)) { + this.args.members.filterBy("timezone").forEach((member) => { + if (this.#shouldAddMemberToGroup(this.filter, member)) { const timezone = member.timezone; const identifier = parseInt(moment.tz(timezone).format("YYYYMDHm"), 10); let groupedTimezone = groupedTimezones.findBy("identifier", identifier); @@ -40,18 +29,18 @@ export default createWidget("discourse-group-timezones", { if (groupedTimezone) { groupedTimezone.members.push(member); } else { - const now = this._roundMoment(moment.tz(timezone)); - const workingDays = this._workingDays(); + const now = this.#roundMoment(moment.tz(timezone)); + const workingDays = this.#workingDays(); const offset = moment.tz(moment.utc(), timezone).utcOffset(); groupedTimezone = { identifier, offset, type: "discourse-group-timezone", - nowWithOffset: now.add(state.localTimeOffset, "minutes"), - closeToWorkingHours: this._closeToWorkingHours(now, workingDays), - inWorkingHours: this._inWorkingHours(now, workingDays), - utcOffset: this._utcOffset(offset), + nowWithOffset: now.add(this.localTimeOffset, "minutes"), + closeToWorkingHours: this.#closeToWorkingHours(now, workingDays), + inWorkingHours: this.#inWorkingHours(now, workingDays), + utcOffset: this.#utcOffset(offset), members: [member], }; groupedTimezones.push(groupedTimezone); @@ -84,36 +73,10 @@ export default createWidget("discourse-group-timezones", { }); } - return { groupedTimezones }; - }, + return groupedTimezones; + } - onChangeFilter(filter) { - this.state.filter = filter && filter.length ? filter : null; - }, - - template: hbs` - {{attach - widget="discourse-group-timezones-header" - attrs=(hash - id=attrs.id - group=attrs.group - localTimeOffset=state.localTimeOffset - ) - }} -
- {{#each transformed.groupedTimezones as |groupedTimezone|}} - {{attach - widget=groupedTimezone.type - attrs=(hash - usersOnHoliday=attrs.usersOnHoliday - groupedTimezone=groupedTimezone - ) - }} - {{/each}} -
- `, - - _shouldAddMemberToGroup(filter, member) { + #shouldAddMemberToGroup(filter, member) { if (filter) { filter = filter.toLowerCase(); if ( @@ -127,17 +90,17 @@ export default createWidget("discourse-group-timezones", { } return false; - }, + } - _roundMoment(date) { - if (this.state.localTimeOffset) { + #roundMoment(date) { + if (this.localTimeOffset) { date = roundTime(date); } return date; - }, + } - _closeToWorkingHours(moment, workingDays) { + #closeToWorkingHours(moment, workingDays) { const hours = moment.hours(); const startHour = this.siteSettings.working_day_start_hour; const endHour = this.siteSettings.working_day_end_hour; @@ -148,18 +111,18 @@ export default createWidget("discourse-group-timezones", { (hours <= Math.min(endHour + extension, 23) && hours >= endHour)) && workingDays.includes(moment.isoWeekday()) ); - }, + } - _inWorkingHours(moment, workingDays) { + #inWorkingHours(moment, workingDays) { const hours = moment.hours(); return ( hours > this.siteSettings.working_day_start_hour && hours < this.siteSettings.working_day_end_hour && workingDays.includes(moment.isoWeekday()) ); - }, + } - _utcOffset(offset) { + #utcOffset(offset) { const sign = Math.sign(offset) === 1 ? "+" : "-"; offset = Math.abs(offset); let hours = Math.floor(offset / 60).toString(); @@ -170,9 +133,9 @@ export default createWidget("discourse-group-timezones", { /:00$/, "" )}`.replace(/-0/, " "); - }, + } - _workingDays() { + #workingDays() { const enMoment = moment().locale("en"); const getIsoWeekday = (day) => enMoment.localeData()._weekdays.indexOf(day) || 7; @@ -180,5 +143,40 @@ export default createWidget("discourse-group-timezones", { .split("|") .filter(Boolean) .map((x) => getIsoWeekday(x)); - }, -}); + } + + @action + handleFilterChange(event) { + this.filter = event.target.value; + } + + +} diff --git a/assets/javascripts/discourse/components/group-timezones/new-day.gjs b/assets/javascripts/discourse/components/group-timezones/new-day.gjs new file mode 100644 index 00000000..121c7cb9 --- /dev/null +++ b/assets/javascripts/discourse/components/group-timezones/new-day.gjs @@ -0,0 +1,16 @@ +import icon from "discourse/helpers/d-icon"; + +const NewDay = ; + +export default NewDay; diff --git a/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs b/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs new file mode 100644 index 00000000..c6f07176 --- /dev/null +++ b/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs @@ -0,0 +1,58 @@ +import Component from "@glimmer/component"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import { not } from "truth-helpers"; +import DButton from "discourse/components/d-button"; +import roundTime from "../../lib/round-time"; + +export default class TimeTraveller extends Component { + get localTimeWithOffset() { + let date = moment().add(this.args.localTimeOffset, "minutes"); + + if (this.args.localTimeOffset) { + date = roundTime(date); + } + + return date.format("HH:mm"); + } + + @action + reset() { + this.args.setOffset(0); + } + + @action + sliderMoved(event) { + const value = parseInt(event.target.value, 10); + const offset = value * 15; + this.args.setOffset(offset); + } + + +} diff --git a/assets/javascripts/discourse/components/group-timezones/timezone.gjs b/assets/javascripts/discourse/components/group-timezones/timezone.gjs new file mode 100644 index 00000000..2e1f854a --- /dev/null +++ b/assets/javascripts/discourse/components/group-timezones/timezone.gjs @@ -0,0 +1,44 @@ +import Component from "@glimmer/component"; +import UserAvatar from "discourse/components/user-avatar"; +import concatClass from "discourse/helpers/concat-class"; + +export default class GroupTimezone extends Component { + get formattedTime() { + return this.args.groupedTimezone.nowWithOffset.format("LT"); + } + + +} diff --git a/assets/javascripts/discourse/initializers/discourse-group-timezones.js b/assets/javascripts/discourse/initializers/discourse-group-timezones.js deleted file mode 100644 index 1d5f4d26..00000000 --- a/assets/javascripts/discourse/initializers/discourse-group-timezones.js +++ /dev/null @@ -1,72 +0,0 @@ -import $ from "jquery"; -import { getRegister } from "discourse/lib/get-owner"; -import { withPluginApi } from "discourse/lib/plugin-api"; -import WidgetGlue from "discourse/widgets/glue"; - -export default { - name: "discourse-group-timezones", - - initialize() { - withPluginApi("0.8.7", (api) => { - let _glued = []; - - function cleanUp() { - _glued.forEach((g) => g.cleanUp()); - _glued = []; - } - - function _attachWidget(container, options) { - const glue = new WidgetGlue( - "discourse-group-timezones", - getRegister(api), - options - ); - glue.appendTo(container); - _glued.push(glue); - } - - function _attachGroupTimezones($elem, post) { - const $groupTimezones = $(".group-timezones", $elem); - - if (!$groupTimezones.length) { - return; - } - - $groupTimezones.each((idx, groupTimezone) => { - const group = groupTimezone.getAttribute("data-group"); - if (!group) { - throw "[group] attribute is necessary when using timezones."; - } - - const members = (post.get("group_timezones") || {})[group] || []; - - _attachWidget(groupTimezone, { - id: `${post.id}-${idx}`, - members, - group, - usersOnHoliday: - api.container.lookup("service:site").users_on_holiday || [], - size: groupTimezone.getAttribute("data-size") || "medium", - }); - }); - } - - function _attachPostWithGroupTimezones($elem, helper) { - if (helper) { - const post = helper.getModel(); - - if (post) { - api.preventCloak(post.id); - _attachGroupTimezones($elem, post); - } - } - } - - api.decorateCooked(_attachPostWithGroupTimezones, { - id: "discourse-group-timezones", - }); - - api.cleanupStream(cleanUp); - }); - }, -}; diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezone-new-day.js b/assets/javascripts/discourse/widgets/discourse-group-timezone-new-day.js deleted file mode 100644 index fdf5e95f..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezone-new-day.js +++ /dev/null @@ -1,17 +0,0 @@ -import hbs from "discourse/widgets/hbs-compiler"; -import { createWidget } from "discourse/widgets/widget"; - -export default createWidget("discourse-group-timezone-new-day", { - tagName: "div.group-timezone-new-day", - - template: hbs` - - {{d-icon "chevron-left"}} - {{this.attrs.groupedTimezone.beforeDate}} - - - {{this.attrs.groupedTimezone.afterDate}} - {{d-icon "chevron-right"}} - - `, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezone.js b/assets/javascripts/discourse/widgets/discourse-group-timezone.js deleted file mode 100644 index 6a856689..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezone.js +++ /dev/null @@ -1,48 +0,0 @@ -import hbs from "discourse/widgets/hbs-compiler"; -import { createWidget } from "discourse/widgets/widget"; - -export default createWidget("discourse-group-timezone", { - tagName: "div.group-timezone", - - buildClasses(attrs) { - const classes = []; - - if (attrs.groupedTimezone.closeToWorkingHours) { - classes.push("close-to-working-hours"); - } - - if (attrs.groupedTimezone.inWorkingHours) { - classes.push("in-working-hours"); - } - - return classes.join(" "); - }, - - transform(attrs) { - return { - formatedTime: attrs.groupedTimezone.nowWithOffset.format("LT"), - }; - }, - - template: hbs` -
- - {{transformed.formatedTime}} - - - {{{attrs.groupedTimezone.utcOffset}}} - -
- - `, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-filter.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-filter.js deleted file mode 100644 index 3b38d55b..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones-filter.js +++ /dev/null @@ -1,28 +0,0 @@ -import { throttle } from "@ember/runloop"; -import { createWidget } from "discourse/widgets/widget"; -import { i18n } from "discourse-i18n"; - -export default createWidget("discourse-group-timezones-filter", { - tagName: "input.group-timezones-filter", - - input(event) { - this.changeFilterThrottler(event.target.value); - }, - - changeFilterThrottler(filter) { - throttle( - this, - function () { - this.sendWidgetAction("onChangeFilter", filter); - }, - 100 - ); - }, - - buildAttributes() { - return { - type: "text", - placeholder: i18n("group_timezones.search"), - }; - }, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-header.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-header.js deleted file mode 100644 index 867904f9..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones-header.js +++ /dev/null @@ -1,29 +0,0 @@ -import hbs from "discourse/widgets/hbs-compiler"; -import { createWidget } from "discourse/widgets/widget"; -import { i18n } from "discourse-i18n"; - -export default createWidget("discourse-group-timezones-header", { - tagName: "div.group-timezones-header", - - transform(attrs) { - return { - title: i18n("group_timezones.group_availability", { - group: attrs.group, - }), - }; - }, - - template: hbs` - {{attach - widget="discourse-group-timezones-time-traveler" - attrs=(hash - id=attrs.id - localTimeOffset=attrs.localTimeOffset - ) - }} - - {{transformed.title}} - - {{attach widget="discourse-group-timezones-filter"}} - `, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-member.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-member.js deleted file mode 100644 index e4f1a73e..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones-member.js +++ /dev/null @@ -1,30 +0,0 @@ -import { h } from "virtual-dom"; -import { formatUsername } from "discourse/lib/utilities"; -import { avatarImg } from "discourse/widgets/post"; -import { createWidget } from "discourse/widgets/widget"; - -export default createWidget("discourse-group-timezones-member", { - tagName: "li.group-timezones-member", - - buildClasses(attrs) { - return attrs.member.on_holiday ? "on-holiday" : "not-on-holiday"; - }, - - html(attrs) { - const { name, username, avatar_template } = attrs.member; - - return h( - "a", - { - attributes: { - class: "group-timezones-member-avatar", - "data-user-card": username, - }, - }, - avatarImg("small", { - template: avatar_template, - username: name || formatUsername(username), - }) - ); - }, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-reset.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-reset.js deleted file mode 100644 index 99c23db2..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones-reset.js +++ /dev/null @@ -1,35 +0,0 @@ -import hbs from "discourse/widgets/hbs-compiler"; -import { createWidget } from "discourse/widgets/widget"; - -export default createWidget("discourse-group-timezones-reset", { - tagName: "div.group-timezones-reset", - - onResetOffset() { - this.sendWidgetAction("onChangeCurrentUserTimeOffset", 0); - - const container = document.getElementById(this.attrs.id); - const slider = container.querySelector( - "input[type=range].group-timezones-slider" - ); - if (slider) { - slider.value = 0; - } - }, - - transform(attrs) { - return { - isDisabled: attrs.localTimeOffset === 0, - }; - }, - - template: hbs` - {{attach - widget="button" - attrs=(hash - disabled=this.transformed.isDisabled - action="onResetOffset" - icon="arrow-rotate-left" - ) - }} - `, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-slider.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-slider.js deleted file mode 100644 index 8b2ec430..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones-slider.js +++ /dev/null @@ -1,40 +0,0 @@ -import { throttle } from "@ember/runloop"; -import { createWidget } from "discourse/widgets/widget"; - -export default createWidget("discourse-group-timezones-slider", { - tagName: "input.group-timezones-slider", - - input(event) { - this._handleSliderEvent(event); - }, - - change(event) { - this._handleSliderEvent(event); - }, - - changeOffsetThrottler(offset) { - throttle( - this, - function () { - this.sendWidgetAction("onChangeCurrentUserTimeOffset", offset); - }, - 75 - ); - }, - - buildAttributes() { - return { - step: 1, - value: 0, - min: -48, - max: 48, - type: "range", - }; - }, - - _handleSliderEvent(event) { - const value = parseInt(event.target.value, 10); - const offset = value * 15; - this.changeOffsetThrottler(offset); - }, -}); diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-time-traveler.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-time-traveler.js deleted file mode 100644 index b25cc4a4..00000000 --- a/assets/javascripts/discourse/widgets/discourse-group-timezones-time-traveler.js +++ /dev/null @@ -1,37 +0,0 @@ -import hbs from "discourse/widgets/hbs-compiler"; -import { createWidget } from "discourse/widgets/widget"; -import roundTime from "../lib/round-time"; - -export default createWidget("discourse-group-timezones-time-traveler", { - tagName: "div.group-timezones-time-traveler", - - transform(attrs) { - let date = moment().add(attrs.localTimeOffset, "minutes"); - - if (attrs.localTimeOffset) { - date = roundTime(date); - } - - return { - localTimeWithOffset: date.format("HH:mm"), - }; - }, - - template: hbs` - - {{transformed.localTimeWithOffset}} - - - {{attach - widget="discourse-group-timezones-slider" - }} - - {{attach - widget="discourse-group-timezones-reset" - attrs=(hash - id=attrs.id - localTimeOffset=attrs.localTimeOffset - ) - }} - `, -}); diff --git a/spec/system/group_timezones_spec.rb b/spec/system/group_timezones_spec.rb new file mode 100644 index 00000000..b5fecbef --- /dev/null +++ b/spec/system/group_timezones_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +describe "Group timezones feature", type: :system do + fab!(:group) { Fabricate(:group, name: "test-group") } + + fab!(:users) do + Fabricate + .times(5, :user) + .each do |user| + user.user_option.timezone = "America/New_York" + user.user_option.save! + group.add(user) + end + end + + before do + Jobs.run_immediately! + SiteSetting.calendar_enabled = true + end + + let(:post) { create_post(raw: <<~RAW) } + [timezones group="test-group"] + [/timezones] + RAW + + it "renders successfully" do + visit(post.url) + expect(page).to have_selector(".group-timezones") + expect(page).to have_selector(".group-timezones-member", count: 5) + end +end