From bdf8869a0186264148ad95a2659fd863400db9e3 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 25 Jun 2025 18:20:38 +1000 Subject: [PATCH] FEATURE: Add event location/description and "My Events" filter (#746) This pull request introduces two major new features to the calendar plugin: the ability to add a location/description to an event, and a new "My Events" view on the upcoming events page. ### Event Location You can now add a `location` to an event. This is a free-text field that can be used for a physical address, a URL, or any other location details. * A `location` field has been added to the event builder modal. * The location is displayed in the event details in the post, complete with a new "location-pin" icon. URLs within the location field are automatically linked. * This is supported by a database migration to add the `location` column, and updates to the event model, serializer, and parser. ### Event description You can now add a `description ` to an event. This is a free-text field that can be used to describe your event. * A `description` field has been added to the event builder modal. * The description is displayed in the event details in the post, complete with a new "circle-info" icon. URLs within the location field are automatically linked. It supports linebreaks. * This is supported by a database migration to add the `description` column, and updates to the event model, serializer, and parser. ### "My Events" Filter The `/upcoming-events` page now includes tabs to switch between "All events" and "My events". * The "My events" tab shows all upcoming events that the current user is "Going" to. * This creates a personalized calendar for users to easily see their own upcoming schedule. * A new `/upcoming-events/mine` route has been added, and the backend event finder now supports filtering by an `attending_user`. ### Other Improvements * The calendar view on the `/upcoming-events` page now defaults to a "list" view on mobile for a better experience. * The "Open Event" and "Close Event" actions now have a disabled/saving state to provide better feedback during the operation. * System tests have been added to cover the new functionality. --- .../discourse_post_event/events_controller.rb | 1 + app/models/discourse_post_event/event.rb | 4 + .../discourse_post_event/event_serializer.rb | 6 ++ .../discourse-post-event/description.gjs | 11 ++ .../components/discourse-post-event/index.gjs | 8 +- .../discourse-post-event/location.gjs | 14 +++ .../discourse-post-event/more-menu.gjs | 28 ++++- .../components/modal/post-event-builder.gjs | 41 +++++-- .../components/upcoming-events-calendar.gjs | 101 ++++++++++++++---- ...course-post-event-upcoming-events-index.js | 4 +- ...scourse-post-event-upcoming-events-mine.js | 5 + ...scourse-event-upcoming-events-route-map.js | 1 + .../discourse/lib/raw-event-helper.js | 19 +++- .../models/discourse-post-event-event.js | 6 ++ ...course-post-event-upcoming-events-index.js | 1 - ...course-post-event-upcoming-events-mine.gjs | 22 ++++ ...ourse-post-event-upcoming-events-index.gjs | 2 +- ...course-post-event-upcoming-events-mine.gjs | 10 ++ .../common/discourse-post-event.scss | 14 ++- .../common/post-event-builder.scss | 11 ++ config/locales/client.en.yml | 8 ++ config/routes.rb | 1 + .../20250616101944_add_location_to_event.rb | 7 ++ ...20250616101945_add_description_to_event.rb | 7 ++ lib/discourse_post_event/event_finder.rb | 27 +++++ lib/discourse_post_event/event_parser.rb | 2 + plugin.rb | 1 + .../discourse_post_event/event_finder_spec.rb | 56 ++++++++++ .../discourse_calendar/post_event.rb | 41 ++++++- .../discourse_calendar/post_event_form.rb | 35 ++++++ spec/system/post_event_spec.rb | 90 ++++++++++------ test/javascripts/lib/raw-event-helper-test.js | 12 +-- 32 files changed, 516 insertions(+), 80 deletions(-) create mode 100644 assets/javascripts/discourse/components/discourse-post-event/description.gjs create mode 100644 assets/javascripts/discourse/components/discourse-post-event/location.gjs create mode 100644 assets/javascripts/discourse/controllers/discourse-post-event-upcoming-events-mine.js create mode 100644 assets/javascripts/discourse/routes/discourse-post-event-upcoming-events-mine.gjs create mode 100644 assets/javascripts/discourse/templates/discourse-post-event-upcoming-events-mine.gjs create mode 100644 db/migrate/20250616101944_add_location_to_event.rb create mode 100644 db/migrate/20250616101945_add_description_to_event.rb create mode 100644 spec/system/page_objects/discourse_calendar/post_event_form.rb diff --git a/app/controllers/discourse_post_event/events_controller.rb b/app/controllers/discourse_post_event/events_controller.rb index fb734255..a54b0dd3 100644 --- a/app/controllers/discourse_post_event/events_controller.rb +++ b/app/controllers/discourse_post_event/events_controller.rb @@ -123,6 +123,7 @@ module DiscoursePostEvent :include_expired, :limit, :before, + :attending_user, ) end end diff --git a/app/models/discourse_post_event/event.rb b/app/models/discourse_post_event/event.rb index 23fb940c..7b690cc0 100644 --- a/app/models/discourse_post_event/event.rb +++ b/app/models/discourse_post_event/event.rb @@ -309,6 +309,8 @@ module DiscoursePostEvent original_starts_at: parsed_starts_at, original_ends_at: parsed_ends_at, url: event_params[:url], + description: event_params[:description], + location: event_params[:location], recurrence: event_params[:recurrence], recurrence_until: parsed_recurrence_until, timezone: event_params[:timezone], @@ -420,6 +422,8 @@ end # raw_invitees :string is an Array # name :string # url :string(1000) +# description :string(1000) +# location :string(1000) # custom_fields :jsonb not null # reminders :string # recurrence :string diff --git a/app/serializers/discourse_post_event/event_serializer.rb b/app/serializers/discourse_post_event/event_serializer.rb index 0ee1e102..ac48921d 100644 --- a/app/serializers/discourse_post_event/event_serializer.rb +++ b/app/serializers/discourse_post_event/event_serializer.rb @@ -31,6 +31,8 @@ module DiscoursePostEvent attributes :timezone attributes :show_local_time attributes :url + attributes :description + attributes :location attributes :watching_invitee attributes :chat_enabled attributes :channel @@ -135,6 +137,10 @@ module DiscoursePostEvent object.post.topic.category_id end + def include_url? + object.url.present? + end + def include_recurrence_rule? object.recurring? end diff --git a/assets/javascripts/discourse/components/discourse-post-event/description.gjs b/assets/javascripts/discourse/components/discourse-post-event/description.gjs new file mode 100644 index 00000000..c83c1a55 --- /dev/null +++ b/assets/javascripts/discourse/components/discourse-post-event/description.gjs @@ -0,0 +1,11 @@ +import CookText from "discourse/components/cook-text"; + +const DiscoursePostEventDescription = ; + +export default DiscoursePostEventDescription; diff --git a/assets/javascripts/discourse/components/discourse-post-event/index.gjs b/assets/javascripts/discourse/components/discourse-post-event/index.gjs index 1bd7b424..df47d81f 100644 --- a/assets/javascripts/discourse/components/discourse-post-event/index.gjs +++ b/assets/javascripts/discourse/components/discourse-post-event/index.gjs @@ -10,8 +10,10 @@ import routeAction from "discourse/helpers/route-action"; import ChatChannel from "./chat-channel"; import Creator from "./creator"; import Dates from "./dates"; +import Description from "./description"; import EventStatus from "./event-status"; import Invitees from "./invitees"; +import Location from "./location"; import MoreMenu from "./more-menu"; import Status from "./status"; import Url from "./url"; @@ -130,16 +132,20 @@ export default class DiscoursePostEvent extends Component { event=@event Section=(component InfoSection event=@event) Url=(component Url url=@event.url) + Description=(component Description description=@event.description) + Location=(component Location location=@event.location) Dates=(component Dates event=@event) Invitees=(component Invitees event=@event) Status=(component Status event=@event) ChatChannel=(component ChatChannel event=@event) }} > - + + + {{#if @event.canUpdateAttendance}} {{/if}} diff --git a/assets/javascripts/discourse/components/discourse-post-event/location.gjs b/assets/javascripts/discourse/components/discourse-post-event/location.gjs new file mode 100644 index 00000000..a0c48ad0 --- /dev/null +++ b/assets/javascripts/discourse/components/discourse-post-event/location.gjs @@ -0,0 +1,14 @@ +import CookText from "discourse/components/cook-text"; +import icon from "discourse/helpers/d-icon"; + +const DiscoursePostEventLocation = ; + +export default DiscoursePostEventLocation; diff --git a/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs b/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs index f830b1fd..ea75d468 100644 --- a/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs +++ b/assets/javascripts/discourse/components/discourse-post-event/more-menu.gjs @@ -1,9 +1,11 @@ import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; import { hash } from "@ember/helper"; import EmberObject, { action } from "@ember/object"; import { service } from "@ember/service"; import DButton from "discourse/components/d-button"; import DropdownMenu from "discourse/components/dropdown-menu"; +import concatClass from "discourse/helpers/concat-class"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { downloadCalendar } from "discourse/lib/download-calendar"; import { exportEntity } from "discourse/lib/export-csv"; @@ -27,6 +29,8 @@ export default class DiscoursePostEventMoreMenu extends Component { @service siteSettings; @service store; + @tracked isSavingEvent = false; + get expiredOrClosed() { return this.args.event.isExpired || this.args.event.isClosed; } @@ -147,6 +151,8 @@ export default class DiscoursePostEventMoreMenu extends Component { this.dialog.yesNoConfirm({ message: i18n("discourse_post_event.builder_modal.confirm_open"), didConfirm: async () => { + this.isSavingEvent = true; + try { const post = await this.store.find("post", this.args.event.id); this.args.event.isClosed = false; @@ -172,6 +178,8 @@ export default class DiscoursePostEventMoreMenu extends Component { } } catch (e) { popupAjaxError(e); + } finally { + this.isSavingEvent = false; } }, }); @@ -208,6 +216,7 @@ export default class DiscoursePostEventMoreMenu extends Component { this.dialog.yesNoConfirm({ message: i18n("discourse_post_event.builder_modal.confirm_close"), didConfirm: () => { + this.isSavingEvent = true; return this.store.find("post", this.args.event.id).then((post) => { this.args.event.isClosed = true; @@ -226,10 +235,14 @@ export default class DiscoursePostEventMoreMenu extends Component { edit_reason: i18n("discourse_post_event.edit_reason_closed"), }; - return cook(newRaw).then((cooked) => { - props.cooked = cooked.string; - return post.save(props); - }); + return cook(newRaw) + .then((cooked) => { + props.cooked = cooked.string; + return post.save(props); + }) + .finally(() => { + this.isSavingEvent = false; + }); } }); }, @@ -239,7 +252,10 @@ export default class DiscoursePostEventMoreMenu extends Component {