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

fix: solve side effects related to duplicate events #1226

Merged
merged 11 commits into from
Jul 27, 2022
14 changes: 9 additions & 5 deletions apps/calendar/examples/00-calendar-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,23 @@
<hr />
<div class="sidebar-item">
<input type="checkbox" id="1" value="1" checked />
<label class="checkbox checkbox-1" for="1">My Calendar</label>
<label class="checkbox checkbox-calendar checkbox-1" for="1">My Calendar</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="2" value="2" checked />
<label class="checkbox checkbox-2" for="2">Work</label>
<label class="checkbox checkbox-calendar checkbox-2" for="2">Work</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="3" value="3" checked />
<label class="checkbox checkbox-3" for="3">Family</label>
<label class="checkbox checkbox-calendar checkbox-3" for="3">Family</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="4" value="4" checked />
<label class="checkbox checkbox-4" for="4">Friends</label>
<label class="checkbox checkbox-calendar checkbox-4" for="4">Friends</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="5" value="5" checked />
<label class="checkbox checkbox-5" for="5">Travel</label>
<label class="checkbox checkbox-calendar checkbox-5" for="5">Travel</label>
</div>
<hr />
<div class="app-footer">© NHN Cloud Corp.</div>
Expand Down Expand Up @@ -107,6 +107,10 @@
/>
</button>
<span class="navbar--range"></span>
<div class="nav-checkbox">
<input class="checkbox-collapse" type="checkbox" id="collapse" value="collapse" />
<label for="collapse">Collapse duplicate events and disable the detail popup</label>
Copy link
Member Author

Choose a reason for hiding this comment

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

중복 일정이 겹쳐지고 펼쳐지는 동작이 잘 보이도록 collapseDuplicateEvents를 활성화하면 useDetailPopup를 끄도록 만들었습니다.

</div>
</nav>
<main id="app"></main>
</section>
Expand Down
20 changes: 18 additions & 2 deletions apps/calendar/examples/scripts/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
var dropdownTrigger = $('.dropdown-trigger');
var dropdownTriggerIcon = $('.dropdown-icon');
var dropdownContent = $('.dropdown-content');
var checkboxCollapse = $('.checkbox-collapse');
var sidebar = $('.sidebar');

// App State
Expand Down Expand Up @@ -73,7 +74,7 @@
}

