mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support board preview on web (#5384)
This commit is contained in:
parent
aa07393253
commit
b7bc847107
@ -107,6 +107,7 @@
|
|||||||
"@types/quill": "^2.0.10",
|
"@types/quill": "^2.0.10",
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
"@types/react-beautiful-dnd": "^13.1.3",
|
"@types/react-beautiful-dnd": "^13.1.3",
|
||||||
|
"@types/react-big-calendar": "^1.8.9",
|
||||||
"@types/react-color": "^3.0.6",
|
"@types/react-color": "^3.0.6",
|
||||||
"@types/react-custom-scrollbars": "^4.0.13",
|
"@types/react-custom-scrollbars": "^4.0.13",
|
||||||
"@types/react-datepicker": "^4.19.3",
|
"@types/react-datepicker": "^4.19.3",
|
||||||
|
@ -256,6 +256,9 @@ devDependencies:
|
|||||||
'@types/react-beautiful-dnd':
|
'@types/react-beautiful-dnd':
|
||||||
specifier: ^13.1.3
|
specifier: ^13.1.3
|
||||||
version: 13.1.3
|
version: 13.1.3
|
||||||
|
'@types/react-big-calendar':
|
||||||
|
specifier: ^1.8.9
|
||||||
|
version: 1.8.9
|
||||||
'@types/react-color':
|
'@types/react-color':
|
||||||
specifier: ^3.0.6
|
specifier: ^3.0.6
|
||||||
version: 3.0.6
|
version: 3.0.6
|
||||||
@ -2618,6 +2621,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.24.0
|
'@babel/types': 7.24.0
|
||||||
|
|
||||||
|
/@types/date-arithmetic@4.1.4:
|
||||||
|
resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/estree@1.0.5:
|
/@types/estree@1.0.5:
|
||||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||||
|
|
||||||
@ -2723,6 +2730,14 @@ packages:
|
|||||||
'@types/react': 18.2.66
|
'@types/react': 18.2.66
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/react-big-calendar@1.8.9:
|
||||||
|
resolution: {integrity: sha512-HIHLUxR3PzWHrFdZ00VnCMvDjAh5uzlL0vMC2b7tL3bKaAJsqq9T8h+x0GVeDbZfMfHAd1cs5tZBhVvourNJXQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/date-arithmetic': 4.1.4
|
||||||
|
'@types/prop-types': 15.7.12
|
||||||
|
'@types/react': 18.2.66
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/react-color@3.0.6:
|
/@types/react-color@3.0.6:
|
||||||
resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==}
|
resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -241,6 +241,9 @@ export enum YjsDatabaseKey {
|
|||||||
condition = 'condition',
|
condition = 'condition',
|
||||||
format = 'format',
|
format = 'format',
|
||||||
filter_type = 'filter_type',
|
filter_type = 'filter_type',
|
||||||
|
visible = 'visible',
|
||||||
|
hide_ungrouped_column = 'hide_ungrouped_column',
|
||||||
|
collapse_hidden_groups = 'collapse_hidden_groups',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface YDoc extends Y.Doc {
|
export interface YDoc extends Y.Doc {
|
||||||
@ -425,18 +428,48 @@ export type YDatabaseFieldOrders = Y.Array<unknown>; // [ { id: FieldId } ]
|
|||||||
|
|
||||||
export type YDatabaseRowOrders = Y.Array<YDatabaseRowOrder>; // [ { id: RowId, height: number } ]
|
export type YDatabaseRowOrders = Y.Array<YDatabaseRowOrder>; // [ { id: RowId, height: number } ]
|
||||||
|
|
||||||
export type YDatabaseGroups = Y.Array<unknown>;
|
export type YDatabaseGroups = Y.Array<YDatabaseGroup>;
|
||||||
|
|
||||||
export type YDatabaseFilters = Y.Array<YDatabaseFilter>;
|
export type YDatabaseFilters = Y.Array<YDatabaseFilter>;
|
||||||
|
|
||||||
export type YDatabaseSorts = Y.Array<YDatabaseSort>;
|
export type YDatabaseSorts = Y.Array<YDatabaseSort>;
|
||||||
|
|
||||||
export type YDatabaseLayoutSettings = Y.Map<unknown>;
|
export type YDatabaseLayoutSettings = Y.Map<YDatabaseLayoutSetting>;
|
||||||
|
|
||||||
export type YDatabaseCalculations = Y.Array<YDatabaseCalculation>;
|
export type YDatabaseCalculations = Y.Array<YDatabaseCalculation>;
|
||||||
|
|
||||||
export type SortId = string;
|
export type SortId = string;
|
||||||
|
|
||||||
|
export type GroupId = string;
|
||||||
|
|
||||||
|
export interface YDatabaseLayoutSetting extends Y.Map<unknown> {
|
||||||
|
// DatabaseViewLayout.Board
|
||||||
|
get(key: '2'): YDatabaseBoardLayoutSetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YDatabaseBoardLayoutSetting extends Y.Map<unknown> {
|
||||||
|
get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface YDatabaseGroup extends Y.Map<unknown> {
|
||||||
|
get(key: YjsDatabaseKey.id): GroupId;
|
||||||
|
|
||||||
|
get(key: YjsDatabaseKey.field_id): FieldId;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||||
|
get(key: YjsDatabaseKey.content): string;
|
||||||
|
|
||||||
|
get(key: YjsDatabaseKey.groups): YDatabaseGroupColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type YDatabaseGroupColumns = Y.Array<YDatabaseGroupColumn>;
|
||||||
|
|
||||||
|
export interface YDatabaseGroupColumn extends Y.Map<unknown> {
|
||||||
|
get(key: YjsDatabaseKey.id): string;
|
||||||
|
|
||||||
|
get(key: YjsDatabaseKey.visible): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface YDatabaseRowOrder extends Y.Map<unknown> {
|
export interface YDatabaseRowOrder extends Y.Map<unknown> {
|
||||||
get(key: YjsDatabaseKey.id): SortId;
|
get(key: YjsDatabaseKey.id): SortId;
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { filterBy } from '@/application/database-yjs/filter';
|
|
||||||
import { Row } from '@/application/database-yjs/selector';
|
import { Row } from '@/application/database-yjs/selector';
|
||||||
import { sortBy } from '@/application/database-yjs/sort';
|
import { createContext, useContext } from 'react';
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
import debounce from 'lodash-es/debounce';
|
|
||||||
|
|
||||||
export interface DatabaseContextState {
|
export interface DatabaseContextState {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
@ -56,72 +53,16 @@ export function useDatabaseFields() {
|
|||||||
return database.get(YjsDatabaseKey.fields);
|
return database.get(YjsDatabaseKey.fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridRowsState {
|
export interface RowsState {
|
||||||
rowOrders: Row[];
|
rowOrders: Row[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GridRowsContext = createContext<GridRowsState | null>(null);
|
export const RowsContext = createContext<RowsState | null>(null);
|
||||||
|
|
||||||
export function useGridRowsContext() {
|
export function useRowsContext() {
|
||||||
return useContext(GridRowsContext);
|
return useContext(RowsContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGridRows() {
|
export function useRows() {
|
||||||
return useGridRowsContext()?.rowOrders;
|
return useRowsContext()?.rowOrders;
|
||||||
}
|
|
||||||
|
|
||||||
export function useGridRowOrders() {
|
|
||||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
|
||||||
const [rowOrders, setRowOrders] = useState<Row[]>();
|
|
||||||
const view = useDatabaseView();
|
|
||||||
const sorts = view?.get(YjsDatabaseKey.sorts);
|
|
||||||
const fields = useDatabaseFields();
|
|
||||||
const filters = view?.get(YjsDatabaseKey.filters);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onConditionsChange = () => {
|
|
||||||
const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON();
|
|
||||||
|
|
||||||
if (!originalRowOrders || !rows) return;
|
|
||||||
|
|
||||||
console.log('sort or filter changed');
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debounceConditionsChange = debounce(onConditionsChange, 200);
|
|
||||||
|
|
||||||
onConditionsChange();
|
|
||||||
sorts?.observeDeep(debounceConditionsChange);
|
|
||||||
filters?.observeDeep(debounceConditionsChange);
|
|
||||||
fields?.observeDeep(debounceConditionsChange);
|
|
||||||
rows?.observeDeep(debounceConditionsChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
sorts?.unobserveDeep(debounceConditionsChange);
|
|
||||||
filters?.unobserveDeep(debounceConditionsChange);
|
|
||||||
fields?.unobserveDeep(debounceConditionsChange);
|
|
||||||
rows?.observeDeep(debounceConditionsChange);
|
|
||||||
};
|
|
||||||
}, [fields, rows, sorts, filters, view]);
|
|
||||||
|
|
||||||
return rowOrders;
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { YDatabaseField, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { FieldType } from '@/application/database-yjs/database.type';
|
||||||
|
import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields';
|
||||||
|
import { Row } from '@/application/database-yjs/selector';
|
||||||
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
|
export function groupByField(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabaseField) {
|
||||||
|
const fieldType = Number(field.get(YjsDatabaseKey.type));
|
||||||
|
const isSelectOptionField = [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType);
|
||||||
|
|
||||||
|
if (!isSelectOptionField) return;
|
||||||
|
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[]>();
|
||||||
|
const typeOption = parseSelectOptionTypeOptions(field);
|
||||||
|
|
||||||
|
if (!typeOption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeOption.options.length === 0) {
|
||||||
|
result.set(fieldId, rows);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach((row) => {
|
||||||
|
const cellData = getCellData(row.id, fieldId, rowMetas);
|
||||||
|
|
||||||
|
const selectedIds = (cellData as string)?.split(',') ?? [];
|
||||||
|
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
const group = result.get(fieldId) ?? [];
|
||||||
|
|
||||||
|
group.push(row);
|
||||||
|
result.set(fieldId, group);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIds.forEach((id) => {
|
||||||
|
const option = typeOption.options.find((option) => option.id === id);
|
||||||
|
const groupName = option?.id ?? fieldId;
|
||||||
|
const group = result.get(groupName) ?? [];
|
||||||
|
|
||||||
|
group.push(row);
|
||||||
|
result.set(groupName, group);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
@ -1,9 +1,22 @@
|
|||||||
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type';
|
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||||
import { useDatabase, useGridRows, useViewId } from '@/application/database-yjs/context';
|
import {
|
||||||
import { parseFilter } from '@/application/database-yjs/filter';
|
DatabaseContext,
|
||||||
|
useDatabase,
|
||||||
|
useDatabaseFields,
|
||||||
|
useDatabaseView,
|
||||||
|
useRowMeta,
|
||||||
|
useRows,
|
||||||
|
useViewId,
|
||||||
|
} from '@/application/database-yjs/context';
|
||||||
|
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 { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
||||||
|
import debounce from 'lodash-es/debounce';
|
||||||
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type';
|
import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
export interface Column {
|
export interface Column {
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
@ -19,11 +32,43 @@ export interface Row {
|
|||||||
|
|
||||||
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
||||||
|
|
||||||
export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibility[] = defaultVisible) {
|
export function useDatabaseViewsSelector() {
|
||||||
|
const database = useDatabase();
|
||||||
|
const { viewsId: visibleViewsId } = useViewsIdSelector();
|
||||||
|
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 = () => {
|
||||||
|
setViewIds(Array.from(views.keys()).filter((id) => visibleViewsId.includes(id)));
|
||||||
|
};
|
||||||
|
|
||||||
|
observerEvent();
|
||||||
|
views.observe(observerEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
views.unobserve(observerEvent);
|
||||||
|
};
|
||||||
|
}, [visibleViewsId, views]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
childViews,
|
||||||
|
viewIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisible) {
|
||||||
|
const viewId = useViewId();
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
const [columns, setColumns] = useState<Column[]>([]);
|
const [columns, setColumns] = useState<Column[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!viewId) return;
|
||||||
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
|
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
|
||||||
const fields = database?.get(YjsDatabaseKey.fields);
|
const fields = database?.get(YjsDatabaseKey.fields);
|
||||||
const fieldsOrder = view?.get(YjsDatabaseKey.field_orders);
|
const fieldsOrder = view?.get(YjsDatabaseKey.field_orders);
|
||||||
@ -39,11 +84,15 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil
|
|||||||
return {
|
return {
|
||||||
fieldId,
|
fieldId,
|
||||||
width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH,
|
width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH,
|
||||||
visibility: parseInt(setting?.get(YjsDatabaseKey.visibility)) as FieldVisibility,
|
visibility: Number(
|
||||||
|
setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown
|
||||||
|
) as FieldVisibility,
|
||||||
wrap: setting?.get(YjsDatabaseKey.wrap),
|
wrap: setting?.get(YjsDatabaseKey.wrap),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((column) => visibilitys.includes(column.visibility));
|
.filter((column) => {
|
||||||
|
return visibilitys.includes(column.visibility);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const observerEvent = () => setColumns(getColumns());
|
const observerEvent = () => setColumns(getColumns());
|
||||||
@ -62,8 +111,8 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil
|
|||||||
return columns;
|
return columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGridRowsSelector() {
|
export function useRowsSelector() {
|
||||||
const rowOrders = useGridRows();
|
const rowOrders = useRows();
|
||||||
|
|
||||||
return useMemo(() => rowOrders ?? [], [rowOrders]);
|
return useMemo(() => rowOrders ?? [], [rowOrders]);
|
||||||
}
|
}
|
||||||
@ -81,10 +130,10 @@ export function useFieldSelector(fieldId: string) {
|
|||||||
setField(field || null);
|
setField(field || null);
|
||||||
const observerEvent = () => setClock((prev) => prev + 1);
|
const observerEvent = () => setClock((prev) => prev + 1);
|
||||||
|
|
||||||
field.observe(observerEvent);
|
field?.observe(observerEvent);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
field.unobserve(observerEvent);
|
field?.unobserve(observerEvent);
|
||||||
};
|
};
|
||||||
}, [database, fieldId]);
|
}, [database, fieldId]);
|
||||||
|
|
||||||
@ -225,3 +274,207 @@ export function useSortSelector(sortId: SortId) {
|
|||||||
|
|
||||||
return sortValue;
|
return sortValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGroupsSelector() {
|
||||||
|
const database = useDatabase();
|
||||||
|
const viewId = useViewId();
|
||||||
|
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.toJSON().map((item) => item.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const observerEvent = () => setGroups(getGroups());
|
||||||
|
|
||||||
|
setGroups(getGroups());
|
||||||
|
|
||||||
|
groupOrders.observe(observerEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
groupOrders.unobserve(observerEvent);
|
||||||
|
};
|
||||||
|
}, [database, viewId]);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupColumn {
|
||||||
|
id: string;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGroup(groupId: string) {
|
||||||
|
const database = useDatabase();
|
||||||
|
const viewId = useViewId() as string;
|
||||||
|
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
|
||||||
|
const group = view
|
||||||
|
?.get(YjsDatabaseKey.groups)
|
||||||
|
?.toArray()
|
||||||
|
.find((group) => group.get(YjsDatabaseKey.id) === groupId);
|
||||||
|
const groupColumns = group?.get(YjsDatabaseKey.groups);
|
||||||
|
const [fieldId, setFieldId] = useState<string | null>(null);
|
||||||
|
const [columns, setColumns] = useState<GroupColumn[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewId) return;
|
||||||
|
|
||||||
|
const observerEvent = () => {
|
||||||
|
setFieldId(group?.get(YjsDatabaseKey.field_id) as string);
|
||||||
|
};
|
||||||
|
|
||||||
|
observerEvent();
|
||||||
|
group?.observe(observerEvent);
|
||||||
|
|
||||||
|
const observerColumns = () => {
|
||||||
|
if (!groupColumns) return;
|
||||||
|
setColumns(groupColumns.toJSON());
|
||||||
|
};
|
||||||
|
|
||||||
|
observerColumns();
|
||||||
|
groupColumns?.observe(observerColumns);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
group?.unobserve(observerEvent);
|
||||||
|
groupColumns?.unobserve(observerColumns);
|
||||||
|
};
|
||||||
|
}, [database, viewId, groupId, group, groupColumns]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
fieldId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRowsByGroup(groupId: string) {
|
||||||
|
const { columns, fieldId } = useGroup(groupId);
|
||||||
|
const rows = useContext(DatabaseContext)?.rowDocMap;
|
||||||
|
const rowOrders = useRowOrdersSelector();
|
||||||
|
const fields = useDatabaseFields();
|
||||||
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
const [groupResult, setGroupResult] = useState<Map<string, Row[]>>(new Map());
|
||||||
|
|
||||||
|
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 groupResult = groupByField(rowOrders, rows, field);
|
||||||
|
|
||||||
|
if (!groupResult) {
|
||||||
|
setGroupResult(newResult);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroupResult(groupResult);
|
||||||
|
};
|
||||||
|
|
||||||
|
onConditionsChange();
|
||||||
|
|
||||||
|
const debounceConditionsChange = debounce(onConditionsChange, 200);
|
||||||
|
|
||||||
|
fields.observeDeep(debounceConditionsChange);
|
||||||
|
return () => {
|
||||||
|
fields.unobserveDeep(debounceConditionsChange);
|
||||||
|
};
|
||||||
|
}, [fieldId, fields, rowOrders, rows]);
|
||||||
|
|
||||||
|
const visibleColumns = columns.filter((column) => column.visible);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldId,
|
||||||
|
groupResult,
|
||||||
|
columns: visibleColumns,
|
||||||
|
notFound,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRowOrdersSelector() {
|
||||||
|
const rows = useContext(DatabaseContext)?.rowDocMap;
|
||||||
|
const [rowOrders, setRowOrders] = useState<Row[]>();
|
||||||
|
const view = useDatabaseView();
|
||||||
|
const sorts = view?.get(YjsDatabaseKey.sorts);
|
||||||
|
const fields = useDatabaseFields();
|
||||||
|
const filters = view?.get(YjsDatabaseKey.filters);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onConditionsChange = () => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceConditionsChange = debounce(onConditionsChange, 200);
|
||||||
|
|
||||||
|
onConditionsChange();
|
||||||
|
sorts?.observeDeep(debounceConditionsChange);
|
||||||
|
filters?.observeDeep(debounceConditionsChange);
|
||||||
|
fields?.observeDeep(debounceConditionsChange);
|
||||||
|
rows?.observeDeep(debounceConditionsChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
sorts?.unobserveDeep(debounceConditionsChange);
|
||||||
|
filters?.unobserveDeep(debounceConditionsChange);
|
||||||
|
fields?.unobserveDeep(debounceConditionsChange);
|
||||||
|
rows?.observeDeep(debounceConditionsChange);
|
||||||
|
};
|
||||||
|
}, [fields, rows, sorts, filters, view]);
|
||||||
|
|
||||||
|
return rowOrders;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) {
|
||||||
|
const row = useRowMeta(rowId);
|
||||||
|
const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId);
|
||||||
|
const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cell) return;
|
||||||
|
setCellValue(parseYDatabaseCellToCell(cell));
|
||||||
|
const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell));
|
||||||
|
|
||||||
|
cell.observe(observerEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cell.unobserve(observerEvent);
|
||||||
|
};
|
||||||
|
}, [cell]);
|
||||||
|
|
||||||
|
return cellValue;
|
||||||
|
}
|
||||||
|
@ -12,11 +12,14 @@ export function useViewsIdSelector() {
|
|||||||
const views = folder.get(YjsFolderKey.views);
|
const views = folder.get(YjsFolderKey.views);
|
||||||
const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
|
const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
|
||||||
const meta = folder.get(YjsFolderKey.meta);
|
const meta = folder.get(YjsFolderKey.meta);
|
||||||
|
const trashUid = Array.from(trash?.keys())[0];
|
||||||
|
const userTrash = trash?.get(trashUid);
|
||||||
|
|
||||||
console.log('folder', folder.toJSON());
|
|
||||||
const collectIds = () => {
|
const collectIds = () => {
|
||||||
|
const trashIds = userTrash?.toJSON()?.map((item) => item.id) || [];
|
||||||
|
|
||||||
return Array.from(views.keys()).filter(
|
return Array.from(views.keys()).filter(
|
||||||
(id) => !trash?.has(id) && id !== meta?.get(YjsFolderKey.current_workspace)
|
(id) => !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,9 +27,11 @@ export function useViewsIdSelector() {
|
|||||||
const observerEvent = () => setViewsId(collectIds());
|
const observerEvent = () => setViewsId(collectIds());
|
||||||
|
|
||||||
folder.observe(observerEvent);
|
folder.observe(observerEvent);
|
||||||
|
userTrash.observe(observerEvent);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
folder.unobserve(observerEvent);
|
folder.unobserve(observerEvent);
|
||||||
|
userTrash.unobserve(observerEvent);
|
||||||
};
|
};
|
||||||
}, [folder]);
|
}, [folder]);
|
||||||
|
|
||||||
|
@ -1,27 +1,20 @@
|
|||||||
import { DatabaseViewLayout, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
import { YDoc, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
|
import RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { Board } from '@/components/database/board';
|
import DatabaseViews from '@/components/database/DatabaseViews';
|
||||||
import { Calendar } from '@/components/database/calendar';
|
|
||||||
import { DatabaseConditionsContext } from '@/components/database/components/conditions/context';
|
|
||||||
import { Grid } from '@/components/database/grid';
|
|
||||||
import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs';
|
|
||||||
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||||
import DatabaseTitle from '@/components/database/DatabaseTitle';
|
import DatabaseTitle from '@/components/database/DatabaseTitle';
|
||||||
import { Log } from '@/utils/log';
|
import { Log } from '@/utils/log';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import React, { memo, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import SwipeableViews from 'react-swipeable-views';
|
|
||||||
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export const Database = memo(() => {
|
export const Database = memo(() => {
|
||||||
const { objectId, workspaceId } = useId() || {};
|
const { objectId, workspaceId } = useId() || {};
|
||||||
const [search, setSearch] = useSearchParams();
|
const [search, setSearch] = useSearchParams();
|
||||||
const viewId = search.get('v');
|
const viewId = search.get('v');
|
||||||
|
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||||
const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc
|
const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
const [notFound, setNotFound] = useState<boolean>(false);
|
||||||
@ -48,10 +41,6 @@ export const Database = memo(() => {
|
|||||||
void handleOpenDocument();
|
void handleOpenDocument();
|
||||||
}, [handleOpenDocument]);
|
}, [handleOpenDocument]);
|
||||||
|
|
||||||
const database = useMemo(() => doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase, [doc]);
|
|
||||||
|
|
||||||
const views = useMemo(() => database?.get(YjsDatabaseKey.views), [database]);
|
|
||||||
|
|
||||||
const handleChangeView = useCallback(
|
const handleChangeView = useCallback(
|
||||||
(viewId: string) => {
|
(viewId: string) => {
|
||||||
setSearch({ v: viewId });
|
setSearch({ v: viewId });
|
||||||
@ -59,32 +48,6 @@ export const Database = memo(() => {
|
|||||||
[setSearch]
|
[setSearch]
|
||||||
);
|
);
|
||||||
|
|
||||||
const viewIds = useMemo(() => (views ? Array.from(views.keys()) : []), [views]);
|
|
||||||
|
|
||||||
const value = useMemo(() => {
|
|
||||||
return Math.max(
|
|
||||||
0,
|
|
||||||
viewIds.findIndex((id) => id === (viewId ?? objectId))
|
|
||||||
);
|
|
||||||
}, [viewId, viewIds, objectId]);
|
|
||||||
|
|
||||||
const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => {
|
|
||||||
switch (layout) {
|
|
||||||
case DatabaseViewLayout.Grid:
|
|
||||||
return Grid;
|
|
||||||
case DatabaseViewLayout.Board:
|
|
||||||
return Board;
|
|
||||||
case DatabaseViewLayout.Calendar:
|
|
||||||
return Calendar;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [conditionsExpanded, setConditionsExpanded] = useState<boolean>(false);
|
|
||||||
const toggleExpanded = useCallback(() => {
|
|
||||||
setConditionsExpanded((prev) => !prev);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
console.log('viewId', viewId, 'objectId', doc, objectId, database);
|
|
||||||
if (!objectId) return null;
|
if (!objectId) return null;
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
@ -104,41 +67,7 @@ export const Database = memo(() => {
|
|||||||
<DatabaseTitle viewId={objectId} />
|
<DatabaseTitle viewId={objectId} />
|
||||||
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
|
||||||
<DatabaseContextProvider viewId={viewId || objectId} doc={doc} rowDocMap={rows} readOnly={true}>
|
<DatabaseContextProvider viewId={viewId || objectId} doc={doc} rowDocMap={rows} readOnly={true}>
|
||||||
<DatabaseConditionsContext.Provider
|
<DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} />
|
||||||
value={{
|
|
||||||
expanded: conditionsExpanded,
|
|
||||||
toggleExpanded,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DatabaseTabs selectedViewId={viewId || objectId} setSelectedViewId={handleChangeView} viewIds={viewIds} />
|
|
||||||
<DatabaseConditions />
|
|
||||||
</DatabaseConditionsContext.Provider>
|
|
||||||
<SwipeableViews
|
|
||||||
slideStyle={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
className={'h-full w-full flex-1 overflow-hidden'}
|
|
||||||
axis={'x'}
|
|
||||||
index={value}
|
|
||||||
containerStyle={{ height: '100%' }}
|
|
||||||
>
|
|
||||||
{viewIds.map((viewId, index) => {
|
|
||||||
const layout = Number(views.get(viewId)?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
|
||||||
const Component = getDatabaseViewComponent(layout);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabPanel
|
|
||||||
data-view-id={viewId}
|
|
||||||
className={'flex h-full w-full flex-col'}
|
|
||||||
key={viewId}
|
|
||||||
index={index}
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
<Component />
|
|
||||||
</TabPanel>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</SwipeableViews>
|
|
||||||
</DatabaseContextProvider>
|
</DatabaseContextProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import { useDatabaseViewsSelector } from '@/application/database-yjs';
|
||||||
|
import { Board } from '@/components/database/board';
|
||||||
|
import { Calendar } from '@/components/database/calendar';
|
||||||
|
import { DatabaseConditionsContext } from '@/components/database/components/conditions/context';
|
||||||
|
import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs';
|
||||||
|
import { Grid } from '@/components/database/grid';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import SwipeableViews from 'react-swipeable-views';
|
||||||
|
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
||||||
|
|
||||||
|
function DatabaseViews({
|
||||||
|
onChangeView,
|
||||||
|
currentViewId,
|
||||||
|
}: {
|
||||||
|
onChangeView: (viewId: string) => void;
|
||||||
|
currentViewId: string;
|
||||||
|
}) {
|
||||||
|
const { childViews, viewIds } = useDatabaseViewsSelector();
|
||||||
|
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
viewIds.findIndex((id) => id === currentViewId)
|
||||||
|
);
|
||||||
|
}, [currentViewId, viewIds]);
|
||||||
|
|
||||||
|
const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => {
|
||||||
|
switch (layout) {
|
||||||
|
case DatabaseViewLayout.Grid:
|
||||||
|
return Grid;
|
||||||
|
case DatabaseViewLayout.Board:
|
||||||
|
return Board;
|
||||||
|
case DatabaseViewLayout.Calendar:
|
||||||
|
return Calendar;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [conditionsExpanded, setConditionsExpanded] = useState<boolean>(false);
|
||||||
|
const toggleExpanded = useCallback(() => {
|
||||||
|
setConditionsExpanded((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DatabaseConditionsContext.Provider
|
||||||
|
value={{
|
||||||
|
expanded: conditionsExpanded,
|
||||||
|
toggleExpanded,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseTabs selectedViewId={currentViewId} setSelectedViewId={onChangeView} viewIds={viewIds} />
|
||||||
|
<DatabaseConditions />
|
||||||
|
</DatabaseConditionsContext.Provider>
|
||||||
|
<SwipeableViews
|
||||||
|
slideStyle={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
className={'h-full w-full flex-1 overflow-hidden'}
|
||||||
|
axis={'x'}
|
||||||
|
index={value}
|
||||||
|
containerStyle={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
{childViews.map((view, index) => {
|
||||||
|
const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
||||||
|
const Component = getDatabaseViewComponent(layout);
|
||||||
|
const viewId = viewIds[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabPanel
|
||||||
|
data-view-id={viewId}
|
||||||
|
className={'flex h-full w-full flex-col'}
|
||||||
|
key={viewId}
|
||||||
|
index={index}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
<Component />
|
||||||
|
</TabPanel>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SwipeableViews>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DatabaseViews;
|
@ -1,7 +1,34 @@
|
|||||||
|
import { useDatabase, useGroupsSelector } from '@/application/database-yjs';
|
||||||
|
import { Group } from '@/components/database/components/board';
|
||||||
|
import { CircularProgress } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { DragDropContext } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
export function Board() {
|
export function Board() {
|
||||||
return <div>Board</div>;
|
const database = useDatabase();
|
||||||
|
const groups = useGroupsSelector();
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return (
|
||||||
|
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragDropContext
|
||||||
|
onDragEnd={() => {
|
||||||
|
//
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={'grid-board flex w-full flex-1 flex-col'}>
|
||||||
|
{groups.map((groupId) => (
|
||||||
|
<Group key={groupId} groupId={groupId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DragDropContext>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Board;
|
export default Board;
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Calendar as BigCalendar, dayjsLocalizer } from 'react-big-calendar';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import './calendar.scss';
|
||||||
|
|
||||||
|
const localizer = dayjsLocalizer(dayjs);
|
||||||
|
|
||||||
export function Calendar() {
|
export function Calendar() {
|
||||||
return <div>Calendar</div>;
|
return (
|
||||||
|
<div className={'max-ms:px-4 px-24 py-4'}>
|
||||||
|
<BigCalendar localizer={localizer} startAccessor='start' endAccessor='end' style={{ height: 500 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Calendar;
|
export default Calendar;
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
@import 'react-big-calendar/lib/sass/styles';
|
||||||
|
@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD
|
@ -0,0 +1,50 @@
|
|||||||
|
import { useFieldsSelector } from '@/application/database-yjs';
|
||||||
|
import CardField from '@/components/database/components/board/card/CardField';
|
||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
export interface CardProps {
|
||||||
|
groupFieldId: string;
|
||||||
|
rowId: string;
|
||||||
|
onResize?: (height: number) => void;
|
||||||
|
isDragging?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) {
|
||||||
|
const fields = useFieldsSelector();
|
||||||
|
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]);
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDragging) return;
|
||||||
|
const el = ref.current;
|
||||||
|
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
onResize?.(el.offsetHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(el);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [onResize, isDragging]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
minHeight: '38px',
|
||||||
|
}}
|
||||||
|
className='flex cursor-pointer flex-col rounded-lg border border-line-divider p-3 shadow-sm hover:bg-fill-list-active hover:shadow'
|
||||||
|
>
|
||||||
|
{showFields.map((field, index) => {
|
||||||
|
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card;
|
@ -0,0 +1,48 @@
|
|||||||
|
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import { useCellSelector, useFieldSelector } from '@/application/database-yjs';
|
||||||
|
import Cell from '@/components/database/components/cell/Cell';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; index: number }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { field } = useFieldSelector(fieldId);
|
||||||
|
const cell = useCellSelector({
|
||||||
|
rowId,
|
||||||
|
fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrimary = field?.get(YjsDatabaseKey.is_primary);
|
||||||
|
const style = useMemo(() => {
|
||||||
|
const styleProperties = {
|
||||||
|
fontSize: '12px',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPrimary) {
|
||||||
|
Object.assign(styleProperties, {
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index !== 0) {
|
||||||
|
Object.assign(styleProperties, {
|
||||||
|
marginTop: '8px',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return styleProperties;
|
||||||
|
}, [index, isPrimary]);
|
||||||
|
|
||||||
|
if (isPrimary && !cell?.data) {
|
||||||
|
return (
|
||||||
|
<div className={'text-text-caption'} style={style}>
|
||||||
|
{t('grid.row.titlePlaceholder')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Cell style={style} readOnly cell={cell} rowId={rowId} fieldId={fieldId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardField;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Card';
|
@ -0,0 +1,130 @@
|
|||||||
|
import { Row } from '@/application/database-yjs';
|
||||||
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
|
import { Tag } from '@/components/_shared/tag';
|
||||||
|
import ListItem from '@/components/database/components/board/column/ListItem';
|
||||||
|
import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn';
|
||||||
|
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
|
||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { Draggable, DraggableProvided, Droppable } from 'react-beautiful-dnd';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { VariableSizeList } from 'react-window';
|
||||||
|
|
||||||
|
export interface ColumnProps {
|
||||||
|
id: string;
|
||||||
|
rows?: Row[];
|
||||||
|
fieldId: string;
|
||||||
|
provided: DraggableProvided;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Column({ id, rows, fieldId, provided }: ColumnProps) {
|
||||||
|
const { header } = useRenderColumn(id, fieldId);
|
||||||
|
const ref = React.useRef<VariableSizeList | null>(null);
|
||||||
|
const forceUpdate = useCallback((index: number) => {
|
||||||
|
ref.current?.resetAfterIndex(index, true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
forceUpdate(0);
|
||||||
|
}, [rows, forceUpdate]);
|
||||||
|
|
||||||
|
const measureRows = useMemo(
|
||||||
|
() =>
|
||||||
|
rows?.map((row) => {
|
||||||
|
return {
|
||||||
|
rowId: row.id,
|
||||||
|
};
|
||||||
|
}) || [],
|
||||||
|
[rows]
|
||||||
|
);
|
||||||
|
const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows });
|
||||||
|
|
||||||
|
const Row = useCallback(
|
||||||
|
({ index, style, data }: { index: number; style: React.CSSProperties; data: Row[] }) => {
|
||||||
|
const item = data[index];
|
||||||
|
|
||||||
|
// We are rendering an extra item for the placeholder
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onResizeCallback = (height: number) => {
|
||||||
|
onResize(index, 0, {
|
||||||
|
width: 0,
|
||||||
|
height: height + 8,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable isDragDisabled draggableId={item.id} index={index} key={item.id}>
|
||||||
|
{(provided) => (
|
||||||
|
<ListItem fieldId={fieldId} onResize={onResizeCallback} provided={provided} item={item} style={style} />
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[fieldId, onResize]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getItemSize = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (!rows || index >= rows.length) return 0;
|
||||||
|
const row = rows[index];
|
||||||
|
|
||||||
|
if (!row) return 0;
|
||||||
|
return rowHeight(index);
|
||||||
|
},
|
||||||
|
[rowHeight, rows]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!rows) return <div ref={provided.innerRef} />;
|
||||||
|
return (
|
||||||
|
<div key={id} className='column flex w-[230px] flex-col gap-4' {...provided.draggableProps} ref={provided.innerRef}>
|
||||||
|
<div className='column-header flex h-[24px] items-center text-xs font-medium' {...provided.dragHandleProps}>
|
||||||
|
<Tag label={header?.name} color={header?.color} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'w-full flex-1 overflow-hidden'}>
|
||||||
|
<Droppable
|
||||||
|
droppableId={`column-${id}`}
|
||||||
|
mode='virtual'
|
||||||
|
renderClone={(provided, snapshot, rubric) => (
|
||||||
|
<ListItem
|
||||||
|
provided={provided}
|
||||||
|
isDragging={snapshot.isDragging}
|
||||||
|
item={rows[rubric.source.index]}
|
||||||
|
fieldId={fieldId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{(provided, snapshot) => {
|
||||||
|
// Add an extra item to our list to make space for a dragging item
|
||||||
|
// Usually the DroppableProvided.placeholder does this, but that won't
|
||||||
|
// work in a virtual list
|
||||||
|
const itemCount = snapshot.isUsingPlaceholder ? rows.length + 1 : rows.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoSizer>
|
||||||
|
{({ height, width }: { height: number; width: number }) => {
|
||||||
|
return (
|
||||||
|
<VariableSizeList
|
||||||
|
ref={ref}
|
||||||
|
height={height}
|
||||||
|
itemCount={itemCount}
|
||||||
|
itemSize={getItemSize}
|
||||||
|
width={width}
|
||||||
|
outerElementType={AFScroller}
|
||||||
|
outerRef={provided.innerRef}
|
||||||
|
itemData={rows}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</VariableSizeList>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</AutoSizer>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Droppable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import { Row } from '@/application/database-yjs';
|
||||||
|
import React from 'react';
|
||||||
|
import { DraggableProvided, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd';
|
||||||
|
import Card from 'src/components/database/components/board/card/Card';
|
||||||
|
|
||||||
|
export const ListItem = ({
|
||||||
|
provided,
|
||||||
|
item,
|
||||||
|
style,
|
||||||
|
onResize,
|
||||||
|
fieldId,
|
||||||
|
isDragging,
|
||||||
|
}: {
|
||||||
|
provided: DraggableProvided;
|
||||||
|
item: Row;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
fieldId: string;
|
||||||
|
onResize?: (height: number) => void;
|
||||||
|
isDragging?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={getStyle({
|
||||||
|
draggableStyle: provided.draggableProps.style,
|
||||||
|
virtualStyle: style,
|
||||||
|
isDragging,
|
||||||
|
})}
|
||||||
|
className={`w-full bg-bg-body ${isDragging ? 'is-dragging' : ''}`}
|
||||||
|
>
|
||||||
|
<Card onResize={onResize} rowId={item.id} groupFieldId={fieldId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStyle({
|
||||||
|
draggableStyle,
|
||||||
|
virtualStyle,
|
||||||
|
isDragging,
|
||||||
|
}: {
|
||||||
|
draggableStyle?: DraggingStyle | NotDraggingStyle;
|
||||||
|
virtualStyle?: React.CSSProperties;
|
||||||
|
isDragging?: boolean;
|
||||||
|
}) {
|
||||||
|
// If you don't want any spacing between your items
|
||||||
|
// then you could just return this.
|
||||||
|
// I do a little bit of magic to have some nice visual space
|
||||||
|
// between the row items
|
||||||
|
const combined = {
|
||||||
|
...virtualStyle,
|
||||||
|
...draggableStyle,
|
||||||
|
} as {
|
||||||
|
height: number;
|
||||||
|
left: number;
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Being lazy: this is defined in our css file
|
||||||
|
const grid = 1;
|
||||||
|
|
||||||
|
// when dragging we want to use the draggable style for placement, otherwise use the virtual style
|
||||||
|
|
||||||
|
return {
|
||||||
|
...combined,
|
||||||
|
height: isDragging ? combined.height : combined.height - grid,
|
||||||
|
left: isDragging ? combined.left : combined.left + grid,
|
||||||
|
width: isDragging ? (draggableStyle as DraggingStyle)?.width : `calc(${combined.width} - ${grid * 2}px)`,
|
||||||
|
marginBottom: grid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ListItem;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Column';
|
@ -0,0 +1,31 @@
|
|||||||
|
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
|
import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs';
|
||||||
|
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export function useRenderColumn(id: string, fieldId: string) {
|
||||||
|
const { field } = useFieldSelector(fieldId);
|
||||||
|
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||||
|
const fieldName = field?.get(YjsDatabaseKey.name) || '';
|
||||||
|
const header = useMemo(() => {
|
||||||
|
if (!field) return null;
|
||||||
|
switch (fieldType) {
|
||||||
|
case FieldType.SingleSelect:
|
||||||
|
case FieldType.MultiSelect: {
|
||||||
|
const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: option?.name || `No ${fieldName}`,
|
||||||
|
color: option?.color ? SelectOptionColorMap[option?.color] : 'transparent',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [field, fieldName, fieldType, id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
header,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
import { useRowsByGroup } from '@/application/database-yjs';
|
||||||
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
|
import React from 'react';
|
||||||
|
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Column } from '../column';
|
||||||
|
|
||||||
|
export interface GroupProps {
|
||||||
|
groupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Group = ({ groupId }: GroupProps) => {
|
||||||
|
const { columns, groupResult, fieldId, notFound } = useRowsByGroup(groupId);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (notFound) {
|
||||||
|
return (
|
||||||
|
<div className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-24 text-text-caption max-md:px-4'}>
|
||||||
|
<div className={'text-sm font-medium'}>{t('board.noGroup')}</div>
|
||||||
|
<div className={'text-xs'}>{t('board.noGroupDesc')}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columns.length === 0 || !fieldId) return null;
|
||||||
|
return (
|
||||||
|
<AFScroller overflowYHidden className={'relative px-24 max-md:px-4'}>
|
||||||
|
<Droppable
|
||||||
|
droppableId={`group-${groupId}`}
|
||||||
|
direction='horizontal'
|
||||||
|
type='column'
|
||||||
|
renderClone={(provided, snapshot, rubric) => {
|
||||||
|
// we have a transform: * on one of the parents of a <Draggable /> then the positioning logic will be incorrect while dragging
|
||||||
|
// https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md
|
||||||
|
const id = columns[rubric.source.index].id;
|
||||||
|
|
||||||
|
return <Column key={id} rows={groupResult.get(id)} provided={provided} id={id} fieldId={fieldId} />;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(provided) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='columns flex h-full w-fit gap-4 border-t border-line-divider py-4'
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
>
|
||||||
|
{columns.map((data, index) => (
|
||||||
|
<Draggable isDragDisabled key={data.id} draggableId={`column-${data.id}`} index={index}>
|
||||||
|
{(provided) => {
|
||||||
|
return (
|
||||||
|
<Column
|
||||||
|
provided={provided}
|
||||||
|
key={data.id}
|
||||||
|
id={data.id}
|
||||||
|
fieldId={fieldId}
|
||||||
|
rows={groupResult.get(data.id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Droppable>
|
||||||
|
</AFScroller>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Group;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Group';
|
@ -0,0 +1 @@
|
|||||||
|
export * from './group';
|
@ -1,8 +0,0 @@
|
|||||||
import { CalculationType } from '@/application/database-yjs/database.type';
|
|
||||||
|
|
||||||
export interface CalulationCell {
|
|
||||||
value: string;
|
|
||||||
fieldId: string;
|
|
||||||
id: string;
|
|
||||||
type: CalculationType;
|
|
||||||
}
|
|
@ -26,7 +26,7 @@ export function useDateTypeCellDispatcher(fieldId: string) {
|
|||||||
|
|
||||||
const getDateTimeStr = useCallback(
|
const getDateTimeStr = useCallback(
|
||||||
(timeStamp: string, includeTime?: boolean) => {
|
(timeStamp: string, includeTime?: boolean) => {
|
||||||
if (!typeOptionValue) return null;
|
if (!typeOptionValue || !timeStamp) return null;
|
||||||
const timeFormat = getTimeFormat(typeOptionValue.timeFormat);
|
const timeFormat = getTimeFormat(typeOptionValue.timeFormat);
|
||||||
const dateFormat = getDateFormat(typeOptionValue.dateFormat);
|
const dateFormat = getDateFormat(typeOptionValue.dateFormat);
|
||||||
const format = [dateFormat];
|
const format = [dateFormat];
|
||||||
|
@ -1,31 +1,26 @@
|
|||||||
import { FieldId, YjsDatabaseKey } from '@/application/collab.type';
|
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { FieldType } from '@/application/database-yjs/database.type';
|
import { FieldType } from '@/application/database-yjs/database.type';
|
||||||
import { useFieldSelector } from '@/application/database-yjs/selector';
|
import { useFieldSelector } from '@/application/database-yjs/selector';
|
||||||
import RowCreateModifiedTime from '@/components/database/components/cell/RowCreateModifiedTime';
|
import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified';
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, useMemo } from 'react';
|
||||||
import RichTextCell from '@/components/database/components/cell/TextCell';
|
import { TextCell } from '@/components/database/components/cell/text';
|
||||||
import UrlCell from '@/components/database/components/cell/UrlCell';
|
import { UrlCell } from '@/components/database/components/cell/url';
|
||||||
import NumberCell from '@/components/database/components/cell/NumberCell';
|
import { NumberCell } from '@/components/database/components/cell/number';
|
||||||
import CheckboxCell from '@/components/database/components/cell/CheckboxCell';
|
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
||||||
import SelectCell from '@/components/database/components/cell/SelectionCell';
|
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
||||||
import DateTimeCell from '@/components/database/components/cell/DateTimeCell';
|
import { DateTimeCell } from '@/components/database/components/cell/date';
|
||||||
import ChecklistCell from '@/components/database/components/cell/ChecklistCell';
|
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
||||||
import { Cell as CellValue } from '@/components/database/components/cell/cell.type';
|
import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type';
|
||||||
import RelationCell from '@/components/database/components/cell/RelationCell';
|
import { RelationCell } from '@/components/database/components/cell/relation';
|
||||||
|
|
||||||
export interface CellProps {
|
export function Cell(props: CellProps<CellType>) {
|
||||||
rowId: string;
|
const { cell, rowId, fieldId, style } = props;
|
||||||
fieldId: FieldId;
|
|
||||||
cell?: CellValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Cell({ cell, rowId, fieldId }: CellProps) {
|
|
||||||
const { field } = useFieldSelector(fieldId);
|
const { field } = useFieldSelector(fieldId);
|
||||||
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||||
const Component = useMemo(() => {
|
const Component = useMemo(() => {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case FieldType.RichText:
|
case FieldType.RichText:
|
||||||
return RichTextCell;
|
return TextCell;
|
||||||
case FieldType.URL:
|
case FieldType.URL:
|
||||||
return UrlCell;
|
return UrlCell;
|
||||||
case FieldType.Number:
|
case FieldType.Number:
|
||||||
@ -34,7 +29,7 @@ export function Cell({ cell, rowId, fieldId }: CellProps) {
|
|||||||
return CheckboxCell;
|
return CheckboxCell;
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
return SelectCell;
|
return SelectOptionCell;
|
||||||
case FieldType.DateTime:
|
case FieldType.DateTime:
|
||||||
return DateTimeCell;
|
return DateTimeCell;
|
||||||
case FieldType.Checklist:
|
case FieldType.Checklist:
|
||||||
@ -42,21 +37,21 @@ export function Cell({ cell, rowId, fieldId }: CellProps) {
|
|||||||
case FieldType.Relation:
|
case FieldType.Relation:
|
||||||
return RelationCell;
|
return RelationCell;
|
||||||
default:
|
default:
|
||||||
return RichTextCell;
|
return TextCell;
|
||||||
}
|
}
|
||||||
}, [fieldType]) as FC<{ cell?: CellValue; rowId: string; fieldId: FieldId }>;
|
}, [fieldType]) as FC<CellProps<CellType>>;
|
||||||
|
|
||||||
if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) {
|
if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) {
|
||||||
const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified;
|
const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified;
|
||||||
|
|
||||||
return <RowCreateModifiedTime rowId={rowId} fieldId={fieldId} attrName={attrName} />;
|
return <RowCreateModifiedTime style={style} rowId={rowId} fieldId={fieldId} attrName={attrName} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cell?.fieldType !== fieldType) {
|
if (cell && cell.fieldType !== fieldType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Component cell={cell} rowId={rowId} fieldId={fieldId} />;
|
return <Component {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
|
||||||
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
|
||||||
import { CheckboxCell } from '@/components/database/components/cell/cell.type';
|
|
||||||
|
|
||||||
export default function ({ cell }: { cell?: CheckboxCell; rowId: string; fieldId: FieldId }) {
|
|
||||||
const checked = cell?.data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='relative flex w-full cursor-pointer items-center text-lg text-fill-default'>
|
|
||||||
{checked ? <CheckboxCheckSvg /> : <CheckboxUncheckSvg />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { useReadOnly } from '@/application/database-yjs';
|
|
||||||
import { TextCell } from '@/components/database/components/cell/cell.type';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function TextCellComponent({ cell }: { cell?: TextCell; rowId: string; fieldId: FieldId }) {
|
|
||||||
const readOnly = useReadOnly();
|
|
||||||
|
|
||||||
return <div className={`cursor-text ${readOnly ? 'select-text' : ''}`}>{cell?.data}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TextCellComponent;
|
|
@ -1,6 +1,7 @@
|
|||||||
import { RowId } from '@/application/collab.type';
|
import { FieldId, RowId } from '@/application/collab.type';
|
||||||
import { DateFormat, SelectOption, TimeFormat } from '@/application/database-yjs';
|
import { DateFormat, TimeFormat } from '@/application/database-yjs';
|
||||||
import { FieldType } from '@/application/database-yjs/database.type';
|
import { FieldType } from '@/application/database-yjs/database.type';
|
||||||
|
import React from 'react';
|
||||||
import { YArray } from 'yjs/dist/src/types/YArray';
|
import { YArray } from 'yjs/dist/src/types/YArray';
|
||||||
|
|
||||||
export interface Cell {
|
export interface Cell {
|
||||||
@ -32,7 +33,7 @@ export interface UrlCell extends Cell {
|
|||||||
|
|
||||||
export type SelectionId = string;
|
export type SelectionId = string;
|
||||||
|
|
||||||
export interface SelectCell extends Cell {
|
export interface SelectOptionCell extends Cell {
|
||||||
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
|
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
|
||||||
data: SelectionId;
|
data: SelectionId;
|
||||||
}
|
}
|
||||||
@ -51,11 +52,6 @@ export interface DateTimeCell extends Cell {
|
|||||||
reminderId?: string;
|
reminderId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeStampCell extends Cell {
|
|
||||||
fieldType: FieldType.LastEditedTime | FieldType.CreatedTime;
|
|
||||||
data: TimestampCellData;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DateTimeCellData {
|
export interface DateTimeCellData {
|
||||||
date?: string;
|
date?: string;
|
||||||
time?: string;
|
time?: string;
|
||||||
@ -67,11 +63,6 @@ export interface DateTimeCellData {
|
|||||||
isRange?: boolean;
|
isRange?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimestampCellData {
|
|
||||||
dataTime?: string;
|
|
||||||
timestamp?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChecklistCell extends Cell {
|
export interface ChecklistCell extends Cell {
|
||||||
fieldType: FieldType.Checklist;
|
fieldType: FieldType.Checklist;
|
||||||
data: string;
|
data: string;
|
||||||
@ -84,7 +75,10 @@ export interface RelationCell extends Cell {
|
|||||||
|
|
||||||
export type RelationCellData = RowId[];
|
export type RelationCellData = RowId[];
|
||||||
|
|
||||||
export interface ChecklistCellData {
|
export interface CellProps<T extends Cell> {
|
||||||
selected_option_ids?: string[];
|
cell?: T;
|
||||||
options?: SelectOption[];
|
rowId: string;
|
||||||
|
fieldId: FieldId;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
readOnly?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
||||||
|
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
||||||
|
import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type';
|
||||||
|
|
||||||
|
export function CheckboxCell({ cell, style }: CellProps<CheckboxCellType>) {
|
||||||
|
const checked = cell?.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={style} className='relative flex w-full cursor-pointer items-center text-lg text-fill-default'>
|
||||||
|
{checked ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './CheckboxCell';
|
@ -1,10 +1,9 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { parseChecklistData } from '@/application/database-yjs';
|
import { parseChecklistData } from '@/application/database-yjs';
|
||||||
import { ChecklistCell } from '@/components/database/components/cell/cell.type';
|
import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldId: FieldId }) {
|
export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) {
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
return parseChecklistData(cell?.data ?? '');
|
return parseChecklistData(cell?.data ?? '');
|
||||||
}, [cell?.data]);
|
}, [cell?.data]);
|
||||||
@ -14,7 +13,7 @@ export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldI
|
|||||||
|
|
||||||
if (!data || !options || !selectedOptions) return null;
|
if (!data || !options || !selectedOptions) return null;
|
||||||
return (
|
return (
|
||||||
<div className={'cursor-pointer'}>
|
<div style={style} className={'cursor-pointer'}>
|
||||||
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />
|
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -0,0 +1 @@
|
|||||||
|
export * from './ChecklistCell';
|
@ -3,13 +3,15 @@ import { useRowMeta } from '@/application/database-yjs';
|
|||||||
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
function RowCreateModifiedTime({
|
export function RowCreateModifiedTime({
|
||||||
rowId,
|
rowId,
|
||||||
fieldId,
|
fieldId,
|
||||||
attrName,
|
attrName,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
rowId: string;
|
rowId: string;
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at;
|
attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at;
|
||||||
}) {
|
}) {
|
||||||
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
||||||
@ -37,7 +39,8 @@ function RowCreateModifiedTime({
|
|||||||
return getDateTimeStr(value, false);
|
return getDateTimeStr(value, false);
|
||||||
}, [value, getDateTimeStr]);
|
}, [value, getDateTimeStr]);
|
||||||
|
|
||||||
return <div>{time}</div>;
|
if (!time) return null;
|
||||||
|
return <div style={style}>{time}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RowCreateModifiedTime;
|
export default RowCreateModifiedTime;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './RowCreateModifiedTime';
|
@ -1,10 +1,9 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
||||||
import { DateTimeCell } from '@/components/database/components/cell/cell.type';
|
import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';
|
import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';
|
||||||
|
|
||||||
export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string; fieldId: FieldId }) {
|
export function DateTimeCell({ cell, fieldId, style }: CellProps<DateTimeCellType>) {
|
||||||
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
||||||
|
|
||||||
const startDateTime = useMemo(() => {
|
const startDateTime = useMemo(() => {
|
||||||
@ -26,8 +25,9 @@ export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string
|
|||||||
|
|
||||||
const hasReminder = !!cell?.reminderId;
|
const hasReminder = !!cell?.reminderId;
|
||||||
|
|
||||||
|
if (!cell?.data) return null;
|
||||||
return (
|
return (
|
||||||
<div className={'flex cursor-text items-center gap-1'}>
|
<div style={style} className={'flex cursor-text items-center gap-1'}>
|
||||||
{hasReminder && <ReminderSvg className={'h-4 w-4'} />}
|
{hasReminder && <ReminderSvg className={'h-4 w-4'} />}
|
||||||
{dateStr}
|
{dateStr}
|
||||||
</div>
|
</div>
|
@ -0,0 +1 @@
|
|||||||
|
export * from './DateTimeCell';
|
@ -1,10 +1,9 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs';
|
import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs';
|
||||||
import { UrlCell } from '@/components/database/components/cell/cell.type';
|
import { CellProps, NumberCell as NumberCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
|
||||||
export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) {
|
export function NumberCell({ cell, fieldId, style }: CellProps<NumberCellType>) {
|
||||||
const { field } = useFieldSelector(fieldId);
|
const { field } = useFieldSelector(fieldId);
|
||||||
|
|
||||||
const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]);
|
const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]);
|
||||||
@ -23,5 +22,10 @@ export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fie
|
|||||||
return numberFormater(new Decimal(cell.data).toNumber());
|
return numberFormater(new Decimal(cell.data).toNumber());
|
||||||
}, [cell, format]);
|
}, [cell, format]);
|
||||||
|
|
||||||
return <div className={className}>{value}</div>;
|
if (value === undefined) return null;
|
||||||
|
return (
|
||||||
|
<div style={style} className={className}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './NumberCell';
|
@ -0,0 +1,7 @@
|
|||||||
|
import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type';
|
||||||
|
import RelationItems from '@/components/database/components/cell/relation/RelationItems';
|
||||||
|
|
||||||
|
export function RelationCell({ cell, fieldId, style }: CellProps<RelationCellType>) {
|
||||||
|
if (!cell?.data) return null;
|
||||||
|
return <RelationItems cell={cell} fieldId={fieldId} style={style} />;
|
||||||
|
}
|
@ -1,24 +1,16 @@
|
|||||||
import {
|
import { YDatabaseField, YDatabaseFields, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
FieldId,
|
import { parseRelationTypeOption, useFieldSelector } from '@/application/database-yjs';
|
||||||
YDatabaseField,
|
|
||||||
YDatabaseFields,
|
|
||||||
YDatabaseRow,
|
|
||||||
YDoc,
|
|
||||||
YjsDatabaseKey,
|
|
||||||
YjsEditorKey,
|
|
||||||
} from '@/application/collab.type';
|
|
||||||
import { useFieldSelector, parseRelationTypeOption } from '@/application/database-yjs';
|
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
|
||||||
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
||||||
|
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: string; rowId: string }) {
|
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
||||||
const { field } = useFieldSelector(fieldId);
|
const { field } = useFieldSelector(fieldId);
|
||||||
const workspaceId = useId()?.workspaceId;
|
const workspaceId = useId()?.workspaceId;
|
||||||
const rowIds = useMemo(() => (cell?.data.toJSON() as RelationCellData) ?? [], [cell?.data]);
|
const rowIds = useMemo(() => (cell.data.toJSON() as RelationCellData) ?? [], [cell.data]);
|
||||||
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
||||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||||
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
||||||
@ -43,7 +35,7 @@ export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: stri
|
|||||||
}, [workspaceId, databaseId, databaseService]);
|
}, [workspaceId, databaseId, databaseService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center gap-2'}>
|
<div style={style} className={'flex items-center gap-2'}>
|
||||||
{rowIds.map((rowId) => {
|
{rowIds.map((rowId) => {
|
||||||
const rowDoc = rows?.get(rowId);
|
const rowDoc = rows?.get(rowId);
|
||||||
|
|
||||||
@ -59,26 +51,4 @@ export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: stri
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {
|
export default RelationItems;
|
||||||
const [text, setText] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
|
||||||
const cells = row.get(YjsDatabaseKey.cells);
|
|
||||||
const primaryCell = cells.get(fieldId);
|
|
||||||
|
|
||||||
if (!primaryCell) return;
|
|
||||||
const observeHandler = () => {
|
|
||||||
setText(parseYDatabaseCellToCell(primaryCell).data as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
observeHandler();
|
|
||||||
|
|
||||||
primaryCell.observe(observeHandler);
|
|
||||||
return () => {
|
|
||||||
primaryCell.unobserve(observeHandler);
|
|
||||||
};
|
|
||||||
}, [rowDoc, fieldId]);
|
|
||||||
|
|
||||||
return <div>{text}</div>;
|
|
||||||
}
|
|
@ -0,0 +1,27 @@
|
|||||||
|
import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {
|
||||||
|
const [text, setText] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
||||||
|
const cells = row.get(YjsDatabaseKey.cells);
|
||||||
|
const primaryCell = cells.get(fieldId);
|
||||||
|
|
||||||
|
if (!primaryCell) return;
|
||||||
|
const observeHandler = () => {
|
||||||
|
setText(parseYDatabaseCellToCell(primaryCell).data as string);
|
||||||
|
};
|
||||||
|
|
||||||
|
observeHandler();
|
||||||
|
|
||||||
|
primaryCell.observe(observeHandler);
|
||||||
|
return () => {
|
||||||
|
primaryCell.unobserve(observeHandler);
|
||||||
|
};
|
||||||
|
}, [rowDoc, fieldId]);
|
||||||
|
|
||||||
|
return <div>{text}</div>;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './RelationCell';
|
@ -1,11 +1,10 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs';
|
import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs';
|
||||||
import { Tag } from '@/components/_shared/tag';
|
import { Tag } from '@/components/_shared/tag';
|
||||||
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
|
||||||
import { SelectCell } from '@/components/database/components/cell/cell.type';
|
import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string; fieldId: FieldId }) {
|
export function SelectOptionCell({ cell, fieldId, style }: CellProps<SelectOptionCellType>) {
|
||||||
const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]);
|
const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]);
|
||||||
const { field } = useFieldSelector(fieldId);
|
const { field } = useFieldSelector(fieldId);
|
||||||
const typeOption = useMemo(() => {
|
const typeOption = useMemo(() => {
|
||||||
@ -24,9 +23,11 @@ export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string;
|
|||||||
[typeOption]
|
[typeOption]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!typeOption || !selectOptionIds?.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}>
|
<div style={style} className={'flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}>
|
||||||
{selectOptionIds ? renderSelectedOptions(selectOptionIds) : null}
|
{renderSelectedOptions(selectOptionIds)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './SelectOptionCell';
|
@ -0,0 +1,14 @@
|
|||||||
|
import { useReadOnly } from '@/application/database-yjs';
|
||||||
|
import { CellProps, TextCell as TextCellType } from '@/components/database/components/cell/cell.type';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function TextCell({ cell, style }: CellProps<TextCellType>) {
|
||||||
|
const readOnly = useReadOnly();
|
||||||
|
|
||||||
|
if (!cell?.data) return null;
|
||||||
|
return (
|
||||||
|
<div style={style} className={`cursor-text leading-[1.2] ${readOnly ? 'select-text' : ''}`}>
|
||||||
|
{cell?.data}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './TextCell';
|
@ -1,10 +1,9 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
|
||||||
import { useReadOnly } from '@/application/database-yjs';
|
import { useReadOnly } from '@/application/database-yjs';
|
||||||
import { UrlCell } from '@/components/database/components/cell/cell.type';
|
import { CellProps, UrlCell as UrlCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import { openUrl, processUrl } from '@/utils/url';
|
import { openUrl, processUrl } from '@/utils/url';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) {
|
export function UrlCell({ cell, style }: CellProps<UrlCellType>) {
|
||||||
const readOnly = useReadOnly();
|
const readOnly = useReadOnly();
|
||||||
|
|
||||||
const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]);
|
const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]);
|
||||||
@ -21,8 +20,11 @@ export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: Fie
|
|||||||
return classList.join(' ');
|
return classList.join(' ');
|
||||||
}, [isUrl]);
|
}, [isUrl]);
|
||||||
|
|
||||||
|
if (!cell?.data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
style={style}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!isUrl || !cell) return;
|
if (!isUrl || !cell) return;
|
||||||
if (readOnly) {
|
if (readOnly) {
|
@ -0,0 +1 @@
|
|||||||
|
export * from './UrlCell';
|
@ -0,0 +1,53 @@
|
|||||||
|
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs';
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
export function useMeasureHeight({
|
||||||
|
forceUpdate,
|
||||||
|
rows,
|
||||||
|
}: {
|
||||||
|
forceUpdate: (index: number) => void;
|
||||||
|
rows: {
|
||||||
|
rowId?: string;
|
||||||
|
}[];
|
||||||
|
}) {
|
||||||
|
const heightRef = useRef<{ [rowId: string]: number }>({});
|
||||||
|
const rowHeight = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const row = rows[index];
|
||||||
|
|
||||||
|
if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT;
|
||||||
|
|
||||||
|
return heightRef.current[row.rowId] || DEFAULT_ROW_HEIGHT;
|
||||||
|
},
|
||||||
|
[rows]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setRowHeight = useCallback(
|
||||||
|
(index: number, height: number) => {
|
||||||
|
const row = rows[index];
|
||||||
|
const rowId = row.rowId;
|
||||||
|
|
||||||
|
if (!row || !rowId) return;
|
||||||
|
const oldHeight = heightRef.current[rowId];
|
||||||
|
|
||||||
|
heightRef.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height);
|
||||||
|
|
||||||
|
if (oldHeight !== height) {
|
||||||
|
forceUpdate(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[forceUpdate, rows]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onResize = useCallback(
|
||||||
|
(rowIndex: number, columnIndex: number, size: { width: number; height: number }) => {
|
||||||
|
setRowHeight(rowIndex, size.height);
|
||||||
|
},
|
||||||
|
[setRowHeight]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowHeight,
|
||||||
|
onResize,
|
||||||
|
};
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
export * from './GridColumn';
|
|
||||||
export * from './useRenderColumns';
|
|
@ -1,9 +1,19 @@
|
|||||||
import { CalculationType } from '@/application/database-yjs/database.type';
|
import { CalculationType } from '@/application/database-yjs/database.type';
|
||||||
import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type';
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function CalculationCell({ cell }: { cell?: CalulationCell }) {
|
export interface ICalculationCell {
|
||||||
|
value: string;
|
||||||
|
fieldId: string;
|
||||||
|
id: string;
|
||||||
|
type: CalculationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculationCellProps {
|
||||||
|
cell?: ICalculationCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CalculationCell({ cell }: CalculationCellProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const prefix = useMemo(() => {
|
const prefix = useMemo(() => {
|
@ -1,9 +1,8 @@
|
|||||||
import { FieldId, YjsDatabaseKey } from '@/application/collab.type';
|
import { FieldId } from '@/application/collab.type';
|
||||||
import { useRowMeta } from '@/application/database-yjs';
|
import { useCellSelector } from '@/application/database-yjs';
|
||||||
import { useFieldSelector } from '@/application/database-yjs/selector';
|
import { useFieldSelector } from '@/application/database-yjs/selector';
|
||||||
import { Cell } from '@/components/database/components/cell';
|
import { Cell } from '@/components/database/components/cell';
|
||||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
import React, { useEffect } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export interface GridCellProps {
|
export interface GridCellProps {
|
||||||
rowId: string;
|
rowId: string;
|
||||||
@ -16,21 +15,10 @@ export interface GridCellProps {
|
|||||||
export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) {
|
export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) {
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
const field = useFieldSelector(fieldId);
|
const field = useFieldSelector(fieldId);
|
||||||
const row = useRowMeta(rowId);
|
const cell = useCellSelector({
|
||||||
const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId);
|
rowId,
|
||||||
const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined));
|
fieldId,
|
||||||
|
});
|
||||||
useEffect(() => {
|
|
||||||
if (!cell) return;
|
|
||||||
setCellValue(parseYDatabaseCellToCell(cell));
|
|
||||||
const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell));
|
|
||||||
|
|
||||||
cell.observe(observerEvent);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cell.unobserve(observerEvent);
|
|
||||||
};
|
|
||||||
}, [cell]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = ref.current;
|
const el = ref.current;
|
||||||
@ -56,7 +44,7 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr
|
|||||||
if (!field) return null;
|
if (!field) return null;
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={'grid-cell w-full cursor-text overflow-hidden text-xs'}>
|
<div ref={ref} className={'grid-cell w-full cursor-text overflow-hidden text-xs'}>
|
||||||
<Cell cell={cellValue} rowId={rowId} fieldId={fieldId} />
|
<Cell cell={cell} rowId={rowId} fieldId={fieldId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './GridColumn';
|
||||||
|
export * from 'src/components/database/components/grid/grid-column/useRenderFields';
|
@ -1,6 +1,6 @@
|
|||||||
import { FieldId } from '@/application/collab.type';
|
import { FieldId } from '@/application/collab.type';
|
||||||
import { FieldVisibility } from '@/application/database-yjs/database.type';
|
import { FieldVisibility } from '@/application/database-yjs/database.type';
|
||||||
import { useGridColumnsSelector } from '@/application/database-yjs/selector';
|
import { useFieldsSelector } from '@/application/database-yjs/selector';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
export enum GridColumnType {
|
export enum GridColumnType {
|
||||||
@ -9,8 +9,6 @@ export enum GridColumnType {
|
|||||||
NewProperty,
|
NewProperty,
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultVisibilitys = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
|
||||||
|
|
||||||
export type RenderColumn = {
|
export type RenderColumn = {
|
||||||
type: GridColumnType;
|
type: GridColumnType;
|
||||||
visibility?: FieldVisibility;
|
visibility?: FieldVisibility;
|
||||||
@ -19,12 +17,12 @@ export type RenderColumn = {
|
|||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useRenderColumns(viewId: string) {
|
export function useRenderFields() {
|
||||||
const columns = useGridColumnsSelector(viewId, defaultVisibilitys);
|
const fields = useFieldsSelector();
|
||||||
|
|
||||||
console.log('columns', columns);
|
console.log('columns', fields);
|
||||||
const renderColumns = useMemo(() => {
|
const renderColumns = useMemo(() => {
|
||||||
const fields = columns.map((column) => ({
|
const data = fields.map((column) => ({
|
||||||
...column,
|
...column,
|
||||||
type: GridColumnType.Field,
|
type: GridColumnType.Field,
|
||||||
}));
|
}));
|
||||||
@ -34,7 +32,7 @@ export function useRenderColumns(viewId: string) {
|
|||||||
type: GridColumnType.Action,
|
type: GridColumnType.Action,
|
||||||
width: 96,
|
width: 96,
|
||||||
},
|
},
|
||||||
...fields,
|
...data,
|
||||||
{
|
{
|
||||||
type: GridColumnType.NewProperty,
|
type: GridColumnType.NewProperty,
|
||||||
width: 150,
|
width: 150,
|
||||||
@ -44,7 +42,7 @@ export function useRenderColumns(viewId: string) {
|
|||||||
width: 96,
|
width: 96,
|
||||||
},
|
},
|
||||||
].filter(Boolean) as RenderColumn[];
|
].filter(Boolean) as RenderColumn[];
|
||||||
}, [columns]);
|
}, [fields]);
|
||||||
|
|
||||||
const columnWidth = useCallback(
|
const columnWidth = useCallback(
|
||||||
(index: number, containerWidth: number) => {
|
(index: number, containerWidth: number) => {
|
||||||
@ -67,7 +65,7 @@ export function useRenderColumns(viewId: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns: renderColumns,
|
fields: renderColumns,
|
||||||
columnWidth,
|
columnWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { useDatabaseView } from '@/application/database-yjs';
|
import { useDatabaseView } from '@/application/database-yjs';
|
||||||
import { CalculationType } from '@/application/database-yjs/database.type';
|
import { CalculationType } from '@/application/database-yjs/database.type';
|
||||||
import { CalculationCell } from '@/components/database/components/calculation-cell';
|
import { CalculationCell, ICalculationCell } from '../grid-calculation-cell';
|
||||||
import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export interface GridCalculateRowCellProps {
|
export interface GridCalculateRowCellProps {
|
||||||
@ -11,7 +10,7 @@ export interface GridCalculateRowCellProps {
|
|||||||
|
|
||||||
export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) {
|
export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) {
|
||||||
const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations);
|
const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations);
|
||||||
const [calculation, setCalculation] = useState<CalulationCell>();
|
const [calculation, setCalculation] = useState<ICalculationCell>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!calculations) return;
|
if (!calculations) return;
|
@ -1,6 +1,6 @@
|
|||||||
import { GridColumnType } from '@/components/database/components/grid-column';
|
import { GridColumnType } from '../grid-column';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import GridCell from 'src/components/database/components/grid-cell/GridCell';
|
import GridCell from '../grid-cell/GridCell';
|
||||||
|
|
||||||
export interface GridRowCellProps {
|
export interface GridRowCellProps {
|
||||||
rowId: string;
|
rowId: string;
|
@ -1,6 +1,5 @@
|
|||||||
import { useReadOnly } from '@/application/database-yjs';
|
import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowsSelector } from '@/application/database-yjs';
|
||||||
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const';
|
|
||||||
import { useGridRowsSelector } from '@/application/database-yjs/selector';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export enum RenderRowType {
|
export enum RenderRowType {
|
||||||
@ -16,7 +15,7 @@ export type RenderRow = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useRenderRows() {
|
export function useRenderRows() {
|
||||||
const rows = useGridRowsSelector();
|
const rows = useRowsSelector();
|
||||||
const readOnly = useReadOnly();
|
const readOnly = useReadOnly();
|
||||||
|
|
||||||
const renderRows = useMemo(() => {
|
const renderRows = useMemo(() => {
|
@ -1,12 +1,7 @@
|
|||||||
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const';
|
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const';
|
||||||
import { AFScroller } from '@/components/_shared/scroller';
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
import { GridColumnType, RenderColumn } from '@/components/database/components/grid-column';
|
import { GridColumnType, RenderColumn } from '../grid-column';
|
||||||
import {
|
import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row';
|
||||||
GridCalculateRowCell,
|
|
||||||
GridRowCell,
|
|
||||||
RenderRowType,
|
|
||||||
useRenderRows,
|
|
||||||
} from '@/components/database/components/grid-row';
|
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { GridChildComponentProps, VariableSizeGrid } from 'react-window';
|
import { GridChildComponentProps, VariableSizeGrid } from 'react-window';
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from './grid-table';
|
||||||
|
export * from './grid-header';
|
||||||
|
export * from './grid-column';
|
@ -52,7 +52,10 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
|||||||
|
|
||||||
if (viewIds.length === 0) return null;
|
if (viewIds.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className='mx-24 flex items-center overflow-hidden text-text-title max-md:mx-4'>
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className='mx-24 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4'
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: 'calc(100% - 120px)',
|
width: 'calc(100% - 120px)',
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import { GridRowsContext, useDatabase, useGridRowOrders, useViewId } from '@/application/database-yjs';
|
import { RowsContext, useDatabase, useRowOrdersSelector, useViewId } from '@/application/database-yjs';
|
||||||
import { useRenderColumns } from '@/components/database/components/grid-column';
|
import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { GridHeader } from 'src/components/database/components/grid-header';
|
|
||||||
import { GridTable } from 'src/components/database/components/grid-table';
|
|
||||||
|
|
||||||
export function Grid() {
|
export function Grid() {
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
|
||||||
const viewId = useViewId() || '';
|
const viewId = useViewId() || '';
|
||||||
const { columns, columnWidth } = useRenderColumns(viewId);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
const rowOrders = useGridRowOrders();
|
|
||||||
|
const { fields, columnWidth } = useRenderFields();
|
||||||
|
const rowOrders = useRowOrdersSelector();
|
||||||
|
|
||||||
if (!database || !rowOrders) {
|
if (!database || !rowOrders) {
|
||||||
return (
|
return (
|
||||||
@ -21,24 +20,24 @@ export function Grid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GridRowsContext.Provider
|
<RowsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
rowOrders,
|
rowOrders,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={'flex w-full flex-1 flex-col'}>
|
<div className={'flex w-full flex-1 flex-col'}>
|
||||||
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={columns} onScrollLeft={setScrollLeft} />
|
<GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} />
|
||||||
<div className={'grid-scroll-table w-full flex-1'}>
|
<div className={'grid-scroll-table w-full flex-1'}>
|
||||||
<GridTable
|
<GridTable
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
scrollLeft={scrollLeft}
|
scrollLeft={scrollLeft}
|
||||||
columnWidth={columnWidth}
|
columnWidth={columnWidth}
|
||||||
columns={columns}
|
columns={fields}
|
||||||
onScrollLeft={setScrollLeft}
|
onScrollLeft={setScrollLeft}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GridRowsContext.Provider>
|
</RowsContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
opacity: 60%;
|
opacity: 60%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspaces, .database-conditions, .grid-scroll-table {
|
.workspaces, .database-conditions, .grid-scroll-table, .grid-board {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
@ -1252,7 +1252,9 @@
|
|||||||
"showGroup": "Show group",
|
"showGroup": "Show group",
|
||||||
"showGroupContent": "Are you sure you want to show this group on the board?",
|
"showGroupContent": "Are you sure you want to show this group on the board?",
|
||||||
"failedToLoad": "Failed to load board view"
|
"failedToLoad": "Failed to load board view"
|
||||||
}
|
},
|
||||||
|
"noGroup": "No group by property",
|
||||||
|
"noGroupDesc": "Board views require a property to group by in order to display"
|
||||||
},
|
},
|
||||||
"calendar": {
|
"calendar": {
|
||||||
"menuName": "Calendar",
|
"menuName": "Calendar",
|
||||||
|
Loading…
Reference in New Issue
Block a user