Skip to content

Commit

Permalink
feat: support preview the calendar view on web (#5394)
Browse files Browse the repository at this point in the history
  • Loading branch information
qinluhe committed May 22, 2024
1 parent 68c4e19 commit acae348
Show file tree
Hide file tree
Showing 42 changed files with 815 additions and 124 deletions.
16 changes: 8 additions & 8 deletions frontend/appflowy_web_app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 15 additions & 4 deletions frontend/appflowy_web_app/src/application/collab.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export enum YjsDatabaseKey {
visible = 'visible',
hide_ungrouped_column = 'hide_ungrouped_column',
collapse_hidden_groups = 'collapse_hidden_groups',
first_day_of_week = 'first_day_of_week',
show_week_numbers = 'show_week_numbers',
show_weekends = 'show_weekends',
layout_ty = 'layout_ty',
}

export interface YDoc extends Y.Doc {
Expand Down Expand Up @@ -434,23 +438,30 @@ export type YDatabaseFilters = Y.Array<YDatabaseFilter>;

export type YDatabaseSorts = Y.Array<YDatabaseSort>;

export type YDatabaseLayoutSettings = Y.Map<YDatabaseLayoutSetting>;

export type YDatabaseCalculations = Y.Array<YDatabaseCalculation>;

export type SortId = string;

export type GroupId = string;

export interface YDatabaseLayoutSetting extends Y.Map<unknown> {
export interface YDatabaseLayoutSettings extends Y.Map<unknown> {
// DatabaseViewLayout.Board
get(key: '2'): YDatabaseBoardLayoutSetting;
get(key: '1'): YDatabaseBoardLayoutSetting;

// DatabaseViewLayout.Calendar
get(key: '2'): YDatabaseCalendarLayoutSetting;
}

export interface YDatabaseBoardLayoutSetting extends Y.Map<unknown> {
get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean;
}

export interface YDatabaseCalendarLayoutSetting extends Y.Map<unknown> {
get(key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string;

get(key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean;
}

export interface YDatabaseGroup extends Y.Map<unknown> {
get(key: YjsDatabaseKey.id): GroupId;

Expand Down
14 changes: 14 additions & 0 deletions frontend/appflowy_web_app/src/application/database-yjs/const.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import * as Y from 'yjs';

export const DEFAULT_ROW_HEIGHT = 37;
export const MIN_COLUMN_WIDTH = 100;

export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
const rowMeta = rowMetas.get(rowId);
const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;

return meta?.get(YjsDatabaseKey.cells)?.get(fieldId);
};

export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data);
};
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,17 @@ export interface Filter {
id: string;
content: string;
}

export enum CalendarLayout {
MonthLayout = 0,
WeekLayout = 1,
DayLayout = 2,
}

export interface CalendarLayoutSetting {
fieldId: string;
firstDayOfWeek: number;
showWeekNumbers: boolean;
showWeekends: boolean;
layout: CalendarLayout;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface ChecklistCellData {
export function parseChecklistData(data: string): ChecklistCellData | null {
try {
const { options, selected_option_ids } = JSON.parse(data);
const percentage = (selected_option_ids.length / options.length) * 100;
const percentage = selected_option_ids.length / options.length;

return {
percentage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,10 @@ export function checklistFilterCheck(data: string, content: string, condition: n
const percentage = parseChecklistData(data)?.percentage ?? 0;

if (condition === ChecklistFilterCondition.IsComplete) {
return percentage === 100;
return percentage === 1;
}

return percentage !== 100;
return percentage !== 1;
}

export function selectOptionFilterCheck(data: string, content: string, condition: number) {
Expand Down
10 changes: 2 additions & 8 deletions frontend/appflowy_web_app/src/application/database-yjs/group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { YDatabaseField, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { YDatabaseField, YDoc, YjsDatabaseKey } from '@/application/collab.type';
import { getCellData } from '@/application/database-yjs/const';
import { FieldType } from '@/application/database-yjs/database.type';
import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields';
import { Row } from '@/application/database-yjs/selector';
Expand All @@ -12,13 +13,6 @@ export function groupByField(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabas
return groupBySelectOption(rows, rowMetas, field);
}

function getCellData(rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) {
const rowMeta = rowMetas.get(rowId);
const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;

return meta?.get(YjsDatabaseKey.cells)?.get(fieldId)?.get(YjsDatabaseKey.data);
}

export function groupBySelectOption(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabaseField) {
const fieldId = field.get(YjsDatabaseKey.id);
const result = new Map<string, Row[]>();
Expand Down
119 changes: 113 additions & 6 deletions frontend/appflowy_web_app/src/application/database-yjs/selector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type';
import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsFolderKey } from '@/application/collab.type';
import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
import {
DatabaseContext,
useDatabase,
Expand All @@ -13,10 +13,13 @@ import { filterBy, parseFilter } from '@/application/database-yjs/filter';
import { groupByField } from '@/application/database-yjs/group';
import { sortBy } from '@/application/database-yjs/sort';
import { useViewsIdSelector } from '@/application/folder-yjs';
import { useId } from '@/components/_shared/context-provider/IdProvider';
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
import dayjs from 'dayjs';
import debounce from 'lodash-es/debounce';
import { useContext, useEffect, useMemo, useState } from 'react';
import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type';
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, SortCondition } from './database.type';

export interface Column {
fieldId: string;
Expand All @@ -34,7 +37,8 @@ const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmp

export function useDatabaseViewsSelector() {
const database = useDatabase();
const { viewsId: visibleViewsId } = useViewsIdSelector();
const { objectId: currentViewId } = useId();
const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector();
const views = database?.get(YjsDatabaseKey.views);
const [viewIds, setViewIds] = useState<string[]>([]);
const childViews = useMemo(() => {
Expand All @@ -45,7 +49,16 @@ export function useDatabaseViewsSelector() {
if (!views) return;

const observerEvent = () => {
setViewIds(Array.from(views.keys()).filter((id) => visibleViewsId.includes(id)));
setViewIds(
Array.from(views.keys()).filter((id) => {
const view = folderViews?.get(id);

return (
visibleViewsId.includes(id) &&
(view?.get(YjsFolderKey.bid) === currentViewId || view?.get(YjsFolderKey.id) === currentViewId)
);
})
);
};

observerEvent();
Expand All @@ -54,7 +67,7 @@ export function useDatabaseViewsSelector() {
return () => {
views.unobserve(observerEvent);
};
}, [visibleViewsId, views]);
}, [visibleViewsId, views, folderViews, currentViewId]);

return {
childViews,
Expand Down Expand Up @@ -478,3 +491,97 @@ export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: st

return cellValue;
}

export interface CalendarEvent {
start?: Date;
end?: Date;
id: string;
}

export function useCalendarEventsSelector() {
const setting = useCalendarLayoutSetting();
const filedId = setting.fieldId;
const { field } = useFieldSelector(filedId);
const rowOrders = useRowOrdersSelector();
const rows = useContext(DatabaseContext)?.rowDocMap;
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [emptyEvents, setEmptyEvents] = useState<CalendarEvent[]>([]);

useEffect(() => {
if (!field || !rowOrders || !rows) return;
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;

if (fieldType !== FieldType.DateTime) return;
const newEvents: CalendarEvent[] = [];
const emptyEvents: CalendarEvent[] = [];

rowOrders?.forEach((row) => {
const cell = getCell(row.id, filedId, rows);

if (!cell) {
emptyEvents.push({
id: `${row.id}:${filedId}`,
});
return;
}

const value = parseYDatabaseCellToCell(cell) as DateTimeCell;

if (!value || !value.data) {
emptyEvents.push({
id: `${row.id}:${filedId}`,
});
return;
}

const getDate = (timestamp: string) => {
const dayjsResult = timestamp.length === 10 ? dayjs.unix(Number(timestamp)) : dayjs(timestamp);

return dayjsResult.toDate();
};

newEvents.push({
id: `${row.id}:${filedId}`,
start: getDate(value.data),
end: value.endTimestamp && value.isRange ? getDate(value.endTimestamp) : getDate(value.data),
});
});

setEvents(newEvents);
setEmptyEvents(emptyEvents);
}, [field, rowOrders, rows, filedId]);

return { events, emptyEvents };
}

export function useCalendarLayoutSetting() {
const view = useDatabaseView();
const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('2');
const [setting, setSetting] = useState<CalendarLayoutSetting>({
fieldId: '',
firstDayOfWeek: 0,
showWeekNumbers: true,
showWeekends: true,
layout: 0,
});

useEffect(() => {
const observerHandler = () => {
setSetting({
fieldId: layoutSetting?.get(YjsDatabaseKey.field_id) as string,
firstDayOfWeek: Number(layoutSetting?.get(YjsDatabaseKey.first_day_of_week)),
showWeekNumbers: Boolean(layoutSetting?.get(YjsDatabaseKey.show_week_numbers)),
showWeekends: Boolean(layoutSetting?.get(YjsDatabaseKey.show_weekends)),
layout: Number(layoutSetting?.get(YjsDatabaseKey.layout_ty)),
});
};

observerHandler();
layoutSetting?.observe(observerHandler);
return () => {
layoutSetting?.unobserve(observerHandler);
};
}, [layoutSetting]);

return setting;
}
29 changes: 15 additions & 14 deletions frontend/appflowy_web_app/src/application/folder-yjs/selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,39 @@ import { useEffect, useState } from 'react';
export function useViewsIdSelector() {
const folder = useFolderContext();
const [viewsId, setViewsId] = useState<string[]>([]);
const views = folder?.get(YjsFolderKey.views);
const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
const meta = folder?.get(YjsFolderKey.meta);

useEffect(() => {
if (!folder) return;
if (!views) return;

const views = folder.get(YjsFolderKey.views);
const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
const meta = folder.get(YjsFolderKey.meta);
const trashUid = Array.from(trash?.keys())[0];
const userTrash = trash?.get(trashUid);
const trashUid = trash ? Array.from(trash.keys())[0] : null;
const userTrash = trashUid ? trash?.get(trashUid) : null;

const collectIds = () => {
const trashIds = userTrash?.toJSON()?.map((item) => item.id) || [];

return Array.from(views.keys()).filter(
(id) => !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace)
);
return Array.from(views.keys()).filter((id) => {
return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace);
});
};

setViewsId(collectIds());
const observerEvent = () => setViewsId(collectIds());

folder.observe(observerEvent);
userTrash.observe(observerEvent);
views.observe(observerEvent);
userTrash?.observe(observerEvent);

return () => {
folder.unobserve(observerEvent);
userTrash.unobserve(observerEvent);
views.unobserve(observerEvent);
userTrash?.unobserve(observerEvent);
};
}, [folder]);
}, [views, trash, meta]);

return {
viewsId,
views,
};
}

Expand Down

0 comments on commit acae348

Please sign in to comment.