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 { 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();
|
||||||
|
};
|
||||||
|
@ -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 = () => {
|
||||||
|
@ -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',
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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>;
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
@ -60,4 +80,10 @@ $today-highlight-bg: transparent;
|
|||||||
display: inline-table !important;
|
display: inline-table !important;
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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 (
|
||||||
|
@ -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} />;
|
||||||
})}
|
})}
|
||||||
|
@ -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',
|
||||||
|
@ -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:
|
||||||
|
@ -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} />
|
||||||
|
@ -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;
|
||||||
|
@ -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(() => {
|
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'}>
|
||||||
|
@ -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;
|
||||||
|
@ -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');
|
||||||
|
@ -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'}>
|
||||||
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
||||||
|
@ -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 { 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:
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
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={{
|
||||||
|
@ -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'));
|
||||||
|
@ -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>
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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} />
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -270,4 +272,8 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
|||||||
@apply ml-5;
|
@apply ml-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block-icon {
|
||||||
|
@apply flex items-center justify-center;
|
||||||
}
|
}
|
@ -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();
|
||||||
|
@ -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]
|
||||||
|
@ -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 {
|
||||||
@ -97,4 +95,4 @@
|
|||||||
backgroundColor: var(--bg-body);
|
backgroundColor: var(--bg-body);
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user