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/react": "^18.2.66",
|
||||
"@types/react-beautiful-dnd": "^13.1.3",
|
||||
"@types/react-big-calendar": "^1.8.9",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-custom-scrollbars": "^4.0.13",
|
||||
"@types/react-datepicker": "^4.19.3",
|
||||
|
@ -256,6 +256,9 @@ devDependencies:
|
||||
'@types/react-beautiful-dnd':
|
||||
specifier: ^13.1.3
|
||||
version: 13.1.3
|
||||
'@types/react-big-calendar':
|
||||
specifier: ^1.8.9
|
||||
version: 1.8.9
|
||||
'@types/react-color':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
@ -2618,6 +2621,10 @@ packages:
|
||||
dependencies:
|
||||
'@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:
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
|
||||
@ -2723,6 +2730,14 @@ packages:
|
||||
'@types/react': 18.2.66
|
||||
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:
|
||||
resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==}
|
||||
dependencies:
|
||||
|
@ -241,6 +241,9 @@ export enum YjsDatabaseKey {
|
||||
condition = 'condition',
|
||||
format = 'format',
|
||||
filter_type = 'filter_type',
|
||||
visible = 'visible',
|
||||
hide_ungrouped_column = 'hide_ungrouped_column',
|
||||
collapse_hidden_groups = 'collapse_hidden_groups',
|
||||
}
|
||||
|
||||
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 YDatabaseGroups = Y.Array<unknown>;
|
||||
export type YDatabaseGroups = Y.Array<YDatabaseGroup>;
|
||||
|
||||
export type YDatabaseFilters = Y.Array<YDatabaseFilter>;
|
||||
|
||||
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 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> {
|
||||
get(key: YjsDatabaseKey.id): SortId;
|
||||
|
||||
|
@ -1,10 +1,7 @@
|
||||
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 { sortBy } from '@/application/database-yjs/sort';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import * as Y from 'yjs';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
|
||||
export interface DatabaseContextState {
|
||||
readOnly: boolean;
|
||||
@ -56,72 +53,16 @@ export function useDatabaseFields() {
|
||||
return database.get(YjsDatabaseKey.fields);
|
||||
}
|
||||
|
||||
export interface GridRowsState {
|
||||
export interface RowsState {
|
||||
rowOrders: Row[];
|
||||
}
|
||||
|
||||
export const GridRowsContext = createContext<GridRowsState | null>(null);
|
||||
export const RowsContext = createContext<RowsState | null>(null);
|
||||
|
||||
export function useGridRowsContext() {
|
||||
return useContext(GridRowsContext);
|
||||
export function useRowsContext() {
|
||||
return useContext(RowsContext);
|
||||
}
|
||||
|
||||
export function useGridRows() {
|
||||
return useGridRowsContext()?.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;
|
||||
export function useRows() {
|
||||
return useRowsContext()?.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 { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||
import { useDatabase, useGridRows, useViewId } from '@/application/database-yjs/context';
|
||||
import { parseFilter } from '@/application/database-yjs/filter';
|
||||
import {
|
||||
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 { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export interface Column {
|
||||
fieldId: string;
|
||||
@ -19,11 +32,43 @@ export interface Row {
|
||||
|
||||
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 [columns, setColumns] = useState<Column[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!viewId) return;
|
||||
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
|
||||
const fields = database?.get(YjsDatabaseKey.fields);
|
||||
const fieldsOrder = view?.get(YjsDatabaseKey.field_orders);
|
||||
@ -39,11 +84,15 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil
|
||||
return {
|
||||
fieldId,
|
||||
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),
|
||||
};
|
||||
})
|
||||
.filter((column) => visibilitys.includes(column.visibility));
|
||||
.filter((column) => {
|
||||
return visibilitys.includes(column.visibility);
|
||||
});
|
||||
};
|
||||
|
||||
const observerEvent = () => setColumns(getColumns());
|
||||
@ -62,8 +111,8 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil
|
||||
return columns;
|
||||
}
|
||||
|
||||
export function useGridRowsSelector() {
|
||||
const rowOrders = useGridRows();
|
||||
export function useRowsSelector() {
|
||||
const rowOrders = useRows();
|
||||
|
||||
return useMemo(() => rowOrders ?? [], [rowOrders]);
|
||||
}
|
||||
@ -81,10 +130,10 @@ export function useFieldSelector(fieldId: string) {
|
||||
setField(field || null);
|
||||
const observerEvent = () => setClock((prev) => prev + 1);
|
||||
|
||||
field.observe(observerEvent);
|
||||
field?.observe(observerEvent);
|
||||
|
||||
return () => {
|
||||
field.unobserve(observerEvent);
|
||||
field?.unobserve(observerEvent);
|
||||
};
|
||||
}, [database, fieldId]);
|
||||
|
||||
@ -225,3 +274,207 @@ export function useSortSelector(sortId: SortId) {
|
||||
|
||||
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 trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash);
|
||||
const meta = folder.get(YjsFolderKey.meta);
|
||||
const trashUid = Array.from(trash?.keys())[0];
|
||||
const userTrash = trash?.get(trashUid);
|
||||
|
||||
console.log('folder', folder.toJSON());
|
||||
const collectIds = () => {
|
||||
const trashIds = userTrash?.toJSON()?.map((item) => item.id) || [];
|
||||
|
||||
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());
|
||||
|
||||
folder.observe(observerEvent);
|
||||
userTrash.observe(observerEvent);
|
||||
|
||||
return () => {
|
||||
folder.unobserve(observerEvent);
|
||||
userTrash.unobserve(observerEvent);
|
||||
};
|
||||
}, [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 RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
|
||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import { Board } from '@/components/database/board';
|
||||
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 DatabaseViews from '@/components/database/DatabaseViews';
|
||||
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||
import DatabaseTitle from '@/components/database/DatabaseTitle';
|
||||
import { Log } from '@/utils/log';
|
||||
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 SwipeableViews from 'react-swipeable-views';
|
||||
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export const Database = memo(() => {
|
||||
const { objectId, workspaceId } = useId() || {};
|
||||
const [search, setSearch] = useSearchParams();
|
||||
const viewId = search.get('v');
|
||||
|
||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||
const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc
|
||||
const [notFound, setNotFound] = useState<boolean>(false);
|
||||
@ -48,10 +41,6 @@ export const Database = memo(() => {
|
||||
void 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(
|
||||
(viewId: string) => {
|
||||
setSearch({ v: viewId });
|
||||
@ -59,32 +48,6 @@ export const Database = memo(() => {
|
||||
[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 (!doc) {
|
||||
@ -104,41 +67,7 @@ export const Database = memo(() => {
|
||||
<DatabaseTitle viewId={objectId} />
|
||||
<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}>
|
||||
<DatabaseConditionsContext.Provider
|
||||
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>
|
||||
<DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} />
|
||||
</DatabaseContextProvider>
|
||||
</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 { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
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;
|
||||
|
@ -1,7 +1,16 @@
|
||||
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() {
|
||||
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;
|
||||
|
@ -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(
|
||||
(timeStamp: string, includeTime?: boolean) => {
|
||||
if (!typeOptionValue) return null;
|
||||
if (!typeOptionValue || !timeStamp) return null;
|
||||
const timeFormat = getTimeFormat(typeOptionValue.timeFormat);
|
||||
const dateFormat = getDateFormat(typeOptionValue.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 { 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 RichTextCell from '@/components/database/components/cell/TextCell';
|
||||
import UrlCell from '@/components/database/components/cell/UrlCell';
|
||||
import NumberCell from '@/components/database/components/cell/NumberCell';
|
||||
import CheckboxCell from '@/components/database/components/cell/CheckboxCell';
|
||||
import SelectCell from '@/components/database/components/cell/SelectionCell';
|
||||
import DateTimeCell from '@/components/database/components/cell/DateTimeCell';
|
||||
import ChecklistCell from '@/components/database/components/cell/ChecklistCell';
|
||||
import { Cell as CellValue } from '@/components/database/components/cell/cell.type';
|
||||
import RelationCell from '@/components/database/components/cell/RelationCell';
|
||||
import { TextCell } from '@/components/database/components/cell/text';
|
||||
import { UrlCell } from '@/components/database/components/cell/url';
|
||||
import { NumberCell } from '@/components/database/components/cell/number';
|
||||
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
||||
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
||||
import { DateTimeCell } from '@/components/database/components/cell/date';
|
||||
import { ChecklistCell } from '@/components/database/components/cell/checklist';
|
||||
import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type';
|
||||
import { RelationCell } from '@/components/database/components/cell/relation';
|
||||
|
||||
export interface CellProps {
|
||||
rowId: string;
|
||||
fieldId: FieldId;
|
||||
cell?: CellValue;
|
||||
}
|
||||
|
||||
export function Cell({ cell, rowId, fieldId }: CellProps) {
|
||||
export function Cell(props: CellProps<CellType>) {
|
||||
const { cell, rowId, fieldId, style } = props;
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||
const Component = useMemo(() => {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
return RichTextCell;
|
||||
return TextCell;
|
||||
case FieldType.URL:
|
||||
return UrlCell;
|
||||
case FieldType.Number:
|
||||
@ -34,7 +29,7 @@ export function Cell({ cell, rowId, fieldId }: CellProps) {
|
||||
return CheckboxCell;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return SelectCell;
|
||||
return SelectOptionCell;
|
||||
case FieldType.DateTime:
|
||||
return DateTimeCell;
|
||||
case FieldType.Checklist:
|
||||
@ -42,21 +37,21 @@ export function Cell({ cell, rowId, fieldId }: CellProps) {
|
||||
case FieldType.Relation:
|
||||
return RelationCell;
|
||||
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) {
|
||||
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 <Component cell={cell} rowId={rowId} fieldId={fieldId} />;
|
||||
return <Component {...props} />;
|
||||
}
|
||||
|
||||
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 { DateFormat, SelectOption, TimeFormat } from '@/application/database-yjs';
|
||||
import { FieldId, RowId } from '@/application/collab.type';
|
||||
import { DateFormat, TimeFormat } from '@/application/database-yjs';
|
||||
import { FieldType } from '@/application/database-yjs/database.type';
|
||||
import React from 'react';
|
||||
import { YArray } from 'yjs/dist/src/types/YArray';
|
||||
|
||||
export interface Cell {
|
||||
@ -32,7 +33,7 @@ export interface UrlCell extends Cell {
|
||||
|
||||
export type SelectionId = string;
|
||||
|
||||
export interface SelectCell extends Cell {
|
||||
export interface SelectOptionCell extends Cell {
|
||||
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
|
||||
data: SelectionId;
|
||||
}
|
||||
@ -51,11 +52,6 @@ export interface DateTimeCell extends Cell {
|
||||
reminderId?: string;
|
||||
}
|
||||
|
||||
export interface TimeStampCell extends Cell {
|
||||
fieldType: FieldType.LastEditedTime | FieldType.CreatedTime;
|
||||
data: TimestampCellData;
|
||||
}
|
||||
|
||||
export interface DateTimeCellData {
|
||||
date?: string;
|
||||
time?: string;
|
||||
@ -67,11 +63,6 @@ export interface DateTimeCellData {
|
||||
isRange?: boolean;
|
||||
}
|
||||
|
||||
export interface TimestampCellData {
|
||||
dataTime?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface ChecklistCell extends Cell {
|
||||
fieldType: FieldType.Checklist;
|
||||
data: string;
|
||||
@ -84,7 +75,10 @@ export interface RelationCell extends Cell {
|
||||
|
||||
export type RelationCellData = RowId[];
|
||||
|
||||
export interface ChecklistCellData {
|
||||
selected_option_ids?: string[];
|
||||
options?: SelectOption[];
|
||||
export interface CellProps<T extends Cell> {
|
||||
cell?: T;
|
||||
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 { 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 React, { useMemo } from 'react';
|
||||
|
||||
export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldId: FieldId }) {
|
||||
export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) {
|
||||
const data = useMemo(() => {
|
||||
return parseChecklistData(cell?.data ?? '');
|
||||
}, [cell?.data]);
|
||||
@ -14,7 +13,7 @@ export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldI
|
||||
|
||||
if (!data || !options || !selectedOptions) return null;
|
||||
return (
|
||||
<div className={'cursor-pointer'}>
|
||||
<div style={style} className={'cursor-pointer'}>
|
||||
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />
|
||||
</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 React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
function RowCreateModifiedTime({
|
||||
export function RowCreateModifiedTime({
|
||||
rowId,
|
||||
fieldId,
|
||||
attrName,
|
||||
style,
|
||||
}: {
|
||||
rowId: string;
|
||||
fieldId: string;
|
||||
style?: React.CSSProperties;
|
||||
attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at;
|
||||
}) {
|
||||
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
||||
@ -37,7 +39,8 @@ function RowCreateModifiedTime({
|
||||
return getDateTimeStr(value, false);
|
||||
}, [value, getDateTimeStr]);
|
||||
|
||||
return <div>{time}</div>;
|
||||
if (!time) return null;
|
||||
return <div style={style}>{time}</div>;
|
||||
}
|
||||
|
||||
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 { 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 { 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 startDateTime = useMemo(() => {
|
||||
@ -26,8 +25,9 @@ export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string
|
||||
|
||||
const hasReminder = !!cell?.reminderId;
|
||||
|
||||
if (!cell?.data) return null;
|
||||
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'} />}
|
||||
{dateStr}
|
||||
</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 { 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 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 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());
|
||||
}, [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 {
|
||||
FieldId,
|
||||
YDatabaseField,
|
||||
YDatabaseFields,
|
||||
YDatabaseRow,
|
||||
YDoc,
|
||||
YjsDatabaseKey,
|
||||
YjsEditorKey,
|
||||
} from '@/application/collab.type';
|
||||
import { useFieldSelector, parseRelationTypeOption } from '@/application/database-yjs';
|
||||
import { YDatabaseField, YDatabaseFields, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { parseRelationTypeOption, useFieldSelector } from '@/application/database-yjs';
|
||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||
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 { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
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 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 databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
||||
@ -43,7 +35,7 @@ export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: stri
|
||||
}, [workspaceId, databaseId, databaseService]);
|
||||
|
||||
return (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div style={style} className={'flex items-center gap-2'}>
|
||||
{rowIds.map((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 }) {
|
||||
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>;
|
||||
}
|
||||
export default RelationItems;
|
@ -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 { Tag } from '@/components/_shared/tag';
|
||||
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';
|
||||
|
||||
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 { field } = useFieldSelector(fieldId);
|
||||
const typeOption = useMemo(() => {
|
||||
@ -24,9 +23,11 @@ export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string;
|
||||
[typeOption]
|
||||
);
|
||||
|
||||
if (!typeOption || !selectOptionIds?.length) return null;
|
||||
|
||||
return (
|
||||
<div className={'flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}>
|
||||
{selectOptionIds ? renderSelectedOptions(selectOptionIds) : null}
|
||||
<div style={style} className={'flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}>
|
||||
{renderSelectedOptions(selectOptionIds)}
|
||||
</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 { 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 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 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(' ');
|
||||
}, [isUrl]);
|
||||
|
||||
if (!cell?.data) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
onClick={() => {
|
||||
if (!isUrl || !cell) return;
|
||||
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,11 +1,21 @@
|
||||
import { CalculationType } from '@/application/database-yjs/database.type';
|
||||
import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type';
|
||||
import React, { useMemo } from 'react';
|
||||
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 prefix = useMemo(() => {
|
||||
if (!cell) return '';
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { FieldId, YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { useRowMeta } from '@/application/database-yjs';
|
||||
import { FieldId } from '@/application/collab.type';
|
||||
import { useCellSelector } from '@/application/database-yjs';
|
||||
import { useFieldSelector } from '@/application/database-yjs/selector';
|
||||
import { Cell } from '@/components/database/components/cell';
|
||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export interface GridCellProps {
|
||||
rowId: string;
|
||||
@ -16,21 +15,10 @@ export interface GridCellProps {
|
||||
export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const field = useFieldSelector(fieldId);
|
||||
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]);
|
||||
const cell = useCellSelector({
|
||||
rowId,
|
||||
fieldId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
@ -56,7 +44,7 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr
|
||||
if (!field) return null;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
@ -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 { 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';
|
||||
|
||||
export enum GridColumnType {
|
||||
@ -9,8 +9,6 @@ export enum GridColumnType {
|
||||
NewProperty,
|
||||
}
|
||||
|
||||
const defaultVisibilitys = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
|
||||
|
||||
export type RenderColumn = {
|
||||
type: GridColumnType;
|
||||
visibility?: FieldVisibility;
|
||||
@ -19,12 +17,12 @@ export type RenderColumn = {
|
||||
wrap?: boolean;
|
||||
};
|
||||
|
||||
export function useRenderColumns(viewId: string) {
|
||||
const columns = useGridColumnsSelector(viewId, defaultVisibilitys);
|
||||
export function useRenderFields() {
|
||||
const fields = useFieldsSelector();
|
||||
|
||||
console.log('columns', columns);
|
||||
console.log('columns', fields);
|
||||
const renderColumns = useMemo(() => {
|
||||
const fields = columns.map((column) => ({
|
||||
const data = fields.map((column) => ({
|
||||
...column,
|
||||
type: GridColumnType.Field,
|
||||
}));
|
||||
@ -34,7 +32,7 @@ export function useRenderColumns(viewId: string) {
|
||||
type: GridColumnType.Action,
|
||||
width: 96,
|
||||
},
|
||||
...fields,
|
||||
...data,
|
||||
{
|
||||
type: GridColumnType.NewProperty,
|
||||
width: 150,
|
||||
@ -44,7 +42,7 @@ export function useRenderColumns(viewId: string) {
|
||||
width: 96,
|
||||
},
|
||||
].filter(Boolean) as RenderColumn[];
|
||||
}, [columns]);
|
||||
}, [fields]);
|
||||
|
||||
const columnWidth = useCallback(
|
||||
(index: number, containerWidth: number) => {
|
||||
@ -67,7 +65,7 @@ export function useRenderColumns(viewId: string) {
|
||||
);
|
||||
|
||||
return {
|
||||
columns: renderColumns,
|
||||
fields: renderColumns,
|
||||
columnWidth,
|
||||
};
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { useDatabaseView } from '@/application/database-yjs';
|
||||
import { CalculationType } from '@/application/database-yjs/database.type';
|
||||
import { CalculationCell } from '@/components/database/components/calculation-cell';
|
||||
import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type';
|
||||
import { CalculationCell, ICalculationCell } from '../grid-calculation-cell';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export interface GridCalculateRowCellProps {
|
||||
@ -11,7 +10,7 @@ export interface GridCalculateRowCellProps {
|
||||
|
||||
export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) {
|
||||
const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations);
|
||||
const [calculation, setCalculation] = useState<CalulationCell>();
|
||||
const [calculation, setCalculation] = useState<ICalculationCell>();
|
||||
|
||||
useEffect(() => {
|
||||
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 GridCell from 'src/components/database/components/grid-cell/GridCell';
|
||||
import GridCell from '../grid-cell/GridCell';
|
||||
|
||||
export interface GridRowCellProps {
|
||||
rowId: string;
|
@ -1,6 +1,5 @@
|
||||
import { useReadOnly } from '@/application/database-yjs';
|
||||
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const';
|
||||
import { useGridRowsSelector } from '@/application/database-yjs/selector';
|
||||
import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowsSelector } from '@/application/database-yjs';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export enum RenderRowType {
|
||||
@ -16,7 +15,7 @@ export type RenderRow = {
|
||||
};
|
||||
|
||||
export function useRenderRows() {
|
||||
const rows = useGridRowsSelector();
|
||||
const rows = useRowsSelector();
|
||||
const readOnly = useReadOnly();
|
||||
|
||||
const renderRows = useMemo(() => {
|
@ -1,12 +1,7 @@
|
||||
import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const';
|
||||
import { AFScroller } from '@/components/_shared/scroller';
|
||||
import { GridColumnType, RenderColumn } from '@/components/database/components/grid-column';
|
||||
import {
|
||||
GridCalculateRowCell,
|
||||
GridRowCell,
|
||||
RenderRowType,
|
||||
useRenderRows,
|
||||
} from '@/components/database/components/grid-row';
|
||||
import { GridColumnType, RenderColumn } from '../grid-column';
|
||||
import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
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;
|
||||
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
|
||||
style={{
|
||||
width: 'calc(100% - 120px)',
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { GridRowsContext, useDatabase, useGridRowOrders, useViewId } from '@/application/database-yjs';
|
||||
import { useRenderColumns } from '@/components/database/components/grid-column';
|
||||
import { RowsContext, useDatabase, useRowOrdersSelector, useViewId } from '@/application/database-yjs';
|
||||
import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
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() {
|
||||
const database = useDatabase();
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
const viewId = useViewId() || '';
|
||||
const { columns, columnWidth } = useRenderColumns(viewId);
|
||||
const rowOrders = useGridRowOrders();
|
||||
const [scrollLeft, setScrollLeft] = useState(0);
|
||||
|
||||
const { fields, columnWidth } = useRenderFields();
|
||||
const rowOrders = useRowOrdersSelector();
|
||||
|
||||
if (!database || !rowOrders) {
|
||||
return (
|
||||
@ -21,24 +20,24 @@ export function Grid() {
|
||||
}
|
||||
|
||||
return (
|
||||
<GridRowsContext.Provider
|
||||
<RowsContext.Provider
|
||||
value={{
|
||||
rowOrders,
|
||||
}}
|
||||
>
|
||||
<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'}>
|
||||
<GridTable
|
||||
viewId={viewId}
|
||||
scrollLeft={scrollLeft}
|
||||
columnWidth={columnWidth}
|
||||
columns={columns}
|
||||
columns={fields}
|
||||
onScrollLeft={setScrollLeft}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</GridRowsContext.Provider>
|
||||
</RowsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.workspaces, .database-conditions, .grid-scroll-table {
|
||||
.workspaces, .database-conditions, .grid-scroll-table, .grid-board {
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
@ -1252,7 +1252,9 @@
|
||||
"showGroup": "Show group",
|
||||
"showGroupContent": "Are you sure you want to show this group on the board?",
|
||||
"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": {
|
||||
"menuName": "Calendar",
|
||||
|
Loading…
Reference in New Issue
Block a user