Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize calendar performance and fix bugs #93

Merged
merged 18 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
python-version: [3.9]
node-version: [16.x]
node-version: [18.x]
steps:
- uses: actions/checkout@v3

Expand All @@ -36,5 +36,8 @@ jobs:
- name: Prepare for tests
run: yarn run pretest

- name: Install chromedriver
run: ./node_modules/selenium-webdriver/bin/linux/selenium-manager --driver chromedriver

- name: Run tests
run: MOCHA_WEBDRIVER_HEADLESS=1 yarn run test
124 changes: 65 additions & 59 deletions calendar/page.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// to keep all calendar related logic;
let calendarHandler;

const CALENDAR_NAME = 'standardCalendar';

const t = i18next.t;

const urlParams = new URLSearchParams(window.location.search);
const isReadOnly = urlParams.get('readonly') === 'true' ||
(urlParams.has('access') && urlParams.get('access') !== 'full');

//for tests
let dataVersion = Date.now();

function testGetDataVersion() {
return dataVersion;
}

// Expose a few test variables on `window`.
window.gristCalendar = {
calendarHandler,
CALENDAR_NAME,
dataVersion: Date.now(),
};

function getLanguage() {
if (this._lang) {
Expand Down Expand Up @@ -208,7 +209,6 @@ class CalendarHandler {
container.classList.add('readonly')
}
const options = this._getCalendarOptions();
this.previousIds = new Set();
this.calendar = new tui.Calendar(container, options);

// Not sure how to get a reference to this constructor, so doing it in a roundabout way.
Expand Down Expand Up @@ -263,6 +263,12 @@ class CalendarHandler {
container.querySelector('button.toastui-calendar-popup-confirm')?.click();
}
});

// All events, indexed by id.
this._allEvents = new Map();

// Ids of visible events that fall within the current date range. */
this._visibleEventIds = new Set();
}

