Files
AppFlowy-Web/src/application/database-yjs/selector.ts

1344 lines
36 KiB
TypeScript

import dayjs from 'dayjs';
import { debounce } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
import { DateTimeCell } from '@/application/database-yjs/cell.type';
import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
import {
useDatabase,
useDatabaseFields,
useDatabaseView,
useDatabaseViewId,
useRowDocMap,
} from '@/application/database-yjs/context';
import {
getDateCellStr,
getFieldDateTimeFormats,
getTypeOptions,
parseRelationTypeOption,
parseSelectOptionTypeOptions,
SelectOption,
} from '@/application/database-yjs/fields';
import { filterBy, parseFilter } from '@/application/database-yjs/filter';
import { groupByField } from '@/application/database-yjs/group';
import { getMetaJSON } from '@/application/database-yjs/row_meta';
import { sortBy } from '@/application/database-yjs/sort';
import {
DatabaseViewLayout,
FieldId,
SortId,
TimeFormat,
YDatabase,
YDatabaseMetas,
YDatabaseRow,
YDoc,
YjsDatabaseKey,
YjsEditorKey,
YSharedRoot,
} from '@/application/types';
import { MetadataKey } from '@/application/user-metadata';
import { useCurrentUser } from '@/components/main/app.hooks';
import { getDateFormat, getTimeFormat, renderDate } from '@/utils/time';
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMeta, SortCondition } from './database.type';
export interface Column {
fieldId: string;
width: number;
visibility: FieldVisibility;
wrap?: boolean;
isPrimary: boolean;
}
export interface Row {
id: string;
height: number;
}
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
export function useDatabaseViewsSelector(_iidIndex: string, visibleViewIds?: string[]) {
const database = useDatabase();
const views = database?.get(YjsDatabaseKey.views);
const [viewIds, setViewIds] = useState<string[]>([]);
const childViews = useMemo(() => {
return viewIds.map((viewId) => views?.get(viewId));
}, [viewIds, views]);
useEffect(() => {
if (!views) return;
const observerEvent = () => {
const viewsObj = views.toJSON() as Record<
string,
{
created_at: string;
}
>;
const viewsSorted =
visibleViewIds ??
Object.entries(viewsObj)
.sort((a, b) => {
const [, viewA] = a;
const [, viewB] = b;
return Date.parse(viewB.created_at) - Date.parse(viewA.created_at);
})
.map(([key]) => key);
setViewIds(
viewsSorted.filter((id) => {
return !visibleViewIds || visibleViewIds.includes(id);
})
);
};
observerEvent();
views.observe(observerEvent);
return () => {
views.unobserve(observerEvent);
};
}, [views, visibleViewIds]);
return {
childViews,
viewIds,
};
}
export function useDatabaseViewLayout() {
const view = useDatabaseView();
const [layout, setLayout] = useState<DatabaseViewLayout | null>(null);
useEffect(() => {
const observerEvent = () => {
const layoutValue = view?.get(YjsDatabaseKey.layout);
if (layoutValue !== undefined) {
setLayout(Number(layoutValue) as DatabaseViewLayout);
} else {
setLayout(null);
}
};
observerEvent();
view?.observe(observerEvent);
return () => {
view?.unobserve(observerEvent);
};
}, [view]);
return layout;
}
export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisible) {
const view = useDatabaseView();
const database = useDatabase();
const [columns, setColumns] = useState<Column[]>([]);
useEffect(() => {
if (!view) return;
const fields = database?.get(YjsDatabaseKey.fields);
const fieldsOrder = view?.get(YjsDatabaseKey.field_orders);
const fieldSettings = view?.get(YjsDatabaseKey.field_settings);
const getColumns = () => {
if (!fields || !fieldsOrder) return [];
const fieldIds = (fieldsOrder.toJSON() as { id: string }[]).map((item) => item.id);
return fieldIds
.map((fieldId) => {
const setting = fieldSettings?.get(fieldId);
const field = fields.get(fieldId);
return {
fieldId,
isPrimary: field?.get(YjsDatabaseKey.is_primary),
width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH,
visibility: Number(
setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown
) as FieldVisibility,
wrap: setting?.get(YjsDatabaseKey.wrap) ?? true,
fieldType: Number(field?.get(YjsDatabaseKey.type)) as FieldType,
};
})
.filter((column) => {
return visibilitys.includes(column.visibility);
});
};
const observerEvent = () => setColumns(getColumns());
setColumns(getColumns());
fieldsOrder?.observeDeep(observerEvent);
fieldSettings?.observeDeep(observerEvent);
fields?.observe(observerEvent);
return () => {
fieldsOrder?.unobserveDeep(observerEvent);
fieldSettings?.unobserveDeep(observerEvent);
fields?.unobserve(observerEvent);
};
}, [database, view, visibilitys]);
return columns;
}
export function useFieldType(fieldId: string) {
const database = useDatabase();
const field = database?.get(YjsDatabaseKey.fields)?.get(fieldId);
const [fieldType, setFieldType] = useState<FieldType>(FieldType.RichText);
useEffect(() => {
if (!field) return;
const observerEvent = () => {
setFieldType(Number(field.get(YjsDatabaseKey.type)) as FieldType);
};
observerEvent();
field.observe(observerEvent);
return () => {
field.unobserve(observerEvent);
};
}, [database, field]);
return fieldType;
}
export function useFieldVisibility(fieldId: string) {
const view = useDatabaseView();
const fieldSettings = view?.get(YjsDatabaseKey.field_settings);
const fieldSetting = fieldSettings?.get(fieldId);
const [visibility, setVisibility] = useState<FieldVisibility>(
Number(fieldSetting?.get(YjsDatabaseKey.visibility)) ?? FieldVisibility.AlwaysShown
);
useEffect(() => {
if (!view) return;
const observerEvent = () => {
setVisibility(Number(fieldSetting?.get(YjsDatabaseKey.visibility)) ?? FieldVisibility.AlwaysShown);
};
observerEvent();
fieldSettings?.observeDeep(observerEvent);
return () => {
fieldSettings?.unobserveDeep(observerEvent);
};
}, [view, fieldId, fieldSettings, fieldSetting]);
return visibility;
}
export function useFieldWrap(fieldId: string) {
const view = useDatabaseView();
const database = useDatabase();
const fieldSettings = view?.get(YjsDatabaseKey.field_settings);
const fieldSetting = fieldSettings?.get(fieldId);
const [wrap, setWrap] = useState(fieldSetting?.get(YjsDatabaseKey.wrap) ?? true);
useEffect(() => {
if (!view) return;
const observerEvent = () => {
setWrap(fieldSetting?.get(YjsDatabaseKey.wrap) ?? true);
};
observerEvent();
fieldSettings?.observeDeep(observerEvent);
return () => {
fieldSettings?.unobserveDeep(observerEvent);
};
}, [database, view, fieldId, fieldSettings, fieldSetting]);
return wrap;
}
export function useFieldSelector(fieldId: string) {
const database = useDatabase();
const [clock, setClock] = useState<number>(0);
const field = database.get(YjsDatabaseKey.fields)?.get(fieldId);
useEffect(() => {
if (!database) return;
const observerEvent = () => setClock((prev) => prev + 1);
field?.observeDeep(observerEvent);
return () => {
field?.unobserveDeep(observerEvent);
};
}, [database, field, fieldId]);
return {
field,
clock,
};
}
export function useDatabaseIdFromField(fieldId: string) {
const database = useDatabase();
const field = database?.get(YjsDatabaseKey.fields)?.get(fieldId);
const [databaseId, setDatabaseId] = useState<string | null>(null);
useEffect(() => {
if (!field) return;
const observerEvent = () => {
setDatabaseId(parseRelationTypeOption(field)?.database_id);
};
observerEvent();
field.observe(observerEvent);
return () => {
field.unobserve(observerEvent);
};
}, [database, field, fieldId]);
return databaseId;
}
export function useFiltersSelector() {
const database = useDatabase();
const viewId = useDatabaseViewId();
const [filters, setFilters] = useState<{ id: string; fieldId: string }[]>([]);
useEffect(() => {
if (!viewId) return;
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const filterOrders = view?.get(YjsDatabaseKey.filters);
if (!filterOrders) return;
const getFilters = () => {
return (filterOrders.toJSON() as { id: string; field_id: string }[]).map((item) => {
return {
id: item.id,
fieldId: item.field_id,
};
});
};
const observerEvent = () => {
setFilters(getFilters());
};
observerEvent();
filterOrders.observe(observerEvent);
return () => {
filterOrders.unobserve(observerEvent);
};
}, [database, viewId]);
return filters;
}
export function useFilterSelector(filterId: string) {
const database = useDatabase();
const viewId = useDatabaseViewId();
const fields = database?.get(YjsDatabaseKey.fields);
const [filterValue, setFilterValue] = useState<Filter | null>(null);
useEffect(() => {
if (!viewId) return;
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const filter = view
?.get(YjsDatabaseKey.filters)
.toArray()
.find((filter) => filter.get(YjsDatabaseKey.id) === filterId);
const field = fields?.get(filter?.get(YjsDatabaseKey.field_id) as FieldId);
const observerEvent = () => {
if (!filter || !field) return;
const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType;
setFilterValue(parseFilter(fieldType, filter));
};
observerEvent();
field?.observe(observerEvent);
filter?.observe(observerEvent);
return () => {
field?.unobserve(observerEvent);
filter?.unobserve(observerEvent);
};
}, [fields, viewId, filterId, database]);
return filterValue;
}
export function useSortsSelector() {
const database = useDatabase();
const viewId = useDatabaseViewId();
const [sorts, setSorts] = useState<{ id: string; fieldId: string }[]>([]);
useEffect(() => {
if (!viewId) return;
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const sortOrders = view?.get(YjsDatabaseKey.sorts);
if (!sortOrders) return;
const getSorts = () => {
return (sortOrders.toJSON() as { id: string; field_id: string }[]).map((item) => {
return {
id: item.id,
fieldId: item.field_id,
};
});
};
const observerEvent = () => setSorts(getSorts());
setSorts(getSorts());
sortOrders.observe(observerEvent);
return () => {
sortOrders.unobserve(observerEvent);
};
}, [database, viewId]);
return sorts;
}
export interface Sort {
fieldId: FieldId;
condition: SortCondition;
id: SortId;
}
export function useSortSelector(sortId: SortId) {
const database = useDatabase();
const viewId = useDatabaseViewId();
const [sortValue, setSortValue] = useState<Sort | null>(null);
const views = database?.get(YjsDatabaseKey.views);
useEffect(() => {
if (!viewId) return;
const view = views?.get(viewId);
const sort = view
?.get(YjsDatabaseKey.sorts)
.toArray()
.find((sort) => sort.get(YjsDatabaseKey.id) === sortId);
const observerEvent = () => {
setSortValue({
fieldId: sort?.get(YjsDatabaseKey.field_id) as FieldId,
condition: Number(sort?.get(YjsDatabaseKey.condition)),
id: sort?.get(YjsDatabaseKey.id) as SortId,
});
};
observerEvent();
sort?.observe(observerEvent);
return () => {
sort?.unobserve(observerEvent);
};
}, [viewId, sortId, views]);
return sortValue;
}
export function useGroupsSelector() {
const database = useDatabase();
const viewId = useDatabaseViewId();
const [groups, setGroups] = useState<string[]>([]);
useEffect(() => {
if (!viewId) return;
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const groupOrders = view?.get(YjsDatabaseKey.groups);
if (!groupOrders) return;
const getGroups = () => {
return groupOrders.toArray().map((item) => item.get(YjsDatabaseKey.id));
};
const observerEvent = () => setGroups(getGroups());
setGroups(getGroups());
groupOrders.observeDeep(observerEvent);
return () => {
groupOrders.unobserveDeep(observerEvent);
};
}, [database, viewId]);
return groups;
}
export interface GroupColumn {
id: string;
visible: boolean;
}
export function useGroup(groupId: string) {
const database = useDatabase();
const viewId = useDatabaseViewId();
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const group = view
?.get(YjsDatabaseKey.groups)
?.toArray()
.find((group) => group.get(YjsDatabaseKey.id) === groupId);
const [fieldId, setFieldId] = useState<string | null>(null);
const [columns, setColumns] = useState<GroupColumn[]>([]);
useEffect(() => {
if (!viewId || !group) return;
const observerEvent = () => {
const groupFieldId = group.get(YjsDatabaseKey.field_id);
setFieldId(groupFieldId);
const groupColumnsVisible = group.get(YjsDatabaseKey.groups);
const visibleArray = groupColumnsVisible?.toArray() || [];
setColumns(visibleArray);
};
observerEvent();
group?.observeDeep(observerEvent);
return () => {
group?.unobserveDeep(observerEvent);
};
}, [database, viewId, groupId, group]);
return {
columns,
fieldId,
};
}
export function useBoardLayoutSettings() {
const view = useDatabaseView();
const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('1');
const [isCollapsed, setIsCollapsed] = useState(true);
const [hideUnGroup, setHideUnGroup] = useState(true);
const groups = view?.get(YjsDatabaseKey.groups);
const [fieldId, setFieldId] = useState<string | null>(null);
useEffect(() => {
if (!layoutSetting) return;
const observerEvent = () => {
setIsCollapsed(Boolean(layoutSetting?.get(YjsDatabaseKey.collapse_hidden_groups)));
setHideUnGroup(Boolean(layoutSetting?.get(YjsDatabaseKey.hide_ungrouped_column)));
};
observerEvent();
layoutSetting.observe(observerEvent);
return () => {
layoutSetting.unobserve(observerEvent);
};
}, [view, layoutSetting]);
useEffect(() => {
const observerEvent = () => {
const group = groups?.toArray()?.[0];
if (!group) return;
const groupFieldId = group.get(YjsDatabaseKey.field_id);
setFieldId(groupFieldId);
};
observerEvent();
groups?.observeDeep(observerEvent);
return () => {
groups?.unobserveDeep(observerEvent);
};
}, [groups]);
return {
isCollapsed,
hideUnGroup,
fieldId,
};
}
export function useGetBoardHiddenGroup(groupId: string) {
const { columns, fieldId } = useGroup(groupId);
const [hiddenColumns, setHiddenColumns] = useState<GroupColumn[]>([]);
const { hideUnGroup } = useBoardLayoutSettings();
useEffect(() => {
if (!columns) return;
const hiddenColumns = columns.filter((column) => {
if (column.id === fieldId) return hideUnGroup;
return !column.visible;
});
setHiddenColumns(hiddenColumns);
}, [columns, fieldId, hideUnGroup]);
return {
hiddenColumns,
};
}
export function useRowsByGroup(groupId: string) {
const { columns, fieldId } = useGroup(groupId);
const rows = useRowDocMap();
const rowOrders = useRowOrdersSelector();
const [visibleColumns, setVisibleColumns] = useState<GroupColumn[]>([]);
const fields = useDatabaseFields();
const [notFound, setNotFound] = useState(false);
const [groupResult, setGroupResult] = useState<Map<string, Row[]>>(new Map());
const view = useDatabaseView();
const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('1');
const filters = view?.get(YjsDatabaseKey.filters);
useEffect(() => {
if (!fieldId || !rowOrders || !rows) return;
const onConditionsChange = () => {
const newResult = new Map<string, Row[]>();
const field = fields.get(fieldId);
if (!field) {
setNotFound(true);
setGroupResult(newResult);
return;
}
const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType;
if (![FieldType.SingleSelect, FieldType.MultiSelect, FieldType.Checkbox].includes(fieldType)) {
setNotFound(true);
setGroupResult(newResult);
return;
}
const filter = filters?.toArray().find((filter) => filter.get(YjsDatabaseKey.field_id) === fieldId);
const groupResult = groupByField(rowOrders, rows, field, filter);
if (!groupResult) {
setGroupResult(newResult);
return;
}
setGroupResult(groupResult);
};
onConditionsChange();
fields.observeDeep(onConditionsChange);
filters?.observeDeep(onConditionsChange);
const debouncedConditionsChange = debounce(onConditionsChange, 150);
const observerRowsEvent = () => {
debouncedConditionsChange();
};
Object.values(rows).forEach((row) => {
row.getMap(YjsEditorKey.data_section).observeDeep(observerRowsEvent);
});
return () => {
debouncedConditionsChange.cancel();
fields.unobserveDeep(onConditionsChange);
filters?.unobserveDeep(onConditionsChange);
Object.values(rows).forEach((row) => {
row.getMap(YjsEditorKey.data_section).unobserveDeep(observerRowsEvent);
});
};
}, [fieldId, fields, rowOrders, rows, filters]);
useEffect(() => {
const observeEvent = () => {
const newVisibleColumns = columns.filter((column) => {
if (column.id === fieldId) return !layoutSetting?.get(YjsDatabaseKey.hide_ungrouped_column);
return column.visible;
});
setVisibleColumns(newVisibleColumns);
};
observeEvent();
layoutSetting?.observe(observeEvent);
return () => {
layoutSetting?.unobserve(observeEvent);
};
}, [layoutSetting, columns, fieldId]);
return {
fieldId,
groupResult,
columns: visibleColumns,
notFound,
};
}
export function useRowOrdersSelector() {
const rows = useRowDocMap();
const [rowOrders, setRowOrders] = useState<Row[]>();
const view = useDatabaseView();
const sorts = view?.get(YjsDatabaseKey.sorts);
const fields = useDatabaseFields();
const filters = view?.get(YjsDatabaseKey.filters);
const onConditionsChange = useCallback(() => {
const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON();
if (!originalRowOrders || !rows) return;
if (sorts?.length === 0 && filters?.length === 0) {
setRowOrders(originalRowOrders);
return;
}
let rowOrders: Row[] | undefined;
if (sorts?.length) {
rowOrders = sortBy(originalRowOrders, sorts, fields, rows);
}
if (filters?.length) {
rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows);
}
if (rowOrders) {
setRowOrders(rowOrders);
} else {
setRowOrders(originalRowOrders);
}
}, [fields, filters, rows, sorts, view]);
useEffect(() => {
onConditionsChange();
}, [onConditionsChange]);
useEffect(() => {
const throttleChange = debounce(onConditionsChange, 200);
view?.get(YjsDatabaseKey.row_orders)?.observeDeep(throttleChange);
sorts?.observeDeep(throttleChange);
filters?.observeDeep(throttleChange);
fields?.observeDeep(throttleChange);
const debouncedConditionsChange = debounce(onConditionsChange, 150);
const observerRowsEvent = () => {
debouncedConditionsChange();
};
Object.values(rows || {}).forEach((row) => {
row.getMap(YjsEditorKey.data_section).observeDeep(observerRowsEvent);
});
return () => {
view?.get(YjsDatabaseKey.row_orders)?.unobserveDeep(throttleChange);
sorts?.unobserveDeep(throttleChange);
filters?.unobserveDeep(throttleChange);
fields?.unobserveDeep(throttleChange);
Object.values(rows || {}).forEach((row) => {
row.getMap(YjsEditorKey.data_section).unobserveDeep(observerRowsEvent);
});
};
}, [onConditionsChange, view, fields, filters, sorts, rows]);
return rowOrders;
}
export function useRowDataSelector(rowId: string) {
const rowMap = useRowDocMap();
const rowDoc = rowMap?.[rowId];
const rowSharedRoot = rowDoc?.getMap(YjsEditorKey.data_section);
const row = rowSharedRoot?.get(YjsEditorKey.database_row);
return {
row,
};
}
export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) {
const { row } = useRowDataSelector(rowId);
const cells = row?.get(YjsDatabaseKey.cells);
const cell = cells?.get(fieldId);
const [, setClock] = useState<number>(0);
const [cellValue, setCellValue] = useState(() => {
return cell ? parseYDatabaseCellToCell(cell) : undefined;
});
useEffect(() => {
const observerEvent = () => {
setClock((prev) => prev + 1);
setCellValue(cell ? parseYDatabaseCellToCell(cell) : undefined);
};
observerEvent();
cell?.observeDeep(observerEvent);
return () => {
cell?.unobserveDeep(observerEvent);
};
}, [cell]);
useEffect(() => {
if (!cells) return;
const observerEvent = () => {
const cell = cells.get(fieldId);
if (!cell) {
setCellValue(undefined);
return;
} else {
const cellValue = parseYDatabaseCellToCell(cell);
setCellValue(cellValue);
}
};
observerEvent();
cells.observe(observerEvent);
return () => {
cells.unobserve(observerEvent);
};
}, [cells, fieldId]);
return cellValue;
}
export interface CalendarEvent {
start?: Date;
end?: Date;
id: string;
title: string;
allDay: boolean;
rowId: string;
isRange?: boolean;
}
export function useCalendarEventsSelector() {
const setting = useCalendarLayoutSetting();
const filedId = setting?.fieldId || '';
const { field } = useFieldSelector(filedId);
const primaryFieldId = usePrimaryFieldId();
const rowOrders = useRowOrdersSelector();
const rows = useRowDocMap();
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [emptyEvents, setEmptyEvents] = useState<CalendarEvent[]>([]);
useEffect(() => {
if (!field || !rowOrders || !rows || !filedId) return;
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
if (![FieldType.DateTime, FieldType.LastEditedTime, FieldType.CreatedTime].includes(fieldType) || !primaryFieldId) return;
const observerEvent = () => {
const newEvents: CalendarEvent[] = [];
const emptyEvents: CalendarEvent[] = [];
rowOrders?.forEach((row) => {
const cell = getCell(row.id, filedId, rows);
const primaryCell = getCell(row.id, primaryFieldId, rows);
const allDay = !cell?.get(YjsDatabaseKey.include_time);
const title = (primaryCell?.get(YjsDatabaseKey.data) as string) || '';
const doc = rows?.[row.id];
if (!doc) return;
const rowSharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
const databbaseRow = rowSharedRoot?.get(YjsEditorKey.database_row);
if (!databbaseRow) return;
const rowCreatedTime = databbaseRow.get(YjsDatabaseKey.created_at).toString();
const rowLastEditedTime = databbaseRow.get(YjsDatabaseKey.last_modified).toString();
const value = cell ? parseYDatabaseCellToCell(cell) as DateTimeCell : undefined;
if ((!value?.data && fieldType !== FieldType.CreatedTime && fieldType !== FieldType.LastEditedTime) ||
(fieldType === FieldType.CreatedTime && !rowCreatedTime) ||
(fieldType === FieldType.LastEditedTime && !rowLastEditedTime)
) {
emptyEvents.push({
id: `${row.id}`,
title,
allDay,
rowId: row.id,
});
return;
}
const getDate = (timestamp: string) => {
const dayjsResult = timestamp.length === 10 ? dayjs.unix(Number(timestamp)) : dayjs(timestamp);
return dayjsResult.toDate();
};
if ([FieldType.CreatedTime, FieldType.LastEditedTime].includes(fieldType)) {
newEvents.push({
id: `${row.id}`,
start: fieldType === FieldType.CreatedTime ? getDate(rowCreatedTime) : getDate(rowLastEditedTime),
title,
allDay,
rowId: row.id,
});
} else if (value) {
newEvents.push({
id: `${row.id}`,
start: getDate(value.data),
isRange: value.isRange || false,
end: value.endTimestamp && value.isRange ? getDate(value.endTimestamp) : dayjs(getDate(value.data)).add(30, 'minute').toDate(),
title,
allDay,
rowId: row.id,
});
}
});
setEvents(newEvents);
setEmptyEvents(emptyEvents);
}
observerEvent();
field?.observeDeep(observerEvent);
const debouncedObserverEvent = debounce(observerEvent, 150);
// for every row
rowOrders?.forEach((row) => {
const rowDoc = rows?.[row.id];
if (!rowDoc) return;
rowDoc.getMap(YjsEditorKey.data_section).observeDeep(debouncedObserverEvent);
});
return () => {
debouncedObserverEvent.cancel();
field?.unobserveDeep(observerEvent);
rowOrders?.forEach((row) => {
const rowDoc = rows?.[row.id];
if (!rowDoc) return;
rowDoc.getMap(YjsEditorKey.data_section).unobserveDeep(debouncedObserverEvent);
});
};
}, [field, rowOrders, rows, filedId, primaryFieldId]);
return { events, emptyEvents };
}
export function useCalendarLayoutSetting() {
const currentUser = useCurrentUser();
const startWeekOn = Number(currentUser?.metadata?.[MetadataKey.StartWeekOn] || 0);
const timeFormat = currentUser?.metadata?.[MetadataKey.TimeFormat] || TimeFormat.TwelveHour;
const database = useDatabase();
const [setting, setSetting] = useState<CalendarLayoutSetting | null>(null);
const viewId = useDatabaseViewId();
useEffect(() => {
const view = database.get(YjsDatabaseKey.views)?.get(viewId);
const observerHandler = () => {
const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('2');
const firstDayOfWeek = layoutSetting?.get(YjsDatabaseKey.first_day_of_week) === undefined ? startWeekOn : Number(layoutSetting?.get(YjsDatabaseKey.first_day_of_week) || 0);
setSetting({
fieldId: layoutSetting?.get(YjsDatabaseKey.field_id),
firstDayOfWeek,
showWeekNumbers: Boolean(layoutSetting?.get(YjsDatabaseKey.show_week_numbers)),
showWeekends: Boolean(layoutSetting?.get(YjsDatabaseKey.show_weekends)),
layout: Number(layoutSetting?.get(YjsDatabaseKey.layout_ty)),
numberOfDays: layoutSetting?.get(YjsDatabaseKey.number_of_days) || 7,
use24Hour: timeFormat === TimeFormat.TwentyFourHour,
});
};
observerHandler();
view?.observeDeep(observerHandler);
return () => {
view?.unobserveDeep(observerHandler);
};
}, [startWeekOn, timeFormat, database, viewId]);
return setting;
}
export function getPrimaryFieldId(database: YDatabase) {
const fields = database?.get(YjsDatabaseKey.fields);
return Array.from(fields?.keys() || []).find((fieldId) => {
return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary);
});
}
export function usePrimaryFieldId() {
const database = useDatabase();
const [primaryFieldId, setPrimaryFieldId] = useState<string | null>(null);
useEffect(() => {
setPrimaryFieldId(getPrimaryFieldId(database) || null);
}, [database]);
return primaryFieldId;
}
export const useRowMetaSelector = (rowId: string) => {
const [meta, setMeta] = useState<RowMeta | null>();
const rowMap = useRowDocMap();
const updateMeta = useCallback(() => {
const row = rowMap?.[rowId];
if (!row || !row.share.has(YjsEditorKey.data_section)) return;
const rowSharedRoot = row.getMap(YjsEditorKey.data_section);
const yMeta = rowSharedRoot?.get(YjsEditorKey.meta);
if (!yMeta) return;
const meta = getMetaJSON(rowId, yMeta);
setMeta(meta);
}, [rowId, rowMap]);
useEffect(() => {
if (!rowMap) return;
updateMeta();
const observerEvent = () => updateMeta();
const rowDoc = rowMap[rowId];
if (!rowDoc || !rowDoc.share.has(YjsEditorKey.data_section)) return;
const rowSharedRoot = rowDoc.getMap(YjsEditorKey.data_section);
const meta = rowSharedRoot?.get(YjsEditorKey.meta) as YDatabaseMetas;
meta?.observeDeep(observerEvent);
return () => {
meta?.unobserveDeep(observerEvent);
};
}, [rowId, rowMap, updateMeta]);
return meta;
};
export const useFieldCellsSelector = (fieldId: string) => {
const rows = useRowOrdersSelector();
const [cells, setCells] = useState<Map<string, unknown> | null>(null);
const rowMap = useRowDocMap();
const cellObserverEventsRef = useRef<(() => void)[]>([]);
useEffect(() => {
if (!rows || !rowMap) return;
setCells(null);
rows.forEach((row) => {
const rowDoc = rowMap?.[row.id];
const rowSharedRoot = rowDoc?.getMap(YjsEditorKey.data_section);
const databaseRow = rowSharedRoot?.get(YjsEditorKey.database_row) as YDatabaseRow;
if (!databaseRow) return;
const cells = databaseRow.get(YjsDatabaseKey.cells);
const observerEvent = () => {
const cell = databaseRow.get(YjsDatabaseKey.cells)?.get(fieldId);
if (!cell) {
setCells((prev) => {
const newMap = new Map(prev);
newMap.set(row.id, '');
return newMap;
});
return;
}
const cellData = cell.get(YjsDatabaseKey.data);
setCells((prev) => {
const newMap = new Map(prev);
newMap.set(row.id, cellData);
return newMap;
});
};
observerEvent();
cells?.observeDeep(observerEvent);
cellObserverEventsRef.current.push(() => {
cells?.unobserveDeep(observerEvent);
});
});
return () => {
cellObserverEventsRef.current.forEach((unobserverEvent) => {
unobserverEvent();
});
cellObserverEventsRef.current = [];
};
}, [rows, rowMap, fieldId]);
return {
cells,
};
};
export const usePropertiesSelector = (isFilterHidden?: boolean) => {
const database = useDatabase();
const view = useDatabaseView();
const fieldSettings = view?.get(YjsDatabaseKey.field_settings);
const fieldOrders = view?.get(YjsDatabaseKey.field_orders);
const fields = database?.get(YjsDatabaseKey.fields);
const [hiddenProperties, setHiddenProperties] = useState<
{
id: string;
visible: boolean;
name: string;
type: FieldType;
}[]
>([]);
const [properties, setProperties] = useState<{ id: string; visible: boolean; name: string; type: FieldType }[]>([]);
useEffect(() => {
if (!fieldOrders) return;
const observeEvent = () => {
const newProperties: {
id: string;
visible: boolean;
name: string;
type: FieldType;
}[] = [];
const hiddenProperties: {
id: string;
visible: boolean;
name: string;
type: FieldType;
}[] = [];
fieldOrders.toArray().forEach((item) => {
const fieldSetting = fieldSettings?.get(item.id);
const visible = fieldSetting
? Number(fieldSetting.get(YjsDatabaseKey.visibility)) !== FieldVisibility.AlwaysHidden
: true;
const field = fields?.get(item.id);
if (!visible) {
hiddenProperties.push({
id: item.id,
name: field?.get(YjsDatabaseKey.name) || '',
visible,
type: Number(field?.get(YjsDatabaseKey.type)) as FieldType,
});
}
if (isFilterHidden && !visible) {
return;
} else {
newProperties.push({
id: item.id,
name: field?.get(YjsDatabaseKey.name) || '',
visible,
type: Number(field?.get(YjsDatabaseKey.type)) as FieldType,
});
}
});
setProperties(newProperties);
setHiddenProperties(hiddenProperties);
};
observeEvent();
fields.observeDeep(observeEvent);
fieldOrders.observeDeep(observeEvent);
fieldSettings?.observeDeep(observeEvent);
return () => {
fields.unobserveDeep(observeEvent);
fieldOrders.unobserveDeep(observeEvent);
fieldSettings?.unobserveDeep(observeEvent);
};
}, [fieldOrders, fieldSettings, fields, isFilterHidden]);
return {
properties,
hiddenProperties,
};
};
export const useDateTimeCellString = (cell: DateTimeCell | undefined, fieldId: string) => {
const currentUser = useCurrentUser();
const { field, clock } = useFieldSelector(fieldId);
return useMemo(() => {
if (!cell) return null;
return getDateCellStr({ cell, field, currentUser });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [cell, field, clock, currentUser]);
};
export const useRowTimeString = (rowId: string, fieldId: string, attrName: string) => {
const currentUser = useCurrentUser();
const { field, clock } = useFieldSelector(fieldId);
const typeOptionValue = useMemo(() => {
const typeOption = getTypeOptions(field);
const { dateFormat, timeFormat } = getFieldDateTimeFormats(typeOption, currentUser);
const includeTimeRaw = typeOption?.get(YjsDatabaseKey.include_time);
return {
dateFormat,
timeFormat,
includeTime: typeof includeTimeRaw === 'boolean' ? includeTimeRaw : Boolean(includeTimeRaw),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [field, clock, currentUser?.metadata]);
const getDateTimeStr = useCallback(
(timeStamp: string, includeTime?: boolean) => {
if (!typeOptionValue || !timeStamp) return null;
const timeFormat = getTimeFormat(typeOptionValue.timeFormat);
const dateFormat = getDateFormat(typeOptionValue.dateFormat);
const format = [dateFormat];
if (includeTime || typeOptionValue.includeTime) {
format.push(timeFormat);
}
return renderDate(timeStamp, format.join(' '), true);
},
[typeOptionValue]
);
const { row: rowData } = useRowDataSelector(rowId);
const [value, setValue] = useState<string | null>(null);
useEffect(() => {
if (!rowData) return;
const observeHandler = () => {
setValue(rowData.get(attrName));
};
observeHandler();
rowData.observe(observeHandler);
return () => {
rowData.unobserve(observeHandler);
};
}, [rowData, attrName]);
const time = useMemo(() => {
if (!value) return null;
return getDateTimeStr(value);
}, [value, getDateTimeStr]);
return time;
};
export const useSelectFieldOptions = (fieldId: string, searchValue?: string) => {
const { field, clock } = useFieldSelector(fieldId);
return useMemo(() => {
const typeOption = field ? parseSelectOptionTypeOptions(field) : null;
if (!typeOption) return [] as SelectOption[];
const normalizedOptions = typeOption.options.filter((option) => {
return Boolean(option && option.id);
});
return normalizedOptions.filter((option) => {
const optionName = typeof option.name === 'string' ? option.name : '';
if (!searchValue) return true;
return optionName.toLowerCase().includes(searchValue.toLowerCase());
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [field, searchValue, clock]);
};
export function useRowPrimaryContentSelector(rowDoc: YDoc | null, primaryFieldId: string) {
const [primaryContent, setPrimaryContent] = useState<string | null>(null);
const rowSharedRoot = rowDoc?.getMap(YjsEditorKey.data_section);
const row = rowSharedRoot?.get(YjsEditorKey.database_row) as YDatabaseRow;
useEffect(() => {
const observerEvent = () => {
if (!row) return;
const cell = row.get(YjsDatabaseKey.cells)?.get(primaryFieldId);
if (!cell) return;
const cellValue = parseYDatabaseCellToCell(cell);
if (cellValue) {
setPrimaryContent(cellValue.data as string);
} else {
setPrimaryContent(null);
}
};
observerEvent();
row?.observeDeep(observerEvent);
return () => {
row?.unobserveDeep(observerEvent);
};
}, [primaryFieldId, row, rowDoc]);
return primaryContent;
}