From b7bc847107b686c82b628c443e1482889b889ae8 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Tue, 21 May 2024 17:26:00 +0800 Subject: [PATCH] feat: support board preview on web (#5384) --- frontend/appflowy_web_app/package.json | 1 + frontend/appflowy_web_app/pnpm-lock.yaml | 15 + .../src/application/collab.type.ts | 37 ++- .../src/application/database-yjs/context.ts | 73 +---- .../src/application/database-yjs/group.ts | 60 ++++ .../src/application/database-yjs/selector.ts | 273 +++++++++++++++++- .../src/application/folder-yjs/selector.ts | 9 +- .../src/components/database/Database.tsx | 79 +---- .../src/components/database/DatabaseViews.tsx | 86 ++++++ .../src/components/database/board/Board.tsx | 29 +- .../components/database/calendar/Calendar.tsx | 11 +- .../database/calendar/calendar.scss | 2 + .../database/components/board/card/Card.tsx | 50 ++++ .../components/board/card/CardField.tsx | 48 +++ .../database/components/board/card/index.ts | 1 + .../components/board/column/Column.tsx | 130 +++++++++ .../components/board/column/ListItem.tsx | 74 +++++ .../database/components/board/column/index.ts | 1 + .../board/column/useRenderColumn.ts | 31 ++ .../database/components/board/group/Group.tsx | 71 +++++ .../database/components/board/group/index.ts | 1 + .../database/components/board/index.ts | 1 + .../components/calculation-cell/cell.type.ts | 8 - .../database/components/cell/Cell.hooks.ts | 2 +- .../database/components/cell/Cell.tsx | 45 ++- .../database/components/cell/CheckboxCell.tsx | 14 - .../database/components/cell/TextCell.tsx | 12 - .../database/components/cell/cell.type.ts | 26 +- .../components/cell/checkbox/CheckboxCell.tsx | 13 + .../components/cell/checkbox/index.ts | 1 + .../cell/{ => checklist}/ChecklistCell.tsx | 7 +- .../components/cell/checklist/index.ts | 1 + .../RowCreateModifiedTime.tsx | 7 +- .../components/cell/created-modified/index.ts | 1 + .../cell/{ => date}/DateTimeCell.tsx | 8 +- .../database/components/cell/date/index.ts | 1 + .../cell/{ => number}/NumberCell.tsx | 12 +- .../database/components/cell/number/index.ts | 1 + .../components/cell/relation/RelationCell.tsx | 7 + .../RelationItems.tsx} | 44 +-- .../cell/relation/RelationPrimaryValue.tsx | 27 ++ .../components/cell/relation/index.ts | 1 + .../SelectOptionCell.tsx} | 11 +- .../components/cell/select-option/index.ts | 1 + .../components/cell/text/TextCell.tsx | 14 + .../database/components/cell/text/index.ts | 1 + .../components/cell/{ => url}/UrlCell.tsx | 8 +- .../database/components/cell/url/index.ts | 1 + .../database/components/cell/useMeasure.ts | 53 ++++ .../database/components/grid-column/index.ts | 2 - .../CalculationCell.tsx | 16 +- .../grid-calculation-cell}/index.ts | 0 .../{ => grid}/grid-cell/GridCell.tsx | 28 +- .../components/{ => grid}/grid-cell/index.ts | 0 .../{ => grid}/grid-column/GridColumn.tsx | 0 .../components/grid/grid-column/index.ts | 2 + .../grid-column/useRenderFields.tsx} | 18 +- .../{ => grid}/grid-header/GridHeader.tsx | 0 .../{ => grid}/grid-header/index.ts | 0 .../grid-row/GridCalculateRowCell.tsx | 5 +- .../{ => grid}/grid-row/GridRowCell.tsx | 4 +- .../components/{ => grid}/grid-row/index.ts | 0 .../{ => grid}/grid-row/useRenderRows.tsx | 7 +- .../{ => grid}/grid-table/GridTable.tsx | 9 +- .../components/{ => grid}/grid-table/index.ts | 0 .../database/components/grid/index.ts | 3 + .../database/components/tabs/DatabaseTabs.tsx | 5 +- .../src/components/database/grid/Grid.tsx | 21 +- .../src/components/layout/layout.scss | 2 +- frontend/resources/translations/en.json | 4 +- 70 files changed, 1179 insertions(+), 357 deletions(-) create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/group.ts create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/calendar.scss create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/card/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/group/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/index.ts delete mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts delete mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx delete mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => checklist}/ChecklistCell.tsx (68%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => created-modified}/RowCreateModifiedTime.tsx (88%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => date}/DateTimeCell.tsx (75%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => number}/NumberCell.tsx (69%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx rename frontend/appflowy_web_app/src/components/database/components/cell/{RelationCell.tsx => relation/RelationItems.tsx} (58%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{SelectionCell.tsx => select-option/SelectOptionCell.tsx} (68%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => url}/UrlCell.tsx (77%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts delete mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts rename frontend/appflowy_web_app/src/components/database/components/{calculation-cell => grid/grid-calculation-cell}/CalculationCell.tsx (83%) rename frontend/appflowy_web_app/src/components/database/components/{calculation-cell => grid/grid-calculation-cell}/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-cell/GridCell.tsx (57%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-cell/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-column/GridColumn.tsx (100%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts rename frontend/appflowy_web_app/src/components/database/components/{grid-column/useRenderColumns.tsx => grid/grid-column/useRenderFields.tsx} (75%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-header/GridHeader.tsx (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-header/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/GridCalculateRowCell.tsx (83%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/GridRowCell.tsx (81%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/useRenderRows.tsx (76%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-table/GridTable.tsx (95%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-table/index.ts (100%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid/index.ts diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 2dafe5e66d..6eeb31ee09 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -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", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 770298d3b9..4cc4e224c2 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -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: diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 9a2bcfe186..ac6be1f3f8 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -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; // [ { id: FieldId } ] export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] -export type YDatabaseGroups = Y.Array; +export type YDatabaseGroups = Y.Array; export type YDatabaseFilters = Y.Array; export type YDatabaseSorts = Y.Array; -export type YDatabaseLayoutSettings = Y.Map; +export type YDatabaseLayoutSettings = Y.Map; export type YDatabaseCalculations = Y.Array; export type SortId = string; +export type GroupId = string; + +export interface YDatabaseLayoutSetting extends Y.Map { + // DatabaseViewLayout.Board + get(key: '2'): YDatabaseBoardLayoutSetting; +} + +export interface YDatabaseBoardLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; +} + +export interface YDatabaseGroup extends Y.Map { + 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; + +export interface YDatabaseGroupColumn extends Y.Map { + get(key: YjsDatabaseKey.id): string; + + get(key: YjsDatabaseKey.visible): boolean; +} + export interface YDatabaseRowOrder extends Y.Map { get(key: YjsDatabaseKey.id): SortId; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts index 8717aa0ffe..73feb8d0d7 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -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(null); +export const RowsContext = createContext(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(); - 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; } diff --git a/frontend/appflowy_web_app/src/application/database-yjs/group.ts b/frontend/appflowy_web_app/src/application/database-yjs/group.ts new file mode 100644 index 0000000000..ddefab9a26 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/group.ts @@ -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, 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) { + 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, field: YDatabaseField) { + const fieldId = field.get(YjsDatabaseKey.id); + const result = new Map(); + 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; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index c3222fdf65..f115ff5eb4 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -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([]); + 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([]); 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([]); + + 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(null); + const [columns, setColumns] = useState([]); + + 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>(new Map()); + + useEffect(() => { + if (!fieldId || !rowOrders || !rows) return; + + const onConditionsChange = () => { + const newResult = new Map(); + + 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(); + 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; +} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts index 295315874b..648e27c9d3 100644 --- a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts @@ -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]); diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 9e54b68ad0..fd09fadd41 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -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(null); const [rows, setRows] = useState | null>(null); // Map(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(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(() => {
- - - - - - {viewIds.map((viewId, index) => { - const layout = Number(views.get(viewId)?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; - const Component = getDatabaseViewComponent(layout); - - return ( - - - - ); - })} - +
diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx new file mode 100644 index 0000000000..5d055780b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -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(false); + const toggleExpanded = useCallback(() => { + setConditionsExpanded((prev) => !prev); + }, []); + + return ( + <> + + + + + + {childViews.map((view, index) => { + const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + const Component = getDatabaseViewComponent(layout); + const viewId = viewIds[index]; + + return ( + + + + ); + })} + + + ); +} + +export default DatabaseViews; diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx index eabc9c2631..27c43bf8b3 100644 --- a/frontend/appflowy_web_app/src/components/database/board/Board.tsx +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -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
Board
; + const database = useDatabase(); + const groups = useGroupsSelector(); + + if (!database) { + return ( +
+ +
+ ); + } + + return ( + { + // + }} + > +
+ {groups.map((groupId) => ( + + ))} +
+
+ ); } export default Board; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx index c21e37b362..4face8913f 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -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
Calendar
; + return ( +
+ +
+ ); } export default Calendar; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss new file mode 100644 index 0000000000..3a3aebd3db --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -0,0 +1,2 @@ +@import 'react-big-calendar/lib/sass/styles'; +@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx new file mode 100644 index 0000000000..7dbe829662 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -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(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 ( +
+ {showFields.map((field, index) => { + return ; + })} +
+ ); +} + +export default Card; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx new file mode 100644 index 0000000000..585a3d2ce0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx @@ -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 ( +
+ {t('grid.row.titlePlaceholder')} +
+ ); + } + + return ; +} + +export default CardField; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts new file mode 100644 index 0000000000..ca0b060473 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts @@ -0,0 +1 @@ +export * from './Card'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx new file mode 100644 index 0000000000..765bbb0a19 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx @@ -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(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 ( + + {(provided) => ( + + )} + + ); + }, + [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
; + return ( +
+
+ +
+ +
+ ( + + )} + > + {(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 ( + + {({ height, width }: { height: number; width: number }) => { + return ( + + {Row} + + ); + }} + + ); + }} + +
+
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx new file mode 100644 index 0000000000..ac1e3bb82b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx @@ -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 ( +
+ +
+ ); +}; + +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; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts new file mode 100644 index 0000000000..f59b699c20 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts @@ -0,0 +1 @@ +export * from './Column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts new file mode 100644 index 0000000000..c845d4b5a3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts @@ -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, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx new file mode 100644 index 0000000000..7d5e3630be --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -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 ( +
+
{t('board.noGroup')}
+
{t('board.noGroupDesc')}
+
+ ); + } + + if (columns.length === 0 || !fieldId) return null; + return ( + + { + // we have a transform: * on one of the parents of a 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 ; + }} + > + {(provided) => { + return ( +
+ {columns.map((data, index) => ( + + {(provided) => { + return ( + + ); + }} + + ))} +
+ ); + }} +
+
+ ); +}; + +export default Group; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts new file mode 100644 index 0000000000..8401278d65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts @@ -0,0 +1 @@ +export * from './Group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/index.ts new file mode 100644 index 0000000000..8a78f59377 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/index.ts @@ -0,0 +1 @@ +export * from './group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts deleted file mode 100644 index ef44e2e745..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CalculationType } from '@/application/database-yjs/database.type'; - -export interface CalulationCell { - value: string; - fieldId: string; - id: string; - type: CalculationType; -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts index 1012dd4543..2e752f8f6e 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts @@ -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]; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx index ee3cde673b..e299c922c9 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -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) { + 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>; if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; - return ; + return ; } - if (cell?.fieldType !== fieldType) { + if (cell && cell.fieldType !== fieldType) { return null; } - return ; + return ; } export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx deleted file mode 100644 index 558c424f62..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx +++ /dev/null @@ -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 ( -
- {checked ? : } -
- ); -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx deleted file mode 100644 index f9c8749258..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx +++ /dev/null @@ -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
{cell?.data}
; -} - -export default TextCellComponent; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts index 185cca9409..bd13ef29d0 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts @@ -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 { + cell?: T; + rowId: string; + fieldId: FieldId; + style?: React.CSSProperties; + readOnly?: boolean; } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx new file mode 100644 index 0000000000..cc665474ec --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx @@ -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) { + const checked = cell?.data; + + return ( +
+ {checked ? : } +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts new file mode 100644 index 0000000000..f1cb1ac4bf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts @@ -0,0 +1 @@ +export * from './CheckboxCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx similarity index 68% rename from frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx index 32d97d758f..3eaa8254a4 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -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) { 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 ( -
+
); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts new file mode 100644 index 0000000000..b12d47b6c5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx similarity index 88% rename from frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx index d685b53cf9..7716ba1552 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -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
{time}
; + if (!time) return null; + return
{time}
; } export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts new file mode 100644 index 0000000000..ed951f3521 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts @@ -0,0 +1 @@ +export * from './RowCreateModifiedTime'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx similarity index 75% rename from frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx index 490a2bd95e..bc90a9fa7a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -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) { 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 ( -
+
{hasReminder && } {dateStr}
diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts new file mode 100644 index 0000000000..e05bb1674a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts @@ -0,0 +1 @@ +export * from './DateTimeCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx similarity index 69% rename from frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx index 851e14a34e..4d6ce6d44a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -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) { 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
{value}
; + if (value === undefined) return null; + return ( +
+ {value} +
+ ); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts new file mode 100644 index 0000000000..3e1686c783 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts @@ -0,0 +1 @@ +export * from './NumberCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx new file mode 100644 index 0000000000..3545bc026b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx @@ -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) { + if (!cell?.data) return null; + return ; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx similarity index 58% rename from frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx index 56c1e8d27b..a91dfe57af 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -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(undefined); @@ -43,7 +35,7 @@ export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: stri }, [workspaceId, databaseId, databaseService]); return ( -
+
{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(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
{text}
; -} +export default RelationItems; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx new file mode 100644 index 0000000000..174a3693f0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -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(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
{text}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts new file mode 100644 index 0000000000..95a0aa3668 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts @@ -0,0 +1 @@ +export * from './RelationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx similarity index 68% rename from frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx index a915d31a9b..bd7038a8a7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -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) { 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 ( -
- {selectOptionIds ? renderSelectedOptions(selectOptionIds) : null} +
+ {renderSelectedOptions(selectOptionIds)}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts new file mode 100644 index 0000000000..40df2f3d7d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx new file mode 100644 index 0000000000..e27f1e835f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx @@ -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) { + const readOnly = useReadOnly(); + + if (!cell?.data) return null; + return ( +
+ {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts new file mode 100644 index 0000000000..64bcb41a7f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts @@ -0,0 +1 @@ +export * from './TextCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx similarity index 77% rename from frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx index e2d3d2c87f..97de0c0fdb 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -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) { 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 (
{ if (!isUrl || !cell) return; if (readOnly) { diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts new file mode 100644 index 0000000000..9f45924c97 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts @@ -0,0 +1 @@ +export * from './UrlCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts new file mode 100644 index 0000000000..d4d7020523 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts @@ -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, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts deleted file mode 100644 index 6de83c7026..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './GridColumn'; -export * from './useRenderColumns'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx similarity index 83% rename from frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx index eeefee18bb..1ddb4e2d32 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx @@ -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 ''; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx similarity index 57% rename from frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx index b9a5017b38..ce47153c70 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx @@ -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(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 (
- +
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts new file mode 100644 index 0000000000..3c71a6b899 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts @@ -0,0 +1,2 @@ +export * from './GridColumn'; +export * from 'src/components/database/components/grid/grid-column/useRenderFields'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx similarity index 75% rename from frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx index c0041b5c5e..2e5c42e93a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -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, }; } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx similarity index 83% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx index 650ed3bfbe..4d7abb7a2c 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx @@ -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(); + const [calculation, setCalculation] = useState(); useEffect(() => { if (!calculations) return; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx similarity index 81% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx index ef4be68406..11f14135e3 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx @@ -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; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx similarity index 76% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx index e5038cafff..8b2e6597b8 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx @@ -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(() => { diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx similarity index 95% rename from frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index dd3ed13bfe..b855c8b4cb 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -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'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts new file mode 100644 index 0000000000..2e9a6988f4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts @@ -0,0 +1,3 @@ +export * from './grid-table'; +export * from './grid-header'; +export * from './grid-column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index 65a1b238bb..e7d78e7033 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -52,7 +52,10 @@ export const DatabaseTabs = forwardRef( if (viewIds.length === 0) return null; return ( -
+
- +
- + ); } diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index bac3baae69..fceeab367c 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -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; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 1b286e2498..ca0a190367 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -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",