_isMultidayInMonthViewEvent(rec) {
Expand All @@ -279,13 +285,14 @@ class CalendarHandler {
if (!isRecordValid(record) || this._selectedRecordId === record.id) {
return;
}

if (this._selectedRecordId) {
this._clearHighlightEvent(this._selectedRecordId);
}
this._selectedRecordId = record.id;
const [startType] = await colTypesFetcher.getColTypes();
const startDate = getAdjustedDate(record.startDate, startType);
this.calendar.setDate(startDate);
this._selectedRecordId = record.id;
updateUIAfterNavigation();

// If the view has a vertical timeline, scroll to the start of the event.
Expand Down Expand Up @@ -327,7 +334,7 @@ class CalendarHandler {
_clearHighlightEvent(eventId) {
const event = this.calendar.getEvent(eventId, CALENDAR_NAME);
if (!event) { return; }
// We will highlight it by changing the background color. Otherwise wi will change the border color.
// We will highlight it by changing the background color. Otherwise we will change the border color.
this.calendar.updateEvent(eventId, CALENDAR_NAME, {
borderColor: event.raw?.['backgroundColor'] ?? this._mainColor,
backgroundColor: event.raw?.['backgroundColor'] ?? this._mainColor,
Expand Down Expand Up @@ -364,38 +371,57 @@ class CalendarHandler {
}
}

// update calendar events based on the collection of records from the grist table.
async updateCalendarEvents(calendarEvents) {
// we need to keep track the ids of the events that are currently in the calendar to compare it
// with the new set of events when they come.
const currentIds = new Set();
for (const record of calendarEvents) {
// check if an event already exists in the calendar - update it if so, create new otherwise
const event = this.calendar.getEvent(record.id, CALENDAR_NAME);
const eventData = record;
if (!event) {
this.calendar.createEvents([eventData]);
getEvents() {
return this._allEvents;
}

setEvents(events) {
this._allEvents = events;
}

/**
* Adds/updates events that fall within the current date range, and removes
* events that do not.
*/
renderVisibleEvents() {
const newVisibleEventIds = new Set();
const dateRangeStart = this.calendar.getDateRangeStart();
const dateRangeEnd = this.calendar.getDateRangeEnd().setHours(23, 99, 99, 999);

// Add or update events that are now visible.
for (const event of this._allEvents.values()) {
const isEventInRange = (
(event.start >= dateRangeStart && event.start <= dateRangeEnd) ||
(event.end >= dateRangeStart && event.end <= dateRangeEnd) ||
(event.start < dateRangeStart && event.end > dateRangeEnd)
);
if (!isEventInRange) { continue; }

const calendarEvent = this.calendar.getEvent(event.id, CALENDAR_NAME);
if (!calendarEvent) {
this.calendar.createEvents([event]);
paulfitz marked this conversation as resolved.
Show resolved Hide resolved
} else {
this.calendar.updateEvent(record.id, CALENDAR_NAME, eventData);
this.calendar.updateEvent(event.id, CALENDAR_NAME, event);
}
currentIds.add(record.id);
newVisibleEventIds.add(event.id);
}
// if some events are not in the new set of events, we need to remove them from the calendar
if (this.previousIds) {
for (const id of this.previousIds) {
if (!currentIds.has(id)) {
this.calendar.deleteEvent(id, CALENDAR_NAME);
}

// Remove events that are no longer visible.
for (const eventId of this._visibleEventIds) {
if (!newVisibleEventIds.has(eventId)) {
this.calendar.deleteEvent(eventId, CALENDAR_NAME);
}
}
this.previousIds = currentIds;

this._visibleEventIds = newVisibleEventIds;
}
}

// when a document is ready, register the calendar and subscribe to grist events
ready(async () => {
await translatePage();
calendarHandler = new CalendarHandler();
window.gristCalendar.calendarHandler = calendarHandler;
await configureGristSettings();

});
Expand Down Expand Up @@ -447,6 +473,7 @@ function getGristOptions() {


function updateUIAfterNavigation() {
calendarHandler.renderVisibleEvents();
// update name of the month and year displayed on the top of the widget
document.getElementById('calendar-title').innerText = getMonthName();
// refresh colors of selected event (in month view it's different from in other views)
Expand Down Expand Up @@ -601,11 +628,11 @@ function selectRadioButton(value) {
for (const element of document.getElementsByName('calendar-options')) {
if (element.value === value) {
element.checked = true;
element.parentElement.classList.add('active')
element.parentElement.classList.add('active');
}
else{
element.checked = false;
element.parentElement.classList.remove('active')
element.parentElement.classList.remove('active');
}
}
}
Expand All @@ -630,7 +657,7 @@ function buildCalendarEventObject(record, colTypes, colOptions) {
let [,,type] = colOptions;
endType = endType || startType;
start = getAdjustedDate(start, startType);
end = end ? getAdjustedDate(end, endType) : start
end = end ? getAdjustedDate(end, endType) : start;

// Normalize records with invalid start/end times so that they're visible
// in the calendar.
Expand Down Expand Up @@ -688,12 +715,13 @@ async function updateCalendar(records, mappings) {
if (mappedRecords) {
const colTypes = await colTypesFetcher.getColTypes();
const colOptions = await colTypesFetcher.getColOptions();
const CalendarEventObjects = mappedRecords.filter(isRecordValid)
.map(r => buildCalendarEventObject(r, colTypes, colOptions));
await calendarHandler.updateCalendarEvents(CalendarEventObjects);
const events = mappedRecords
.filter(isRecordValid)
.map(r => buildCalendarEventObject(r, colTypes, colOptions));
calendarHandler.setEvents(new Map(events.map(event => ([event.id, event]))));
updateUIAfterNavigation();
}
dataVersion = Date.now();
window.gristCalendar.dataVersion = Date.now();
}

function focusWidget() {
Expand Down Expand Up @@ -765,28 +793,6 @@ class ColTypesFetcher {

const colTypesFetcher = new ColTypesFetcher();

function testGetCalendarEvent(eventId) {
const calendarObject = calendarHandler.calendar.getEvent(eventId, CALENDAR_NAME);
if (calendarObject) {
const eventData = {
title: calendarObject?.title,
startDate: calendarObject?.start.d.d,
endDate: calendarObject?.end.d.d,
isAllDay: calendarObject?.isAllday ?? false,
selected: calendarObject?.borderColor === calendarHandler._selectedColor ||
calendarObject?.backgroundColor === calendarHandler._selectedColor,
};
return JSON.stringify(eventData);
} else {
return null;
}
}

function testGetCalendarViewName() {
// noinspection JSUnresolvedReference
return calendarHandler.calendar.getViewName();
}

function safeParse(value) {
try {
return JSON.parse(value);
Expand Down
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,22 @@
"dev": "echo 'Starting local server and watching for changes.\nStart Grist with an environmental variable GRIST_WIDGET_LIST_URL=http://localhost:8585/manifest.json' && npm run watch 1> /dev/null & npm run serve:dev 1> /dev/null",
"test": "docker image inspect gristlabs/grist --format 'gristlabs/grist image present' && NODE_PATH=_build SELENIUM_BROWSER=chrome mocha -g \"${GREP_TESTS}\" _build/test/*.js",
"test:ci": "MOCHA_WEBDRIVER_HEADLESS=1 npm run test",
"pretest": "docker pull gristlabs/grist && tsc --build && node ./buildtools/publish.js http://localhost:9998",
"grist": "docker run -t -i --rm --name grist-dev --network=host -e PORT=8484 -e GRIST_SINGLE_ORG=preview -e GRIST_WIDGET_LIST_URL=http://localhost:8585/manifest.json gristlabs/grist"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing --network=host here and elsewhere because it's unsupported on macOS.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this supposed to do? I don't see any exposed port to connect to? Also what is host-gateway used for?

Once I figure out how this is supposed to be used, I'd like to double check it, since in the past host.docker.internal worked differently on macs vs linux (hopefully that's all fixed long ago, but I'd like to double check)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point - I'm looking now and this script looks unused. May be ok to remove?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I added it, so that it is easy to just build widgets and test them manually. Fine with removing it, or just leaving it as it is for linux users :).
(as this uses host network, docker container is just available on localhost:8484, there is no network isolation, so no ports forwarding)

"pretest": "docker pull gristlabs/grist && tsc --build && node ./buildtools/publish.js http://localhost:9998"
},
"devDependencies": {
"@types/chai": "^4.3.5",
"@types/mocha": "^10.0.1",
"@types/node": "^20.3.1",
"@types/node": "18.11.9",
"@types/node-fetch": "^2.6.4",
"@types/selenium-webdriver": "^4.1.15",
"chromedriver": "114",
"live-server": "^1.2.1",
"mocha": "^10.2.0",
"mocha-webdriver": "^0.2.13",
"mocha-webdriver": "0.3.1",
"node-fetch": "^2",
"nodemon": "^2.0.15",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
},
"resolutions": {
"chromedriver": "114.0.2"
},
"dependencies": {},
"mocha": {
"require": [
Expand Down
Loading