FEATURE: customizable event colors by tags and adjustable max rows in calendar (#482)
* FEATURE: customizable event colors by tags and adjustable max rows
This commit is contained in:
parent
07ff8f4f1b
commit
d27e52e340
2
.streerc
2
.streerc
|
@ -1,3 +1,3 @@
|
|||
--print-width=100
|
||||
--plugins=plugin/trailing_comma,plugin/disable_auto_ternary
|
||||
--ignore-files=vendor/*
|
||||
--ignore-files=vendor/*
|
||||
|
|
|
@ -51,4 +51,4 @@ DEPENDENCIES
|
|||
syntax_tree
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.4.22
|
||||
|
|
|
@ -14,7 +14,7 @@ module DiscoursePostEvent
|
|||
# lightweight post object containing
|
||||
# only needed info for client
|
||||
def post
|
||||
{
|
||||
post_hash = {
|
||||
id: object.post.id,
|
||||
post_number: object.post.post_number,
|
||||
url: object.post.url,
|
||||
|
@ -23,6 +23,13 @@ module DiscoursePostEvent
|
|||
title: object.post.topic.title,
|
||||
},
|
||||
}
|
||||
|
||||
if JSON.parse(SiteSetting.map_events_to_color).size > 0
|
||||
post_hash[:topic][:category_slug] = object.post.topic&.category&.slug
|
||||
post_hash[:topic][:tags] = object.post.topic.tags&.map(&:name)
|
||||
end
|
||||
|
||||
post_hash
|
||||
end
|
||||
|
||||
def category_id
|
||||
|
|
|
@ -5,6 +5,7 @@ import loadScript from "discourse/lib/load-script";
|
|||
import getURL from "discourse-common/lib/get-url";
|
||||
import { formatEventName } from "../helpers/format-event-name";
|
||||
import { isNotFullDayEvent } from "../lib/guess-best-date-format";
|
||||
import { buildPopover, destroyPopover } from "../lib/popover";
|
||||
|
||||
export default Component.extend({
|
||||
tagName: "",
|
||||
|
@ -46,6 +47,8 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
_renderCalendar() {
|
||||
const siteSettings = this.site.siteSettings;
|
||||
|
||||
const calendarNode = document.getElementById("upcoming-events-calendar");
|
||||
if (!calendarNode) {
|
||||
return;
|
||||
|
@ -54,7 +57,47 @@ export default Component.extend({
|
|||
calendarNode.innerHTML = "";
|
||||
|
||||
this._loadCalendar().then(() => {
|
||||
this._calendar = new window.FullCalendar.Calendar(calendarNode, {});
|
||||
const fullCalendar = new window.FullCalendar.Calendar(calendarNode, {
|
||||
eventClick: function () {
|
||||
destroyPopover();
|
||||
},
|
||||
eventPositioned: (info) => {
|
||||
if (siteSettings.events_max_rows === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fcContent = info.el.querySelector(".fc-content");
|
||||
let computedStyle = window.getComputedStyle(fcContent);
|
||||
let lineHeight = parseInt(computedStyle.lineHeight, 10);
|
||||
|
||||
if (lineHeight === 0) {
|
||||
lineHeight = 20;
|
||||
}
|
||||
let maxHeight = lineHeight * siteSettings.events_max_rows;
|
||||
|
||||
if (fcContent) {
|
||||
fcContent.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
|
||||
let fcTitle = info.el.querySelector(".fc-title");
|
||||
if (fcTitle) {
|
||||
fcTitle.style.overflow = "hidden";
|
||||
fcTitle.style.whiteSpace = "pre-wrap";
|
||||
}
|
||||
fullCalendar.updateSize();
|
||||
},
|
||||
eventMouseEnter: function ({ event, jsEvent }) {
|
||||
destroyPopover();
|
||||
const htmlContent = event.title;
|
||||
buildPopover(jsEvent, htmlContent);
|
||||
},
|
||||
eventMouseLeave: function () {
|
||||
destroyPopover();
|
||||
},
|
||||
});
|
||||
this._calendar = fullCalendar;
|
||||
|
||||
const tagsColorsMap = JSON.parse(siteSettings.map_events_to_color);
|
||||
|
||||
const originalEventAndRecurrents = this.addRecurrentEvents(
|
||||
this.events.content
|
||||
|
@ -62,8 +105,28 @@ export default Component.extend({
|
|||
|
||||
(originalEventAndRecurrents || []).forEach((event) => {
|
||||
const { starts_at, ends_at, post, category_id } = event;
|
||||
const categoryColor = this.site.categoriesById[category_id]?.color;
|
||||
const backgroundColor = categoryColor ? `#${categoryColor}` : undefined;
|
||||
|
||||
let backgroundColor;
|
||||
|
||||
if (post.topic.tags) {
|
||||
const tagColorEntry = tagsColorsMap.find(
|
||||
(entry) =>
|
||||
entry.type === "tag" && post.topic.tags.includes(entry.slug)
|
||||
);
|
||||
backgroundColor = tagColorEntry ? tagColorEntry.color : null;
|
||||
}
|
||||
|
||||
if (!backgroundColor) {
|
||||
const categoryColorFromMap = tagsColorsMap.find(
|
||||
(entry) =>
|
||||
entry.type === "category" &&
|
||||
entry.slug === post.topic.category_slug
|
||||
)?.color;
|
||||
backgroundColor =
|
||||
categoryColorFromMap ||
|
||||
`#${this.site.categoriesById[category_id]?.color}`;
|
||||
}
|
||||
|
||||
this._calendar.addEvent({
|
||||
title: formatEventName(event),
|
||||
start: starts_at,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { isPresent } from "@ember/utils";
|
||||
import { createPopper } from "@popperjs/core";
|
||||
import $ from "jquery";
|
||||
import { Promise } from "rsvp";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
@ -15,6 +14,7 @@ import I18n from "I18n";
|
|||
import { formatEventName } from "../helpers/format-event-name";
|
||||
import { colorToHex, contrastColor, stringToColor } from "../lib/colors";
|
||||
import { isNotFullDayEvent } from "../lib/guess-best-date-format";
|
||||
import { buildPopover, destroyPopover } from "../lib/popover";
|
||||
|
||||
function loadFullCalendar() {
|
||||
return loadScript(
|
||||
|
@ -36,9 +36,6 @@ function getCalendarButtonsText() {
|
|||
};
|
||||
}
|
||||
|
||||
let eventPopper;
|
||||
const EVENT_POPOVER_ID = "event-popover";
|
||||
|
||||
function initializeDiscourseCalendar(api) {
|
||||
const siteSettings = api.container.lookup("service:site-settings");
|
||||
|
||||
|
@ -140,8 +137,44 @@ function initializeDiscourseCalendar(api) {
|
|||
let fullCalendar = new window.FullCalendar.Calendar(
|
||||
categoryEventNode,
|
||||
{
|
||||
eventClick: function () {
|
||||
destroyPopover();
|
||||
},
|
||||
locale: getCurrentBcp47Locale(),
|
||||
buttonText: getCalendarButtonsText(),
|
||||
eventPositioned: (info) => {
|
||||
if (siteSettings.events_max_rows === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fcContent = info.el.querySelector(".fc-content");
|
||||
let computedStyle = window.getComputedStyle(fcContent);
|
||||
let lineHeight = parseInt(computedStyle.lineHeight, 10);
|
||||
|
||||
if (lineHeight === 0) {
|
||||
lineHeight = 20;
|
||||
}
|
||||
let maxHeight = lineHeight * siteSettings.events_max_rows;
|
||||
|
||||
if (fcContent) {
|
||||
fcContent.style.maxHeight = `${maxHeight}px`;
|
||||
}
|
||||
|
||||
let fcTitle = info.el.querySelector(".fc-title");
|
||||
if (fcTitle) {
|
||||
fcTitle.style.overflow = "hidden";
|
||||
fcTitle.style.whiteSpace = "pre-wrap";
|
||||
}
|
||||
fullCalendar.updateSize();
|
||||
},
|
||||
eventMouseEnter: function ({ event, jsEvent }) {
|
||||
destroyPopover();
|
||||
const htmlContent = event.title;
|
||||
buildPopover(jsEvent, htmlContent);
|
||||
},
|
||||
eventMouseLeave: function () {
|
||||
destroyPopover();
|
||||
},
|
||||
}
|
||||
);
|
||||
const loadEvents = ajax(
|
||||
|
@ -151,9 +184,32 @@ function initializeDiscourseCalendar(api) {
|
|||
Promise.all([loadEvents]).then((results) => {
|
||||
const events = results[0];
|
||||
|
||||
const tagsColorsMap = JSON.parse(siteSettings.map_events_to_color);
|
||||
|
||||
events[Object.keys(events)[0]].forEach((event) => {
|
||||
const { starts_at, ends_at, post, category_id } = event;
|
||||
const backgroundColor = `#${site.categoriesById[category_id]?.color}`;
|
||||
|
||||
let backgroundColor;
|
||||
|
||||
if (post.topic.tags) {
|
||||
const tagColorEntry = tagsColorsMap.find(
|
||||
(entry) =>
|
||||
entry.type === "tag" && post.topic.tags.includes(entry.slug)
|
||||
);
|
||||
backgroundColor = tagColorEntry ? tagColorEntry.color : null;
|
||||
}
|
||||
|
||||
if (!backgroundColor) {
|
||||
const categoryColorFromMap = tagsColorsMap.find(
|
||||
(entry) =>
|
||||
entry.type === "category" &&
|
||||
entry.slug === post.topic.category_slug
|
||||
)?.color;
|
||||
backgroundColor =
|
||||
categoryColorFromMap ||
|
||||
`#${site.categoriesById[category_id]?.color}`;
|
||||
}
|
||||
|
||||
fullCalendar.addEvent({
|
||||
title: formatEventName(event),
|
||||
start: starts_at,
|
||||
|
@ -438,41 +494,6 @@ function initializeDiscourseCalendar(api) {
|
|||
});
|
||||
}
|
||||
|
||||
function _buildPopover(jsEvent, htmlContent) {
|
||||
const node = document.createElement("div");
|
||||
node.setAttribute("id", EVENT_POPOVER_ID);
|
||||
node.innerHTML = htmlContent;
|
||||
|
||||
const arrow = document.createElement("span");
|
||||
arrow.dataset.popperArrow = true;
|
||||
node.appendChild(arrow);
|
||||
document.body.appendChild(node);
|
||||
|
||||
eventPopper = createPopper(
|
||||
jsEvent.target,
|
||||
document.getElementById(EVENT_POPOVER_ID),
|
||||
{
|
||||
placement: "bottom",
|
||||
modifiers: [
|
||||
{
|
||||
name: "arrow",
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [20, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function _destroyPopover() {
|
||||
eventPopper?.destroy();
|
||||
document.getElementById(EVENT_POPOVER_ID)?.remove();
|
||||
}
|
||||
|
||||
function _setDynamicCalendarOptions(calendar, $calendar) {
|
||||
const skipWeekends = $calendar.attr("data-weekends") === "false";
|
||||
const hiddenDays = $calendar.attr("data-hidden-days");
|
||||
|
@ -489,7 +510,7 @@ function initializeDiscourseCalendar(api) {
|
|||
}
|
||||
|
||||
calendar.setOption("eventClick", ({ event, jsEvent }) => {
|
||||
_destroyPopover();
|
||||
destroyPopover();
|
||||
const { htmlContent, postNumber, postUrl } = event.extendedProps;
|
||||
|
||||
if (postUrl) {
|
||||
|
@ -499,18 +520,18 @@ function initializeDiscourseCalendar(api) {
|
|||
_topicController || api.container.lookup("controller:topic");
|
||||
_topicController.send("jumpToPost", postNumber);
|
||||
} else if (isMobileView && htmlContent) {
|
||||
_buildPopover(jsEvent, htmlContent);
|
||||
buildPopover(jsEvent, htmlContent);
|
||||
}
|
||||
});
|
||||
|
||||
calendar.setOption("eventMouseEnter", ({ event, jsEvent }) => {
|
||||
_destroyPopover();
|
||||
destroyPopover();
|
||||
const { htmlContent } = event.extendedProps;
|
||||
_buildPopover(jsEvent, htmlContent);
|
||||
buildPopover(jsEvent, htmlContent);
|
||||
});
|
||||
|
||||
calendar.setOption("eventMouseLeave", () => {
|
||||
_destroyPopover();
|
||||
destroyPopover();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { createPopper } from "@popperjs/core";
|
||||
|
||||
let eventPopper;
|
||||
const EVENT_POPOVER_ID = "event-popover";
|
||||
|
||||
export function buildPopover(jsEvent, htmlContent) {
|
||||
const node = document.createElement("div");
|
||||
node.setAttribute("id", EVENT_POPOVER_ID);
|
||||
node.innerHTML = htmlContent;
|
||||
|
||||
const arrow = document.createElement("span");
|
||||
arrow.dataset.popperArrow = true;
|
||||
node.appendChild(arrow);
|
||||
document.body.appendChild(node);
|
||||
|
||||
eventPopper = createPopper(
|
||||
jsEvent.target,
|
||||
document.getElementById(EVENT_POPOVER_ID),
|
||||
{
|
||||
placement: "bottom",
|
||||
modifiers: [
|
||||
{
|
||||
name: "arrow",
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [20, 10],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function destroyPopover() {
|
||||
eventPopper?.destroy();
|
||||
document.getElementById(EVENT_POPOVER_ID)?.remove();
|
||||
}
|
|
@ -119,10 +119,19 @@ export default createWidget("discourse-post-event", {
|
|||
this.state.eventModel.watching_invitee.id,
|
||||
{ status: newStatus, post_id: this.state.eventModel.id }
|
||||
);
|
||||
|
||||
this.appEvents.trigger("calendar:update-invitee-status", {
|
||||
status: newStatus,
|
||||
postId: this.state.eventModel.id,
|
||||
});
|
||||
} else {
|
||||
this.store
|
||||
.createRecord("discourse-post-event-invitee")
|
||||
.save({ post_id: this.state.eventModel.id, status });
|
||||
this.appEvents.trigger("calendar:create-invitee-status", {
|
||||
status,
|
||||
postId: this.state.eventModel.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -180,12 +189,14 @@ export default createWidget("discourse-post-event", {
|
|||
post_id: postId,
|
||||
})
|
||||
.then((invitees) => {
|
||||
invitees
|
||||
.find(
|
||||
(invitee) =>
|
||||
invitee.id === this.state.eventModel.watching_invitee.id
|
||||
)
|
||||
.destroyRecord();
|
||||
let invitee = invitees.find(
|
||||
(inv) => inv.id === this.state.eventModel.watching_invitee.id
|
||||
);
|
||||
this.appEvents.trigger("calendar:invitee-left-event", {
|
||||
invitee,
|
||||
postId,
|
||||
});
|
||||
invitee.destroyRecord();
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ en:
|
|||
```
|
||||
|
||||
site_settings:
|
||||
events_max_rows: "Maximum text rows per event in the Calendar."
|
||||
map_events_to_color: "Assign a color to each tag or category."
|
||||
calendar_enabled: "Enable the discourse-calendar plugin. This will add support for a [calendar][/calendar] tag in the first post of a topic."
|
||||
discourse_post_event_enabled: "Enables the Event features. Note: also needs `calendar enabled` to be enabled."
|
||||
displayed_invitees_limit: "Limits the numbers of invitees displayed on an event."
|
||||
|
|
|
@ -105,3 +105,10 @@ discourse_post_event:
|
|||
sidebar_show_upcoming_events:
|
||||
default: true
|
||||
client: true
|
||||
events_max_rows:
|
||||
default: 2
|
||||
client: true
|
||||
map_events_to_color:
|
||||
client: true
|
||||
default: "[]"
|
||||
json_schema: DiscourseCalendar::SiteSettings::MapEventTagColorsJsonSchema
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseCalendar
|
||||
module SiteSettings
|
||||
class MapEventTagColorsJsonSchema
|
||||
def self.schema
|
||||
@schema ||= {
|
||||
type: "array",
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: "object",
|
||||
title: "Color Mapping",
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
description: "Type of mapping (tag or category)",
|
||||
enum: %w[tag category],
|
||||
},
|
||||
slug: {
|
||||
type: "string",
|
||||
description: "Slug of the tag or category",
|
||||
},
|
||||
color: {
|
||||
type: "string",
|
||||
description: "Color associated with the tag or category",
|
||||
pattern: "^#(?:[0-9a-fA-F]{3}){1,2}$",
|
||||
},
|
||||
},
|
||||
required: %w[slug type color],
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -77,6 +77,10 @@ end
|
|||
|
||||
require_relative "lib/discourse_calendar/engine"
|
||||
|
||||
Dir
|
||||
.glob(File.expand_path("../lib/discourse_calendar/site_settings/*.rb", __FILE__))
|
||||
.each { |f| require(f) }
|
||||
|
||||
after_initialize do
|
||||
reloadable_patch do
|
||||
Category.register_custom_field_type("sort_topics_by_event_start_date", :boolean)
|
||||
|
|
|
@ -148,4 +148,36 @@ describe DiscoursePostEvent::EventSummarySerializer do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "map_events_to_color" do
|
||||
context "when map_events_to_color is empty" do
|
||||
let(:json) do
|
||||
DiscoursePostEvent::EventSummarySerializer.new(event, scope: Guardian.new).as_json
|
||||
end
|
||||
|
||||
it "returns the event summary with category_slug and tags" do
|
||||
summary = json[:event_summary]
|
||||
expect(summary[:post][:topic][:category_slug]).to be_nil
|
||||
expect(summary[:post][:topic][:tags]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when map_events_to_color is set" do
|
||||
let(:json) do
|
||||
DiscoursePostEvent::EventSummarySerializer.new(event, scope: Guardian.new).as_json
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.map_events_to_color = [
|
||||
{ type: "tag", color: "#21d939", slug: "nice-tag" },
|
||||
].to_json
|
||||
end
|
||||
|
||||
it "returns the event summary with category_slug and tags" do
|
||||
summary = json[:event_summary]
|
||||
expect(summary[:post][:topic][:category_slug]).to eq(category.slug)
|
||||
expect(summary[:post][:topic][:tags]).to eq(topic.tags.map(&:name))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,7 @@ const eventsPretender = (server, helper) => {
|
|||
topic: {
|
||||
id: 18449,
|
||||
title: "This is an event",
|
||||
tags: ["awesome-event"],
|
||||
},
|
||||
},
|
||||
name: "Awesome Event",
|
||||
|
|
|
@ -10,6 +10,18 @@ acceptance("Discourse Calendar - Category Events Calendar", function (needs) {
|
|||
discourse_post_event_enabled: true,
|
||||
events_calendar_categories: "1",
|
||||
calendar_categories: "",
|
||||
map_events_to_color: JSON.stringify([
|
||||
{
|
||||
type: "tag",
|
||||
color: "rgb(231, 76, 60)",
|
||||
slug: "awesome-tag",
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
color: "rgb(140,24,193)",
|
||||
slug: "awesome-category",
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
|
@ -18,8 +30,14 @@ acceptance("Discourse Calendar - Category Events Calendar", function (needs) {
|
|||
events: [
|
||||
{
|
||||
id: 67501,
|
||||
starts_at: "2022-04-25T15:14:00.000Z",
|
||||
ends_at: "2022-04-30T16:14:00.000Z",
|
||||
starts_at: moment()
|
||||
.tz("Asia/Calcutta")
|
||||
.add(1, "days")
|
||||
.format("YYYY-MM-DDT15:14:00.000Z"),
|
||||
ends_at: moment()
|
||||
.tz("Asia/Calcutta")
|
||||
.add(1, "days")
|
||||
.format("YYYY-MM-DDT16:14:00.000Z"),
|
||||
timezone: "Asia/Calcutta",
|
||||
post: {
|
||||
id: 67501,
|
||||
|
@ -28,15 +46,55 @@ acceptance("Discourse Calendar - Category Events Calendar", function (needs) {
|
|||
topic: {
|
||||
id: 18449,
|
||||
title: "This is an event",
|
||||
tags: ["awesome-tag"],
|
||||
},
|
||||
},
|
||||
name: "Awesome Event",
|
||||
},
|
||||
{
|
||||
id: 67502,
|
||||
starts_at: moment()
|
||||
.tz("Asia/Calcutta")
|
||||
.add(2, "days")
|
||||
.format("YYYY-MM-DDT15:14:00.000Z"),
|
||||
ends_at: moment()
|
||||
.tz("Asia/Calcutta")
|
||||
.add(2, "days")
|
||||
.format("YYYY-MM-DDT16:14:00.000Z"),
|
||||
timezone: "Asia/Calcutta",
|
||||
post: {
|
||||
id: 67502,
|
||||
post_number: 1,
|
||||
url: "/t/this-is-an-event/18450/1",
|
||||
topic: {
|
||||
id: 18450,
|
||||
title: "This is an event",
|
||||
category_slug: "awesome-category",
|
||||
},
|
||||
},
|
||||
name: "Awesome Event 2",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("events display the color configured in the map_events_to_color site setting", async (assert) => {
|
||||
await visit("/c/bug/1");
|
||||
|
||||
assert
|
||||
.dom(".fc-event")
|
||||
.exists({ count: 2 }, "One event is displayed on the calendar");
|
||||
|
||||
assert.dom(".fc-event[href='/t/-/18449/1']").hasStyle({
|
||||
"background-color": "rgb(231, 76, 60)",
|
||||
});
|
||||
|
||||
assert.dom(".fc-event[href='/t/-/18450/1']").hasStyle({
|
||||
"background-color": "rgb(140, 24, 193)",
|
||||
});
|
||||
});
|
||||
|
||||
test("shows event calendar on category page", async (assert) => {
|
||||
await visit("/c/bug/1");
|
||||
|
||||
|
|
Loading…
Reference in New Issue