function setAllCheckboxes(checked) {
var checkboxes = $$('input[type="checkbox"]');
var checkboxes = $$('.sidebar-item > input[type="checkbox"]');

checkboxes.forEach(function (checkbox) {
checkbox.checked = checked;
Expand Down Expand Up @@ -125,13 +126,28 @@
});

dropdownContent.addEventListener('click', function (e) {
var targetViewName;

if ('viewName' in e.target.dataset) {
cal.changeView(e.target.dataset.viewName);
targetViewName = e.target.dataset.viewName;
cal.changeView(targetViewName);
checkboxCollapse.disabled = targetViewName === 'month';
toggleDropdownState();
update();
}
});

checkboxCollapse.addEventListener('change', function (e) {
if ('checked' in e.target) {
cal.setOptions({
week: {
collapseDuplicateEvents: !!e.target.checked,
},
useDetailPopup: !e.target.checked,
});
}
});

sidebar.addEventListener('click', function (e) {
if ('value' in e.target) {
if (e.target.value === 'all') {
Expand Down
15 changes: 13 additions & 2 deletions apps/calendar/examples/scripts/mock-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,25 @@ function generateRandomEvent(calendar, renderStart, renderEnd) {
}

function generateRandomEvents(viewName, renderStart, renderEnd) {
var i;
var event;
var i, j;
var event, duplicateEvent;
var events = [];

MOCK_CALENDARS.forEach(function(calendar) {
for (i = 0; i < chance.integer({ min: 20, max: 50 }); i += 1) {
event = generateRandomEvent(calendar, renderStart, renderEnd);
events.push(event);

if (i % 5 === 0) {
for (j = 0; j < chance.integer({min: 0, max: 2}); j+= 1) {
duplicateEvent = JSON.parse(JSON.stringify(event));
duplicateEvent.id += `-${j}`;
duplicateEvent.calendarId = chance.integer({min: 1, max: 5}).toString();
duplicateEvent.goingDuration = 30 * chance.integer({min: 0, max: 4});
duplicateEvent.comingDuration = 30 * chance.integer({min: 0, max: 4});
events.push(duplicateEvent);
}
}
}
});

Expand Down
11 changes: 10 additions & 1 deletion apps/calendar/examples/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@
font-size: 1.25rem;
}

.navbar .nav-checkbox {
margin-left: auto;
}

input:disabled + label {
color: #ccc;
cursor: not-allowed;
}

.toastui-calendar-template-time strong {
color: inherit;
}
Expand All @@ -93,7 +102,7 @@
position: relative;
}

.checkbox:not(.checkbox-all)::before {
.checkbox-calendar::before {
content: "";
position: absolute;
left: -1.5rem;
Expand Down
55 changes: 42 additions & 13 deletions apps/calendar/playwright/week/timeGridEventClick.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { expect, test } from '@playwright/test';

import type { MockedWeekViewEvents } from '@stories/mocks/types';

import { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';
import { WEEK_VIEW_DUPLICATE_EVENTS_PAGE_URL, WEEK_VIEW_PAGE_URL } from '../configs';
import { getBoundingBox, getTimeEventSelector } from '../utils';
Expand Down Expand Up @@ -34,23 +32,54 @@ test.describe('Collapse duplicate events', () => {
await page.goto(WEEK_VIEW_DUPLICATE_EVENTS_PAGE_URL);
});

const collapsedEvent = mockWeekViewEvents.find(
({ title }) => title === 'duplicate event'
) as MockedWeekViewEvents;
const collapsedEvents = mockWeekViewEvents.filter(({ title }) =>
title.match(/duplicate event(\s\d)?$/)
);
const [collapsedEvent] = collapsedEvents;

test('When clicking the collapsed duplicate event, it should be expanded.', async ({ page }) => {
test('The duplicate events are sorted according to the result of getDuplicateEvents option.', ({
page,
}) => {
// Given
const collapsedEventLocator = page.locator(getTimeEventSelector(collapsedEvent.title));
const { x, y, width: widthBeforeClick } = await getBoundingBox(collapsedEventLocator);
// getDuplicateEvents: sort by calendarId in descending order
const sortedDuplicateEvents = mockWeekViewEvents
.filter(({ title }) => title.startsWith('duplicate event 2'))
.sort((a, b) => (b.calendarId > a.calendarId ? 1 : -1));
let prevX = -1;

// When
await page.mouse.move(x + 2, y + 2);
await page.mouse.down();
await page.mouse.up();
// Nothing

// Then
const { width: widthAfterClick } = await getBoundingBox(collapsedEventLocator);
expect(widthAfterClick).toBeGreaterThan(widthBeforeClick);
sortedDuplicateEvents.forEach(async (event) => {
const eventLocator = page.locator(getTimeEventSelector(event.title));
const { x } = await getBoundingBox(eventLocator);
expect(prevX).toBeLessThan(x);

prevX = x;
});
adhrinae marked this conversation as resolved.
Show resolved Hide resolved
});

collapsedEvents.forEach((event) => {
test(`When clicking the collapsed duplicate event, it should be expanded. - ${event.title}`, async ({
page,
}) => {
// Given
const collapsedEventLocator = page.locator(getTimeEventSelector(event.title));
const { x, y, width: widthBeforeClick } = await getBoundingBox(collapsedEventLocator);
const mainEventLocator = page.locator(getTimeEventSelector(`${event.title} - main`));
const { width: mainEventWidth } = await getBoundingBox(mainEventLocator);

// When
await page.mouse.move(x + 2, y + 2);
adhrinae marked this conversation as resolved.
Show resolved Hide resolved
await page.mouse.down();
await page.mouse.up();

// Then
const { width: widthAfterClick } = await getBoundingBox(collapsedEventLocator);
expect(widthAfterClick).toBeGreaterThan(widthBeforeClick);
expect(widthAfterClick).toBeCloseTo(mainEventWidth, -1);
});
});

const otherEvents = mockWeekViewEvents.filter(({ title }) => {
Expand Down
33 changes: 26 additions & 7 deletions apps/calendar/src/components/events/timeEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useEffect, useRef, useState } from 'preact/hooks';

import { Template } from '@src/components/template';
import { DEFAULT_DUPLICATE_EVENT_CID } from '@src/constants/layout';
import { TIME_EVENT_CONTAINER_MARGIN_LEFT } from '@src/constants/style';
import { useDispatch, useStore } from '@src/contexts/calendarStore';
import { useEventBus } from '@src/contexts/eventBus';
import { useLayoutContainer } from '@src/contexts/layoutContainer';
import { cls, getEventColors, toPercent } from '@src/helpers/css';
import { cls, extractPercentPx, getEventColors, toPercent } from '@src/helpers/css';
import { DRAGGING_TYPE_CREATORS } from '@src/helpers/drag';
import { useCalendarColor } from '@src/hooks/calendar/useCalendarColor';
import { useDrag } from '@src/hooks/common/useDrag';
Expand All @@ -15,7 +16,7 @@ import type EventUIModel from '@src/model/eventUIModel';
import { dndSelector, optionsSelector } from '@src/selectors';
import { DraggingState } from '@src/slices/dnd';
import type TZDate from '@src/time/date';
import { isPresent } from '@src/utils/type';
import { isPresent, isString } from '@src/utils/type';

import type { StyleProp } from '@t/components/common';
import type { CalendarColor } from '@t/options';
Expand All @@ -36,6 +37,23 @@ interface Props {
minHeight?: number;
}

function getMarginLeft(left: number | string) {
const { percent, px } = extractPercentPx(`${left}`);

return left > 0 || percent > 0 || px > 0 ? TIME_EVENT_CONTAINER_MARGIN_LEFT : 0;
adhrinae marked this conversation as resolved.
Show resolved Hide resolved
}

function getContainerWidth(width: number | string, marginLeft: number) {
if (isString(width)) {
return width;
}
if (width >= 0) {
return `calc(${toPercent(width)} - ${marginLeft}px)`;
}

return '';
}

function getStyles({
uiModel,
isDraggingTarget,
Expand All @@ -54,6 +72,8 @@ function getStyles({
left,
height,
width,
duplicateLeft,
duplicateWidth,
goingDurationHeight,
modelDurationHeight,
comingDurationHeight,
Expand All @@ -63,20 +83,18 @@ function getStyles({
// TODO: check and get theme values
const travelBorderColor = 'white';
const borderRadius = 2;
const paddingLeft = 2;
const defaultMarginBottom = 2;
const marginLeft = left > 0 ? paddingLeft : 0;
const marginLeft = getMarginLeft(left);

const { color, backgroundColor, borderColor, dragBackgroundColor } = getEventColors(
uiModel,
calendarColor
);
const containerStyle: StyleProp = {
width: width >= 0 ? `calc(${toPercent(width)} - ${marginLeft}px)` : '',
minWidth: '9px',
width: getContainerWidth(duplicateWidth || width, marginLeft),
height: `calc(${toPercent(Math.max(height, minHeight))} - ${defaultMarginBottom}px)`,
top: toPercent(top),
left: toPercent(left),
left: duplicateLeft || toPercent(left),
borderRadius,
borderLeft: `3px solid ${borderColor}`,
marginLeft,
Expand All @@ -85,6 +103,7 @@ function getStyles({
opacity: isDraggingTarget ? 0.5 : 1,
zIndex: hasNextStartTime ? 1 : 0,
};

const goingDurationStyle = {
height: toPercent(goingDurationHeight),
borderBottom: `1px dashed ${travelBorderColor}`,
Expand Down
4 changes: 3 additions & 1 deletion apps/calendar/src/constants/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ export const DEFAULT_EVENT_COLORS = {
borderColor: '#000',
};

export const COLLAPSED_DUPLICATE_EVENT_WIDTH = 5;
export const TIME_EVENT_CONTAINER_MARGIN_LEFT = 2;

export const COLLAPSED_DUPLICATE_EVENT_WIDTH_PX = 9;
29 changes: 20 additions & 9 deletions apps/calendar/src/controller/column.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { DEFAULT_DUPLICATE_EVENT_CID } from '@src/constants/layout';
import { COLLAPSED_DUPLICATE_EVENT_WIDTH } from '@src/constants/style';
import {
COLLAPSED_DUPLICATE_EVENT_WIDTH_PX,
TIME_EVENT_CONTAINER_MARGIN_LEFT,
} from '@src/constants/style';
import { setRenderInfoOfUIModels } from '@src/controller/column';
import EventModel from '@src/model/eventModel';
import EventUIModel from '@src/model/eventUIModel';
Expand Down Expand Up @@ -67,11 +70,11 @@ describe('collapseDuplicateEvents option', () => {

// Then
result.forEach((uiModel) => {
expect(uiModel.width).toBeCloseTo(100 / result.length);
expect(uiModel.width).toBeCloseTo(100 / result.length, 0);
});
});

it('when it sets, the main event is expanded and the others are collasped.', () => {
it('when it sets, the main event is expanded and the others are collapsed.', () => {
// Given
const mainEventId = eventUIModels[0].model.id;
const collapseDuplicateEventsOptions: CollapseDuplicateEventsOptions = {
Expand All @@ -97,11 +100,15 @@ describe('collapseDuplicateEvents option', () => {
// Then
result.forEach((uiModel) => {
if (uiModel.model.id === mainEventId) {
expect(uiModel.width).toBeCloseTo(
100 - (result.length - 1) * COLLAPSED_DUPLICATE_EVENT_WIDTH
expect(uiModel.duplicateWidth).toBe(
`calc(100% - ${
(COLLAPSED_DUPLICATE_EVENT_WIDTH_PX + TIME_EVENT_CONTAINER_MARGIN_LEFT) *
(result.length - 1) +
TIME_EVENT_CONTAINER_MARGIN_LEFT
}px)`
);
} else {
expect(uiModel.width).toBeCloseTo(COLLAPSED_DUPLICATE_EVENT_WIDTH);
expect(uiModel.duplicateWidth).toBe(`${COLLAPSED_DUPLICATE_EVENT_WIDTH_PX}px`);
}
heenakwag marked this conversation as resolved.
Show resolved Hide resolved
});
});
Expand Down Expand Up @@ -133,11 +140,15 @@ describe('collapseDuplicateEvents option', () => {
// Then
result.forEach((uiModel) => {
if (uiModel.cid() === selectedDuplicateEventCid) {
expect(uiModel.width).toBeCloseTo(
100 - (result.length - 1) * COLLAPSED_DUPLICATE_EVENT_WIDTH
expect(uiModel.duplicateWidth).toBe(
`calc(100% - ${
(COLLAPSED_DUPLICATE_EVENT_WIDTH_PX + TIME_EVENT_CONTAINER_MARGIN_LEFT) *
(result.length - 1) +
TIME_EVENT_CONTAINER_MARGIN_LEFT
}px)`
);
} else {
expect(uiModel.width).toBeCloseTo(COLLAPSED_DUPLICATE_EVENT_WIDTH);
expect(uiModel.duplicateWidth).toBe(`${COLLAPSED_DUPLICATE_EVENT_WIDTH_PX}px`);
}
});
});
Expand Down
Loading