feat: support open row page (#5400)

This commit is contained in:
Kilu.He 2024-05-23 16:35:45 +08:00 committed by GitHub
parent 9a5dbbb3ce
commit a0139dd475
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 658 additions and 213 deletions

View File

@ -1,5 +1,7 @@
import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { RowMetaKey } from '@/application/database-yjs/database.type';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { v5 as uuidv5, parse as uuidParse } from 'uuid';
export const DEFAULT_ROW_HEIGHT = 37; export const DEFAULT_ROW_HEIGHT = 37;
export const MIN_COLUMN_WIDTH = 100; export const MIN_COLUMN_WIDTH = 100;
@ -14,3 +16,9 @@ export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) =
export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => { export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data); return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data);
}; };
export const metaIdFromRowId = (rowId: string) => {
const namespace = uuidParse(rowId);
return (key: RowMetaKey) => uuidv5(key, namespace).toString();
};

View File

@ -8,6 +8,7 @@ export interface DatabaseContextState {
doc: YDoc; doc: YDoc;
viewId: string; viewId: string;
rowDocMap: Y.Map<YDoc>; rowDocMap: Y.Map<YDoc>;
navigateToRow?: (rowId: string) => void;
} }
export const DatabaseContext = createContext<DatabaseContextState | null>(null); export const DatabaseContext = createContext<DatabaseContextState | null>(null);
@ -20,12 +21,18 @@ export const useDatabase = () => {
return database; return database;
}; };
export const useRowMeta = (rowId: string) => { export const useNavigateToRow = () => {
const rows = useContext(DatabaseContext)?.rowDocMap; return useContext(DatabaseContext)?.navigateToRow;
const rowMetaDoc = rows?.get(rowId); };
const rowMeta = rowMetaDoc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
return rowMeta; export const useRow = (rowId: string) => {
const rows = useContext(DatabaseContext)?.rowDocMap;
return rows?.get(rowId)?.getMap(YjsEditorKey.data_section);
};
export const useRowData = (rowId: string) => {
return useRow(rowId)?.get(YjsEditorKey.database_row) as YDatabaseRow;
}; };
export const useViewId = () => { export const useViewId = () => {

View File

@ -63,3 +63,10 @@ export interface CalendarLayoutSetting {
showWeekends: boolean; showWeekends: boolean;
layout: CalendarLayout; layout: CalendarLayout;
} }
export enum RowMetaKey {
DocumentId = 'document_id',
IconId = 'icon_id',
CoverId = 'cover_id',
IsDocumentEmpty = 'is_document_empty',
}

View File

@ -1,11 +1,12 @@
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsFolderKey } from '@/application/collab.type'; import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsEditorKey, YjsFolderKey } from '@/application/collab.type';
import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
import { import {
DatabaseContext, DatabaseContext,
useDatabase, useDatabase,
useDatabaseFields, useDatabaseFields,
useDatabaseView, useDatabaseView,
useRowMeta, useRow,
useRowData,
useRows, useRows,
useViewId, useViewId,
} from '@/application/database-yjs/context'; } from '@/application/database-yjs/context';
@ -18,8 +19,8 @@ import { parseYDatabaseCellToCell } from '@/components/database/components/cell/
import { DateTimeCell } from '@/components/database/components/cell/cell.type'; import { DateTimeCell } from '@/components/database/components/cell/cell.type';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { useContext, useEffect, useMemo, useState } from 'react'; import React, { useContext, useEffect, useMemo, useState } from 'react';
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
export interface Column { export interface Column {
fieldId: string; fieldId: string;
@ -473,7 +474,7 @@ export function useRowOrdersSelector() {
} }
export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) {
const row = useRowMeta(rowId); const row = useRowData(rowId);
const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId);
const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined));
@ -585,3 +586,59 @@ export function useCalendarLayoutSetting() {
return setting; return setting;
} }
export function usePrimaryFieldId() {
const database = useDatabase();
const [primaryFieldId, setPrimaryFieldId] = React.useState<string | null>(null);
useEffect(() => {
const fields = database?.get(YjsDatabaseKey.fields);
const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => {
return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary);
});
setPrimaryFieldId(primaryFieldId || null);
}, [database]);
return primaryFieldId;
}
export interface RowMeta {
documentId: string;
cover: string;
icon: string;
isEmptyDocument: boolean;
}
export const useRowMetaSelector = (rowId: string) => {
const [meta, setMeta] = useState<RowMeta | null>();
const yMeta = useRow(rowId)?.get(YjsEditorKey.meta);
useEffect(() => {
if (!yMeta) return;
const onChange = () => {
const metaJson = yMeta.toJSON();
const getData = metaIdFromRowId(rowId);
const icon = metaJson[getData(RowMetaKey.IconId)];
const cover = metaJson[getData(RowMetaKey.CoverId)];
const documentId = getData(RowMetaKey.DocumentId);
const isEmptyDocument = metaJson[getData(RowMetaKey.IsDocumentEmpty)];
return setMeta({
icon,
cover,
documentId,
isEmptyDocument,
});
};
onChange();
yMeta.observe(onChange);
return () => {
yMeta.unobserve(onChange);
};
}, [rowId, yMeta]);
return meta;
};

View File

