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.
This commit is contained in:
parent
781ecefc62
commit
bdf8869a01
|
|
@ -123,6 +123,7 @@ module DiscoursePostEvent
|
||||||
:include_expired,
|
:include_expired,
|
||||||
:limit,
|
:limit,
|
||||||
:before,
|
:before,
|
||||||
|
:attending_user,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -309,6 +309,8 @@ module DiscoursePostEvent
|
||||||
original_starts_at: parsed_starts_at,
|
original_starts_at: parsed_starts_at,
|
||||||
original_ends_at: parsed_ends_at,
|
original_ends_at: parsed_ends_at,
|
||||||
url: event_params[:url],
|
url: event_params[:url],
|
||||||
|
description: event_params[:description],
|
||||||
|
location: event_params[:location],
|
||||||
recurrence: event_params[:recurrence],
|
recurrence: event_params[:recurrence],
|
||||||
recurrence_until: parsed_recurrence_until,
|
recurrence_until: parsed_recurrence_until,
|
||||||
timezone: event_params[:timezone],
|
timezone: event_params[:timezone],
|
||||||
|
|
@ -420,6 +422,8 @@ end
|
||||||
# raw_invitees :string is an Array
|
# raw_invitees :string is an Array
|
||||||
# name :string
|
# name :string
|
||||||
# url :string(1000)
|
# url :string(1000)
|
||||||
|
# description :string(1000)
|
||||||
|
# location :string(1000)
|
||||||
# custom_fields :jsonb not null
|
# custom_fields :jsonb not null
|
||||||
# reminders :string
|
# reminders :string
|
||||||
# recurrence :string
|
# recurrence :string
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ module DiscoursePostEvent
|
||||||
attributes :timezone
|
attributes :timezone
|
||||||
attributes :show_local_time
|
attributes :show_local_time
|
||||||
attributes :url
|
attributes :url
|
||||||
|
attributes :description
|
||||||
|
attributes :location
|
||||||
attributes :watching_invitee
|
attributes :watching_invitee
|
||||||
attributes :chat_enabled
|
attributes :chat_enabled
|
||||||
attributes :channel
|
attributes :channel
|
||||||
|
|
@ -135,6 +137,10 @@ module DiscoursePostEvent
|
||||||
object.post.topic.category_id
|
object.post.topic.category_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_url?
|
||||||
|
object.url.present?
|
||||||
|
end
|
||||||
|
|
||||||
def include_recurrence_rule?
|
def include_recurrence_rule?
|
||||||
object.recurring?
|
object.recurring?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import CookText from "discourse/components/cook-text";
|
||||||
|
|
||||||
|
const DiscoursePostEventDescription = <template>
|
||||||
|
{{#if @description}}
|
||||||
|
<section class="event__section event-description">
|
||||||
|
<CookText @rawText={{@description}} />
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default DiscoursePostEventDescription;
|
||||||
|
|
@ -10,8 +10,10 @@ import routeAction from "discourse/helpers/route-action";
|
||||||
import ChatChannel from "./chat-channel";
|
import ChatChannel from "./chat-channel";
|
||||||
import Creator from "./creator";
|
import Creator from "./creator";
|
||||||
import Dates from "./dates";
|
import Dates from "./dates";
|
||||||
|
import Description from "./description";
|
||||||
import EventStatus from "./event-status";
|
import EventStatus from "./event-status";
|
||||||
import Invitees from "./invitees";
|
import Invitees from "./invitees";
|
||||||
|
import Location from "./location";
|
||||||
import MoreMenu from "./more-menu";
|
import MoreMenu from "./more-menu";
|
||||||
import Status from "./status";
|
import Status from "./status";
|
||||||
import Url from "./url";
|
import Url from "./url";
|
||||||
|
|
@ -130,16 +132,20 @@ export default class DiscoursePostEvent extends Component {
|
||||||
event=@event
|
event=@event
|
||||||
Section=(component InfoSection event=@event)
|
Section=(component InfoSection event=@event)
|
||||||
Url=(component Url url=@event.url)
|
Url=(component Url url=@event.url)
|
||||||
|
Description=(component Description description=@event.description)
|
||||||
|
Location=(component Location location=@event.location)
|
||||||
Dates=(component Dates event=@event)
|
Dates=(component Dates event=@event)
|
||||||
Invitees=(component Invitees event=@event)
|
Invitees=(component Invitees event=@event)
|
||||||
Status=(component Status event=@event)
|
Status=(component Status event=@event)
|
||||||
ChatChannel=(component ChatChannel event=@event)
|
ChatChannel=(component ChatChannel event=@event)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Url @url={{@event.url}} />
|
|
||||||
<Dates @event={{@event}} />
|
<Dates @event={{@event}} />
|
||||||
|
<Location @location={{@event.location}} />
|
||||||
|
<Url @url={{@event.url}} />
|
||||||
<ChatChannel @event={{@event}} />
|
<ChatChannel @event={{@event}} />
|
||||||
<Invitees @event={{@event}} />
|
<Invitees @event={{@event}} />
|
||||||
|
<Description @description={{@event.description}} />
|
||||||
{{#if @event.canUpdateAttendance}}
|
{{#if @event.canUpdateAttendance}}
|
||||||
<Status @event={{@event}} />
|
<Status @event={{@event}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import CookText from "discourse/components/cook-text";
|
||||||
|
import icon from "discourse/helpers/d-icon";
|
||||||
|
|
||||||
|
const DiscoursePostEventLocation = <template>
|
||||||
|
{{#if @location}}
|
||||||
|
<section class="event__section event-location">
|
||||||
|
{{icon "location-pin"}}
|
||||||
|
|
||||||
|
<CookText @rawText={{@location}} />
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default DiscoursePostEventLocation;
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { hash } from "@ember/helper";
|
import { hash } from "@ember/helper";
|
||||||
import EmberObject, { action } from "@ember/object";
|
import EmberObject, { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import DropdownMenu from "discourse/components/dropdown-menu";
|
import DropdownMenu from "discourse/components/dropdown-menu";
|
||||||
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { downloadCalendar } from "discourse/lib/download-calendar";
|
import { downloadCalendar } from "discourse/lib/download-calendar";
|
||||||
import { exportEntity } from "discourse/lib/export-csv";
|
import { exportEntity } from "discourse/lib/export-csv";
|
||||||
|
|
@ -27,6 +29,8 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
|
@tracked isSavingEvent = false;
|
||||||
|
|
||||||
get expiredOrClosed() {
|
get expiredOrClosed() {
|
||||||
return this.args.event.isExpired || this.args.event.isClosed;
|
return this.args.event.isExpired || this.args.event.isClosed;
|
||||||
}
|
}
|
||||||
|
|
@ -147,6 +151,8 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
this.dialog.yesNoConfirm({
|
this.dialog.yesNoConfirm({
|
||||||
message: i18n("discourse_post_event.builder_modal.confirm_open"),
|
message: i18n("discourse_post_event.builder_modal.confirm_open"),
|
||||||
didConfirm: async () => {
|
didConfirm: async () => {
|
||||||
|
this.isSavingEvent = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const post = await this.store.find("post", this.args.event.id);
|
const post = await this.store.find("post", this.args.event.id);
|
||||||
this.args.event.isClosed = false;
|
this.args.event.isClosed = false;
|
||||||
|
|
@ -172,6 +178,8 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
popupAjaxError(e);
|
popupAjaxError(e);
|
||||||
|
} finally {
|
||||||
|
this.isSavingEvent = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -208,6 +216,7 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
this.dialog.yesNoConfirm({
|
this.dialog.yesNoConfirm({
|
||||||
message: i18n("discourse_post_event.builder_modal.confirm_close"),
|
message: i18n("discourse_post_event.builder_modal.confirm_close"),
|
||||||
didConfirm: () => {
|
didConfirm: () => {
|
||||||
|
this.isSavingEvent = true;
|
||||||
return this.store.find("post", this.args.event.id).then((post) => {
|
return this.store.find("post", this.args.event.id).then((post) => {
|
||||||
this.args.event.isClosed = true;
|
this.args.event.isClosed = true;
|
||||||
|
|
||||||
|
|
@ -226,9 +235,13 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
edit_reason: i18n("discourse_post_event.edit_reason_closed"),
|
edit_reason: i18n("discourse_post_event.edit_reason_closed"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return cook(newRaw).then((cooked) => {
|
return cook(newRaw)
|
||||||
|
.then((cooked) => {
|
||||||
props.cooked = cooked.string;
|
props.cooked = cooked.string;
|
||||||
return post.save(props);
|
return post.save(props);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isSavingEvent = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -239,7 +252,10 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
<template>
|
<template>
|
||||||
<DMenu
|
<DMenu
|
||||||
@identifier="discourse-post-event-more-menu"
|
@identifier="discourse-post-event-more-menu"
|
||||||
@triggerClass="more-dropdown"
|
@triggerClass={{concatClass
|
||||||
|
"more-dropdown"
|
||||||
|
(if this.isSavingEvent "--saving")
|
||||||
|
}}
|
||||||
@icon="ellipsis"
|
@icon="ellipsis"
|
||||||
@onRegisterApi={{this.registerMenuApi}}
|
@onRegisterApi={{this.registerMenuApi}}
|
||||||
>
|
>
|
||||||
|
|
@ -333,6 +349,7 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
class="btn-transparent"
|
class="btn-transparent"
|
||||||
@label="discourse_post_event.open_event"
|
@label="discourse_post_event.open_event"
|
||||||
@action={{this.openEvent}}
|
@action={{this.openEvent}}
|
||||||
|
@disabled={{this.isSavingEvent}}
|
||||||
/>
|
/>
|
||||||
</dropdown.item>
|
</dropdown.item>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
@ -351,6 +368,7 @@ export default class DiscoursePostEventMoreMenu extends Component {
|
||||||
@icon="xmark"
|
@icon="xmark"
|
||||||
@label="discourse_post_event.close_event"
|
@label="discourse_post_event.close_event"
|
||||||
@action={{this.closeEvent}}
|
@action={{this.closeEvent}}
|
||||||
|
@disabled={{this.isSavingEvent}}
|
||||||
class="btn-transparent btn-danger"
|
class="btn-transparent btn-danger"
|
||||||
/>
|
/>
|
||||||
</dropdown.item>
|
</dropdown.item>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { Input } from "@ember/component";
|
import { Input, Textarea } from "@ember/component";
|
||||||
import { concat, fn, get } from "@ember/helper";
|
import { concat, fn, get } from "@ember/helper";
|
||||||
import { on } from "@ember/modifier";
|
import { on } from "@ember/modifier";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
|
@ -110,6 +110,10 @@ export default class PostEventBuilder extends Component {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get shouldRenderUrl() {
|
||||||
|
return this.args.model.event.url !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
get availableRecurrences() {
|
get availableRecurrences() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
@ -363,6 +367,19 @@ export default class PostEventBuilder extends Component {
|
||||||
/>
|
/>
|
||||||
</EventField>
|
</EventField>
|
||||||
|
|
||||||
|
<EventField
|
||||||
|
@label="discourse_post_event.builder_modal.location.label"
|
||||||
|
class="location"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
@value={{@model.event.location}}
|
||||||
|
placeholder={{i18n
|
||||||
|
"discourse_post_event.builder_modal.location.placeholder"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EventField>
|
||||||
|
|
||||||
|
{{#if this.shouldRenderUrl}}
|
||||||
<EventField
|
<EventField
|
||||||
@label="discourse_post_event.builder_modal.url.label"
|
@label="discourse_post_event.builder_modal.url.label"
|
||||||
class="url"
|
class="url"
|
||||||
|
|
@ -374,6 +391,19 @@ export default class PostEventBuilder extends Component {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</EventField>
|
</EventField>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<EventField
|
||||||
|
@label="discourse_post_event.builder_modal.description.label"
|
||||||
|
class="description"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
@value={{@model.event.description}}
|
||||||
|
placeholder={{i18n
|
||||||
|
"discourse_post_event.builder_modal.description.placeholder"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</EventField>
|
||||||
|
|
||||||
<EventField
|
<EventField
|
||||||
class="timezone"
|
class="timezone"
|
||||||
|
|
@ -383,7 +413,6 @@ export default class PostEventBuilder extends Component {
|
||||||
@value={{@model.event.timezone}}
|
@value={{@model.event.timezone}}
|
||||||
@onChange={{this.setNewTimezone}}
|
@onChange={{this.setNewTimezone}}
|
||||||
@none="discourse_post_event.builder_modal.timezone.remove_timezone"
|
@none="discourse_post_event.builder_modal.timezone.remove_timezone"
|
||||||
class="input-xxlarge"
|
|
||||||
/>
|
/>
|
||||||
</EventField>
|
</EventField>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,37 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
|
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
import { tagName } from "@ember-decorators/component";
|
import { service } from "@ember/service";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import getURL from "discourse/lib/get-url";
|
import getURL from "discourse/lib/get-url";
|
||||||
import loadScript from "discourse/lib/load-script";
|
import loadScript from "discourse/lib/load-script";
|
||||||
import Category from "discourse/models/category";
|
import Category from "discourse/models/category";
|
||||||
|
import { i18n } from "discourse-i18n";
|
||||||
import { formatEventName } from "../helpers/format-event-name";
|
import { formatEventName } from "../helpers/format-event-name";
|
||||||
import addRecurrentEvents from "../lib/add-recurrent-events";
|
import addRecurrentEvents from "../lib/add-recurrent-events";
|
||||||
import fullCalendarDefaultOptions from "../lib/full-calendar-default-options";
|
import fullCalendarDefaultOptions from "../lib/full-calendar-default-options";
|
||||||
import { isNotFullDayEvent } from "../lib/guess-best-date-format";
|
import { isNotFullDayEvent } from "../lib/guess-best-date-format";
|
||||||
|
|
||||||
@tagName("")
|
|
||||||
export default class UpcomingEventsCalendar extends Component {
|
export default class UpcomingEventsCalendar extends Component {
|
||||||
events = null;
|
@service currentUser;
|
||||||
|
@service site;
|
||||||
|
@service router;
|
||||||
|
|
||||||
init() {
|
_calendar = null;
|
||||||
super.init(...arguments);
|
|
||||||
|
@action
|
||||||
|
teardown() {
|
||||||
|
this._calendar?.destroy?.();
|
||||||
this._calendar = null;
|
this._calendar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
willDestroyElement() {
|
@action
|
||||||
super.willDestroyElement(...arguments);
|
async renderCalendar() {
|
||||||
|
|
||||||
this._calendar && this._calendar.destroy();
|
|
||||||
this._calendar = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
super.didInsertElement(...arguments);
|
|
||||||
|
|
||||||
this._renderCalendar();
|
|
||||||
}
|
|
||||||
|
|
||||||
async _renderCalendar() {
|
|
||||||
const siteSettings = this.site.siteSettings;
|
const siteSettings = this.site.siteSettings;
|
||||||
|
const isMobileView = this.site.mobileView;
|
||||||
|
|
||||||
const calendarNode = document.getElementById("upcoming-events-calendar");
|
const calendarNode = document.getElementById("upcoming-events-calendar");
|
||||||
if (!calendarNode) {
|
if (!calendarNode) {
|
||||||
|
|
@ -44,16 +42,50 @@ export default class UpcomingEventsCalendar extends Component {
|
||||||
|
|
||||||
await this._loadCalendar();
|
await this._loadCalendar();
|
||||||
|
|
||||||
|
const view =
|
||||||
|
this.args.controller.view || (isMobileView ? "listNextYear" : "month");
|
||||||
|
|
||||||
const fullCalendar = new window.FullCalendar.Calendar(calendarNode, {
|
const fullCalendar = new window.FullCalendar.Calendar(calendarNode, {
|
||||||
...fullCalendarDefaultOptions(),
|
...fullCalendarDefaultOptions(),
|
||||||
firstDay: 1,
|
firstDay: 1,
|
||||||
height: "auto",
|
height: "auto",
|
||||||
|
defaultView: view,
|
||||||
|
views: {
|
||||||
|
listNextYear: {
|
||||||
|
type: "list",
|
||||||
|
duration: { days: 365 },
|
||||||
|
buttonText: "list",
|
||||||
|
listDayFormat: {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
weekday: "long",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
left: "prev,next today",
|
||||||
|
center: "title",
|
||||||
|
right: "month,basicWeek,listNextYear",
|
||||||
|
},
|
||||||
|
datesRender: (info) => {
|
||||||
|
// this is renamed in FullCalendar v5 / v6 to datesSet
|
||||||
|
// in unit tests we skip
|
||||||
|
if (this.router?.transitionTo) {
|
||||||
|
this.router.transitionTo({ queryParams: { view: info.view.type } });
|
||||||
|
}
|
||||||
|
},
|
||||||
eventPositioned: (info) => {
|
eventPositioned: (info) => {
|
||||||
if (siteSettings.events_max_rows === 0) {
|
if (siteSettings.events_max_rows === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fcContent = info.el.querySelector(".fc-content");
|
let fcContent = info.el.querySelector(".fc-content");
|
||||||
|
|
||||||
|
if (!fcContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let computedStyle = window.getComputedStyle(fcContent);
|
let computedStyle = window.getComputedStyle(fcContent);
|
||||||
let lineHeight = parseInt(computedStyle.lineHeight, 10);
|
let lineHeight = parseInt(computedStyle.lineHeight, 10);
|
||||||
|
|
||||||
|
|
@ -78,7 +110,7 @@ export default class UpcomingEventsCalendar extends Component {
|
||||||
|
|
||||||
const tagsColorsMap = JSON.parse(siteSettings.map_events_to_color);
|
const tagsColorsMap = JSON.parse(siteSettings.map_events_to_color);
|
||||||
|
|
||||||
const resolvedEvents = await this.events;
|
const resolvedEvents = await this.args.controller.model;
|
||||||
const originalEventAndRecurrents = addRecurrentEvents(resolvedEvents);
|
const originalEventAndRecurrents = addRecurrentEvents(resolvedEvents);
|
||||||
|
|
||||||
(originalEventAndRecurrents || []).forEach((event) => {
|
(originalEventAndRecurrents || []).forEach((event) => {
|
||||||
|
|
@ -143,6 +175,31 @@ export default class UpcomingEventsCalendar extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="upcoming-events-calendar"></div>
|
{{#if this.currentUser}}
|
||||||
|
<ul class="events-filter nav nav-pills">
|
||||||
|
<li>
|
||||||
|
<LinkTo
|
||||||
|
@route="discourse-post-event-upcoming-events.index"
|
||||||
|
class="btn-small"
|
||||||
|
>
|
||||||
|
{{i18n "discourse_post_event.upcoming_events.all_events"}}
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<LinkTo
|
||||||
|
@route="discourse-post-event-upcoming-events.mine"
|
||||||
|
class="btn-small"
|
||||||
|
>
|
||||||
|
{{i18n "discourse_post_event.upcoming_events.my_events"}}
|
||||||
|
</LinkTo>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="upcoming-events-calendar"
|
||||||
|
{{didInsert this.renderCalendar}}
|
||||||
|
{{willDestroy this.teardown}}
|
||||||
|
></div>
|
||||||
</template>
|
</template>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
|
|
||||||
export default class DiscoursePostEventUpcomingEventsIndexController extends Controller {}
|
export default class DiscoursePostEventUpcomingEventsIndexController extends Controller {
|
||||||
|
queryParams = ["view"];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Controller from "@ember/controller";
|
||||||
|
|
||||||
|
export default class DiscoursePostEventUpcomingEventsMineController extends Controller {
|
||||||
|
queryParams = ["view"];
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ export default function () {
|
||||||
{ path: "/upcoming-events" },
|
{ path: "/upcoming-events" },
|
||||||
function () {
|
function () {
|
||||||
this.route("index", { path: "/" });
|
this.route("index", { path: "/" });
|
||||||
|
this.route("mine");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@ export function buildParams(startsAt, endsAt, event, siteSettings) {
|
||||||
params.name = event.name;
|
params.name = event.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.location) {
|
||||||
|
params.location = event.location;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.description) {
|
||||||
|
params.description = event.description;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.url) {
|
if (event.url) {
|
||||||
params.url = event.url;
|
params.url = event.url;
|
||||||
}
|
}
|
||||||
|
|
@ -92,12 +100,16 @@ export function buildParams(startsAt, endsAt, event, siteSettings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceRaw(params, raw) {
|
export function replaceRaw(params, raw) {
|
||||||
const eventRegex = new RegExp(`\\[event\\s(.*?)\\]`, "m");
|
const eventRegex = /\[event (.*?)\](.*?)\[\/event\]/s;
|
||||||
const eventMatches = raw.match(eventRegex);
|
const eventMatches = raw.match(eventRegex);
|
||||||
|
|
||||||
if (eventMatches && eventMatches[1]) {
|
if (eventMatches && eventMatches[1]) {
|
||||||
const markdownParams = [];
|
const markdownParams = [];
|
||||||
|
|
||||||
|
let description = params.description;
|
||||||
|
description = description ? `${description}\n` : "";
|
||||||
|
delete params.description;
|
||||||
|
|
||||||
Object.keys(params).forEach((param) => {
|
Object.keys(params).forEach((param) => {
|
||||||
const value = params[param];
|
const value = params[param];
|
||||||
if (value && value.length) {
|
if (value && value.length) {
|
||||||
|
|
@ -105,7 +117,10 @@ export function replaceRaw(params, raw) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return raw.replace(eventRegex, `[event ${markdownParams.join(" ")}]`);
|
return raw.replace(
|
||||||
|
eventRegex,
|
||||||
|
`[event ${markdownParams.join(" ")}]\n${description}[/event]`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ export default class DiscoursePostEventEvent {
|
||||||
@tracked startsAt;
|
@tracked startsAt;
|
||||||
@tracked endsAt;
|
@tracked endsAt;
|
||||||
@tracked rawInvitees;
|
@tracked rawInvitees;
|
||||||
|
@tracked location;
|
||||||
@tracked url;
|
@tracked url;
|
||||||
|
@tracked description;
|
||||||
@tracked timezone;
|
@tracked timezone;
|
||||||
@tracked showLocalTime;
|
@tracked showLocalTime;
|
||||||
@tracked status;
|
@tracked status;
|
||||||
|
|
@ -63,7 +65,9 @@ export default class DiscoursePostEventEvent {
|
||||||
this.endsAt = args.ends_at;
|
this.endsAt = args.ends_at;
|
||||||
this.rawInvitees = args.raw_invitees;
|
this.rawInvitees = args.raw_invitees;
|
||||||
this.sampleInvitees = args.sample_invitees || [];
|
this.sampleInvitees = args.sample_invitees || [];
|
||||||
|
this.location = args.location;
|
||||||
this.url = args.url;
|
this.url = args.url;
|
||||||
|
this.description = args.description;
|
||||||
this.timezone = args.timezone;
|
this.timezone = args.timezone;
|
||||||
this.showLocalTime = args.show_local_time;
|
this.showLocalTime = args.show_local_time;
|
||||||
this.status = args.status;
|
this.status = args.status;
|
||||||
|
|
@ -145,9 +149,11 @@ export default class DiscoursePostEventEvent {
|
||||||
this.name = event.name;
|
this.name = event.name;
|
||||||
this.startsAt = event.startsAt;
|
this.startsAt = event.startsAt;
|
||||||
this.endsAt = event.endsAt;
|
this.endsAt = event.endsAt;
|
||||||
|
this.location = event.location;
|
||||||
this.url = event.url;
|
this.url = event.url;
|
||||||
this.timezone = event.timezone;
|
this.timezone = event.timezone;
|
||||||
this.showLocalTime = event.showLocalTime;
|
this.showLocalTime = event.showLocalTime;
|
||||||
|
this.description = event.description;
|
||||||
this.status = event.status;
|
this.status = event.status;
|
||||||
this.creator = event.creator;
|
this.creator = event.creator;
|
||||||
this.isClosed = event.isClosed;
|
this.isClosed = event.isClosed;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import DiscourseURL from "discourse/lib/url";
|
||||||
import DiscourseRoute from "discourse/routes/discourse";
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
export default class PostEventUpcomingEventsIndexRoute extends DiscourseRoute {
|
export default class PostEventUpcomingEventsIndexRoute extends DiscourseRoute {
|
||||||
@service discoursePostEventApi;
|
|
||||||
@service discoursePostEventService;
|
@service discoursePostEventService;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { service } from "@ember/service";
|
||||||
|
import DiscourseURL from "discourse/lib/url";
|
||||||
|
import DiscourseRoute from "discourse/routes/discourse";
|
||||||
|
|
||||||
|
export default class PostEventUpcomingEventsIndexRoute extends DiscourseRoute {
|
||||||
|
@service discoursePostEventApi;
|
||||||
|
@service discoursePostEventService;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
@action
|
||||||
|
activate() {
|
||||||
|
if (!this.siteSettings.discourse_post_event_enabled) {
|
||||||
|
DiscourseURL.redirectTo("/404");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async model(params) {
|
||||||
|
params.attending_user = this.currentUser?.username;
|
||||||
|
return await this.discoursePostEventService.fetchEvents(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import UpcomingEventsCalendar from "../components/upcoming-events-calendar";
|
||||||
export default RouteTemplate(
|
export default RouteTemplate(
|
||||||
<template>
|
<template>
|
||||||
<div class="discourse-post-event-upcoming-events">
|
<div class="discourse-post-event-upcoming-events">
|
||||||
<UpcomingEventsCalendar @events={{@controller.model}} />
|
<UpcomingEventsCalendar @controller={{@controller}} />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import RouteTemplate from "ember-route-template";
|
||||||
|
import UpcomingEventsCalendar from "../components/upcoming-events-calendar";
|
||||||
|
|
||||||
|
export default RouteTemplate(
|
||||||
|
<template>
|
||||||
|
<div class="discourse-post-event-upcoming-events">
|
||||||
|
<UpcomingEventsCalendar @controller={{@controller}} />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
);
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
$interested: #fb985d;
|
$interested: #fb985d;
|
||||||
$show-interested: inherit;
|
$show-interested: inherit;
|
||||||
|
|
||||||
|
.cooked > .discourse-post-event {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.discourse-post-event {
|
.discourse-post-event {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.event__section {
|
.event__section {
|
||||||
padding: 0.5em 1em;
|
padding: 0.5em 0.75rem;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-top: 1px solid var(--primary-low);
|
border-top: 1px solid var(--primary-low);
|
||||||
|
|
@ -176,8 +180,6 @@ $show-interested: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.event-creator {
|
.event-creator {
|
||||||
margin-left: 0.25em;
|
|
||||||
|
|
||||||
.topic-invitee-avatar {
|
.topic-invitee-avatar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -348,6 +350,11 @@ $show-interested: inherit;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-description {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-location,
|
||||||
.event-url,
|
.event-url,
|
||||||
.event-dates,
|
.event-dates,
|
||||||
.event-chat-channel,
|
.event-chat-channel,
|
||||||
|
|
@ -400,6 +407,7 @@ $show-interested: inherit;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
color: var(--primary-high);
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-invitees-icon .going {
|
.event-invitees-icon .going {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,10 @@
|
||||||
width: 550px;
|
width: 550px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conditional-loading-section {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
|
|
||||||
|
|
@ -107,6 +111,13 @@
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.description {
|
||||||
|
textarea {
|
||||||
|
border-radius: var(--d-input-border-radius);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.name,
|
&.name,
|
||||||
&.url {
|
&.url {
|
||||||
input {
|
input {
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,8 @@ en:
|
||||||
creator: "Creator"
|
creator: "Creator"
|
||||||
status: "Status"
|
status: "Status"
|
||||||
starts_at: "Starts at"
|
starts_at: "Starts at"
|
||||||
|
all_events: "All events"
|
||||||
|
my_events: "My events"
|
||||||
upcoming_events_list:
|
upcoming_events_list:
|
||||||
title: "Upcoming events"
|
title: "Upcoming events"
|
||||||
empty: "No upcoming events"
|
empty: "No upcoming events"
|
||||||
|
|
@ -472,6 +474,12 @@ en:
|
||||||
url:
|
url:
|
||||||
label: "URL"
|
label: "URL"
|
||||||
placeholder: "Optional"
|
placeholder: "Optional"
|
||||||
|
location:
|
||||||
|
label: "Location"
|
||||||
|
placeholder: "Add a location, link or something."
|
||||||
|
description:
|
||||||
|
label: "Description"
|
||||||
|
placeholder: "Tell people a little bit more about your event. New lines and links are supported."
|
||||||
name:
|
name:
|
||||||
label: "Event name"
|
label: "Event name"
|
||||||
placeholder: "Optional, defaults to topic title"
|
placeholder: "Optional, defaults to topic title"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ DiscoursePostEvent::Engine.routes.draw do
|
||||||
get "/discourse-post-event/events/:post_id/invitees" => "invitees#index"
|
get "/discourse-post-event/events/:post_id/invitees" => "invitees#index"
|
||||||
delete "/discourse-post-event/events/:post_id/invitees/:id" => "invitees#destroy"
|
delete "/discourse-post-event/events/:post_id/invitees/:id" => "invitees#destroy"
|
||||||
get "/upcoming-events" => "upcoming_events#index"
|
get "/upcoming-events" => "upcoming_events#index"
|
||||||
|
get "/upcoming-events/mine" => "upcoming_events#index"
|
||||||
end
|
end
|
||||||
|
|
||||||
Discourse::Application.routes.draw do
|
Discourse::Application.routes.draw do
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddLocationToEvent < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :discourse_post_event_events, :location, :string, limit: 1000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddDescriptionToEvent < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :discourse_post_event_events, :description, :string, limit: 1000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -33,6 +33,33 @@ module DiscoursePostEvent
|
||||||
|
|
||||||
events = events.where(id: Array(params[:post_id])) if params[:post_id]
|
events = events.where(id: Array(params[:post_id])) if params[:post_id]
|
||||||
|
|
||||||
|
if params[:attending_user].present?
|
||||||
|
attending_user = User.find_by(username_lower: params[:attending_user].downcase)
|
||||||
|
if attending_user
|
||||||
|
events =
|
||||||
|
events.joins(:invitees).where(
|
||||||
|
discourse_post_event_invitees: {
|
||||||
|
user_id: attending_user.id,
|
||||||
|
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if !guardian.is_admin?
|
||||||
|
events =
|
||||||
|
events.where(
|
||||||
|
"discourse_post_event_events.status != ? OR discourse_post_event_events.status = ? AND EXISTS (
|
||||||
|
SELECT 1 FROM discourse_post_event_invitees dpoei
|
||||||
|
WHERE dpoei.post_id = discourse_post_event_events.id
|
||||||
|
AND dpoei.user_id = ?
|
||||||
|
)",
|
||||||
|
DiscoursePostEvent::Event.statuses[:private],
|
||||||
|
DiscoursePostEvent::Event.statuses[:private],
|
||||||
|
user&.id,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if params[:before].present?
|
if params[:before].present?
|
||||||
events = events.where("dcped.starts_at < ?", params[:before].to_datetime)
|
events = events.where("dcped.starts_at < ?", params[:before].to_datetime)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ module DiscoursePostEvent
|
||||||
:status,
|
:status,
|
||||||
:"allowed-groups",
|
:"allowed-groups",
|
||||||
:url,
|
:url,
|
||||||
|
:location,
|
||||||
:name,
|
:name,
|
||||||
:reminders,
|
:reminders,
|
||||||
:recurrence,
|
:recurrence,
|
||||||
|
|
@ -59,6 +60,7 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
event[:description] = doc.text.strip if event
|
||||||
event
|
event
|
||||||
end
|
end
|
||||||
.compact
|
.compact
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ register_svg_icon "clock"
|
||||||
register_svg_icon "file-csv"
|
register_svg_icon "file-csv"
|
||||||
register_svg_icon "star"
|
register_svg_icon "star"
|
||||||
register_svg_icon "file-arrow-up"
|
register_svg_icon "file-arrow-up"
|
||||||
|
register_svg_icon "location-pin"
|
||||||
|
|
||||||
module ::DiscourseCalendar
|
module ::DiscourseCalendar
|
||||||
PLUGIN_NAME = "discourse-calendar"
|
PLUGIN_NAME = "discourse-calendar"
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,62 @@ describe DiscoursePostEvent::EventFinder do
|
||||||
Group.refresh_automatic_groups!
|
Group.refresh_automatic_groups!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "by attending user" do
|
||||||
|
fab!(:attending_user) { Fabricate(:user) }
|
||||||
|
fab!(:public_event) { Fabricate(:event, status: DiscoursePostEvent::Event.statuses[:public]) }
|
||||||
|
fab!(:private_event) { Fabricate(:event, status: DiscoursePostEvent::Event.statuses[:private]) }
|
||||||
|
fab!(:another_event) { Fabricate(:event, status: DiscoursePostEvent::Event.statuses[:public]) }
|
||||||
|
|
||||||
|
fab!(:attending_public_event) do
|
||||||
|
DiscoursePostEvent::Invitee.create!(
|
||||||
|
user: attending_user,
|
||||||
|
event: public_event,
|
||||||
|
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:attending_private_event) do
|
||||||
|
DiscoursePostEvent::Invitee.create!(
|
||||||
|
user: attending_user,
|
||||||
|
event: private_event,
|
||||||
|
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
fab!(:not_attending_event) do
|
||||||
|
DiscoursePostEvent::Invitee.create!(
|
||||||
|
user: attending_user,
|
||||||
|
event: another_event,
|
||||||
|
status: DiscoursePostEvent::Invitee.statuses[:not_going],
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns only events the user is attending" do
|
||||||
|
expect(
|
||||||
|
finder.search(current_user, { attending_user: attending_user.username }),
|
||||||
|
).to match_array([public_event])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "includes private events for admin users" do
|
||||||
|
current_user.update!(admin: true)
|
||||||
|
expect(
|
||||||
|
finder.search(current_user, { attending_user: attending_user.username }),
|
||||||
|
).to match_array([public_event, private_event])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "includes private events if the searching user is also invited" do
|
||||||
|
DiscoursePostEvent::Invitee.create!(
|
||||||
|
user: current_user,
|
||||||
|
event: private_event,
|
||||||
|
status: DiscoursePostEvent::Invitee.statuses[:going],
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
finder.search(current_user, { attending_user: attending_user.username }),
|
||||||
|
).to match_array([public_event, private_event])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when the event is associated to a visible post" do
|
context "when the event is associated to a visible post" do
|
||||||
let(:post1) do
|
let(:post1) do
|
||||||
PostCreator.create!(
|
PostCreator.create!(
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,14 @@ module PageObjects
|
||||||
module Pages
|
module Pages
|
||||||
module DiscourseCalendar
|
module DiscourseCalendar
|
||||||
class PostEvent < PageObjects::Pages::Base
|
class PostEvent < PageObjects::Pages::Base
|
||||||
|
TRIGGER_MENU_SELECTOR = ".discourse-post-event-more-menu-trigger"
|
||||||
def open_more_menu
|
def open_more_menu
|
||||||
find(".discourse-post-event-more-menu-trigger").click
|
find(TRIGGER_MENU_SELECTOR).click
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def going
|
||||||
|
find(".going-button").click
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -14,6 +20,39 @@ module PageObjects
|
||||||
find(".dropdown-menu__item.bulk-invite").click
|
find(".dropdown-menu__item.bulk-invite").click
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_location?(text)
|
||||||
|
has_css?(".event-location", text:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_description?(text)
|
||||||
|
has_css?(".event-description", text:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
has_css?(".discourse-post-event .status-and-creators .status:not(.closed)")
|
||||||
|
open_more_menu
|
||||||
|
find(".close-event").click
|
||||||
|
find("#dialog-holder .btn-primary").click
|
||||||
|
has_css?(".discourse-post-event .status-and-creators .status.closed")
|
||||||
|
has_no_css?("#{TRIGGER_MENU_SELECTOR}.--saving")
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def open
|
||||||
|
has_css?(".discourse-post-event .status-and-creators .status.closed")
|
||||||
|
open_more_menu
|
||||||
|
find(".open-event").click
|
||||||
|
find("#dialog-holder .btn-primary").click
|
||||||
|
has_css?(".discourse-post-event .status-and-creators .status:not(.closed)")
|
||||||
|
has_no_css?("#{TRIGGER_MENU_SELECTOR}.--saving")
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
open_more_menu
|
||||||
|
find(".edit-event").click
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Pages
|
||||||
|
module DiscourseCalendar
|
||||||
|
class PostEventForm < PageObjects::Pages::Base
|
||||||
|
MODAL_SELECTOR = ".post-event-builder-modal"
|
||||||
|
|
||||||
|
def fill_location(with)
|
||||||
|
form.find(".event-field.location input").fill_in(with:)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def fill_description(with)
|
||||||
|
form.find(".event-field.description textarea").fill_in(with:)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def form
|
||||||
|
modal.find("form")
|
||||||
|
end
|
||||||
|
|
||||||
|
def modal
|
||||||
|
find(MODAL_SELECTOR)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submit
|
||||||
|
modal.find(".d-modal__footer .btn-primary").click
|
||||||
|
has_no_selector?(MODAL_SELECTOR)
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
describe "Post event", type: :system do
|
describe "Post event", type: :system do
|
||||||
fab!(:admin)
|
fab!(:admin)
|
||||||
fab!(:user) { Fabricate(:admin, username: "jane") }
|
fab!(:user) { Fabricate(:admin) }
|
||||||
fab!(:group) { Fabricate(:group, name: "test_group") }
|
fab!(:group)
|
||||||
|
|
||||||
let(:composer) { PageObjects::Components::Composer.new }
|
let(:composer) { PageObjects::Components::Composer.new }
|
||||||
let(:post_event_page) { PageObjects::Pages::DiscourseCalendar::PostEvent.new }
|
let(:post_event_page) { PageObjects::Pages::DiscourseCalendar::PostEvent.new }
|
||||||
|
let(:post_event_form_page) { PageObjects::Pages::DiscourseCalendar::PostEventForm.new }
|
||||||
let(:bulk_invite_modal_page) { PageObjects::Pages::DiscourseCalendar::BulkInviteModal.new }
|
let(:bulk_invite_modal_page) { PageObjects::Pages::DiscourseCalendar::BulkInviteModal.new }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
@ -15,6 +17,50 @@ describe "Post event", type: :system do
|
||||||
sign_in(admin)
|
sign_in(admin)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with location" do
|
||||||
|
it "can save a location" do
|
||||||
|
post =
|
||||||
|
PostCreator.create(
|
||||||
|
admin,
|
||||||
|
title: "My test meetup event",
|
||||||
|
raw: "[event start='2222-02-22 14:22']\n[/event]",
|
||||||
|
)
|
||||||
|
|
||||||
|
visit(post.topic.url)
|
||||||
|
post_event_page.edit
|
||||||
|
post_event_form_page.fill_location("123 Main St, Brisbane, Australia http://example.com")
|
||||||
|
post_event_form_page.submit
|
||||||
|
|
||||||
|
expect(post_event_page).to have_location(
|
||||||
|
"123 Main St, Brisbane, Australia http://example.com",
|
||||||
|
)
|
||||||
|
expect(page).to have_css(".event-location a[href='http://example.com']")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with description" do
|
||||||
|
it "can save a description" do
|
||||||
|
post =
|
||||||
|
PostCreator.create(
|
||||||
|
admin,
|
||||||
|
title: "My test meetup event",
|
||||||
|
raw: "[event start='2222-02-22 14:22']\n[/event]",
|
||||||
|
)
|
||||||
|
|
||||||
|
visit(post.topic.url)
|
||||||
|
post_event_page.edit
|
||||||
|
post_event_form_page.fill_description(
|
||||||
|
"this is a test description\n and a link http://example.com",
|
||||||
|
)
|
||||||
|
post_event_form_page.submit
|
||||||
|
|
||||||
|
expect(post_event_page).to have_description(
|
||||||
|
%r{this is a test description\s+and a link http://example.com},
|
||||||
|
)
|
||||||
|
expect(page).to have_css(".event-description a[href='http://example.com']")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when showing local time", timezone: "Australia/Brisbane" do
|
context "when showing local time", timezone: "Australia/Brisbane" do
|
||||||
it "correctly shows month/day" do
|
it "correctly shows month/day" do
|
||||||
page.driver.with_playwright_page do |pw_page|
|
page.driver.with_playwright_page do |pw_page|
|
||||||
|
|
@ -67,43 +113,25 @@ describe "Post event", type: :system do
|
||||||
visit "/new-topic"
|
visit "/new-topic"
|
||||||
title = "My upcoming l33t event"
|
title = "My upcoming l33t event"
|
||||||
tomorrow = (Time.zone.now + 1.day).strftime("%Y-%m-%d")
|
tomorrow = (Time.zone.now + 1.day).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
composer.fill_title(title)
|
composer.fill_title(title)
|
||||||
|
|
||||||
composer.fill_content <<~MD
|
composer.fill_content <<~MD
|
||||||
[event start="#{tomorrow} 13:37" status="public"]
|
[event start="#{tomorrow} 13:37" status="public"]
|
||||||
[/event]
|
[/event]
|
||||||
MD
|
MD
|
||||||
|
|
||||||
composer.submit
|
composer.submit
|
||||||
|
|
||||||
expect(page).to have_content(title)
|
expect(page).to have_content(title)
|
||||||
|
|
||||||
find(".more-dropdown").click
|
post_event_page.close
|
||||||
find(".close-event").click
|
post_event_page.open
|
||||||
find("#dialog-holder .btn-primary").click
|
post_event_page.going.open_more_menu
|
||||||
|
|
||||||
expect(page).to have_css(".discourse-post-event .status-and-creators .status.closed")
|
|
||||||
|
|
||||||
# click on a different button to ensure more dropdown is collapsed before reopening
|
|
||||||
find(".btn-primary.create").click
|
|
||||||
find(".more-dropdown").click
|
|
||||||
find(".open-event").click
|
|
||||||
find("#dialog-holder .btn-primary").click
|
|
||||||
|
|
||||||
expect(page).to have_css(".going-button")
|
|
||||||
|
|
||||||
find(".going-button").click
|
|
||||||
find(".discourse-post-event-more-menu-trigger").click
|
|
||||||
find(".show-all-participants").click
|
find(".show-all-participants").click
|
||||||
find(".d-modal input.filter").fill_in(with: "jan")
|
find(".d-modal input.filter").fill_in(with: user.username)
|
||||||
find(".d-modal .add-invitee").click
|
find(".d-modal .add-invitee").click
|
||||||
|
|
||||||
topic_page = PageObjects::Pages::Topic.new
|
topic_page = PageObjects::Pages::Topic.new
|
||||||
|
|
||||||
try_until_success do
|
try_until_success do
|
||||||
topic = Topic.find(topic_page.current_topic_id)
|
topic = Topic.find(topic_page.current_topic_id)
|
||||||
|
|
||||||
event = topic.posts.first.event
|
event = topic.posts.first.event
|
||||||
|
|
||||||
expect(event.invitees.count).to eq(2)
|
expect(event.invitees.count).to eq(2)
|
||||||
|
|
@ -117,9 +145,9 @@ describe "Post event", type: :system do
|
||||||
title: "My test meetup event",
|
title: "My test meetup event",
|
||||||
raw: "[event name='cool-event' status='standalone' start='2222-02-22 00:00' ]\n[/event]",
|
raw: "[event name='cool-event' status='standalone' start='2222-02-22 00:00' ]\n[/event]",
|
||||||
)
|
)
|
||||||
|
|
||||||
visit(post.topic.url)
|
visit(post.topic.url)
|
||||||
page.find(".discourse-post-event-more-menu-trigger").click
|
post_event_page.open_more_menu
|
||||||
|
|
||||||
expect(page).to have_no_css(".show-all-participants")
|
expect(page).to have_no_css(".show-all-participants")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -132,7 +160,8 @@ describe "Post event", type: :system do
|
||||||
)
|
)
|
||||||
|
|
||||||
visit(post.topic.url)
|
visit(post.topic.url)
|
||||||
page.find(".discourse-post-event-more-menu-trigger").click
|
post_event_page.going.open_more_menu
|
||||||
|
|
||||||
expect(page).to have_no_css(".send-pm-to-creator")
|
expect(page).to have_no_css(".send-pm-to-creator")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -143,7 +172,7 @@ describe "Post event", type: :system do
|
||||||
dropdown.expand
|
dropdown.expand
|
||||||
dropdown.select_row_by_name(I18n.t("js.discourse_post_event.builder_modal.attach"))
|
dropdown.select_row_by_name(I18n.t("js.discourse_post_event.builder_modal.attach"))
|
||||||
find(".d-modal input[name=status][value=private]").click
|
find(".d-modal input[name=status][value=private]").click
|
||||||
find(".d-modal input.group-selector").send_keys("test_")
|
find(".d-modal input.group-selector").send_keys(group.name)
|
||||||
find(".autocomplete.ac-group").click
|
find(".autocomplete.ac-group").click
|
||||||
find(".d-modal .custom-field-input").fill_in(with: "custom value")
|
find(".d-modal .custom-field-input").fill_in(with: "custom value")
|
||||||
dropdown = PageObjects::Components::SelectKit.new(".available-recurrences")
|
dropdown = PageObjects::Components::SelectKit.new(".available-recurrences")
|
||||||
|
|
@ -155,11 +184,10 @@ describe "Post event", type: :system do
|
||||||
|
|
||||||
expect(page).to have_css(".discourse-post-event.is-loaded")
|
expect(page).to have_css(".discourse-post-event.is-loaded")
|
||||||
|
|
||||||
find(".discourse-post-event-more-menu-trigger").click
|
post_event_page.edit
|
||||||
find(".edit-event").click
|
|
||||||
|
|
||||||
expect(find(".d-modal input[name=status][value=private]").checked?).to eq(true)
|
expect(find(".d-modal input[name=status][value=private]").checked?).to eq(true)
|
||||||
expect(find(".d-modal")).to have_text("test_group")
|
expect(find(".d-modal")).to have_text(group.name)
|
||||||
expect(find(".d-modal .custom-field-input").value).to eq("custom value")
|
expect(find(".d-modal .custom-field-input").value).to eq("custom value")
|
||||||
expect(page).to have_selector(".d-modal .recurrence-until .date-picker") do |input|
|
expect(page).to have_selector(".d-modal .recurrence-until .date-picker") do |input|
|
||||||
input.value == "#{1.year.from_now.year}-12-30"
|
input.value == "#{1.year.from_now.year}-12-30"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { replaceRaw } from "discourse/plugins/discourse-calendar/discourse/lib/r
|
||||||
|
|
||||||
module("Unit | Lib | raw-event-helper", function () {
|
module("Unit | Lib | raw-event-helper", function () {
|
||||||
test("replaceRaw", function (assert) {
|
test("replaceRaw", function (assert) {
|
||||||
const raw = 'Some text [event param1="value1"] more text';
|
const raw = 'Some text \n[event param1="va]lue1"]\n[/event]\n more text';
|
||||||
const params = {
|
const params = {
|
||||||
param1: "newValue1",
|
param1: "newValue1",
|
||||||
param2: "value2",
|
param2: "value2",
|
||||||
|
|
@ -11,7 +11,7 @@ module("Unit | Lib | raw-event-helper", function () {
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
replaceRaw(params, raw),
|
replaceRaw(params, raw),
|
||||||
'Some text [event param1="newValue1" param2="value2"] more text',
|
'Some text \n[event param1="newValue1" param2="value2"]\n[/event]\n more text',
|
||||||
"updates existing parameters and adds new ones"
|
"updates existing parameters and adds new ones"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -21,14 +21,14 @@ module("Unit | Lib | raw-event-helper", function () {
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
replaceRaw({ foo: 'bar"quoted' }, '[event original="value"]'),
|
replaceRaw({ foo: 'bar"quoted' }, '[event original="value"]\n[/event]'),
|
||||||
'[event foo="barquoted"]',
|
'[event foo="barquoted"]\n[/event]',
|
||||||
"escapes double quotes in parameter values"
|
"escapes double quotes in parameter values"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
replaceRaw({}, '[event param1="value1"]'),
|
replaceRaw({}, '[event param1="value1"]\n[/event]'),
|
||||||
"[event ]",
|
"[event ]\n[/event]",
|
||||||
"handles empty params object"
|
"handles empty params object"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue