mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support open row page (#5400)
This commit is contained in:
parent
9a5dbbb3ce
commit
a0139dd475
@ -1,5 +1,7 @@
|
||||
import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||
import { RowMetaKey } from '@/application/database-yjs/database.type';
|
||||
import * as Y from 'yjs';
|
||||
import { v5 as uuidv5, parse as uuidParse } from 'uuid';
|
||||
|
||||
export const DEFAULT_ROW_HEIGHT = 37;
|
||||
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>) => {
|
||||
return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data);
|
||||
};
|
||||
|
||||
export const metaIdFromRowId = (rowId: string) => {
|
||||
const namespace = uuidParse(rowId);
|
||||
|
||||
return (key: RowMetaKey) => uuidv5(key, namespace).toString();
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ export interface DatabaseContextState {
|
||||
doc: YDoc;
|
||||
viewId: string;
|
||||
rowDocMap: Y.Map<YDoc>;
|
||||
navigateToRow?: (rowId: string) => void;
|
||||
}
|
||||
|
||||
export const DatabaseContext = createContext<DatabaseContextState | null>(null);
|
||||
@ -20,12 +21,18 @@ export const useDatabase = () => {
|
||||
return database;
|
||||
};
|
||||
|
||||
export const useRowMeta = (rowId: string) => {
|
||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
||||
const rowMetaDoc = rows?.get(rowId);
|
||||
const rowMeta = rowMetaDoc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
||||
export const useNavigateToRow = () => {
|
||||
return useContext(DatabaseContext)?.navigateToRow;
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
|
@ -63,3 +63,10 @@ export interface CalendarLayoutSetting {
|
||||
showWeekends: boolean;
|
||||
layout: CalendarLayout;
|
||||
}
|
||||
|
||||
export enum RowMetaKey {
|
||||
DocumentId = 'document_id',
|
||||
IconId = 'icon_id',
|
||||
CoverId = 'cover_id',
|
||||
IsDocumentEmpty = 'is_document_empty',
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsFolderKey } from '@/application/collab.type';
|
||||
import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsEditorKey, YjsFolderKey } from '@/application/collab.type';
|
||||
import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||
import {
|
||||
DatabaseContext,
|
||||
useDatabase,
|
||||
useDatabaseFields,
|
||||
useDatabaseView,
|
||||
useRowMeta,
|
||||
useRow,
|
||||
useRowData,
|
||||
useRows,
|
||||
useViewId,
|
||||
} 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 dayjs from 'dayjs';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, SortCondition } from './database.type';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
|
||||
|
||||
export interface Column {
|
||||
fieldId: string;
|
||||
@ -473,7 +474,7 @@ export function useRowOrdersSelector() {
|
||||
}
|
||||
|
||||
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 [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined));
|
||||
|
||||
@ -585,3 +586,59 @@ export function useCalendarLayoutSetting() {
|
||||
|
||||
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;
|
||||
};
|
||||
|
@ -62,6 +62,7 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
return data ? data : '\uFFFF';
|
||||
case FieldType.Number:
|
||||
return data;
|
||||
case FieldType.Checkbox:
|
||||
|
@ -17,7 +17,8 @@ export class JSDatabaseService implements DatabaseService {
|
||||
|
||||
async getDatabase(
|
||||
workspaceId: string,
|
||||
databaseId: string
|
||||
databaseId: string,
|
||||
rowIds?: string[]
|
||||
): Promise<{
|
||||
databaseDoc: 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 viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString();
|
||||
const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders);
|
||||
const rowIds = rowOrders.toJSON() as {
|
||||
const rowOrdersIds = rowOrders.toJSON() as {
|
||||
id: string;
|
||||
}[];
|
||||
|
||||
if (!rowIds) {
|
||||
if (!rowOrdersIds) {
|
||||
throw new Error('Database rows not found');
|
||||
}
|
||||
|
||||
if (isLoaded) {
|
||||
for (const row of rowIds) {
|
||||
const { doc } = await getCollabStorage(row.id, CollabType.DatabaseRow);
|
||||
const ids = rowIds ? rowIds : rowOrdersIds.map((item) => item.id);
|
||||
|
||||
rowsFolder.set(row.id, doc);
|
||||
if (isLoaded) {
|
||||
for (const id of ids) {
|
||||
const { doc } = await getCollabStorage(id, CollabType.DatabaseRow);
|
||||
|
||||
rowsFolder.set(id, doc);
|
||||
}
|
||||
} else {
|
||||
const rows = await this.loadDatabaseRows(
|
||||
workspaceId,
|
||||
rowIds.map((item) => item.id)
|
||||
);
|
||||
const rows = await this.loadDatabaseRows(workspaceId, ids);
|
||||
|
||||
rows.forEach((row, id) => {
|
||||
rowsFolder.set(id, row);
|
||||
@ -63,6 +63,27 @@ export class JSDatabaseService implements DatabaseService {
|
||||
|
||||
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 {
|
||||
databaseDoc,
|
||||
rows: rowsFolder as Y.Map<YDoc>,
|
||||
@ -71,7 +92,8 @@ export class JSDatabaseService implements DatabaseService {
|
||||
|
||||
async openDatabase(
|
||||
workspaceId: string,
|
||||
viewId: string
|
||||
viewId: string,
|
||||
rowIds?: string[]
|
||||
): Promise<{
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
@ -112,28 +134,8 @@ export class JSDatabaseService implements DatabaseService {
|
||||
throw new Error('Database not found');
|
||||
}
|
||||
|
||||
const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id);
|
||||
const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase;
|
||||
const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders);
|
||||
const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id, 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) => {
|
||||
rows.set(id, row);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => {
|
||||
if (origin === CollabOrigin.LocalSync) {
|
||||
// Send the update to the server
|
||||
|
@ -37,14 +37,16 @@ export interface DocumentService {
|
||||
export interface DatabaseService {
|
||||
openDatabase: (
|
||||
workspaceId: string,
|
||||
viewId: string
|
||||
viewId: string,
|
||||
rowIds?: string[]
|
||||
) => Promise<{
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
}>;
|
||||
getDatabase: (
|
||||
workspaceId: string,
|
||||
databaseId: string
|
||||
databaseId: string,
|
||||
rowIds?: string[]
|
||||
) => Promise<{
|
||||
databaseDoc: YDoc;
|
||||
rows: Y.Map<YDoc>;
|
||||
|
@ -57,15 +57,36 @@ export const YjsEditor = {
|
||||
export function withYjs<T extends Editor>(
|
||||
editor: T,
|
||||
doc: Y.Doc,
|
||||
localOrigin: CollabOrigin = CollabOrigin.Local
|
||||
{
|
||||
localOrigin,
|
||||
includeRoot = true,
|
||||
}: {
|
||||
localOrigin: CollabOrigin;
|
||||
includeRoot?: boolean;
|
||||
}
|
||||
): T & YjsEditor {
|
||||
const e = editor as T & YjsEditor;
|
||||
const { apply, onChange } = e;
|
||||
|
||||
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) => {
|
||||
YjsEditor.flushLocalChanges(e);
|
||||
|
||||
// TODO: handle remote events
|
||||
// This is a temporary implementation to apply remote events to slate
|
||||
initializeDocumentContent();
|
||||
Editor.withoutNormalizing(editor, () => {
|
||||
events.forEach((event) => {
|
||||
translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => {
|
||||
@ -87,17 +108,8 @@ export function withYjs<T extends Editor>(
|
||||
throw new Error('Already connected');
|
||||
}
|
||||
|
||||
const content = yDocToSlateContent(doc, true);
|
||||
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(content);
|
||||
|
||||
initializeDocumentContent();
|
||||
e.sharedRoot.observeDeep(handleYEvents);
|
||||
e.children = content.children;
|
||||
Editor.normalize(editor, { force: true });
|
||||
connectSet.add(e);
|
||||
};
|
||||
|
||||
|
@ -3,10 +3,9 @@ import * as Y from 'yjs';
|
||||
import { Editor, Operation } from 'slate';
|
||||
|
||||
export function translateYArrayEvent(
|
||||
sharedRoot: YSharedRoot,
|
||||
editor: Editor,
|
||||
event: Y.YEvent<Y.Array<string>>
|
||||
_sharedRoot: YSharedRoot,
|
||||
_editor: Editor,
|
||||
_event: Y.YEvent<Y.Array<string>>
|
||||
): Operation[] {
|
||||
console.log('translateYArrayEvent', sharedRoot, editor, event);
|
||||
return [];
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import { translateYTextEvent } from 'src/application/slate-yjs/utils/translateYj
|
||||
* @param op
|
||||
*/
|
||||
export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent<YSharedRoot>): Operation[] {
|
||||
console.log('translateYjsEvent', event);
|
||||
if (event instanceof Y.YMapEvent) {
|
||||
return translateYMapEvent(sharedRoot, editor, event);
|
||||
}
|
||||
|
@ -3,10 +3,9 @@ import * as Y from 'yjs';
|
||||
import { Editor, Operation } from 'slate';
|
||||
|
||||
export function translateYMapEvent(
|
||||
sharedRoot: YSharedRoot,
|
||||
editor: Editor,
|
||||
event: Y.YEvent<Y.Map<unknown>>
|
||||
_sharedRoot: YSharedRoot,
|
||||
_editor: Editor,
|
||||
_event: Y.YEvent<Y.Map<unknown>>
|
||||
): Operation[] {
|
||||
console.log('translateYMapEvent', sharedRoot, editor, event);
|
||||
return [];
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import { YSharedRoot } from '@/application/collab.type';
|
||||
import * as Y from 'yjs';
|
||||
import { Editor, Operation } from 'slate';
|
||||
|
||||
export function translateYTextEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent<Y.Text>): Operation[] {
|
||||
console.log('translateYTextEvent', sharedRoot, editor, event);
|
||||
export function translateYTextEvent(_sharedRoot: YSharedRoot, _editor: Editor, _event: Y.YEvent<Y.Text>): Operation[] {
|
||||
return [];
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ 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 { DatabaseHeader } from '@/components/database/components/header';
|
||||
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, useState } from 'react';
|
||||
@ -14,13 +14,14 @@ import * as Y from 'yjs';
|
||||
export const Database = memo(() => {
|
||||
const { objectId, workspaceId } = useId() || {};
|
||||
const [search, setSearch] = useSearchParams();
|
||||
|
||||
const viewId = search.get('v');
|
||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||
const [rows, setRows] = useState<Y.Map<YDoc> | null>(null); // Map<rowId, YDoc
|
||||
const [notFound, setNotFound] = useState<boolean>(false);
|
||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||
|
||||
const handleOpenDocument = useCallback(async () => {
|
||||
const handleOpenDatabase = useCallback(async () => {
|
||||
if (!databaseService || !workspaceId || !objectId) return;
|
||||
|
||||
try {
|
||||
@ -28,6 +29,8 @@ export const Database = memo(() => {
|
||||
const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId);
|
||||
|
||||
console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON());
|
||||
console.log('rows', rows);
|
||||
|
||||
setDoc(databaseDoc);
|
||||
setRows(rows);
|
||||
} catch (e) {
|
||||
@ -38,8 +41,8 @@ export const Database = memo(() => {
|
||||
|
||||
useEffect(() => {
|
||||
setNotFound(false);
|
||||
void handleOpenDocument();
|
||||
}, [handleOpenDocument]);
|
||||
void handleOpenDatabase();
|
||||
}, [handleOpenDatabase]);
|
||||
|
||||
const handleChangeView = useCallback(
|
||||
(viewId: string) => {
|
||||
@ -48,13 +51,18 @@ export const Database = memo(() => {
|
||||
[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} />;
|
||||
}
|
||||
|
||||
if (!rows) {
|
||||
if (!rows || !doc) {
|
||||
return (
|
||||
<div className={'flex h-full w-full items-center justify-center'}>
|
||||
<CircularProgress />
|
||||
@ -64,9 +72,15 @@ export const Database = memo(() => {
|
||||
|
||||
return (
|
||||
<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'>
|
||||
<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} />
|
||||
</DatabaseContextProvider>
|
||||
</div>
|
||||
|
@ -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;
|
@ -6,7 +6,7 @@ function DatabaseTitle({ viewId }: { viewId: string }) {
|
||||
|
||||
return (
|
||||
<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>{icon}</div>
|
||||
<div className={'font-bold'}>{name}</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { AFScroller } from '@/components/_shared/scroller';
|
||||
import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks';
|
||||
import { Toolbar, Event } from '@/components/database/components/calendar';
|
||||
import React from 'react';
|
||||
@ -9,24 +8,25 @@ export function Calendar() {
|
||||
const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup();
|
||||
|
||||
return (
|
||||
<AFScroller className={'appflowy-calendar'}>
|
||||
<div className={'h-full max-h-[960px] min-h-[560px] px-24 py-4 max-md:px-4'}>
|
||||
<BigCalendar
|
||||
components={{
|
||||
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,
|
||||
eventWrapper: Event,
|
||||
}}
|
||||
events={events}
|
||||
views={['month']}
|
||||
localizer={localizer}
|
||||
formats={formats}
|
||||
dayPropGetter={dayPropGetter}
|
||||
showMultiDayTimes={true}
|
||||
step={1}
|
||||
showAllEvents={true}
|
||||
/>
|
||||
</div>
|
||||
</AFScroller>
|
||||
<div className={'appflowy-calendar h-full max-h-[960px] min-h-[560px] px-16 pt-4 max-md:px-4'}>
|
||||
<BigCalendar
|
||||
components={{
|
||||
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,
|
||||
eventWrapper: Event,
|
||||
}}
|
||||
style={{
|
||||
marginBottom: '24px',
|
||||
}}
|
||||
events={events}
|
||||
views={['month']}
|
||||
localizer={localizer}
|
||||
formats={formats}
|
||||
dayPropGetter={dayPropGetter}
|
||||
showMultiDayTimes={true}
|
||||
step={1}
|
||||
showAllEvents={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ $today-highlight-bg: transparent;
|
||||
|
||||
.rbc-date-cell {
|
||||
min-width: 100px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.rbc-date-cell.rbc-now {
|
||||
@ -25,19 +26,38 @@ $today-highlight-bg: transparent;
|
||||
|
||||
.rbc-month-view {
|
||||
border: none;
|
||||
@apply h-full overflow-auto;
|
||||
|
||||
.rbc-month-row {
|
||||
border: 1px solid var(--line-divider);
|
||||
border-bottom: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid var(--line-divider);
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
&::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: var(--scrollbar-thumb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.rbc-month-header {
|
||||
height: 40px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--bg-body);
|
||||
z-index: 50;
|
||||
@apply border-b border-line-divider;
|
||||
|
||||
.rbc-header {
|
||||
border: none;
|
||||
@ -61,3 +81,9 @@ $today-highlight-bg: transparent;
|
||||
flex: 0 0 0 !important;
|
||||
min-height: 120px !important;
|
||||
}
|
||||
|
||||
.event-properties {
|
||||
.property-label {
|
||||
@apply text-text-caption;
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ export const Group = ({ groupId }: GroupProps) => {
|
||||
|
||||
if (notFound) {
|
||||
return (
|
||||
<div className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-24 text-text-caption max-md:px-4'}>
|
||||
<div className={'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-xs'}>{t('board.noGroupDesc')}</div>
|
||||
</div>
|
||||
@ -25,7 +25,7 @@ export const Group = ({ groupId }: GroupProps) => {
|
||||
|
||||
if (columns.length === 0 || !fieldId) return null;
|
||||
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'>
|
||||
{(provided) => {
|
||||
return (
|
||||
|
@ -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 { IconButton } from '@mui/material';
|
||||
import React from 'react';
|
||||
@ -6,16 +6,22 @@ import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
||||
|
||||
function EventPaper({ rowId }: { rowId: string }) {
|
||||
const fields = useFieldsSelector();
|
||||
const navigateToRow = useNavigateToRow();
|
||||
|
||||
return (
|
||||
<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 w-full items-center justify-end'}>
|
||||
<IconButton size={'small'}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
navigateToRow?.(rowId);
|
||||
}}
|
||||
size={'small'}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
</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) => {
|
||||
return <Property fieldId={field.fieldId} rowId={rowId} key={field.fieldId} />;
|
||||
})}
|
||||
|
@ -1,33 +1,28 @@
|
||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { useCellSelector, useDatabase } from '@/application/database-yjs';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs';
|
||||
import { Cell } from '@/components/database/components/cell';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Cell from 'src/components/database/components/cell/Cell';
|
||||
|
||||
function NoDateRow({ rowId }: { rowId: string }) {
|
||||
const database = useDatabase();
|
||||
const [primaryFieldId, setPrimaryFieldId] = React.useState<string | null>(null);
|
||||
const navigateToRow = useNavigateToRow();
|
||||
const primaryFieldId = usePrimaryFieldId();
|
||||
const cell = useCellSelector({
|
||||
rowId,
|
||||
fieldId: primaryFieldId || '',
|
||||
});
|
||||
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) {
|
||||
return <div className={'text-xs text-text-caption'}>{t('grid.row.titlePlaceholder')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'w-full hover:text-fill-default'}>
|
||||
<div
|
||||
onClick={() => {
|
||||
navigateToRow?.(rowId);
|
||||
}}
|
||||
className={'w-full hover:text-fill-default'}
|
||||
>
|
||||
<Cell
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
|
@ -17,6 +17,7 @@ export function Cell(props: CellProps<CellType>) {
|
||||
const { cell, rowId, fieldId, style } = props;
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
|
||||
|
||||
const Component = useMemo(() => {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
|
@ -3,7 +3,7 @@ import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/data
|
||||
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) {
|
||||
export function ChecklistCell({ cell, style, placeholder }: CellProps<ChecklistCellType>) {
|
||||
const data = useMemo(() => {
|
||||
return parseChecklistData(cell?.data ?? '');
|
||||
}, [cell?.data]);
|
||||
@ -11,7 +11,12 @@ export function ChecklistCell({ cell, style }: CellProps<ChecklistCellType>) {
|
||||
const options = data?.options;
|
||||
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 (
|
||||
<div style={style} className={'w-full cursor-pointer'}>
|
||||
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
@ -15,24 +15,24 @@ export function RowCreateModifiedTime({
|
||||
attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at;
|
||||
}) {
|
||||
const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId);
|
||||
const rowMeta = useRowMeta(rowId);
|
||||
const rowData = useRowData(rowId);
|
||||
const [value, setValue] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rowMeta) return;
|
||||
if (!rowData) return;
|
||||
const observeHandler = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
setValue(rowMeta.get(attrName));
|
||||
setValue(rowData.get(attrName));
|
||||
};
|
||||
|
||||
observeHandler();
|
||||
|
||||
rowMeta.observe(observeHandler);
|
||||
rowData.observe(observeHandler);
|
||||
return () => {
|
||||
rowMeta.unobserve(observeHandler);
|
||||
rowData.unobserve(observeHandler);
|
||||
};
|
||||
}, [rowMeta, attrName]);
|
||||
}, [rowData, attrName]);
|
||||
|
||||
const time = useMemo(() => {
|
||||
if (!value) return null;
|
||||
@ -40,7 +40,11 @@ export function RowCreateModifiedTime({
|
||||
}, [value, getDateTimeStr]);
|
||||
|
||||
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;
|
||||
|
@ -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;
|
@ -0,0 +1 @@
|
||||
export * from './PrimaryCell';
|
@ -18,7 +18,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
.getMap(YjsEditorKey.data_section)
|
||||
.get(YjsEditorKey.database)
|
||||
@ -32,7 +32,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
||||
|
||||
setRows(rows);
|
||||
});
|
||||
}, [workspaceId, databaseId, databaseService]);
|
||||
}, [workspaceId, databaseId, databaseService, rowIds]);
|
||||
|
||||
return (
|
||||
<div style={style} className={'flex items-center gap-2'}>
|
||||
|
@ -5,7 +5,7 @@ import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/component
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
|
||||
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 typeOption = useMemo(() => {
|
||||
if (!field) return null;
|
||||
|
@ -9,7 +9,7 @@ export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
|
||||
const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]);
|
||||
|
||||
const className = useMemo(() => {
|
||||
const classList = ['select-text', 'w-fit'];
|
||||
const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center'];
|
||||
|
||||
if (isUrl) {
|
||||
classList.push('text-content-blue-400', 'underline', 'cursor-pointer');
|
||||
|
@ -18,7 +18,7 @@ export function DatabaseConditions() {
|
||||
borderTopWidth: expanded ? '1px' : '0',
|
||||
}}
|
||||
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'}>
|
||||
|
@ -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;
|
@ -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;
|
@ -0,0 +1,2 @@
|
||||
export * from './DatabaseRowProperties';
|
||||
export * from './DatabaseRowSubDocument';
|
@ -1,8 +1,10 @@
|
||||
import { FieldId } from '@/application/collab.type';
|
||||
import { FieldId, YjsDatabaseKey } 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 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 {
|
||||
rowId: string;
|
||||
@ -13,8 +15,9 @@ export interface GridCellProps {
|
||||
}
|
||||
|
||||
export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const field = useFieldSelector(fieldId);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { field } = useFieldSelector(fieldId);
|
||||
const isPrimary = field?.get(YjsDatabaseKey.is_primary);
|
||||
const cell = useCellSelector({
|
||||
rowId,
|
||||
fieldId,
|
||||
@ -23,15 +26,13 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
|
||||
if (!el) return;
|
||||
if (!el || !cell) return;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (onResize) {
|
||||
onResize(rowIndex, columnIndex, {
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight,
|
||||
});
|
||||
}
|
||||
onResize?.(rowIndex, columnIndex, {
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight,
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(el);
|
||||
@ -39,12 +40,21 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr
|
||||
return () => {
|
||||
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;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={'grid-cell w-full cursor-text overflow-hidden text-xs'}>
|
||||
<Cell cell={cell} rowId={rowId} fieldId={fieldId} />
|
||||
<div ref={ref} className={'grid-cell flex min-h-full w-full cursor-text items-center overflow-hidden text-xs'}>
|
||||
<Component cell={cell} rowId={rowId} fieldId={fieldId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ export type RenderColumn = {
|
||||
export function useRenderFields() {
|
||||
const fields = useFieldsSelector();
|
||||
|
||||
console.log('columns', fields);
|
||||
const renderColumns = useMemo(() => {
|
||||
const data = fields.map((column) => ({
|
||||
...column,
|
||||
@ -30,7 +29,7 @@ export function useRenderFields() {
|
||||
return [
|
||||
{
|
||||
type: GridColumnType.Action,
|
||||
width: 96,
|
||||
width: 64,
|
||||
},
|
||||
...data,
|
||||
{
|
||||
@ -39,7 +38,7 @@ export function useRenderFields() {
|
||||
},
|
||||
{
|
||||
type: GridColumnType.Action,
|
||||
width: 96,
|
||||
width: 64,
|
||||
},
|
||||
].filter(Boolean) as RenderColumn[];
|
||||
}, [fields]);
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -0,0 +1,2 @@
|
||||
export * from './DatabaseHeader';
|
||||
export * from './DatabaseRowHeader';
|
@ -2,7 +2,6 @@ import { YjsDatabaseKey } from '@/application/collab.type';
|
||||
import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs';
|
||||
import { Cell as CellType, CellProps, TextCell } from '@/components/database/components/cell/cell.type';
|
||||
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 { DateTimeCell } from '@/components/database/components/cell/date';
|
||||
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 PropertyWrapper from '@/components/database/components/property/PropertyWrapper';
|
||||
import { TextProperty } from '@/components/database/components/property/text';
|
||||
import { ChecklistProperty } from 'src/components/database/components/property/cheklist';
|
||||
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -39,7 +39,7 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string })
|
||||
case FieldType.DateTime:
|
||||
return DateTimeCell;
|
||||
case FieldType.Checklist:
|
||||
return ChecklistCell;
|
||||
return ChecklistProperty;
|
||||
case FieldType.Relation:
|
||||
return RelationCell;
|
||||
default:
|
||||
|
@ -3,8 +3,8 @@ import React from 'react';
|
||||
|
||||
function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className={'flex w-full items-center gap-2'}>
|
||||
<div className={'w-[100px] text-text-caption'}>
|
||||
<div className={'flex min-h-[28px] w-full gap-2'}>
|
||||
<div className={'property-label flex h-[28px] w-[30%] items-center'}>
|
||||
<FieldDisplay fieldId={fieldId} />
|
||||
</div>
|
||||
<div className={'flex flex-1 flex-wrap pr-1'}>{children}</div>
|
||||
|
@ -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;
|
@ -0,0 +1 @@
|
||||
export * from './ChecklistProperty';
|
@ -54,7 +54,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className='mx-24 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4'
|
||||
className='mx-16 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4'
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const Database = lazy(() => import('./Database'));
|
||||
export const DatabaseRow = lazy(() => import('./DatabaseRow'));
|
||||
|
@ -41,7 +41,7 @@ export const Document = () => {
|
||||
<DocumentHeader doc={doc} viewId={documentId} />
|
||||
<div className={'flex w-full justify-center'}>
|
||||
<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>
|
||||
|
@ -13,18 +13,27 @@ import * as Y from 'yjs';
|
||||
|
||||
const defaultInitialValue: Descendant[] = [];
|
||||
|
||||
function CollaborativeEditor({ doc }: { doc: Y.Doc }) {
|
||||
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);
|
||||
function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeRoot?: boolean }) {
|
||||
const viewId = useId()?.objectId || '';
|
||||
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(() => {
|
||||
if (!editor) return;
|
||||
@ -37,8 +46,8 @@ function CollaborativeEditor({ doc }: { doc: Y.Doc }) {
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !connected) return;
|
||||
CustomEditor.setDocumentTitle(editor, title || '');
|
||||
if (!editor || !connected || title === undefined) return;
|
||||
CustomEditor.setDocumentTitle(editor, title);
|
||||
}, [editor, title, connected]);
|
||||
|
||||
return (
|
||||
|
@ -4,10 +4,18 @@ import { EditorContextProvider } from '@/components/editor/EditorContext';
|
||||
import React from 'react';
|
||||
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 (
|
||||
<EditorContextProvider readOnly={readOnly}>
|
||||
<CollaborativeEditor doc={doc} />
|
||||
<CollaborativeEditor doc={doc} includeRoot={includeRoot} />
|
||||
</EditorContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -10,7 +10,10 @@ export const Callout = memo(
|
||||
<CalloutIcon node={node} />
|
||||
</div>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ function CalloutIcon({ node }: { node: CalloutNode }) {
|
||||
|
||||
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}
|
||||
</span>
|
||||
</>
|
||||
|
@ -15,7 +15,7 @@ export const CodeBlock = memo(
|
||||
<div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}>
|
||||
<pre
|
||||
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>
|
||||
</pre>
|
||||
|
@ -20,7 +20,7 @@ export const MathEquation = memo(
|
||||
>
|
||||
<div
|
||||
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 ? (
|
||||
<KatexMath latex={formula} />
|
||||
|
@ -197,12 +197,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
.bulleted-icon {
|
||||
&:after {
|
||||
content: attr(data-letter);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.numbered-icon {
|
||||
&:after {
|
||||
content: attr(data-number) ".";
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +240,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||
|
||||
&:hover {
|
||||
.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;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type';
|
||||
import React from 'react';
|
||||
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 }) {
|
||||
const navigate = useNavigate();
|
||||
|
@ -19,6 +19,7 @@ function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
if (!folder) return;
|
||||
|
||||
console.log(folder.toJSON());
|
||||
setFolder(folder);
|
||||
},
|
||||
[folderService]
|
||||
|
@ -45,6 +45,7 @@
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
|
||||
.workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database {
|
||||
::-webkit-scrollbar {
|
||||
width: 0;
|
||||
@ -58,9 +59,6 @@
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols;
|
||||
line-height: 1em;
|
||||
white-space: nowrap;
|
||||
//&:hover {
|
||||
// background-color: rgba(156, 156, 156, 0.20);
|
||||
//}
|
||||
}
|
||||
|
||||
.theme-mode-item {
|
||||
|
@ -1,10 +1,16 @@
|
||||
import { Database } from '@/components/database';
|
||||
import { Database, DatabaseRow } from '@/components/database';
|
||||
import React from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
function DatabasePage () {
|
||||
return (
|
||||
<Database />
|
||||
);
|
||||
function DatabasePage() {
|
||||
const [search] = useSearchParams();
|
||||
const rowId = search.get('r');
|
||||
|
||||
if (rowId) {
|
||||
return <DatabaseRow rowId={rowId} />;
|
||||
}
|
||||
|
||||
return <Database />;
|
||||
}
|
||||
|
||||
export default DatabasePage;
|
@ -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 {
|
||||
@apply rounded-xl border border-line-divider px-4 py-3;
|
||||
}
|
||||
|
@ -71,6 +71,9 @@ export default defineConfig({
|
||||
cors: false,
|
||||
},
|
||||
envPrefix: ['AF', 'TAURI_'],
|
||||
esbuild: {
|
||||
drop: ['console', 'debugger'],
|
||||
},
|
||||
build: !!process.env.TAURI_PLATFORM
|
||||
? {
|
||||
// Tauri supports es2021
|
||||
@ -82,15 +85,6 @@ export default defineConfig({
|
||||
}
|
||||
: {
|
||||
target: `esnext`,
|
||||
terserOptions: !isDev
|
||||
? {
|
||||
compress: {
|
||||
keep_infinity: true,
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
reportCompressedSize: true,
|
||||
sourcemap: isDev,
|
||||
rollupOptions: !isDev
|
||||
|
Loading…
Reference in New Issue
Block a user