@ -62,6 +62,7 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole
switch (fieldType) { switch (fieldType) {
case FieldType.RichText: case FieldType.RichText:
case FieldType.URL: case FieldType.URL:
return data ? data : '\uFFFF';
case FieldType.Number: case FieldType.Number:
return data; return data;
case FieldType.Checkbox: case FieldType.Checkbox:

View File

@ -17,7 +17,8 @@ export class JSDatabaseService implements DatabaseService {
async getDatabase( async getDatabase(
workspaceId: string, workspaceId: string,
databaseId: string databaseId: string,
rowIds?: string[]
): Promise<{ ): Promise<{
databaseDoc: YDoc; databaseDoc: YDoc;
rows: Y.Map<YDoc>; rows: Y.Map<YDoc>;
@ -36,25 +37,24 @@ export class JSDatabaseService implements DatabaseService {
const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString();
const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders);
const rowIds = rowOrders.toJSON() as { const rowOrdersIds = rowOrders.toJSON() as {
id: string; id: string;
}[]; }[];
if (!rowIds) { if (!rowOrdersIds) {
throw new Error('Database rows not found'); throw new Error('Database rows not found');
} }
if (isLoaded) { const ids = rowIds ? rowIds : rowOrdersIds.map((item) => item.id);
for (const row of rowIds) {
const { doc } = await getCollabStorage(row.id, CollabType.DatabaseRow);
rowsFolder.set(row.id, doc); if (isLoaded) {
for (const id of ids) {
const { doc } = await getCollabStorage(id, CollabType.DatabaseRow);
rowsFolder.set(id, doc);
} }
} else { } else {
const rows = await this.loadDatabaseRows( const rows = await this.loadDatabaseRows(workspaceId, ids);
workspaceId,
rowIds.map((item) => item.id)
);
rows.forEach((row, id) => { rows.forEach((row, id) => {
rowsFolder.set(id, row); rowsFolder.set(id, row);
@ -63,6 +63,27 @@ export class JSDatabaseService implements DatabaseService {
this.loadedDatabaseId.add(databaseId); this.loadedDatabaseId.add(databaseId);
if (!rowIds) {
// Update rows if new rows are added
rowOrders?.observe((event) => {
if (event.changes.added.size > 0) {
const rowIds = rowOrders.toJSON() as {
id: string;
}[];
console.log('Update rows', rowIds);
void this.loadDatabaseRows(
workspaceId,
rowIds.map((item) => item.id)
).then((newRows) => {
newRows.forEach((row, id) => {
rowsFolder.set(id, row);
});
});
}
});
}
return { return {
databaseDoc, databaseDoc,
rows: rowsFolder as Y.Map<YDoc>, rows: rowsFolder as Y.Map<YDoc>,
@ -71,7 +92,8 @@ export class JSDatabaseService implements DatabaseService {
async openDatabase( async openDatabase(
workspaceId: string, workspaceId: string,
viewId: string viewId: string,
rowIds?: string[]
): Promise<{ ): Promise<{
databaseDoc: YDoc; databaseDoc: YDoc;
rows: Y.Map<YDoc>; rows: Y.Map<YDoc>;
@ -112,28 +134,8 @@ export class JSDatabaseService implements DatabaseService {
throw new Error('Database not found'); throw new Error('Database not found');
} }
const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id); const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id, rowIds);
const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders);
// Update rows if new rows are added
rowOrders?.observe((event) => {
if (event.changes.added.size > 0) {
const rowIds = rowOrders.toJSON() as {
id: string;
}[];
console.log('Update rows', rowIds);
void this.loadDatabaseRows(
workspaceId,
rowIds.map((item) => item.id)
).then((newRows) => {
newRows.forEach((row, id) => {
rows.set(id, row);
});
});
}
});
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
if (origin === CollabOrigin.LocalSync) { if (origin === CollabOrigin.LocalSync) {
// Send the update to the server // Send the update to the server

View File

@ -37,14 +37,16 @@ export interface DocumentService {
export interface DatabaseService { export interface DatabaseService {
openDatabase: ( openDatabase: (
workspaceId: string, workspaceId: string,
viewId: string viewId: string,
rowIds?: string[]
) => Promise<{ ) => Promise<{
databaseDoc: YDoc; databaseDoc: YDoc;
rows: Y.Map<YDoc>; rows: Y.Map<YDoc>;
}>; }>;
getDatabase: ( getDatabase: (
workspaceId: string, workspaceId: string,
databaseId: string databaseId: string,
rowIds?: string[]
) => Promise<{ ) => Promise<{
databaseDoc: YDoc; databaseDoc: YDoc;
rows: Y.Map<YDoc>; rows: Y.Map<YDoc>;

View File

@ -57,15 +57,36 @@ export const YjsEditor = {
export function withYjs<T extends Editor>( export function withYjs<T extends Editor>(
editor: T, editor: T,
doc: Y.Doc, doc: Y.Doc,
localOrigin: CollabOrigin = CollabOrigin.Local {
localOrigin,
includeRoot = true,
}: {
localOrigin: CollabOrigin;
includeRoot?: boolean;
}
): T & YjsEditor { ): T & YjsEditor {
const e = editor as T & YjsEditor; const e = editor as T & YjsEditor;
const { apply, onChange } = e; const { apply, onChange } = e;
e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
const initializeDocumentContent = () => {
const content = yDocToSlateContent(doc, includeRoot);
if (!content) {
return;
}
e.children = content.children;
Editor.normalize(editor, { force: true });
};
e.applyRemoteEvents = (events: Array<YEvent<YSharedRoot>>, _: Transaction) => { e.applyRemoteEvents = (events: Array<YEvent<YSharedRoot>>, _: Transaction) => {
YjsEditor.flushLocalChanges(e); YjsEditor.flushLocalChanges(e);
// TODO: handle remote events
// This is a temporary implementation to apply remote events to slate
initializeDocumentContent();
Editor.withoutNormalizing(editor, () => { Editor.withoutNormalizing(editor, () => {
events.forEach((event) => { events.forEach((event) => {
translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => { translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => {
@ -87,17 +108,8 @@ export function withYjs<T extends Editor>(
throw new Error('Already connected'); throw new Error('Already connected');
} }
const content = yDocToSlateContent(doc, true); initializeDocumentContent();
if (!content) {
return;
}
console.log(content);
e.sharedRoot.observeDeep(handleYEvents); e.sharedRoot.observeDeep(handleYEvents);
e.children = content.children;
Editor.normalize(editor, { force: true });
connectSet.add(e); connectSet.add(e);
}; };

View File

@ -3,10 +3,9 @@ import * as Y from 'yjs';
import { Editor, Operation } from 'slate'; import { Editor, Operation } from 'slate';
export function translateYArrayEvent( export function translateYArrayEvent(
sharedRoot: YSharedRoot, _sharedRoot: YSharedRoot,
editor: Editor, _editor: Editor,
event: Y.YEvent<Y.Array<string>> _event: Y.YEvent<Y.Array<string>>
): Operation[] { ): Operation[] {
console.log('translateYArrayEvent', sharedRoot, editor, event);
return []; return [];
} }

View File

@ -13,7 +13,6 @@ import { translateYTextEvent } from 'src/application/slate-yjs/utils/translateYj
* @param op * @param op
*/ */
export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent<YSharedRoot>): Operation[] { export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent<YSharedRoot>): Operation[] {
console.log('translateYjsEvent', event);
if (event instanceof Y.YMapEvent) { if (event instanceof Y.YMapEvent) {
return translateYMapEvent(sharedRoot, editor, event); return translateYMapEvent(sharedRoot, editor, event);
} }

View File

@ -3,10 +3,9 @@ import * as Y from 'yjs';
import { Editor, Operation } from 'slate'; import { Editor, Operation } from 'slate';
export function translateYMapEvent( export function translateYMapEvent(
sharedRoot: YSharedRoot, _sharedRoot: YSharedRoot,
editor: Editor, _editor: Editor,
event: Y.YEvent<Y.Map<unknown>> _event: Y.YEvent<Y.Map<unknown>>
): Operation[] { ): Operation[] {
console.log('translateYMapEvent', sharedRoot, editor, event);
return []; return [];
} }

View File

@ -2,7 +2,6 @@ import { YSharedRoot } from '@/application/collab.type';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { Editor, Operation } from 'slate'; import { Editor, Operation } from 'slate';
export function translateYTextEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent<Y.Text>): Operation[] { export function translateYTextEvent(_sharedRoot: YSharedRoot, _editor: Editor, _event: Y.YEvent<Y.Text>): Operation[] {
console.log('translateYTextEvent', sharedRoot, editor, event);
return []; return [];
} }

View File

@ -2,9 +2,9 @@ import { YDoc, YjsEditorKey } from '@/application/collab.type';
import { useId } from '@/components/_shared/context-provider/IdProvider'; import { useId } from '@/components/_shared/context-provider/IdProvider';
import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; import RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
import { AFConfigContext } from '@/components/app/AppConfig'; import { AFConfigContext } from '@/components/app/AppConfig';
import { DatabaseHeader } from '@/components/database/components/header';
import DatabaseViews from '@/components/database/DatabaseViews'; import DatabaseViews from '@/components/database/DatabaseViews';
import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
import DatabaseTitle from '@/components/database/DatabaseTitle';
import { Log } from '@/utils/log'; import { Log } from '@/utils/log';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import React, { memo, useCallback, useContext, useEffect, useState } from 'react'; import React, { memo, useCallback, useContext, useEffect, useState } from 'react';
@ -14,13 +14,14 @@ import * as Y from 'yjs';
export const Database = memo(() => { export const Database = memo(() => {
const { objectId, workspaceId } = useId() || {}; const { objectId, workspaceId } = useId() || {};
const [search, setSearch] = useSearchParams(); const [search, setSearch] = useSearchParams();
const viewId = search.get('v'); const viewId = search.get('v');
const [doc, setDoc] = useState<YDoc | null>(null); const [doc, setDoc] = useState<YDoc | null>(null);
const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc
const [notFound, setNotFound] = useState<boolean>(false); const [notFound, setNotFound] = useState<boolean>(false);
const databaseService = useContext(AFConfigContext)?.service?.databaseService; const databaseService = useContext(AFConfigContext)?.service?.databaseService;
const handleOpenDocument = useCallback(async () => { const handleOpenDatabase = useCallback(async () => {
if (!databaseService || !workspaceId || !objectId) return; if (!databaseService || !workspaceId || !objectId) return;
try { try {
@ -28,6 +29,8 @@ export const Database = memo(() => {
const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId); const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId);
console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON());
console.log('rows', rows);
setDoc(databaseDoc); setDoc(databaseDoc);
setRows(rows); setRows(rows);
} catch (e) { } catch (e) {
@ -38,8 +41,8 @@ export const Database = memo(() => {
useEffect(() => { useEffect(() => {
setNotFound(false); setNotFound(false);
void handleOpenDocument(); void handleOpenDatabase();
}, [handleOpenDocument]); }, [handleOpenDatabase]);
const handleChangeView = useCallback( const handleChangeView = useCallback(
(viewId: string) => { (viewId: string) => {
@ -48,13 +51,18 @@ export const Database = memo(() => {
[setSearch] [setSearch]
); );
if (!objectId) return null; const navigateToRow = useCallback(
(rowId: string) => {
setSearch({ r: rowId });
},
[setSearch]
);
if (!doc) { if (notFound || !objectId) {
return <RecordNotFound open={notFound} workspaceId={workspaceId} />; return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
} }
if (!rows) { if (!rows || !doc) {
return ( return (
<div className={'flex h-full w-full items-center justify-center'}> <div className={'flex h-full w-full items-center justify-center'}>
<CircularProgress /> <CircularProgress />
@ -64,9 +72,15 @@ export const Database = memo(() => {
return ( return (
<div className={'relative flex h-full w-full flex-col'}> <div className={'relative flex h-full w-full flex-col'}>
<DatabaseTitle viewId={objectId} /> <DatabaseHeader viewId={objectId} />
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'> <div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
<DatabaseContextProvider viewId={viewId || objectId} doc={doc} rowDocMap={rows} readOnly={true}> <DatabaseContextProvider
navigateToRow={navigateToRow}
viewId={viewId || objectId}
doc={doc}
rowDocMap={rows}
readOnly={true}
>
<DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} /> <DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} />
</DatabaseContextProvider> </DatabaseContextProvider>
</div> </div>

View File

@ -0,0 +1,79 @@
import { YDoc, YjsEditorKey } from '@/application/collab.type';
import { useId } from '@/components/_shared/context-provider/IdProvider';
import { AFConfigContext } from '@/components/app/AppConfig';
import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row';
import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader';
import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
import { Log } from '@/utils/log';
import { Divider } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
import * as Y from 'yjs';
function DatabaseRow({ rowId }: { rowId: string }) {
const { objectId, workspaceId } = useId() || {};
const [doc, setDoc] = useState<YDoc | null>(null);
const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
const [notFound, setNotFound] = useState<boolean>(false);
const handleOpenDatabaseRow = useCallback(async () => {
if (!databaseService || !workspaceId || !objectId) return;
try {
setDoc(null);
const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId, [rowId]);
console.log('database', databaseDoc.getMap(YjsEditorKey.data_section).toJSON());
console.log('row', rows.get(rowId)?.getMap(YjsEditorKey.data_section).toJSON());
const row = rows.get(rowId);
if (!row) {
setNotFound(true);
return;
}
setDoc(databaseDoc);
setRows(rows);
} catch (e) {
Log.error(e);
setNotFound(true);
}
}, [databaseService, workspaceId, objectId, rowId]);
useEffect(() => {
setNotFound(false);
void handleOpenDatabaseRow();
}, [handleOpenDatabaseRow]);
if (notFound || !objectId) {
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
}
if (!rows || !doc) {
return (
<div className={'flex h-full w-full items-center justify-center'}>
<CircularProgress />
</div>
);
}
return (
<div className={'flex w-full flex-col items-center'}>
<div className={'max-w-screen relative flex w-[964px] min-w-0 flex-col gap-4'}>
<DatabaseContextProvider viewId={objectId} doc={doc} rowDocMap={rows} readOnly={true}>
<DatabaseRowHeader rowId={rowId} />
<div className={'flex flex-1 flex-col gap-4'}>
<DatabaseRowProperties rowId={rowId} />
<Divider className={'mx-16 max-md:mx-4'} />
<DatabaseRowSubDocument rowId={rowId} />
</div>
</DatabaseContextProvider>
</div>
</div>
);
}
export default DatabaseRow;

View File

@ -6,7 +6,7 @@ function DatabaseTitle({ viewId }: { viewId: string }) {
return ( return (
<div className={'flex w-full flex-col py-4'}> <div className={'flex w-full flex-col py-4'}>
<div className={'flex w-full items-center px-24 max-md:px-4'}> <div className={'flex w-full items-center px-16 max-md:px-4'}>
<div className={'flex items-center gap-2 text-3xl'}> <div className={'flex items-center gap-2 text-3xl'}>
<div>{icon}</div> <div>{icon}</div>
<div className={'font-bold'}>{name}</div> <div className={'font-bold'}>{name}</div>

View File

@ -1,4 +1,3 @@
import { AFScroller } from '@/components/_shared/scroller';
import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks'; import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks';
import { Toolbar, Event } from '@/components/database/components/calendar'; import { Toolbar, Event } from '@/components/database/components/calendar';
import React from 'react'; import React from 'react';
@ -9,24 +8,25 @@ export function Calendar() {
const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup();
return ( return (
<AFScroller className={'appflowy-calendar'}> <div className={'appflowy-calendar h-full max-h-[960px] min-h-[560px] px-16 pt-4 max-md:px-4'}>
<div className={'h-full max-h-[960px] min-h-[560px] px-24 py-4 max-md:px-4'}> <BigCalendar
<BigCalendar components={{
components={{ toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />, eventWrapper: Event,
eventWrapper: Event, }}
}} style={{
events={events} marginBottom: '24px',
views={['month']} }}
localizer={localizer} events={events}
formats={formats} views={['month']}
dayPropGetter={dayPropGetter} localizer={localizer}
showMultiDayTimes={true} formats={formats}
step={1} dayPropGetter={dayPropGetter}
showAllEvents={true} showMultiDayTimes={true}
/> step={1}
</div> showAllEvents={true}
</AFScroller> />
</div>
); );
} }

View File

@ -12,6 +12,7 @@ $today-highlight-bg: transparent;
.rbc-date-cell { .rbc-date-cell {
min-width: 100px; min-width: 100px;
max-width: 180px;
} }
.rbc-date-cell.rbc-now { .rbc-date-cell.rbc-now {
@ -25,19 +26,38 @@ $today-highlight-bg: transparent;
.rbc-month-view { .rbc-month-view {
border: none; border: none;
@apply h-full overflow-auto;
.rbc-month-row { .rbc-month-row {
border: 1px solid var(--line-divider); border: 1px solid var(--line-divider);
border-bottom: none; border-top: none;
}
&:last-child { &::-webkit-scrollbar {
border-bottom: 1px solid var(--line-divider); width: 4px;
height: 4px;
}
&:hover {
&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb {
border-radius: 4px;
background-color: var(--scrollbar-thumb);
} }
} }
} }
.rbc-month-header { .rbc-month-header {
height: 40px; height: 40px;
position: sticky;
top: 0;
background: var(--bg-body);
z-index: 50;
@apply border-b border-line-divider;
.rbc-header { .rbc-header {
border: none; border: none;
@ -61,3 +81,9 @@ $today-highlight-bg: transparent;
flex: 0 0 0 !important; flex: 0 0 0 !important;
min-height: 120px !important; min-height: 120px !important;
} }
.event-properties {
.property-label {
@apply text-text-caption;
}
}

View File

@ -16,7 +16,7 @@ export const Group = ({ groupId }: GroupProps) => {
if (notFound) { if (notFound) {
return ( return (
<div className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-24 text-text-caption max-md:px-4'}> <div className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'}>
<div className={'text-sm font-medium'}>{t('board.noGroup')}</div> <div className={'text-sm font-medium'}>{t('board.noGroup')}</div>
<div className={'text-xs'}>{t('board.noGroupDesc')}</div> <div className={'text-xs'}>{t('board.noGroupDesc')}</div>
</div> </div>
@ -25,7 +25,7 @@ export const Group = ({ groupId }: GroupProps) => {
if (columns.length === 0 || !fieldId) return null; if (columns.length === 0 || !fieldId) return null;
return ( return (
<AFScroller overflowYHidden className={'relative px-24 max-md:px-4'}> <AFScroller overflowYHidden className={'relative px-16 max-md:px-4'}>
<Droppable droppableId={`group-${groupId}`} direction='horizontal' type='column'> <Droppable droppableId={`group-${groupId}`} direction='horizontal' type='column'>
{(provided) => { {(provided) => {
return ( return (

View File

@ -1,4 +1,4 @@
import { useFieldsSelector } from '@/application/database-yjs'; import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs';
import { Property } from '@/components/database/components/property'; import { Property } from '@/components/database/components/property';
import { IconButton } from '@mui/material'; import { IconButton } from '@mui/material';
import React from 'react'; import React from 'react';
@ -6,16 +6,22 @@ import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
function EventPaper({ rowId }: { rowId: string }) { function EventPaper({ rowId }: { rowId: string }) {
const fields = useFieldsSelector(); const fields = useFieldsSelector();
const navigateToRow = useNavigateToRow();
return ( return (
<div className={'max-h-[260px] w-[360px] overflow-y-auto'}> <div className={'max-h-[260px] w-[360px] overflow-y-auto'}>
<div className={'flex h-fit w-full flex-col items-center justify-center py-2 px-3'}> <div className={'flex h-fit w-full flex-col items-center justify-center py-2 px-3'}>
<div className={'flex w-full items-center justify-end'}> <div className={'flex w-full items-center justify-end'}>
<IconButton size={'small'}> <IconButton
onClick={() => {
navigateToRow?.(rowId);
}}
size={'small'}
>
<ExpandMoreIcon /> <ExpandMoreIcon />
</IconButton> </IconButton>
</div> </div>
<div className={'flex w-full flex-1 flex-col gap-4 overflow-y-auto py-2'}> <div className={'event-properties flex w-full flex-1 flex-col gap-4 overflow-y-auto py-2'}>
{fields.map((field) => { {fields.map((field) => {
return <Property fieldId={field.fieldId} rowId={rowId} key={field.fieldId} />; return <Property fieldId={field.fieldId} rowId={rowId} key={field.fieldId} />;
})} })}

View File

@ -1,33 +1,28 @@
import { YjsDatabaseKey } from '@/application/collab.type'; import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs';
import { useCellSelector, useDatabase } from '@/application/database-yjs'; import { Cell } from '@/components/database/components/cell';
import React, { useEffect } from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Cell from 'src/components/database/components/cell/Cell';
function NoDateRow({ rowId }: { rowId: string }) { function NoDateRow({ rowId }: { rowId: string }) {
const database = useDatabase(); const navigateToRow = useNavigateToRow();
const [primaryFieldId, setPrimaryFieldId] = React.useState<string | null>(null); const primaryFieldId = usePrimaryFieldId();
const cell = useCellSelector({ const cell = useCellSelector({
rowId, rowId,
fieldId: primaryFieldId || '', fieldId: primaryFieldId || '',
}); });
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
const fields = database?.get(YjsDatabaseKey.fields);
const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => {
return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary);
});
setPrimaryFieldId(primaryFieldId || null);
}, [database]);
if (!primaryFieldId || !cell?.data) { if (!primaryFieldId || !cell?.data) {
return <div className={'text-xs text-text-caption'}>{t('grid.row.titlePlaceholder')}</div>; return <div className={'text-xs text-text-caption'}>{t('grid.row.titlePlaceholder')}</div>;
} }
return ( return (
<div className={'w-full hover:text-fill-default'}> <div
onClick={() => {
navigateToRow?.(rowId);
}}
className={'w-full hover:text-fill-default'}
>
<Cell <Cell
style={{ style={{
cursor: 'pointer', cursor: 'pointer',

View File

@ -17,6 +17,7 @@ export function Cell(props: CellProps<CellType>) {
const { cell, rowId, fieldId, style } = props; const { cell, rowId, fieldId, style } = props;
const { field } = useFieldSelector(fieldId); const { field } = useFieldSelector(fieldId);
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
const Component = useMemo(() => { const Component = useMemo(() => {
switch (fieldType) { switch (fieldType) {
case FieldType.RichText: case FieldType.RichText:

View File

@ -3,7 +3,7 @@ import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/data
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) { export function ChecklistCell({ cell, style, placeholder }: CellProps<ChecklistCellType>) {
const data = useMemo(() => { const data = useMemo(() => {
return parseChecklistData(cell?.data ?? ''); return parseChecklistData(cell?.data ?? '');
}, [cell?.data]); }, [cell?.data]);
@ -11,7 +11,12 @@ export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) {
const options = data?.options; const options = data?.options;
const selectedOptions = data?.selectedOptionIds; const selectedOptions = data?.selectedOptionIds;
if (!data || !options || !selectedOptions) return null; if (!data || !options || !selectedOptions)
return placeholder ? (
<div style={style} className={'text-text-placeholder'}>
{placeholder}
</div>
) : null;
return ( return (
<div style={style} className={'w-full cursor-pointer'}> <div style={style} className={'w-full cursor-pointer'}>
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} /> <LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />

View File

@ -1,5 +1,5 @@
import { YjsDatabaseKey } from '@/application/collab.type'; import { YjsDatabaseKey } from '@/application/collab.type';
import { useRowMeta } from '@/application/database-yjs'; import { useRowData } from '@/application/database-yjs';
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
@ -15,24 +15,24 @@ export function RowCreateModifiedTime({
attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at;
}) { }) {
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
const rowMeta = useRowMeta(rowId); const rowData = useRowData(rowId);
const [value, setValue] = useState<string | null>(null); const [value, setValue] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (!rowMeta) return; if (!rowData) return;
const observeHandler = () => { const observeHandler = () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error // @ts-expect-error
setValue(rowMeta.get(attrName)); setValue(rowData.get(attrName));
}; };
observeHandler(); observeHandler();
rowMeta.observe(observeHandler); rowData.observe(observeHandler);
return () => { return () => {
rowMeta.unobserve(observeHandler); rowData.unobserve(observeHandler);
}; };
}, [rowMeta, attrName]); }, [rowData, attrName]);
const time = useMemo(() => { const time = useMemo(() => {
if (!value) return null; if (!value) return null;
@ -40,7 +40,11 @@ export function RowCreateModifiedTime({
}, [value, getDateTimeStr]); }, [value, getDateTimeStr]);
if (!time) return null; if (!time) return null;
return <div style={style}>{time}</div>; return (
<div style={style} className={'flex w-full items-center'}>
{time}
</div>
);
} }
export default RowCreateModifiedTime; export default RowCreateModifiedTime;

View File

@ -0,0 +1,46 @@
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs';
import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type';
import { TextCell } from '@/components/database/components/cell/text';
import { Tooltip } from '@mui/material';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
export function PrimaryCell(props: CellProps<CellType>) {
const navigateToRow = useNavigateToRow();
const { rowId } = props;
// const icon = null;
const icon = useRowMetaSelector(rowId)?.icon;
const [hover, setHover] = useState(false);
const { t } = useTranslation();
return (
<div
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
className={'primary-cell relative flex w-full items-center gap-2'}
>
{icon && <div className={'h-4 w-4'}>{icon}</div>}
<TextCell {...props} />
{hover && (
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
<button
color={'primary'}
className={
'absolute right-0 top-1/2 min-w-0 -translate-y-1/2 transform rounded border border-line-divider bg-bg-body p-1 hover:bg-fill-list-hover'
}
onClick={() => {
navigateToRow?.(rowId);
}}
>
<ExpandMoreIcon />
</button>
</Tooltip>
)}
</div>
);
}
export default PrimaryCell;

View File

@ -0,0 +1 @@
export * from './PrimaryCell';

View File

@ -18,7 +18,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
useEffect(() => { useEffect(() => {
if (!workspaceId || !databaseId) return; if (!workspaceId || !databaseId) return;
void databaseService?.getDatabase(workspaceId, databaseId).then(({ databaseDoc: doc, rows }) => { void databaseService?.getDatabase(workspaceId, databaseId, rowIds).then(({ databaseDoc: doc, rows }) => {
const fields = doc const fields = doc
.getMap(YjsEditorKey.data_section) .getMap(YjsEditorKey.data_section)
.get(YjsEditorKey.database) .get(YjsEditorKey.database)
@ -32,7 +32,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
setRows(rows); setRows(rows);
}); });
}, [workspaceId, databaseId, databaseService]); }, [workspaceId, databaseId, databaseService, rowIds]);
return ( return (
<div style={style} className={'flex items-center gap-2'}> <div style={style} className={'flex items-center gap-2'}>

View File

@ -5,7 +5,7 @@ import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/component
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps<SelectOptionCellType>) { export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps<SelectOptionCellType>) {
const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]); const selectOptionIds = useMemo(() => (!cell?.data ? [] : cell?.data.split(',')), [cell]);
const { field } = useFieldSelector(fieldId); const { field } = useFieldSelector(fieldId);
const typeOption = useMemo(() => { const typeOption = useMemo(() => {
if (!field) return null; if (!field) return null;

View File

@ -9,7 +9,7 @@ export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]);
const className = useMemo(() => { const className = useMemo(() => {
const classList = ['select-text', 'w-fit']; const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center'];
if (isUrl) { if (isUrl) {
classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); classList.push('text-content-blue-400', 'underline', 'cursor-pointer');

View File

@ -18,7 +18,7 @@ export function DatabaseConditions() {
borderTopWidth: expanded ? '1px' : '0', borderTopWidth: expanded ? '1px' : '0',
}} }}
className={ className={
'database-conditions relative mx-24 transform overflow-hidden border-t border-line-divider transition-all max-md:mx-4' 'database-conditions relative mx-16 transform overflow-hidden border-t border-line-divider transition-all max-md:mx-4'
} }
> >
<AFScroller overflowYHidden className={'flex items-center gap-2'}> <AFScroller overflowYHidden className={'flex items-center gap-2'}>

View File

@ -0,0 +1,18 @@
import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs';
import { Property } from '@/components/database/components/property';
import React from 'react';
export function DatabaseRowProperties({ rowId }: { rowId: string }) {
const primaryFieldId = usePrimaryFieldId();
const fields = useFieldsSelector().filter((column) => column.fieldId !== primaryFieldId);
return (
<div className={'row-properties flex w-full flex-1 flex-col gap-4 px-16 py-2 max-md:px-4'}>
{fields.map((field) => {
return <Property fieldId={field.fieldId} rowId={rowId} key={field.fieldId} />;
})}
</div>
);
}
export default DatabaseRowProperties;

View File

@ -0,0 +1,53 @@
import { YDoc } from '@/application/collab.type';
import { useRowMetaSelector } from '@/application/database-yjs';
import { useId } from '@/components/_shared/context-provider/IdProvider';
import { AFConfigContext } from '@/components/app/AppConfig';
import { Editor } from '@/components/editor';
import { Log } from '@/utils/log';
import CircularProgress from '@mui/material/CircularProgress';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
const { workspaceId } = useId() || {};
const documentId = useRowMetaSelector(rowId)?.documentId;
const [doc, setDoc] = useState<YDoc | null>(null);
const [notFound, setNotFound] = useState<boolean>(false);
const documentService = useContext(AFConfigContext)?.service?.documentService;
const handleOpenDocument = useCallback(async () => {
if (!documentService || !workspaceId || !documentId) return;
try {
setDoc(null);
const doc = await documentService.openDocument(workspaceId, documentId);
setDoc(doc);
} catch (e) {
Log.error(e);
setNotFound(true);
}
}, [documentService, workspaceId, documentId]);
useEffect(() => {
setNotFound(false);
void handleOpenDocument();
}, [handleOpenDocument]);
if (notFound || !documentId) {
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
}
if (!doc) {
return (
<div className={'flex h-full w-full items-center justify-center'}>
<CircularProgress />
</div>
);
}
return <Editor doc={doc} readOnly={true} includeRoot={false} />;
}
export default DatabaseRowSubDocument;

View File

@ -0,0 +1,2 @@
export * from './DatabaseRowProperties';
export * from './DatabaseRowSubDocument';

View File

@ -1,8 +1,10 @@
import { FieldId } from '@/application/collab.type'; import { FieldId, YjsDatabaseKey } from '@/application/collab.type';
import { useCellSelector } from '@/application/database-yjs'; import { useCellSelector } from '@/application/database-yjs';
import { useFieldSelector } from '@/application/database-yjs/selector'; import { useFieldSelector } from '@/application/database-yjs/selector';
import { Cell } from '@/components/database/components/cell'; import { Cell } from '@/components/database/components/cell';
import React, { useEffect } from 'react'; import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type';
import { PrimaryCell } from '@/components/database/components/cell/primary';
import React, { useEffect, useMemo, useRef } from 'react';
export interface GridCellProps { export interface GridCellProps {
rowId: string; rowId: string;
@ -13,8 +15,9 @@ export interface GridCellProps {
} }
export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) {
const ref = React.useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const field = useFieldSelector(fieldId); const { field } = useFieldSelector(fieldId);
const isPrimary = field?.get(YjsDatabaseKey.is_primary);
const cell = useCellSelector({ const cell = useCellSelector({
rowId, rowId,
fieldId, fieldId,
@ -23,15 +26,13 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr
useEffect(() => { useEffect(() => {
const el = ref.current; const el = ref.current;
if (!el) return; if (!el || !cell) return;
const observer = new ResizeObserver(() => { const observer = new ResizeObserver(() => {
if (onResize) { onResize?.(rowIndex, columnIndex, {
onResize(rowIndex, columnIndex, { width: el.offsetWidth,
width: el.offsetWidth, height: el.offsetHeight,
height: el.offsetHeight, });
});
}
}); });
observer.observe(el); observer.observe(el);
@ -39,12 +40,21 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr
return () => { return () => {
observer.disconnect(); observer.disconnect();
}; };
}, [columnIndex, onResize, rowIndex]); }, [columnIndex, onResize, rowIndex, cell]);
const Component = useMemo(() => {
if (isPrimary) {
return PrimaryCell;
}
return Cell;
}, [isPrimary]) as React.FC<CellProps<CellType>>;
if (!field) return null; if (!field) return null;
return ( return (
<div ref={ref} className={'grid-cell w-full cursor-text overflow-hidden text-xs'}> <div ref={ref} className={'grid-cell flex min-h-full w-full cursor-text items-center overflow-hidden text-xs'}>
<Cell cell={cell} rowId={rowId} fieldId={fieldId} /> <Component cell={cell} rowId={rowId} fieldId={fieldId} />
</div> </div>
); );
} }

View File

@ -20,7 +20,6 @@ export type RenderColumn = {
export function useRenderFields() { export function useRenderFields() {
const fields = useFieldsSelector(); const fields = useFieldsSelector();
console.log('columns', fields);
const renderColumns = useMemo(() => { const renderColumns = useMemo(() => {
const data = fields.map((column) => ({ const data = fields.map((column) => ({
...column, ...column,
@ -30,7 +29,7 @@ export function useRenderFields() {
return [ return [
{ {
type: GridColumnType.Action, type: GridColumnType.Action,
width: 96, width: 64,
}, },
...data, ...data,
{ {
@ -39,7 +38,7 @@ export function useRenderFields() {
}, },
{ {
type: GridColumnType.Action, type: GridColumnType.Action,
width: 96, width: 64,
}, },
].filter(Boolean) as RenderColumn[]; ].filter(Boolean) as RenderColumn[];
}, [fields]); }, [fields]);

View File

@ -0,0 +1,11 @@
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
import Title from './Title';
import React from 'react';
export function DatabaseHeader({ viewId }: { viewId: string }) {
const { name, icon } = usePageInfo(viewId);
return <Title name={name} icon={icon} />;
}
export default DatabaseHeader;

View File

@ -0,0 +1,16 @@
import { useCellSelector, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs';
import Title from '@/components/database/components/header/Title';
import React from 'react';
function DatabaseRowHeader({ rowId }: { rowId: string }) {
const fieldId = usePrimaryFieldId() || '';
const meta = useRowMetaSelector(rowId);
const cell = useCellSelector({
rowId,
fieldId,
});
return <Title icon={meta?.icon} name={cell?.data as string} />;
}
export default DatabaseRowHeader;

View File

@ -0,0 +1,19 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
export function Title({ icon, name }: { icon?: string; name?: string }) {
const { t } = useTranslation();
return (
<div className={'flex w-full flex-col py-4'}>
<div className={'flex w-full items-center px-16 max-md:px-4'}>
<div className={'flex items-center gap-2 text-3xl'}>
<div>{icon}</div>
<div className={'font-bold'}>{name || t('document.title.placeholder')}</div>
</div>
</div>
</div>
);
}
export default Title;

View File

@ -0,0 +1,2 @@
export * from './DatabaseHeader';
export * from './DatabaseRowHeader';

View File

@ -2,7 +2,6 @@ import { YjsDatabaseKey } from '@/application/collab.type';
import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs'; import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs';
import { Cell as CellType, CellProps, TextCell } from '@/components/database/components/cell/cell.type'; import { Cell as CellType, CellProps, TextCell } from '@/components/database/components/cell/cell.type';
import { CheckboxCell } from '@/components/database/components/cell/checkbox'; import { CheckboxCell } from '@/components/database/components/cell/checkbox';
import { ChecklistCell } from '@/components/database/components/cell/checklist';
import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified';
import { DateTimeCell } from '@/components/database/components/cell/date'; import { DateTimeCell } from '@/components/database/components/cell/date';
import { NumberCell } from '@/components/database/components/cell/number'; import { NumberCell } from '@/components/database/components/cell/number';
@ -11,6 +10,7 @@ import { SelectOptionCell } from '@/components/database/components/cell/select-o
import { UrlCell } from '@/components/database/components/cell/url'; import { UrlCell } from '@/components/database/components/cell/url';
import PropertyWrapper from '@/components/database/components/property/PropertyWrapper'; import PropertyWrapper from '@/components/database/components/property/PropertyWrapper';
import { TextProperty } from '@/components/database/components/property/text'; import { TextProperty } from '@/components/database/components/property/text';
import { ChecklistProperty } from 'src/components/database/components/property/cheklist';
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -39,7 +39,7 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string })
case FieldType.DateTime: case FieldType.DateTime:
return DateTimeCell; return DateTimeCell;
case FieldType.Checklist: case FieldType.Checklist:
return ChecklistCell; return ChecklistProperty;
case FieldType.Relation: case FieldType.Relation:
return RelationCell; return RelationCell;
default: default:

View File

@ -3,8 +3,8 @@ import React from 'react';
function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) { function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) {
return ( return (
<div className={'flex w-full items-center gap-2'}> <div className={'flex min-h-[28px] w-full gap-2'}>
<div className={'w-[100px] text-text-caption'}> <div className={'property-label flex h-[28px] w-[30%] items-center'}>
<FieldDisplay fieldId={fieldId} /> <FieldDisplay fieldId={fieldId} />
</div> </div>
<div className={'flex flex-1 flex-wrap pr-1'}>{children}</div> <div className={'flex flex-1 flex-wrap pr-1'}>{children}</div>

View File

@ -0,0 +1,34 @@
import { parseChecklistData } from '@/application/database-yjs';
import { CellProps, ChecklistCell as CellType } from '@/components/database/components/cell/cell.type';
import { ChecklistCell } from '@/components/database/components/cell/checklist';
import React, { useMemo } from 'react';
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
export function ChecklistProperty(props: CellProps<CellType>) {
const { cell } = props;
const data = useMemo(() => {
return parseChecklistData(cell?.data ?? '');
}, [cell?.data]);
const options = data?.options;
const selectedOptions = data?.selectedOptionIds;
return (
<div className={'flex w-full flex-col gap-2'}>
<ChecklistCell {...props} />
{options?.map((option) => {
const isSelected = selectedOptions?.includes(option.id);
return (
<div key={option.id} className={'flex items-center gap-2 text-xs font-medium'}>
{isSelected ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />}
<div>{option.name}</div>
</div>
);
})}
</div>
);
}
export default ChecklistProperty;

View File

@ -0,0 +1 @@
export * from './ChecklistProperty';

View File

@ -54,7 +54,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
return ( return (
<div <div
ref={ref} ref={ref}
className='mx-24 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4' className='mx-16 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4'
> >
<div <div
style={{ style={{

View File

@ -1,3 +1,4 @@
import { lazy } from 'react'; import { lazy } from 'react';
export const Database = lazy(() => import('./Database')); export const Database = lazy(() => import('./Database'));
export const DatabaseRow = lazy(() => import('./DatabaseRow'));

View File

@ -41,7 +41,7 @@ export const Document = () => {
<DocumentHeader doc={doc} viewId={documentId} /> <DocumentHeader doc={doc} viewId={documentId} />
<div className={'flex w-full justify-center'}> <div className={'flex w-full justify-center'}>
<div className={'max-w-screen w-[964px] min-w-0'}> <div className={'max-w-screen w-[964px] min-w-0'}>
<Editor doc={doc} readOnly={true} /> <Editor doc={doc} readOnly={true} includeRoot={true} />
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,18 +13,27 @@ import * as Y from 'yjs';
const defaultInitialValue: Descendant[] = []; const defaultInitialValue: Descendant[] = [];
function CollaborativeEditor({ doc }: { doc: Y.Doc }) { function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeRoot?: boolean }) {
const context = useEditorContext();
// if readOnly, collabOrigin is Local, otherwise RemoteSync
const collabOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync;
const editor = useMemo(
() => doc && (withPlugins(withReact(withYjs(createEditor(), doc, collabOrigin))) as YjsEditor),
[doc, collabOrigin]
);
const [connected, setIsConnected] = useState(false);
const viewId = useId()?.objectId || ''; const viewId = useId()?.objectId || '';
const { view } = useViewSelector(viewId); const { view } = useViewSelector(viewId);
const title = view?.get(YjsFolderKey.name); const title = includeRoot ? view?.get(YjsFolderKey.name) : undefined;
const context = useEditorContext();
// if readOnly, collabOrigin is Local, otherwise RemoteSync
const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync;
const editor = useMemo(
() =>
doc &&
(withPlugins(
withReact(
withYjs(createEditor(), doc, {
localOrigin,
includeRoot,
})
)
) as YjsEditor),
[doc, localOrigin, includeRoot]
);
const [connected, setIsConnected] = useState(false);
useEffect(() => { useEffect(() => {
if (!editor) return; if (!editor) return;
@ -37,8 +46,8 @@ function CollaborativeEditor({ doc }: { doc: Y.Doc }) {
}, [editor]); }, [editor]);
useEffect(() => { useEffect(() => {
if (!editor || !connected) return; if (!editor || !connected || title === undefined) return;
CustomEditor.setDocumentTitle(editor, title || ''); CustomEditor.setDocumentTitle(editor, title);
}, [editor, title, connected]); }, [editor, title, connected]);
return ( return (

View File

@ -4,10 +4,18 @@ import { EditorContextProvider } from '@/components/editor/EditorContext';
import React from 'react'; import React from 'react';
import './editor.scss'; import './editor.scss';
export const Editor = ({ readOnly, doc }: { readOnly: boolean; doc: YDoc }) => { export const Editor = ({
readOnly,
doc,
includeRoot = true,
}: {
readOnly: boolean;
doc: YDoc;
includeRoot?: boolean;
}) => {
return ( return (
<EditorContextProvider readOnly={readOnly}> <EditorContextProvider readOnly={readOnly}>
<CollaborativeEditor doc={doc} /> <CollaborativeEditor doc={doc} includeRoot={includeRoot} />
</EditorContextProvider> </EditorContextProvider>
); );
}; };

View File

@ -10,7 +10,10 @@ export const Callout = memo(
<CalloutIcon node={node} /> <CalloutIcon node={node} />
</div> </div>
<div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}> <div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}>
<div {...attributes} className={`flex w-full flex-col rounded bg-content-blue-50 py-2 pl-10`}> <div
{...attributes}
className={`flex w-full flex-col rounded border border-line-divider bg-fill-list-active py-2 pl-10`}
>
{children} {children}
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@ function CalloutIcon({ node }: { node: CalloutNode }) {
return ( return (
<> <>
<span contentEditable={false} ref={ref} className={`h-8 w-8 p-1`}> <span contentEditable={false} ref={ref} className={`flex h-8 w-8 items-center p-1`}>
{node.data.icon} {node.data.icon}
</span> </span>
</> </>

View File

@ -15,7 +15,7 @@ export const CodeBlock = memo(
<div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}> <div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}>
<pre <pre
spellCheck={false} spellCheck={false}
className={`flex w-full rounded border border-solid border-line-divider bg-content-blue-50 p-5 pt-20`} className={`flex w-full rounded border border-line-divider bg-fill-list-active p-5 pt-20`}
> >
<code>{children}</code> <code>{children}</code>
</pre> </pre>

View File

@ -20,7 +20,7 @@ export const MathEquation = memo(
> >
<div <div
contentEditable={false} contentEditable={false}
className={`container-bg w-full select-none rounded border border-line-divider bg-content-blue-50 px-3`} className={`container-bg w-full select-none rounded border border-line-divider bg-fill-list-active px-3`}
> >
{formula ? ( {formula ? (
<KatexMath latex={formula} /> <KatexMath latex={formula} />

View File

@ -197,12 +197,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
.bulleted-icon { .bulleted-icon {
&:after { &:after {
content: attr(data-letter); content: attr(data-letter);
font-weight: 500;
} }
} }
.numbered-icon { .numbered-icon {
&:after { &:after {
content: attr(data-number) "."; content: attr(data-number) ".";
font-weight: 500;
} }
} }
@ -238,7 +240,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
&:hover { &:hover {
.container-bg { .container-bg {
background: var(--content-blue-100) !important; background: var(--fill-list-hover) !important;
} }
} }
} }
@ -271,3 +273,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
} }
} }
.text-block-icon {
@apply flex items-center justify-center;
}

View File

@ -1,7 +1,7 @@
import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type'; import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type';
import React from 'react'; import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import Page from 'src/components/_shared/page/Page'; import Page from '@/components/_shared/page/Page';
function ViewItem({ id }: { id: string }) { function ViewItem({ id }: { id: string }) {
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -19,6 +19,7 @@ function Layout({ children }: { children: React.ReactNode }) {
if (!folder) return; if (!folder) return;
console.log(folder.toJSON());
setFolder(folder); setFolder(folder);
}, },
[folderService] [folderService]

View File

@ -45,6 +45,7 @@
opacity: 60%; opacity: 60%;
} }
.workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database { .workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database {
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0; width: 0;
@ -58,9 +59,6 @@
font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols;
line-height: 1em; line-height: 1em;
white-space: nowrap; white-space: nowrap;
//&:hover {
// background-color: rgba(156, 156, 156, 0.20);
//}
} }
.theme-mode-item { .theme-mode-item {

View File

@ -1,10 +1,16 @@
import { Database } from '@/components/database'; import { Database, DatabaseRow } from '@/components/database';
import React from 'react'; import React from 'react';
import { useSearchParams } from 'react-router-dom';
function DatabasePage () { function DatabasePage() {
return ( const [search] = useSearchParams();
<Database /> const rowId = search.get('r');
);
if (rowId) {
return <DatabaseRow rowId={rowId} />;
}
return <Database />;
} }
export default DatabasePage; export default DatabasePage;

View File

@ -31,21 +31,6 @@ textarea {
} }
::-webkit-scrollbar {
width: 8px;
}
:root[data-dark-mode=true] body {
scrollbar-color: #fff var(--bg-body);
}
body {
scrollbar-track-color: var(--bg-body);
scrollbar-shadow-color: var(--bg-body);
}
.btn { .btn {
@apply rounded-xl border border-line-divider px-4 py-3; @apply rounded-xl border border-line-divider px-4 py-3;
} }

View File

@ -71,6 +71,9 @@ export default defineConfig({
cors: false, cors: false,
}, },
envPrefix: ['AF', 'TAURI_'], envPrefix: ['AF', 'TAURI_'],
esbuild: {
drop: ['console', 'debugger'],
},
build: !!process.env.TAURI_PLATFORM build: !!process.env.TAURI_PLATFORM
? { ? {
// Tauri supports es2021 // Tauri supports es2021
@ -82,15 +85,6 @@ export default defineConfig({
} }
: { : {
target: `esnext`, target: `esnext`,
terserOptions: !isDev
? {
compress: {
keep_infinity: true,
drop_console: true,
drop_debugger: true,
},
}
: {},
reportCompressedSize: true, reportCompressedSize: true,
sourcemap: isDev, sourcemap: isDev,
rollupOptions: !isDev rollupOptions: !isDev