mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-25 07:07:32 -04:00
feat: support web layout setting and breadcrumbs (#5425)
* fix: some bugs * fix: performance * feat: support system dark mode and different language * feat: support breadcrumb * feat: support breadcrumb * feat: support new doucment title * feat: support new doucment title
This commit is contained in:
parent
cb44a885a1
commit
b8b7a10b33
113 changed files with 2139 additions and 2168 deletions
|
@ -3,15 +3,44 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/appflowy.svg" />
|
<link rel="icon" type="image/svg+xml" href="/appflowy.svg" />
|
||||||
<meta name="viewport"
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
content="width=device-width,height=device-height,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
|
|
||||||
>
|
|
||||||
<title>AppFlowy</title>
|
<title>AppFlowy</title>
|
||||||
</head>
|
</head>
|
||||||
<body id="body">
|
<body id="body">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const userAgent = window.navigator.userAgent.toLowerCase();
|
||||||
|
const body = document.body;
|
||||||
|
const isWin = userAgent.indexOf('win') > -1;
|
||||||
|
const isMac = userAgent.indexOf('mac') > -1;
|
||||||
|
const isLinux = userAgent.indexOf('linux') > -1;
|
||||||
|
const isFirefox = userAgent.indexOf('firefox') > -1;
|
||||||
|
const isChrome = userAgent.indexOf('chrome') > -1;
|
||||||
|
const isSafari = userAgent.indexOf('safari') > -1;
|
||||||
|
if (isWin) {
|
||||||
|
body.setAttribute('data-os', 'windows');
|
||||||
|
} else if (isMac) {
|
||||||
|
body.setAttribute('data-os', 'mac');
|
||||||
|
} else if (isLinux) {
|
||||||
|
body.setAttribute('data-os', 'linux');
|
||||||
|
} else {
|
||||||
|
body.setAttribute('data-os', 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirefox) {
|
||||||
|
body.setAttribute('data-browser', 'firefox');
|
||||||
|
} else if (isChrome) {
|
||||||
|
body.setAttribute('data-browser', 'chrome');
|
||||||
|
} else if (isSafari) {
|
||||||
|
body.setAttribute('data-browser', 'safari');
|
||||||
|
} else {
|
||||||
|
body.setAttribute('data-browser', 'unknown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"lint": "pnpm run sync:i18n && tsc --noEmit --project tsconfig.web.json && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore.web",
|
"lint": "pnpm run sync:i18n && tsc --noEmit --project tsconfig.web.json && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore.web",
|
||||||
"start": "vite preview --port 3000",
|
"start": "vite preview --port 3000",
|
||||||
"tauri:dev": "tauri dev",
|
"tauri:dev": "tauri dev",
|
||||||
"css:variables": "node style-dictionary/config.cjs",
|
"css:variables": "node scripts/generateTailwindColors.cjs",
|
||||||
"sync:i18n": "node scripts/i18n.cjs",
|
"sync:i18n": "node scripts/i18n.cjs",
|
||||||
"link:client-api": "rm -rf node_modules/.vite && node scripts/create-symlink.cjs",
|
"link:client-api": "rm -rf node_modules/.vite && node scripts/create-symlink.cjs",
|
||||||
"analyze": "cross-env ANALYZE_MODE=true vite build",
|
"analyze": "cross-env ANALYZE_MODE=true vite build",
|
||||||
|
@ -38,6 +38,7 @@
|
||||||
"@types/react-swipeable-views": "^0.13.4",
|
"@types/react-swipeable-views": "^0.13.4",
|
||||||
"async-retry": "^1.3.3",
|
"async-retry": "^1.3.3",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"colorthief": "^2.4.0",
|
||||||
"dayjs": "^1.11.9",
|
"dayjs": "^1.11.9",
|
||||||
"decimal.js": "^10.4.3",
|
"decimal.js": "^10.4.3",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
|
@ -63,6 +64,7 @@
|
||||||
"react-big-calendar": "^1.8.5",
|
"react-big-calendar": "^1.8.5",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-custom-scrollbars": "^4.2.1",
|
"react-custom-scrollbars": "^4.2.1",
|
||||||
|
"react-custom-scrollbars-2": "^4.5.0",
|
||||||
"react-datepicker": "^4.23.0",
|
"react-datepicker": "^4.23.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.0.13",
|
||||||
|
@ -88,7 +90,6 @@
|
||||||
"unsplash-js": "^7.0.19",
|
"unsplash-js": "^7.0.19",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"valtio": "^1.12.1",
|
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
"vite-plugin-wasm": "^3.3.0",
|
||||||
"y-indexeddb": "9.0.12",
|
"y-indexeddb": "9.0.12",
|
||||||
"yjs": "^13.6.14"
|
"yjs": "^13.6.14"
|
||||||
|
|
796
frontend/appflowy_web_app/pnpm-lock.yaml
generated
796
frontend/appflowy_web_app/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
61
frontend/appflowy_web_app/scripts/generateTailwindColors.cjs
Normal file
61
frontend/appflowy_web_app/scripts/generateTailwindColors.cjs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Read CSS file
|
||||||
|
const cssFilePath = path.join(__dirname, '../src/styles/variables/light.variables.css');
|
||||||
|
const cssContent = fs.readFileSync(cssFilePath, 'utf-8');
|
||||||
|
|
||||||
|
// Extract color variables
|
||||||
|
const shadowVariables = cssContent.match(/--shadow:\s.*;/g);
|
||||||
|
const colorVariables = cssContent.match(/--[\w-]+:\s*#[0-9a-fA-F]{6}/g);
|
||||||
|
|
||||||
|
if (!colorVariables) {
|
||||||
|
console.error('No color variables found in CSS file.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadows = shadowVariables.reduce((shadows, variable) => {
|
||||||
|
const [name, value] = variable.split(':').map(str => str.trim());
|
||||||
|
const formattedName = name.replace('--', '').replace(/-/g, '_');
|
||||||
|
const key = 'md';
|
||||||
|
|
||||||
|
shadows[key] = `var(${name})`;
|
||||||
|
return shadows;
|
||||||
|
}, {});
|
||||||
|
// Generate Tailwind CSS colors configuration
|
||||||
|
// Replace -- with _ and - with _ in color variable names
|
||||||
|
const tailwindColors = colorVariables.reduce((colors, variable) => {
|
||||||
|
const [name, value] = variable.split(':').map(str => str.trim());
|
||||||
|
const formattedName = name.replace('--', '').replace(/-/g, '_');
|
||||||
|
const category = formattedName.split('_')[0];
|
||||||
|
const key = formattedName.replace(`${category}_`, '');
|
||||||
|
|
||||||
|
if (!colors[category]) {
|
||||||
|
colors[category] = {};
|
||||||
|
}
|
||||||
|
colors[category][key] = `var(${name})`;
|
||||||
|
return colors;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const tailwindColorsFormatted = JSON.stringify(tailwindColors, null, 2)
|
||||||
|
.replace(/_/g, '-');
|
||||||
|
const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`;
|
||||||
|
|
||||||
|
// Write Tailwind CSS colors configuration to file
|
||||||
|
const tailwindColorTemplate = `
|
||||||
|
${header}
|
||||||
|
module.exports = ${tailwindColorsFormatted};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tailwindShadowTemplate = `
|
||||||
|
${header}
|
||||||
|
module.exports = ${JSON.stringify(shadows, null, 2).replace(/_/g, '-')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tailwindConfigFilePath = path.join(__dirname, '../tailwind/colors.cjs');
|
||||||
|
fs.writeFileSync(tailwindConfigFilePath, tailwindColorTemplate, 'utf-8');
|
||||||
|
|
||||||
|
const tailwindShadowFilePath = path.join(__dirname, '../tailwind/box-shadow.cjs');
|
||||||
|
fs.writeFileSync(tailwindShadowFilePath, tailwindShadowTemplate, 'utf-8');
|
||||||
|
|
||||||
|
console.log('Tailwind CSS colors configuration generated successfully.');
|
|
@ -138,15 +138,15 @@ export interface FolderMeta {
|
||||||
current_workspace: string;
|
current_workspace: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CoverType {
|
export enum DocCoverType {
|
||||||
Color = 'CoverType.color',
|
Color = 'CoverType.color',
|
||||||
Image = 'CoverType.file',
|
Image = 'CoverType.file',
|
||||||
Asset = 'CoverType.asset',
|
Asset = 'CoverType.asset',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PageCover = {
|
export type DocCover = {
|
||||||
image_type?: ImageType;
|
image_type?: ImageType;
|
||||||
cover_selection_type?: CoverType;
|
cover_selection_type?: DocCoverType;
|
||||||
cover_selection?: string;
|
cover_selection?: string;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
|
@ -166,6 +166,7 @@ export enum YjsEditorKey {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values
|
||||||
database_row = 'data',
|
database_row = 'data',
|
||||||
user_awareness = 'user_awareness',
|
user_awareness = 'user_awareness',
|
||||||
|
empty = 'empty',
|
||||||
|
|
||||||
// document
|
// document
|
||||||
blocks = 'blocks',
|
blocks = 'blocks',
|
||||||
|
@ -199,6 +200,10 @@ export enum YjsFolderKey {
|
||||||
id = 'id',
|
id = 'id',
|
||||||
name = 'name',
|
name = 'name',
|
||||||
icon = 'icon',
|
icon = 'icon',
|
||||||
|
extra = 'extra',
|
||||||
|
cover = 'cover',
|
||||||
|
line_height_layout = 'line_height_layout',
|
||||||
|
font_layout = 'font_layout',
|
||||||
type = 'ty',
|
type = 'ty',
|
||||||
value = 'value',
|
value = 'value',
|
||||||
layout = 'layout',
|
layout = 'layout',
|
||||||
|
@ -337,7 +342,7 @@ export interface YView extends Y.Map<unknown> {
|
||||||
get(key: YjsFolderKey.name): string;
|
get(key: YjsFolderKey.name): string;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||||
get(key: YjsFolderKey.icon): string;
|
get(key: YjsFolderKey.icon | YjsFolderKey.extra): string;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||||
get(key: YjsFolderKey.layout): string;
|
get(key: YjsFolderKey.layout): string;
|
||||||
|
@ -607,3 +612,15 @@ export const databaseLayoutMap = {
|
||||||
[DatabaseViewLayout.Board]: 'board',
|
[DatabaseViewLayout.Board]: 'board',
|
||||||
[DatabaseViewLayout.Calendar]: 'calendar',
|
[DatabaseViewLayout.Calendar]: 'calendar',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum FontLayout {
|
||||||
|
small = 'small',
|
||||||
|
normal = 'normal',
|
||||||
|
large = 'large',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LineHeightLayout {
|
||||||
|
small = 'small',
|
||||||
|
normal = 'normal',
|
||||||
|
large = 'large',
|
||||||
|
}
|
||||||
|
|
|
@ -5,9 +5,10 @@ import * as Y from 'yjs';
|
||||||
|
|
||||||
export interface DatabaseContextState {
|
export interface DatabaseContextState {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
doc: YDoc;
|
databaseDoc: YDoc;
|
||||||
viewId: string;
|
viewId: string;
|
||||||
rowDocMap: Y.Map<YDoc>;
|
rowDocMap: Y.Map<YDoc>;
|
||||||
|
isDatabaseRowPage?: boolean;
|
||||||
navigateToRow?: (rowId: string) => void;
|
navigateToRow?: (rowId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,18 +16,30 @@ export const DatabaseContext = createContext<DatabaseContextState | null>(null);
|
||||||
|
|
||||||
export const useDatabase = () => {
|
export const useDatabase = () => {
|
||||||
const database = useContext(DatabaseContext)
|
const database = useContext(DatabaseContext)
|
||||||
?.doc?.getMap(YjsEditorKey.data_section)
|
?.databaseDoc?.getMap(YjsEditorKey.data_section)
|
||||||
.get(YjsEditorKey.database) as YDatabase;
|
.get(YjsEditorKey.database) as YDatabase;
|
||||||
|
|
||||||
return database;
|
return database;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function useDatabaseViewId() {
|
||||||
|
return useContext(DatabaseContext)?.viewId;
|
||||||
|
}
|
||||||
|
|
||||||
export const useNavigateToRow = () => {
|
export const useNavigateToRow = () => {
|
||||||
return useContext(DatabaseContext)?.navigateToRow;
|
return useContext(DatabaseContext)?.navigateToRow;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useRowDocMap = () => {
|
||||||
|
return useContext(DatabaseContext)?.rowDocMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useIsDatabaseRowPage = () => {
|
||||||
|
return useContext(DatabaseContext)?.isDatabaseRowPage;
|
||||||
|
};
|
||||||
|
|
||||||
export const useRow = (rowId: string) => {
|
export const useRow = (rowId: string) => {
|
||||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
const rows = useRowDocMap();
|
||||||
|
|
||||||
return rows?.get(rowId)?.getMap(YjsEditorKey.data_section);
|
return rows?.get(rowId)?.getMap(YjsEditorKey.data_section);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsEditorKey, YjsFolderKey } from '@/application/collab.type';
|
import {
|
||||||
|
FieldId,
|
||||||
|
SortId,
|
||||||
|
YDatabaseField,
|
||||||
|
YDoc,
|
||||||
|
YjsDatabaseKey,
|
||||||
|
YjsEditorKey,
|
||||||
|
YjsFolderKey,
|
||||||
|
} from '@/application/collab.type';
|
||||||
import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
|
||||||
import {
|
import {
|
||||||
DatabaseContext,
|
|
||||||
useDatabase,
|
useDatabase,
|
||||||
useDatabaseFields,
|
useDatabaseFields,
|
||||||
useDatabaseView,
|
useDatabaseView,
|
||||||
useRow,
|
useIsDatabaseRowPage,
|
||||||
useRowData,
|
useRowDocMap,
|
||||||
useRows,
|
useRows,
|
||||||
useViewId,
|
useViewId,
|
||||||
} from '@/application/database-yjs/context';
|
} from '@/application/database-yjs/context';
|
||||||
|
@ -18,8 +25,9 @@ import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse';
|
||||||
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 { throttle } from 'lodash-es';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import Y from 'yjs';
|
||||||
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
|
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
|
||||||
|
|
||||||
export interface Column {
|
export interface Column {
|
||||||
|
@ -368,8 +376,9 @@ export function useGroup(groupId: string) {
|
||||||
|
|
||||||
export function useRowsByGroup(groupId: string) {
|
export function useRowsByGroup(groupId: string) {
|
||||||
const { columns, fieldId } = useGroup(groupId);
|
const { columns, fieldId } = useGroup(groupId);
|
||||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
const rows = useRowDocMap();
|
||||||
const rowOrders = useRowOrdersSelector();
|
const rowOrders = useRowOrdersSelector();
|
||||||
|
|
||||||
const fields = useDatabaseFields();
|
const fields = useDatabaseFields();
|
||||||
const [notFound, setNotFound] = useState(false);
|
const [notFound, setNotFound] = useState(false);
|
||||||
const [groupResult, setGroupResult] = useState<Map<string, Row[]>>(new Map());
|
const [groupResult, setGroupResult] = useState<Map<string, Row[]>>(new Map());
|
||||||
|
@ -378,6 +387,8 @@ export function useRowsByGroup(groupId: string) {
|
||||||
if (!fieldId || !rowOrders || !rows) return;
|
if (!fieldId || !rowOrders || !rows) return;
|
||||||
|
|
||||||
const onConditionsChange = () => {
|
const onConditionsChange = () => {
|
||||||
|
if (rows.size !== rowOrders?.length) return;
|
||||||
|
|
||||||
const newResult = new Map<string, Row[]>();
|
const newResult = new Map<string, Row[]>();
|
||||||
|
|
||||||
const field = fields.get(fieldId);
|
const field = fields.get(fieldId);
|
||||||
|
@ -400,11 +411,9 @@ export function useRowsByGroup(groupId: string) {
|
||||||
|
|
||||||
onConditionsChange();
|
onConditionsChange();
|
||||||
|
|
||||||
const debounceConditionsChange = debounce(onConditionsChange, 200);
|
fields.observeDeep(onConditionsChange);
|
||||||
|
|
||||||
fields.observeDeep(debounceConditionsChange);
|
|
||||||
return () => {
|
return () => {
|
||||||
fields.unobserveDeep(debounceConditionsChange);
|
fields.unobserveDeep(onConditionsChange);
|
||||||
};
|
};
|
||||||
}, [fieldId, fields, rowOrders, rows]);
|
}, [fieldId, fields, rowOrders, rows]);
|
||||||
|
|
||||||
|
@ -419,19 +428,19 @@ export function useRowsByGroup(groupId: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRowOrdersSelector() {
|
export function useRowOrdersSelector() {
|
||||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
const isDatabaseRowPage = useIsDatabaseRowPage();
|
||||||
|
const { rows, clock } = useRowDocMapSelector();
|
||||||
const [rowOrders, setRowOrders] = useState<Row[]>();
|
const [rowOrders, setRowOrders] = useState<Row[]>();
|
||||||
const view = useDatabaseView();
|
const view = useDatabaseView();
|
||||||
const sorts = view?.get(YjsDatabaseKey.sorts);
|
const sorts = view?.get(YjsDatabaseKey.sorts);
|
||||||
const fields = useDatabaseFields();
|
const fields = useDatabaseFields();
|
||||||
const filters = view?.get(YjsDatabaseKey.filters);
|
const filters = view?.get(YjsDatabaseKey.filters);
|
||||||
|
const onConditionsChange = useCallback(() => {
|
||||||
useEffect(() => {
|
|
||||||
const onConditionsChange = () => {
|
|
||||||
const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON();
|
const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON();
|
||||||
|
|
||||||
if (!originalRowOrders || !rows) return;
|
if (!originalRowOrders || !rows) return;
|
||||||
|
|
||||||
|
if (originalRowOrders.length !== rows.size && !isDatabaseRowPage) return;
|
||||||
if (sorts?.length === 0 && filters?.length === 0) {
|
if (sorts?.length === 0 && filters?.length === 0) {
|
||||||
setRowOrders(originalRowOrders);
|
setRowOrders(originalRowOrders);
|
||||||
return;
|
return;
|
||||||
|
@ -452,29 +461,106 @@ export function useRowOrdersSelector() {
|
||||||
} else {
|
} else {
|
||||||
setRowOrders(originalRowOrders);
|
setRowOrders(originalRowOrders);
|
||||||
}
|
}
|
||||||
};
|
}, [fields, filters, rows, sorts, view, isDatabaseRowPage]);
|
||||||
|
|
||||||
const debounceConditionsChange = debounce(onConditionsChange, 200);
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
onConditionsChange();
|
onConditionsChange();
|
||||||
sorts?.observeDeep(debounceConditionsChange);
|
}, [onConditionsChange, clock]);
|
||||||
filters?.observeDeep(debounceConditionsChange);
|
|
||||||
fields?.observeDeep(debounceConditionsChange);
|
useEffect(() => {
|
||||||
rows?.observeDeep(debounceConditionsChange);
|
const throttleChange = throttle(onConditionsChange, 200);
|
||||||
|
|
||||||
|
sorts?.observeDeep(throttleChange);
|
||||||
|
filters?.observeDeep(throttleChange);
|
||||||
|
fields?.observeDeep(throttleChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sorts?.unobserveDeep(debounceConditionsChange);
|
sorts?.unobserveDeep(throttleChange);
|
||||||
filters?.unobserveDeep(debounceConditionsChange);
|
filters?.unobserveDeep(throttleChange);
|
||||||
fields?.unobserveDeep(debounceConditionsChange);
|
fields?.unobserveDeep(throttleChange);
|
||||||
rows?.observeDeep(debounceConditionsChange);
|
|
||||||
};
|
};
|
||||||
}, [fields, rows, sorts, filters, view]);
|
}, [onConditionsChange, fields, filters, sorts]);
|
||||||
|
|
||||||
return rowOrders;
|
return rowOrders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useRowDocMapSelector() {
|
||||||
|
const rowMap = useRowDocMap();
|
||||||
|
const [clock, setClock] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rowMap) return;
|
||||||
|
const observerEvent = () => setClock((prev) => prev + 1);
|
||||||
|
|
||||||
|
const rowIds = Array.from(rowMap?.keys() || []);
|
||||||
|
|
||||||
|
rowMap.observe(observerEvent);
|
||||||
|
|
||||||
|
const observers = rowIds.map((rowId) => {
|
||||||
|
return observeDeepRow(rowId, rowMap, observerEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rowMap.unobserve(observerEvent);
|
||||||
|
observers.forEach((observer) => observer());
|
||||||
|
};
|
||||||
|
}, [rowMap]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: rowMap,
|
||||||
|
clock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function observeDeepRow(
|
||||||
|
rowId: string,
|
||||||
|
rowMap: Y.Map<YDoc>,
|
||||||
|
observerEvent: () => void,
|
||||||
|
key: YjsEditorKey.meta | YjsEditorKey.database_row = YjsEditorKey.database_row
|
||||||
|
) {
|
||||||
|
const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section);
|
||||||
|
const row = rowSharedRoot?.get(key);
|
||||||
|
|
||||||
|
rowSharedRoot?.observe(observerEvent);
|
||||||
|
row?.observeDeep(observerEvent);
|
||||||
|
return () => {
|
||||||
|
rowSharedRoot?.unobserve(observerEvent);
|
||||||
|
row?.unobserveDeep(observerEvent);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRowDataSelector(rowId: string) {
|
||||||
|
const rowMap = useRowDocMap();
|
||||||
|
const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section);
|
||||||
|
const row = rowSharedRoot?.get(YjsEditorKey.database_row);
|
||||||
|
|
||||||
|
const [clock, setClock] = useState<number>(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!rowMap) return;
|
||||||
|
const onChange = () => {
|
||||||
|
setClock((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = observeDeepRow(rowId, rowMap, onChange);
|
||||||
|
|
||||||
|
rowMap.observe(onChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rowMap.unobserve(onChange);
|
||||||
|
observer();
|
||||||
|
};
|
||||||
|
}, [rowId, rowMap]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
row,
|
||||||
|
clock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) {
|
export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) {
|
||||||
const row = useRowData(rowId);
|
const { row } = useRowDataSelector(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));
|
||||||
|
|
||||||
|
@ -504,7 +590,7 @@ export function useCalendarEventsSelector() {
|
||||||
const filedId = setting.fieldId;
|
const filedId = setting.fieldId;
|
||||||
const { field } = useFieldSelector(filedId);
|
const { field } = useFieldSelector(filedId);
|
||||||
const rowOrders = useRowOrdersSelector();
|
const rowOrders = useRowOrdersSelector();
|
||||||
const rows = useContext(DatabaseContext)?.rowDocMap;
|
const rows = useRowDocMap();
|
||||||
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
const [events, setEvents] = useState<CalendarEvent[]>([]);
|
||||||
const [emptyEvents, setEmptyEvents] = useState<CalendarEvent[]>([]);
|
const [emptyEvents, setEmptyEvents] = useState<CalendarEvent[]>([]);
|
||||||
|
|
||||||
|
@ -610,35 +696,67 @@ export interface RowMeta {
|
||||||
isEmptyDocument: boolean;
|
isEmptyDocument: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metaIdMapFromRowIdMap = new Map<string, Map<RowMetaKey, string>>();
|
||||||
|
|
||||||
|
function getMetaIdMap(rowId: string) {
|
||||||
|
const hasMetaIdMap = metaIdMapFromRowIdMap.has(rowId);
|
||||||
|
|
||||||
|
if (!hasMetaIdMap) {
|
||||||
|
const parser = metaIdFromRowId(rowId);
|
||||||
|
const map = new Map<RowMetaKey, string>();
|
||||||
|
|
||||||
|
map.set(RowMetaKey.IconId, parser(RowMetaKey.IconId));
|
||||||
|
map.set(RowMetaKey.CoverId, parser(RowMetaKey.CoverId));
|
||||||
|
map.set(RowMetaKey.DocumentId, parser(RowMetaKey.DocumentId));
|
||||||
|
map.set(RowMetaKey.IsDocumentEmpty, parser(RowMetaKey.IsDocumentEmpty));
|
||||||
|
metaIdMapFromRowIdMap.set(rowId, map);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metaIdMapFromRowIdMap.get(rowId) as Map<RowMetaKey, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export const useRowMetaSelector = (rowId: string) => {
|
export const useRowMetaSelector = (rowId: string) => {
|
||||||
const [meta, setMeta] = useState<RowMeta | null>();
|
const [meta, setMeta] = useState<RowMeta | null>();
|
||||||
const yMeta = useRow(rowId)?.get(YjsEditorKey.meta);
|
const rowMap = useRowDocMap();
|
||||||
|
|
||||||
|
const updateMeta = useCallback(() => {
|
||||||
|
const metaKeyMap = getMetaIdMap(rowId);
|
||||||
|
|
||||||
|
const iconKey = metaKeyMap.get(RowMetaKey.IconId) ?? '';
|
||||||
|
const coverKey = metaKeyMap.get(RowMetaKey.CoverId) ?? '';
|
||||||
|
const documentId = metaKeyMap.get(RowMetaKey.DocumentId) ?? '';
|
||||||
|
const isEmptyDocumentKey = metaKeyMap.get(RowMetaKey.IsDocumentEmpty) ?? '';
|
||||||
|
const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section);
|
||||||
|
const yMeta = rowSharedRoot?.get(YjsEditorKey.meta);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!yMeta) return;
|
if (!yMeta) return;
|
||||||
const onChange = () => {
|
|
||||||
const metaJson = yMeta.toJSON();
|
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({
|
const icon = metaJson[iconKey];
|
||||||
|
const cover = metaJson[coverKey];
|
||||||
|
const isEmptyDocument = metaJson[isEmptyDocumentKey];
|
||||||
|
|
||||||
|
setMeta({
|
||||||
icon,
|
icon,
|
||||||
cover,
|
cover,
|
||||||
documentId,
|
documentId,
|
||||||
isEmptyDocument,
|
isEmptyDocument,
|
||||||
});
|
});
|
||||||
};
|
}, [rowId, rowMap]);
|
||||||
|
|
||||||
onChange();
|
useEffect(() => {
|
||||||
|
if (!rowMap) return;
|
||||||
|
updateMeta();
|
||||||
|
const observer = observeDeepRow(rowId, rowMap, updateMeta, YjsEditorKey.meta);
|
||||||
|
|
||||||
|
rowMap.observe(updateMeta);
|
||||||
|
|
||||||
yMeta.observe(onChange);
|
|
||||||
return () => {
|
return () => {
|
||||||
yMeta.unobserve(onChange);
|
rowMap.unobserve(updateMeta);
|
||||||
|
observer();
|
||||||
};
|
};
|
||||||
}, [rowId, yMeta]);
|
}, [rowId, rowMap, updateMeta]);
|
||||||
|
|
||||||
return meta;
|
return meta;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,49 @@
|
||||||
import { YFolder } from '@/application/collab.type';
|
import { ViewLayout, YFolder, YjsFolderKey } from '@/application/collab.type';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useCallback, useContext } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
export const FolderContext = createContext<YFolder | null>(null);
|
export interface Crumb {
|
||||||
|
viewId: string;
|
||||||
|
rowId?: string;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FolderContext = createContext<{
|
||||||
|
folder: YFolder | null;
|
||||||
|
onNavigateToView?: (viewId: string) => void;
|
||||||
|
crumbs?: Crumb[];
|
||||||
|
setCrumbs?: React.Dispatch<React.SetStateAction<Crumb[]>>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
export const useFolderContext = () => {
|
export const useFolderContext = () => {
|
||||||
return useContext(FolderContext);
|
return useContext(FolderContext)?.folder;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useViewLayout = () => {
|
||||||
|
const folder = useFolderContext();
|
||||||
|
const { objectId } = useParams();
|
||||||
|
const views = folder?.get(YjsFolderKey.views);
|
||||||
|
const view = objectId ? views?.get(objectId) : null;
|
||||||
|
|
||||||
|
return Number(view?.get(YjsFolderKey.layout)) as ViewLayout;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNavigateToView = () => {
|
||||||
|
return useContext(FolderContext)?.onNavigateToView;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCrumbs = () => {
|
||||||
|
return useContext(FolderContext)?.crumbs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePushCrumb = () => {
|
||||||
|
const { setCrumbs } = useContext(FolderContext) || {};
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(crumb: Crumb) => {
|
||||||
|
setCrumbs?.((prevCrumbs) => [...prevCrumbs, crumb]);
|
||||||
|
},
|
||||||
|
[setCrumbs]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export enum CoverType {
|
||||||
|
NormalColor = 'color',
|
||||||
|
GradientColor = 'gradient',
|
||||||
|
BuildInImage = 'none',
|
||||||
|
CustomImage = 'custom',
|
||||||
|
LocalImage = 'local',
|
||||||
|
UpsplashImage = 'unsplash',
|
||||||
|
}
|
|
@ -54,10 +54,10 @@ export function useViewSelector(viewId: string) {
|
||||||
setView(view || null);
|
setView(view || null);
|
||||||
const observerEvent = () => setClock((prev) => prev + 1);
|
const observerEvent = () => setClock((prev) => prev + 1);
|
||||||
|
|
||||||
view.observe(observerEvent);
|
view?.observe(observerEvent);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
view.unobserve(observerEvent);
|
view?.unobserve(observerEvent);
|
||||||
};
|
};
|
||||||
}, [folder, viewId]);
|
}, [folder, viewId]);
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ import * as Y from 'yjs';
|
||||||
export class JSDatabaseService implements DatabaseService {
|
export class JSDatabaseService implements DatabaseService {
|
||||||
private loadedDatabaseId: Set<string> = new Set();
|
private loadedDatabaseId: Set<string> = new Set();
|
||||||
|
|
||||||
|
private cacheDatabaseRowDocMap: Map<string, Y.Doc> = new Map();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
@ -23,9 +25,20 @@ export class JSDatabaseService implements DatabaseService {
|
||||||
databaseDoc: YDoc;
|
databaseDoc: YDoc;
|
||||||
rows: Y.Map<YDoc>;
|
rows: Y.Map<YDoc>;
|
||||||
}> {
|
}> {
|
||||||
const rootRowsDoc = new Y.Doc();
|
|
||||||
const rowsFolder = rootRowsDoc.getMap();
|
|
||||||
const isLoaded = this.loadedDatabaseId.has(databaseId);
|
const isLoaded = this.loadedDatabaseId.has(databaseId);
|
||||||
|
|
||||||
|
const rootRowsDoc =
|
||||||
|
this.cacheDatabaseRowDocMap.get(databaseId) ??
|
||||||
|
new Y.Doc({
|
||||||
|
guid: databaseId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.cacheDatabaseRowDocMap.has(databaseId)) {
|
||||||
|
this.cacheDatabaseRowDocMap.set(databaseId, rootRowsDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
|
||||||
|
|
||||||
let databaseDoc: YDoc | undefined = undefined;
|
let databaseDoc: YDoc | undefined = undefined;
|
||||||
|
|
||||||
if (isLoaded) {
|
if (isLoaded) {
|
||||||
|
@ -51,13 +64,15 @@ export class JSDatabaseService implements DatabaseService {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
const { doc } = await getCollabStorage(id, CollabType.DatabaseRow);
|
const { doc } = await getCollabStorage(id, CollabType.DatabaseRow);
|
||||||
|
|
||||||
|
if (!rowsFolder.has(id)) {
|
||||||
rowsFolder.set(id, doc);
|
rowsFolder.set(id, doc);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const rows = await this.loadDatabaseRows(workspaceId, ids);
|
void this.loadDatabaseRows(workspaceId, ids, (id, row) => {
|
||||||
|
if (!rowsFolder.has(id)) {
|
||||||
rows.forEach((row, id) => {
|
|
||||||
rowsFolder.set(id, row);
|
rowsFolder.set(id, row);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,19 +89,20 @@ export class JSDatabaseService implements DatabaseService {
|
||||||
console.log('Update rows', rowIds);
|
console.log('Update rows', rowIds);
|
||||||
void this.loadDatabaseRows(
|
void this.loadDatabaseRows(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
rowIds.map((item) => item.id)
|
rowIds.map((item) => item.id),
|
||||||
).then((newRows) => {
|
(rowId: string, rowDoc) => {
|
||||||
newRows.forEach((row, id) => {
|
if (!rowsFolder.has(rowId)) {
|
||||||
rowsFolder.set(id, row);
|
rowsFolder.set(rowId, rowDoc);
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
databaseDoc,
|
databaseDoc,
|
||||||
rows: rowsFolder as Y.Map<YDoc>,
|
rows: rowsFolder,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,6 +160,7 @@ export class JSDatabaseService implements DatabaseService {
|
||||||
};
|
};
|
||||||
|
|
||||||
databaseDoc.on('update', handleUpdate);
|
databaseDoc.on('update', handleUpdate);
|
||||||
|
console.log('Database loaded', rows.toJSON());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
databaseDoc,
|
databaseDoc,
|
||||||
|
@ -151,9 +168,7 @@ export class JSDatabaseService implements DatabaseService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadDatabaseRows(workspaceId: string, rowIds: string[]) {
|
async loadDatabaseRows(workspaceId: string, rowIds: string[], rowCallback: (rowId: string, rowDoc: YDoc) => void) {
|
||||||
const rows = new Map<string, YDoc>();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await batchCollabs(
|
await batchCollabs(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
@ -161,12 +176,14 @@ export class JSDatabaseService implements DatabaseService {
|
||||||
object_id: id,
|
object_id: id,
|
||||||
collab_type: CollabType.DatabaseRow,
|
collab_type: CollabType.DatabaseRow,
|
||||||
})),
|
})),
|
||||||
(id, rowDoc) => rows.set(id, rowDoc)
|
rowCallback
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rows;
|
async closeDatabase(databaseId: string) {
|
||||||
|
this.cacheDatabaseRowDocMap.delete(databaseId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import * as Y from 'yjs';
|
||||||
export async function openCollabDB(docName: string): Promise<YDoc> {
|
export async function openCollabDB(docName: string): Promise<YDoc> {
|
||||||
const name = `${databasePrefix}_${docName}`;
|
const name = `${databasePrefix}_${docName}`;
|
||||||
const doc = new Y.Doc();
|
const doc = new Y.Doc();
|
||||||
|
|
||||||
const provider = new IndexeddbPersistence(name, doc);
|
const provider = new IndexeddbPersistence(name, doc);
|
||||||
|
|
||||||
let resolve: (value: unknown) => void;
|
let resolve: (value: unknown) => void;
|
||||||
|
@ -26,14 +27,6 @@ export async function openCollabDB(docName: string): Promise<YDoc> {
|
||||||
return doc as YDoc;
|
return doc as YDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteCollabDB(docName: string) {
|
|
||||||
const name = `${databasePrefix}_${docName}`;
|
|
||||||
const doc = new Y.Doc();
|
|
||||||
const provider = new IndexeddbPersistence(name, doc);
|
|
||||||
|
|
||||||
await provider.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDBName(id: string, type: string) {
|
export function getDBName(id: string, type: string) {
|
||||||
const { uuid } = getAuthInfo() || {};
|
const { uuid } = getAuthInfo() || {};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CollabType, YDoc, YjsEditorKey } from '@/application/collab.type';
|
import { CollabType, YDoc, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
||||||
import { getDBName, openCollabDB } from '@/application/services/js-services/db';
|
import { getDBName, openCollabDB } from '@/application/services/js-services/db';
|
||||||
import { APIService } from '@/application/services/js-services/wasm';
|
import { APIService } from '@/application/services/js-services/wasm';
|
||||||
import { applyYDoc } from '@/application/ydoc/apply';
|
import { applyYDoc } from '@/application/ydoc/apply';
|
||||||
|
@ -30,11 +30,28 @@ function collabTypeToDBType(type: CollabType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collabSharedRootKeyMap = {
|
||||||
|
[CollabType.Folder]: YjsEditorKey.folder,
|
||||||
|
[CollabType.Document]: YjsEditorKey.document,
|
||||||
|
[CollabType.Database]: YjsEditorKey.database,
|
||||||
|
[CollabType.WorkspaceDatabase]: YjsEditorKey.workspace_database,
|
||||||
|
[CollabType.DatabaseRow]: YjsEditorKey.database_row,
|
||||||
|
[CollabType.UserAwareness]: YjsEditorKey.user_awareness,
|
||||||
|
[CollabType.Empty]: YjsEditorKey.empty,
|
||||||
|
};
|
||||||
|
|
||||||
export async function getCollabStorage(id: string, type: CollabType) {
|
export async function getCollabStorage(id: string, type: CollabType) {
|
||||||
const name = getDBName(id, collabTypeToDBType(type));
|
const name = getDBName(id, collabTypeToDBType(type));
|
||||||
|
|
||||||
const doc = await openCollabDB(name);
|
const doc = await openCollabDB(name);
|
||||||
const localExist = doc.share.has(YjsEditorKey.data_section);
|
let localExist = false;
|
||||||
|
const existData = doc.share.has(YjsEditorKey.data_section);
|
||||||
|
|
||||||
|
if (existData) {
|
||||||
|
const data = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
|
||||||
|
localExist = data.has(collabSharedRootKeyMap[type] as string);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
doc,
|
doc,
|
||||||
|
@ -74,15 +91,13 @@ export async function batchCollabs(
|
||||||
for (const item of params) {
|
for (const item of params) {
|
||||||
const { object_id, collab_type } = item;
|
const { object_id, collab_type } = item;
|
||||||
|
|
||||||
const { doc } = await getCollabStorage(object_id, collab_type);
|
const { doc, localExist } = await getCollabStorage(object_id, collab_type);
|
||||||
|
|
||||||
if (rowCallback) {
|
if (rowCallback && localExist) {
|
||||||
rowCallback(object_id, doc);
|
rowCallback(object_id, doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async fetch collab data and apply to Y.Doc
|
|
||||||
void (async () => {
|
|
||||||
const res = await batchFetchCollab(workspaceId, params);
|
const res = await batchFetchCollab(workspaceId, params);
|
||||||
|
|
||||||
for (const id of Object.keys(res)) {
|
for (const id of Object.keys(res)) {
|
||||||
|
@ -96,6 +111,7 @@ export async function batchCollabs(
|
||||||
const { doc } = await getCollabStorage(id, type);
|
const { doc } = await getCollabStorage(id, type);
|
||||||
|
|
||||||
applyYDoc(doc, data);
|
applyYDoc(doc, data);
|
||||||
|
|
||||||
|
rowCallback?.(id, doc);
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ export interface DatabaseService {
|
||||||
databaseDoc: YDoc;
|
databaseDoc: YDoc;
|
||||||
rows: Y.Map<YDoc>;
|
rows: Y.Map<YDoc>;
|
||||||
}>;
|
}>;
|
||||||
|
closeDatabase: (databaseId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserService {
|
export interface UserService {
|
||||||
|
|
|
@ -7,6 +7,10 @@ export class TauriDatabaseService implements DatabaseService {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async closeDatabase(_databaseId: string) {
|
||||||
|
return Promise.reject('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
async openDatabase(
|
async openDatabase(
|
||||||
_workspaceId: string,
|
_workspaceId: string,
|
||||||
_viewId: string
|
_viewId: string
|
||||||
|
|
|
@ -59,10 +59,8 @@ export function withYjs<T extends Editor>(
|
||||||
doc: Y.Doc,
|
doc: Y.Doc,
|
||||||
{
|
{
|
||||||
localOrigin,
|
localOrigin,
|
||||||
includeRoot = true,
|
|
||||||
}: {
|
}: {
|
||||||
localOrigin: CollabOrigin;
|
localOrigin: CollabOrigin;
|
||||||
includeRoot?: boolean;
|
|
||||||
}
|
}
|
||||||
): T & YjsEditor {
|
): T & YjsEditor {
|
||||||
const e = editor as T & YjsEditor;
|
const e = editor as T & YjsEditor;
|
||||||
|
@ -71,7 +69,7 @@ export function withYjs<T extends Editor>(
|
||||||
e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
|
||||||
const initializeDocumentContent = () => {
|
const initializeDocumentContent = () => {
|
||||||
const content = yDocToSlateContent(doc, includeRoot);
|
const content = yDocToSlateContent(doc);
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Operation, Node } from 'slate';
|
import { Operation, Node } from 'slate';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
export function applySlateOp(ydoc: Y.Doc, slateRoot: Node, op: Operation) {
|
// transform slate op to yjs op and apply it to ydoc
|
||||||
console.log('applySlateOp', op);
|
export function applySlateOp(_ydoc: Y.Doc, _slateRoot: Node, _op: Operation) {
|
||||||
|
// console.log('applySlateOp', op);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,7 @@ interface BlockJson {
|
||||||
external_id?: string;
|
external_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function yDocToSlateContent(doc: YDoc, includeRoot?: boolean): Element | undefined {
|
export function yDocToSlateContent(doc: YDoc): Element | undefined {
|
||||||
console.log(doc);
|
|
||||||
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot;
|
||||||
|
|
||||||
console.log(sharedRoot.toJSON());
|
console.log(sharedRoot.toJSON());
|
||||||
|
@ -107,13 +106,6 @@ export function yDocToSlateContent(doc: YDoc, includeRoot?: boolean): Element |
|
||||||
|
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
if (!includeRoot) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { children, ...rootNode } = result;
|
|
||||||
|
|
||||||
// load font family
|
|
||||||
if (fontFamilys.length > 0) {
|
if (fontFamilys.length > 0) {
|
||||||
window.WebFont?.load({
|
window.WebFont?.load({
|
||||||
google: {
|
google: {
|
||||||
|
@ -122,21 +114,7 @@ export function yDocToSlateContent(doc: YDoc, includeRoot?: boolean): Element |
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return result;
|
||||||
children: [
|
|
||||||
{
|
|
||||||
...rootNode,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
textId: pageId,
|
|
||||||
type: YjsEditorKey.text,
|
|
||||||
children: [{ text: '' }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
...children,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function blockToSlateNode(block: BlockJson): Element {
|
export function blockToSlateNode(block: BlockJson): Element {
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
import { YFolder } from '@/application/collab.type';
|
import { YFolder } from '@/application/collab.type';
|
||||||
import { FolderContext } from '@/application/folder-yjs';
|
import { Crumb, FolderContext } from '@/application/folder-yjs';
|
||||||
|
|
||||||
export const FolderProvider: React.FC<{ folder: YFolder | null; children?: React.ReactNode }> = ({
|
export const FolderProvider: React.FC<{
|
||||||
|
folder: YFolder | null;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onNavigateToView?: (viewId: string) => void;
|
||||||
|
crumbs?: Crumb[];
|
||||||
|
setCrumbs?: React.Dispatch<React.SetStateAction<Crumb[]>>;
|
||||||
|
}> = ({ folder, children, onNavigateToView, crumbs, setCrumbs }) => {
|
||||||
|
return (
|
||||||
|
<FolderContext.Provider
|
||||||
|
value={{
|
||||||
folder,
|
folder,
|
||||||
children,
|
onNavigateToView,
|
||||||
}) => {
|
crumbs,
|
||||||
return <FolderContext.Provider value={folder}>{children}</FolderContext.Provider>;
|
setCrumbs,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FolderContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Button, Dialog, DialogActions, DialogContent, DialogContentText, Dialog
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function RecordNotFound({ open, workspaceId }: { workspaceId: string; open: boolean }) {
|
export function RecordNotFound({ open, workspaceId, title }: { workspaceId: string; open: boolean; title?: string }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -10,13 +10,13 @@ export function RecordNotFound({ open, workspaceId }: { workspaceId: string; ope
|
||||||
<DialogTitle>Oops.. something went wrong</DialogTitle>
|
<DialogTitle>Oops.. something went wrong</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id='alert-dialog-description'>
|
<DialogContentText id='alert-dialog-description'>
|
||||||
Sorry, the page you are looking for does not exist.
|
{title ? title : 'The record you are looking for does not exist.'}
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions className={'flex w-full items-center justify-center'}>
|
<DialogActions className={'flex w-full items-center justify-center'}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/workspace/${workspaceId}`);
|
navigate(`/view/${workspaceId}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Go back
|
Go back
|
||||||
|
|
|
@ -1,17 +1,54 @@
|
||||||
import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type';
|
import { FontLayout, LineHeightLayout, ViewLayout, YjsFolderKey, YView } from '@/application/collab.type';
|
||||||
import { useViewSelector } from '@/application/folder-yjs';
|
import { useViewSelector } from '@/application/folder-yjs';
|
||||||
import React, { useMemo } from 'react';
|
import { CoverType } from '@/application/folder-yjs/folder.type';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg';
|
import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg';
|
||||||
import { ReactComponent as GridSvg } from '$icons/16x/grid.svg';
|
import { ReactComponent as GridSvg } from '$icons/16x/grid.svg';
|
||||||
import { ReactComponent as BoardSvg } from '$icons/16x/board.svg';
|
import { ReactComponent as BoardSvg } from '$icons/16x/board.svg';
|
||||||
import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg';
|
import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export interface PageCover {
|
||||||
|
type: CoverType;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageExtra {
|
||||||
|
cover: PageCover | null;
|
||||||
|
fontLayout: FontLayout;
|
||||||
|
lineHeightLayout: LineHeightLayout;
|
||||||
|
font?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExtra(extra: string): PageExtra {
|
||||||
|
let extraObj;
|
||||||
|
|
||||||
|
try {
|
||||||
|
extraObj = JSON.parse(extra);
|
||||||
|
} catch (e) {
|
||||||
|
extraObj = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cover: extraObj.cover
|
||||||
|
? {
|
||||||
|
type: extraObj.cover.type,
|
||||||
|
value: extraObj.cover.value,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
fontLayout: extraObj.font_layout || FontLayout.normal,
|
||||||
|
lineHeightLayout: extraObj.line_height_layout || LineHeightLayout.normal,
|
||||||
|
font: extraObj.font,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function usePageInfo(id: string) {
|
export function usePageInfo(id: string) {
|
||||||
const { view } = useViewSelector(id);
|
const { view } = useViewSelector(id);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const layout = view?.get(YjsFolderKey.layout);
|
const layout = view?.get(YjsFolderKey.layout);
|
||||||
const icon = view?.get(YjsFolderKey.icon);
|
const icon = view?.get(YjsFolderKey.icon);
|
||||||
|
const extra = view?.get(YjsFolderKey.extra);
|
||||||
const name = view?.get(YjsFolderKey.name) || '';
|
const name = view?.get(YjsFolderKey.name) || '';
|
||||||
const iconObj = useMemo(() => {
|
const iconObj = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
|
@ -20,6 +57,11 @@ export function usePageInfo(id: string) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [icon]);
|
}, [icon]);
|
||||||
|
|
||||||
|
const extraObj = useMemo(() => {
|
||||||
|
return parseExtra(extra || '');
|
||||||
|
}, [extra]);
|
||||||
|
|
||||||
const defaultIcon = useMemo(() => {
|
const defaultIcon = useMemo(() => {
|
||||||
switch (parseInt(layout ?? '0')) {
|
switch (parseInt(layout ?? '0')) {
|
||||||
case ViewLayout.Document:
|
case ViewLayout.Document:
|
||||||
|
@ -37,9 +79,14 @@ export function usePageInfo(id: string) {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!view);
|
||||||
|
}, [view]);
|
||||||
return {
|
return {
|
||||||
icon: iconObj?.value || defaultIcon,
|
icon: iconObj?.value || defaultIcon,
|
||||||
name: name || t('menuAppHeader.defaultNewPageName'),
|
name: name || t('menuAppHeader.defaultNewPageName'),
|
||||||
view: view as YView,
|
view: view as YView,
|
||||||
|
loading,
|
||||||
|
extra: extraObj,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function ComponentLoading() {
|
||||||
|
return (
|
||||||
|
<div className={'flex h-[260px] w-full items-center justify-center'}>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ComponentLoading;
|
|
@ -39,7 +39,7 @@ function LinearProgressWithLabel({
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-[30px] text-center text-xs text-text-title'}>{result}</div>
|
<div className={'min-w-[30px] text-center text-text-title'}>{result}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Scrollbars } from 'react-custom-scrollbars';
|
import { Scrollbars } from 'react-custom-scrollbars-2';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export interface AFScrollerProps {
|
export interface AFScrollerProps {
|
||||||
|
@ -18,8 +18,7 @@ export const AFScroller = React.forwardRef(
|
||||||
autoHide
|
autoHide
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
const scrollEl = el.container?.firstChild as HTMLElement;
|
const scrollEl = el.container?.firstChild as HTMLElement;
|
||||||
|
|
||||||
if (!scrollEl) return;
|
if (!scrollEl) return;
|
||||||
|
@ -62,7 +61,7 @@ export const AFScroller = React.forwardRef(
|
||||||
marginRight: 0,
|
marginRight: 0,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
}}
|
}}
|
||||||
className={className}
|
className={`${className} appflowy-custom-scroller`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,11 +8,11 @@ export interface TagProps {
|
||||||
|
|
||||||
export const Tag: FC<TagProps> = ({ color, size = 'small', label }) => {
|
export const Tag: FC<TagProps> = ({ color, size = 'small', label }) => {
|
||||||
const className = useMemo(() => {
|
const className = useMemo(() => {
|
||||||
const classList = ['rounded-md', 'font-medium', 'text-xs', 'leading-[18px]'];
|
const classList = ['rounded-md', 'font-medium', 'leading-[18px]'];
|
||||||
|
|
||||||
if (color) classList.push(`text-text-title`);
|
if (color) classList.push(`text-text-title`);
|
||||||
if (size === 'small') classList.push('text-xs', 'px-2', 'py-[2px]');
|
if (size === 'small') classList.push('px-2', 'py-[2px]');
|
||||||
if (size === 'medium') classList.push('text-sm', 'px-3', 'py-1');
|
if (size === 'medium') classList.push('px-3', 'py-1');
|
||||||
return classList.join(' ');
|
return classList.join(' ');
|
||||||
}, [color, size]);
|
}, [color, size]);
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ const AppMain = withAppWrapper(() => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||||
<Route path={'/workspace/:workspaceId'} element={<FolderPage />} />
|
<Route path={'/view/:workspaceId'} element={<FolderPage />} />
|
||||||
<Route path={'/workspace/:workspaceId/:type/:objectId'} element={<ProductPage />} />
|
<Route path={'/view/:workspaceId/:objectId'} element={<ProductPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={'/login'} element={<LoginPage />} />
|
<Route path={'/login'} element={<LoginPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useAppLanguage } from '@/components/app/useAppLanguage';
|
||||||
import React, { createContext, useEffect, useMemo, useState } from 'react';
|
import React, { createContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { AFService } from '@/application/services/services.type';
|
import { AFService } from '@/application/services/services.type';
|
||||||
import { getService } from '@/application/services';
|
import { getService } from '@/application/services';
|
||||||
|
@ -14,6 +15,8 @@ function AppConfig ({ children }: { children: React.ReactNode }) {
|
||||||
const appConfig = useAppSelector((state) => state.app.appConfig);
|
const appConfig = useAppSelector((state) => state.app.appConfig);
|
||||||
const [service, setService] = useState<AFService>();
|
const [service, setService] = useState<AFService>();
|
||||||
|
|
||||||
|
useAppLanguage();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (!appConfig) return;
|
if (!appConfig) return;
|
||||||
|
@ -25,7 +28,7 @@ function AppConfig ({ children }: { children: React.ReactNode }) {
|
||||||
() => ({
|
() => ({
|
||||||
service,
|
service,
|
||||||
}),
|
}),
|
||||||
[service],
|
[service]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <AFConfigContext.Provider value={config}>{children}</AFConfigContext.Provider>;
|
return <AFConfigContext.Provider value={config}>{children}</AFConfigContext.Provider>;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useAppThemeMode } from '@/components/app/useAppThemeMode';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import createTheme from '@mui/material/styles/createTheme';
|
import createTheme from '@mui/material/styles/createTheme';
|
||||||
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
||||||
|
@ -7,7 +8,7 @@ import 'src/styles/tailwind.css';
|
||||||
import 'src/styles/template.css';
|
import 'src/styles/template.css';
|
||||||
|
|
||||||
function AppTheme({ children }: { children: React.ReactNode }) {
|
function AppTheme({ children }: { children: React.ReactNode }) {
|
||||||
const isDark = false;
|
const { isDark } = useAppThemeMode();
|
||||||
const theme = useMemo(
|
const theme = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createTheme({
|
createTheme({
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export function useAppLanguage() {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const detectLanguageChange = () => {
|
||||||
|
const language = window.navigator.language;
|
||||||
|
|
||||||
|
void i18n.changeLanguage(language);
|
||||||
|
};
|
||||||
|
|
||||||
|
detectLanguageChange();
|
||||||
|
|
||||||
|
window.addEventListener('languagechange', detectLanguageChange);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('languagechange', detectLanguageChange);
|
||||||
|
};
|
||||||
|
}, [i18n]);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useAppThemeMode() {
|
||||||
|
const [isDark, setIsDark] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function detectColorScheme() {
|
||||||
|
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
|
setIsDark(darkModeMediaQuery.matches);
|
||||||
|
document.documentElement.setAttribute('data-dark-mode', darkModeMediaQuery.matches ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
detectColorScheme();
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectColorScheme);
|
||||||
|
return () => {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', detectColorScheme);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDark,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { lazy, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useAuth } from '@/components/auth/auth.hooks';
|
import { useAuth } from '@/components/auth/auth.hooks';
|
||||||
import { currentUserActions, LoginState } from '@/stores/currentUser/slice';
|
import { currentUserActions, LoginState } from '@/stores/currentUser/slice';
|
||||||
import { useAppDispatch } from '@/stores/store';
|
import { useAppDispatch } from '@/stores/store';
|
||||||
|
@ -42,8 +42,18 @@ function ProtectedRoutes() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentUser.user?.workspaceId && (window.location.pathname === '/' || window.location.pathname === '')) {
|
||||||
|
navigate(`/view/${currentUser.user.workspaceId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'relative h-screen w-screen'}>
|
<div
|
||||||
|
className={'relative h-screen w-screen bg-bg-body'}
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{checked ? (
|
{checked ? (
|
||||||
<SplashScreen />
|
<SplashScreen />
|
||||||
) : (
|
) : (
|
||||||
|
@ -53,7 +63,7 @@ function ProtectedRoutes() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && <StartLoading />}
|
{isLoading && <StartLoading />}
|
||||||
<Suspense>{platform.isTauri && <TauriAuth />}</Suspense>
|
{platform.isTauri && <TauriAuth />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -79,7 +89,7 @@ const StartLoading = () => {
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<div className={'fixed inset-0 z-[1400] flex h-full w-full items-center justify-center bg-bg-mask bg-opacity-50'}>
|
<div className={'bg-bg-mask fixed inset-0 z-[1400] flex h-full w-full items-center justify-center bg-opacity-50'}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { YDoc, YjsEditorKey } from '@/application/collab.type';
|
import { YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { DatabaseContextState } from '@/application/database-yjs';
|
||||||
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';
|
||||||
|
@ -8,15 +9,14 @@ 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';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
export const Database = memo(() => {
|
export const Database = memo((props?: { onNavigateToRow?: (viewId: string, rowId: string) => void }) => {
|
||||||
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<DatabaseContextState['rowDocMap'] | 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;
|
||||||
|
|
||||||
|
@ -52,11 +52,27 @@ export const Database = memo(() => {
|
||||||
|
|
||||||
const navigateToRow = useCallback(
|
const navigateToRow = useCallback(
|
||||||
(rowId: string) => {
|
(rowId: string) => {
|
||||||
|
const currentViewId = objectId || viewId;
|
||||||
|
|
||||||
|
if (props?.onNavigateToRow && currentViewId) {
|
||||||
|
props.onNavigateToRow(currentViewId, rowId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setSearch({ r: rowId });
|
setSearch({ r: rowId });
|
||||||
},
|
},
|
||||||
[setSearch]
|
[props, setSearch, viewId, objectId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const databaseId = doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database)?.get(YjsDatabaseKey.id) as string;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!databaseId || !databaseService) return;
|
||||||
|
return () => {
|
||||||
|
void databaseService.closeDatabase(databaseId);
|
||||||
|
};
|
||||||
|
}, [databaseService, databaseId]);
|
||||||
|
|
||||||
if (notFound || !objectId) {
|
if (notFound || !objectId) {
|
||||||
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
||||||
}
|
}
|
||||||
|
@ -74,7 +90,7 @@ export const Database = memo(() => {
|
||||||
<DatabaseContextProvider
|
<DatabaseContextProvider
|
||||||
navigateToRow={navigateToRow}
|
navigateToRow={navigateToRow}
|
||||||
viewId={viewId || objectId}
|
viewId={viewId || objectId}
|
||||||
doc={doc}
|
databaseDoc={doc}
|
||||||
rowDocMap={rows}
|
rowDocMap={rows}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { YDoc, YjsEditorKey } from '@/application/collab.type';
|
import { YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
|
import { DatabaseContextState } from '@/application/database-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
|
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row';
|
import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row';
|
||||||
import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader';
|
import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader';
|
||||||
|
@ -7,16 +9,16 @@ import { DatabaseContextProvider } from '@/components/database/DatabaseContext';
|
||||||
import { Log } from '@/utils/log';
|
import { Log } from '@/utils/log';
|
||||||
import { Divider } from '@mui/material';
|
import { Divider } from '@mui/material';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { Suspense, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
function DatabaseRow({ rowId }: { rowId: string }) {
|
function DatabaseRow({ rowId }: { rowId: string }) {
|
||||||
const { objectId, workspaceId } = useId() || {};
|
const { objectId, workspaceId } = useId() || {};
|
||||||
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<DatabaseContextState['rowDocMap'] | null>(null); // Map<rowId, YDoc
|
||||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
const [notFound, setNotFound] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleOpenDatabaseRow = useCallback(async () => {
|
const handleOpenDatabaseRow = useCallback(async () => {
|
||||||
if (!databaseService || !workspaceId || !objectId) return;
|
if (!databaseService || !workspaceId || !objectId) return;
|
||||||
|
|
||||||
|
@ -24,16 +26,6 @@ function DatabaseRow({ rowId }: { rowId: string }) {
|
||||||
setDoc(null);
|
setDoc(null);
|
||||||
const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId, [rowId]);
|
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);
|
setDoc(databaseDoc);
|
||||||
setRows(rows);
|
setRows(rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -41,12 +33,20 @@ function DatabaseRow({ rowId }: { rowId: string }) {
|
||||||
setNotFound(true);
|
setNotFound(true);
|
||||||
}
|
}
|
||||||
}, [databaseService, workspaceId, objectId, rowId]);
|
}, [databaseService, workspaceId, objectId, rowId]);
|
||||||
|
const databaseId = doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database)?.get(YjsDatabaseKey.id) as string;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotFound(false);
|
setNotFound(false);
|
||||||
void handleOpenDatabaseRow();
|
void handleOpenDatabaseRow();
|
||||||
}, [handleOpenDatabaseRow]);
|
}, [handleOpenDatabaseRow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!databaseId || !databaseService) return;
|
||||||
|
return () => {
|
||||||
|
void databaseService.closeDatabase(databaseId);
|
||||||
|
};
|
||||||
|
}, [databaseService, databaseId]);
|
||||||
|
|
||||||
if (notFound || !objectId) {
|
if (notFound || !objectId) {
|
||||||
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
||||||
}
|
}
|
||||||
|
@ -60,19 +60,31 @@ function DatabaseRow({ rowId }: { rowId: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-col items-center'}>
|
<div className={'flex w-full justify-center'}>
|
||||||
<div className={'max-w-screen relative flex w-[964px] min-w-0 flex-col gap-4'}>
|
<div className={'max-w-screen w-[964px] min-w-0'}>
|
||||||
<DatabaseContextProvider viewId={objectId} doc={doc} rowDocMap={rows} readOnly={true}>
|
<div className={' relative flex flex-col gap-4'}>
|
||||||
|
<DatabaseContextProvider
|
||||||
|
isDatabaseRowPage={true}
|
||||||
|
viewId={objectId}
|
||||||
|
databaseDoc={doc}
|
||||||
|
rowDocMap={rows}
|
||||||
|
readOnly={true}
|
||||||
|
>
|
||||||
<DatabaseRowHeader rowId={rowId} />
|
<DatabaseRowHeader rowId={rowId} />
|
||||||
|
|
||||||
<div className={'flex flex-1 flex-col gap-4'}>
|
<div className={'flex flex-1 flex-col gap-4'}>
|
||||||
|
<Suspense>
|
||||||
<DatabaseRowProperties rowId={rowId} />
|
<DatabaseRowProperties rowId={rowId} />
|
||||||
|
</Suspense>
|
||||||
<Divider className={'mx-16 max-md:mx-4'} />
|
<Divider className={'mx-16 max-md:mx-4'} />
|
||||||
|
<Suspense fallback={<ComponentLoading />}>
|
||||||
<DatabaseRowSubDocument rowId={rowId} />
|
<DatabaseRowSubDocument rowId={rowId} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</DatabaseContextProvider>
|
</DatabaseContextProvider>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type';
|
import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { useDatabaseViewsSelector } from '@/application/database-yjs';
|
import { useDatabaseViewsSelector } from '@/application/database-yjs';
|
||||||
|
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
||||||
import { Board } from '@/components/database/board';
|
import { Board } from '@/components/database/board';
|
||||||
import { Calendar } from '@/components/database/calendar';
|
import { Calendar } from '@/components/database/calendar';
|
||||||
import { DatabaseConditionsContext } from '@/components/database/components/conditions/context';
|
import { DatabaseConditionsContext } from '@/components/database/components/conditions/context';
|
||||||
import { DatabaseTabs } from '@/components/database/components/tabs';
|
import { DatabaseTabs } from '@/components/database/components/tabs';
|
||||||
import { Grid } from '@/components/database/grid';
|
import { Grid } from '@/components/database/grid';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import { ElementFallbackRender } from '@/components/error/ElementFallbackRender';
|
||||||
|
import React, { Suspense, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions';
|
||||||
|
|
||||||
function DatabaseViews({
|
function DatabaseViews({
|
||||||
|
@ -58,7 +61,11 @@ function DatabaseViews({
|
||||||
<DatabaseTabs selectedViewId={currentViewId} setSelectedViewId={onChangeView} viewIds={viewIds} />
|
<DatabaseTabs selectedViewId={currentViewId} setSelectedViewId={onChangeView} viewIds={viewIds} />
|
||||||
<DatabaseConditions />
|
<DatabaseConditions />
|
||||||
</DatabaseConditionsContext.Provider>
|
</DatabaseConditionsContext.Provider>
|
||||||
<div className={'flex h-full w-full flex-1 flex-col overflow-hidden'}>{view}</div>
|
<div className={'flex h-full w-full flex-1 flex-col overflow-hidden'}>
|
||||||
|
<Suspense fallback={<ComponentLoading />}>
|
||||||
|
<ErrorBoundary fallbackRender={ElementFallbackRender}>{view}</ErrorBoundary>
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { useDatabase, useGroupsSelector } from '@/application/database-yjs';
|
||||||
import { Group } from '@/components/database/components/board';
|
import { Group } from '@/components/database/components/board';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
export function Board() {
|
export function Board() {
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
|
@ -17,17 +16,11 @@ export function Board() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext
|
|
||||||
onDragEnd={() => {
|
|
||||||
//
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={'grid-board flex w-full flex-1 flex-col'}>
|
<div className={'grid-board flex w-full flex-1 flex-col'}>
|
||||||
{groups.map((groupId) => (
|
{groups.map((groupId) => (
|
||||||
<Group key={groupId} groupId={groupId} />
|
<Group key={groupId} groupId={groupId} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</DragDropContext>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export * from './Board';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const Board = lazy(() => import('./Board'));
|
||||||
|
|
|
@ -12,8 +12,7 @@ $today-highlight-bg: transparent;
|
||||||
|
|
||||||
|
|
||||||
.rbc-date-cell, .rbc-header {
|
.rbc-date-cell, .rbc-header {
|
||||||
min-width: 120px;
|
min-width: 97px;
|
||||||
max-width: 180px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rbc-date-cell.rbc-now {
|
.rbc-date-cell.rbc-now {
|
||||||
|
@ -82,7 +81,8 @@ $today-highlight-bg: transparent;
|
||||||
.rbc-month-row {
|
.rbc-month-row {
|
||||||
display: inline-table !important;
|
display: inline-table !important;
|
||||||
flex: 0 0 0 !important;
|
flex: 0 0 0 !important;
|
||||||
min-height: 120px !important;
|
min-height: 97px !important;
|
||||||
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-properties {
|
.event-properties {
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export * from './Calendar';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const Calendar = lazy(() => import('./Calendar'));
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { useFieldsSelector } from '@/application/database-yjs';
|
import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs';
|
||||||
|
import OpenAction from '@/components/database/components/database-row/OpenAction';
|
||||||
import CardField from '@/components/database/components/field/CardField';
|
import CardField from '@/components/database/components/field/CardField';
|
||||||
|
import { getPlatform } from '@/utils/platform';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
export interface CardProps {
|
export interface CardProps {
|
||||||
|
@ -13,6 +15,7 @@ export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) {
|
||||||
const fields = useFieldsSelector();
|
const fields = useFieldsSelector();
|
||||||
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]);
|
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]);
|
||||||
|
|
||||||
|
const [isHovering, setIsHovering] = React.useState(false);
|
||||||
const ref = React.useRef<HTMLDivElement | null>(null);
|
const ref = React.useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -32,17 +35,33 @@ export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) {
|
||||||
};
|
};
|
||||||
}, [onResize, isDragging]);
|
}, [onResize, isDragging]);
|
||||||
|
|
||||||
|
const isMobile = useMemo(() => {
|
||||||
|
return getPlatform().isMobile;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navigateToRow = useNavigateToRow();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (isMobile) {
|
||||||
|
navigateToRow?.(rowId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
style={{
|
style={{
|
||||||
minHeight: '38px',
|
minHeight: '38px',
|
||||||
}}
|
}}
|
||||||
className='flex cursor-pointer flex-col rounded-lg border border-line-divider p-3 shadow-sm hover:bg-fill-list-active hover:shadow'
|
className='relative flex cursor-pointer flex-col rounded-lg border border-line-divider p-3 text-xs shadow-sm hover:bg-fill-list-active hover:shadow'
|
||||||
>
|
>
|
||||||
{showFields.map((field, index) => {
|
{showFields.map((field, index) => {
|
||||||
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
||||||
})}
|
})}
|
||||||
|
<div className={`absolute top-1.5 right-1.5 ${isHovering ? 'block' : 'hidden'}`}>
|
||||||
|
<OpenAction rowId={rowId} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import ListItem from '@/components/database/components/board/column/ListItem';
|
||||||
import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn';
|
import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn';
|
||||||
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
|
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { Draggable, DraggableProvided, Droppable } from 'react-beautiful-dnd';
|
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { VariableSizeList } from 'react-window';
|
import { VariableSizeList } from 'react-window';
|
||||||
|
|
||||||
|
@ -13,10 +12,9 @@ export interface ColumnProps {
|
||||||
id: string;
|
id: string;
|
||||||
rows?: Row[];
|
rows?: Row[];
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
provided: DraggableProvided;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Column({ id, rows, fieldId, provided }: ColumnProps) {
|
export function Column({ id, rows, fieldId }: ColumnProps) {
|
||||||
const { header } = useRenderColumn(id, fieldId);
|
const { header } = useRenderColumn(id, fieldId);
|
||||||
const ref = React.useRef<VariableSizeList | null>(null);
|
const ref = React.useRef<VariableSizeList | null>(null);
|
||||||
const forceUpdate = useCallback((index: number) => {
|
const forceUpdate = useCallback((index: number) => {
|
||||||
|
@ -54,13 +52,7 @@ export function Column({ id, rows, fieldId, provided }: ColumnProps) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <ListItem fieldId={fieldId} onResize={onResizeCallback} item={item} style={style} />;
|
||||||
<Draggable isDragDisabled draggableId={item.id} index={index} key={item.id}>
|
|
||||||
{(provided) => (
|
|
||||||
<ListItem fieldId={fieldId} onResize={onResizeCallback} provided={provided} item={item} style={style} />
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[fieldId, onResize]
|
[fieldId, onResize]
|
||||||
);
|
);
|
||||||
|
@ -75,45 +67,25 @@ export function Column({ id, rows, fieldId, provided }: ColumnProps) {
|
||||||
},
|
},
|
||||||
[rowHeight, rows]
|
[rowHeight, rows]
|
||||||
);
|
);
|
||||||
|
const rowCount = rows?.length || 0;
|
||||||
|
|
||||||
if (!rows) return <div ref={provided.innerRef} />;
|
|
||||||
return (
|
return (
|
||||||
<div key={id} className='column flex w-[230px] flex-col gap-4' {...provided.draggableProps} ref={provided.innerRef}>
|
<div key={id} className='column flex w-[230px] flex-col gap-4'>
|
||||||
<div className='column-header flex h-[24px] items-center text-xs font-medium' {...provided.dragHandleProps}>
|
<div className='column-header flex h-[24px] items-center text-xs font-medium'>
|
||||||
<Tag label={header?.name} color={header?.color} />
|
<Tag label={header?.name} color={header?.color} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'w-full flex-1 overflow-hidden'}>
|
<div className={'w-full flex-1 overflow-hidden'}>
|
||||||
<Droppable
|
|
||||||
droppableId={`column-${id}`}
|
|
||||||
mode='virtual'
|
|
||||||
renderClone={(provided, snapshot, rubric) => (
|
|
||||||
<ListItem
|
|
||||||
provided={provided}
|
|
||||||
isDragging={snapshot.isDragging}
|
|
||||||
item={rows[rubric.source.index]}
|
|
||||||
fieldId={fieldId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{(provided, snapshot) => {
|
|
||||||
// Add an extra item to our list to make space for a dragging item
|
|
||||||
// Usually the DroppableProvided.placeholder does this, but that won't
|
|
||||||
// work in a virtual list
|
|
||||||
const itemCount = snapshot.isUsingPlaceholder ? rows.length + 1 : rows.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }: { height: number; width: number }) => {
|
{({ height, width }: { height: number; width: number }) => {
|
||||||
return (
|
return (
|
||||||
<VariableSizeList
|
<VariableSizeList
|
||||||
ref={ref}
|
ref={ref}
|
||||||
height={height}
|
height={height}
|
||||||
itemCount={itemCount}
|
itemCount={rowCount}
|
||||||
itemSize={getItemSize}
|
itemSize={getItemSize}
|
||||||
width={width}
|
width={width}
|
||||||
outerElementType={AFScroller}
|
outerElementType={AFScroller}
|
||||||
outerRef={provided.innerRef}
|
|
||||||
itemData={rows}
|
itemData={rows}
|
||||||
>
|
>
|
||||||
{Row}
|
{Row}
|
||||||
|
@ -121,9 +93,6 @@ export function Column({ id, rows, fieldId, provided }: ColumnProps) {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Droppable>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,74 +1,29 @@
|
||||||
import { Row } from '@/application/database-yjs';
|
import { Row } from '@/application/database-yjs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { DraggableProvided, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd';
|
|
||||||
import Card from 'src/components/database/components/board/card/Card';
|
import Card from 'src/components/database/components/board/card/Card';
|
||||||
|
|
||||||
export const ListItem = ({
|
export const ListItem = ({
|
||||||
provided,
|
|
||||||
item,
|
item,
|
||||||
style,
|
style,
|
||||||
onResize,
|
onResize,
|
||||||
fieldId,
|
fieldId,
|
||||||
isDragging,
|
|
||||||
}: {
|
}: {
|
||||||
provided: DraggableProvided;
|
item?: Row;
|
||||||
item: Row;
|
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
onResize?: (height: number) => void;
|
onResize?: (height: number) => void;
|
||||||
isDragging?: boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={provided.innerRef}
|
style={{
|
||||||
{...provided.draggableProps}
|
...style,
|
||||||
{...provided.dragHandleProps}
|
width: 'calc(100% - 2px)',
|
||||||
style={getStyle({
|
}}
|
||||||
draggableStyle: provided.draggableProps.style,
|
className={`w-full bg-bg-body`}
|
||||||
virtualStyle: style,
|
|
||||||
isDragging,
|
|
||||||
})}
|
|
||||||
className={`w-full bg-bg-body ${isDragging ? 'is-dragging' : ''}`}
|
|
||||||
>
|
>
|
||||||
<Card onResize={onResize} rowId={item.id} groupFieldId={fieldId} />
|
{item?.id ? <Card onResize={onResize} rowId={item.id} groupFieldId={fieldId} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getStyle({
|
|
||||||
draggableStyle,
|
|
||||||
virtualStyle,
|
|
||||||
isDragging,
|
|
||||||
}: {
|
|
||||||
draggableStyle?: DraggingStyle | NotDraggingStyle;
|
|
||||||
virtualStyle?: React.CSSProperties;
|
|
||||||
isDragging?: boolean;
|
|
||||||
}) {
|
|
||||||
// If you don't want any spacing between your items
|
|
||||||
// then you could just return this.
|
|
||||||
// I do a little bit of magic to have some nice visual space
|
|
||||||
// between the row items
|
|
||||||
const combined = {
|
|
||||||
...virtualStyle,
|
|
||||||
...draggableStyle,
|
|
||||||
} as {
|
|
||||||
height: number;
|
|
||||||
left: number;
|
|
||||||
width: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Being lazy: this is defined in our css file
|
|
||||||
const grid = 1;
|
|
||||||
|
|
||||||
// when dragging we want to use the draggable style for placement, otherwise use the virtual style
|
|
||||||
|
|
||||||
return {
|
|
||||||
...combined,
|
|
||||||
height: isDragging ? combined.height : combined.height - grid,
|
|
||||||
left: isDragging ? combined.left : combined.left + grid,
|
|
||||||
width: isDragging ? (draggableStyle as DraggingStyle)?.width : `calc(${combined.width} - ${grid * 2}px)`,
|
|
||||||
marginBottom: grid,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListItem;
|
export default ListItem;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useRowsByGroup } from '@/application/database-yjs';
|
import { useRowsByGroup } from '@/application/database-yjs';
|
||||||
import { AFScroller } from '@/components/_shared/scroller';
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Column } from '../column';
|
import { Column } from '../column';
|
||||||
|
|
||||||
|
@ -26,34 +25,11 @@ 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-16 max-md:px-4'}>
|
<AFScroller overflowYHidden className={'relative px-16 max-md:px-4'}>
|
||||||
<Droppable droppableId={`group-${groupId}`} direction='horizontal' type='column'>
|
<div className='columns flex h-full w-fit min-w-full gap-4 border-t border-line-divider py-4'>
|
||||||
{(provided) => {
|
{columns.map((data) => (
|
||||||
return (
|
<Column key={data.id} id={data.id} fieldId={fieldId} rows={groupResult.get(data.id)} />
|
||||||
<div
|
|
||||||
className='columns flex h-full w-fit gap-4 border-t border-line-divider py-4'
|
|
||||||
{...provided.droppableProps}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
|
||||||
{columns.map((data, index) => (
|
|
||||||
<Draggable isDragDisabled key={data.id} draggableId={`column-${data.id}`} index={index}>
|
|
||||||
{(provided) => {
|
|
||||||
return (
|
|
||||||
<Column
|
|
||||||
provided={provided}
|
|
||||||
key={data.id}
|
|
||||||
id={data.id}
|
|
||||||
fieldId={fieldId}
|
|
||||||
rows={groupResult.get(data.id)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Draggable>
|
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Droppable>
|
|
||||||
</AFScroller>
|
</AFScroller>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs';
|
import { CalendarEvent, useFieldsSelector, useNavigateToRow } from '@/application/database-yjs';
|
||||||
import { RichTooltip } from '@/components/_shared/popover';
|
import { RichTooltip } from '@/components/_shared/popover';
|
||||||
import EventPaper from '@/components/database/components/calendar/event/EventPaper';
|
import EventPaper from '@/components/database/components/calendar/event/EventPaper';
|
||||||
import CardField from '@/components/database/components/field/CardField';
|
import CardField from '@/components/database/components/field/CardField';
|
||||||
|
@ -11,19 +11,36 @@ export function Event({ event }: EventWrapperProps<CalendarEvent>) {
|
||||||
const fields = useFieldsSelector();
|
const fields = useFieldsSelector();
|
||||||
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]);
|
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]);
|
||||||
|
|
||||||
|
const navigateToRow = useNavigateToRow();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'px-1 py-0.5'}>
|
<div className={'px-1 py-0.5'}>
|
||||||
<RichTooltip content={<EventPaper rowId={rowId} />} open={open} placement='right' onClose={() => setOpen(false)}>
|
<RichTooltip content={<EventPaper rowId={rowId} />} open={open} placement='right' onClose={() => setOpen(false)}>
|
||||||
<div
|
<div
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
onClick={() => {
|
||||||
|
if (window.innerWidth < 768) {
|
||||||
|
navigateToRow?.(rowId);
|
||||||
|
} else {
|
||||||
|
setOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={
|
className={
|
||||||
'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 shadow-sm hover:bg-fill-list-active hover:shadow'
|
'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 text-xs shadow-sm hover:bg-fill-list-active hover:shadow'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{showFields.map((field) => {
|
{showFields.map((field) => {
|
||||||
return <CardField index={0} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;
|
return (
|
||||||
|
<div
|
||||||
|
key={field.fieldId}
|
||||||
|
style={{
|
||||||
|
fontSize: '0.85em',
|
||||||
|
}}
|
||||||
|
className={'overflow-x-hidden truncate'}
|
||||||
|
>
|
||||||
|
<CardField index={0} rowId={rowId} fieldId={field.fieldId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</RichTooltip>
|
</RichTooltip>
|
||||||
|
|
|
@ -1,32 +1,22 @@
|
||||||
import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs';
|
import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs';
|
||||||
|
import EventPaperTitle from '@/components/database/components/calendar/event/EventPaperTitle';
|
||||||
|
import OpenAction from '@/components/database/components/database-row/OpenAction';
|
||||||
import { Property } from '@/components/database/components/property';
|
import { Property } from '@/components/database/components/property';
|
||||||
import { Tooltip } from '@mui/material';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
function EventPaper({ rowId }: { rowId: string }) {
|
function EventPaper({ rowId }: { rowId: string }) {
|
||||||
const fields = useFieldsSelector();
|
const primaryFieldId = usePrimaryFieldId();
|
||||||
const navigateToRow = useNavigateToRow();
|
|
||||||
const { t } = useTranslation();
|
const fields = useFieldsSelector().filter((column) => column.fieldId !== primaryFieldId);
|
||||||
|
|
||||||
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'}>
|
||||||
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
|
<OpenAction rowId={rowId} />
|
||||||
<button
|
|
||||||
color={'primary'}
|
|
||||||
className={'rounded bg-bg-body p-1 hover:bg-fill-list-hover'}
|
|
||||||
onClick={() => {
|
|
||||||
navigateToRow?.(rowId);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandMoreIcon />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={'event-properties 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'}>
|
||||||
|
{primaryFieldId && <EventPaperTitle rowId={rowId} fieldId={primaryFieldId} />}
|
||||||
{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} />;
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { useCellSelector } from '@/application/database-yjs';
|
||||||
|
import { TextCell } from '@/components/database/components/cell/cell.type';
|
||||||
|
import { TextProperty } from '@/components/database/components/property/text';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function EventPaperTitle({ fieldId, rowId }: { fieldId: string; rowId: string }) {
|
||||||
|
const cell = useCellSelector({
|
||||||
|
fieldId,
|
||||||
|
rowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <TextProperty cell={cell as TextCell} fieldId={fieldId} rowId={rowId} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventPaperTitle;
|
|
@ -33,7 +33,7 @@ function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) {
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
variant={'outlined'}
|
variant={'outlined'}
|
||||||
className={'rounded-md border-line-divider '}
|
className={'whitespace-nowrap rounded-md border-line-divider'}
|
||||||
color={'inherit'}
|
color={'inherit'}
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -22,8 +22,8 @@ export function Toolbar({
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex items-center justify-between'}>
|
<div className={'flex items-center justify-between overflow-x-auto overflow-y-hidden'}>
|
||||||
<div className={'text-sm font-medium'}>{dateStr}</div>
|
<div className={'whitespace-nowrap text-sm font-medium'}>{dateStr}</div>
|
||||||
<div className={'flex items-center justify-end gap-2'}>
|
<div className={'flex items-center justify-end gap-2'}>
|
||||||
<IconButton size={'small'} onClick={() => onNavigate('PREV')}>
|
<IconButton size={'small'} onClick={() => onNavigate('PREV')}>
|
||||||
<LeftArrow />
|
<LeftArrow />
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
|
||||||
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
|
||||||
|
import { FieldType } from '@/application/database-yjs';
|
||||||
import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type';
|
||||||
|
|
||||||
export function CheckboxCell({ cell, style }: CellProps<CheckboxCellType>) {
|
export function CheckboxCell({ cell, style }: CellProps<CheckboxCellType>) {
|
||||||
const checked = cell?.data;
|
const checked = cell?.data;
|
||||||
|
|
||||||
|
if (cell?.fieldType !== FieldType.Checkbox) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className='relative flex w-full cursor-pointer items-center text-lg text-fill-default'>
|
<div style={style} className='relative flex w-full cursor-pointer items-center text-lg text-fill-default'>
|
||||||
{checked ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />}
|
{checked ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { parseChecklistData } from '@/application/database-yjs';
|
import { FieldType, parseChecklistData } from '@/application/database-yjs';
|
||||||
import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
@ -11,6 +11,8 @@ export function ChecklistCell({ cell, style, placeholder }: CellProps<ChecklistC
|
||||||
const options = data?.options;
|
const options = data?.options;
|
||||||
const selectedOptions = data?.selectedOptionIds;
|
const selectedOptions = data?.selectedOptionIds;
|
||||||
|
|
||||||
|
if (cell?.fieldType !== FieldType.Checklist) return null;
|
||||||
|
|
||||||
if (!data || !options || !selectedOptions)
|
if (!data || !options || !selectedOptions)
|
||||||
return placeholder ? (
|
return placeholder ? (
|
||||||
<div style={style} className={'text-text-placeholder'}>
|
<div style={style} className={'text-text-placeholder'}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { useRowData } from '@/application/database-yjs';
|
import { useRowDataSelector } 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,14 +15,12 @@ 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 rowData = useRowData(rowId);
|
const { row: rowData } = useRowDataSelector(rowId);
|
||||||
const [value, setValue] = useState<string | null>(null);
|
const [value, setValue] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!rowData) return;
|
if (!rowData) return;
|
||||||
const observeHandler = () => {
|
const observeHandler = () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error
|
|
||||||
setValue(rowData.get(attrName));
|
setValue(rowData.get(attrName));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FieldType } from '@/application/database-yjs';
|
||||||
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks';
|
||||||
import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
@ -20,11 +21,12 @@ export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps<Da
|
||||||
}, [cell, getDateTimeStr]);
|
}, [cell, getDateTimeStr]);
|
||||||
|
|
||||||
const dateStr = useMemo(() => {
|
const dateStr = useMemo(() => {
|
||||||
return [startDateTime, endDateTime].filter(Boolean).join(' -> ');
|
return [startDateTime, endDateTime].filter(Boolean).join(' - ');
|
||||||
}, [startDateTime, endDateTime]);
|
}, [startDateTime, endDateTime]);
|
||||||
|
|
||||||
const hasReminder = !!cell?.reminderId;
|
const hasReminder = !!cell?.reminderId;
|
||||||
|
|
||||||
|
if (cell?.fieldType !== FieldType.DateTime) return null;
|
||||||
if (!cell?.data)
|
if (!cell?.data)
|
||||||
return placeholder ? (
|
return placeholder ? (
|
||||||
<div style={style} className={'text-text-placeholder'}>
|
<div style={style} className={'text-text-placeholder'}>
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs';
|
import {
|
||||||
|
currencyFormaterMap,
|
||||||
|
NumberFormat,
|
||||||
|
useFieldSelector,
|
||||||
|
parseNumberTypeOptions,
|
||||||
|
FieldType,
|
||||||
|
} from '@/application/database-yjs';
|
||||||
import { CellProps, NumberCell as NumberCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, NumberCell as NumberCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import Decimal from 'decimal.js';
|
import Decimal from 'decimal.js';
|
||||||
|
@ -15,7 +21,7 @@ export function NumberCell({ cell, fieldId, style, placeholder }: CellProps<Numb
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
if (!cell) return '';
|
if (!cell || cell.fieldType !== FieldType.Number) return '';
|
||||||
const numberFormater = currencyFormaterMap[format];
|
const numberFormater = currencyFormaterMap[format];
|
||||||
|
|
||||||
if (!numberFormater) return cell.data;
|
if (!numberFormater) return cell.data;
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
|
||||||
import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs';
|
import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs';
|
||||||
import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type';
|
import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type';
|
||||||
import { TextCell } from '@/components/database/components/cell/text';
|
import { TextCell } from '@/components/database/components/cell/text';
|
||||||
import { Tooltip } from '@mui/material';
|
import OpenAction from '@/components/database/components/database-row/OpenAction';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { getPlatform } from '@/utils/platform';
|
||||||
import { useTranslation } from 'react-i18next';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
export function PrimaryCell(props: CellProps<CellType>) {
|
export function PrimaryCell(props: CellProps<CellType>) {
|
||||||
const navigateToRow = useNavigateToRow();
|
|
||||||
const { rowId } = props;
|
const { rowId } = props;
|
||||||
const icon = useRowMetaSelector(rowId)?.icon;
|
const meta = useRowMetaSelector(rowId);
|
||||||
|
const icon = meta?.icon;
|
||||||
|
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const table = document.querySelector('.grid-table');
|
const table = document.querySelector('.grid-table');
|
||||||
|
@ -31,32 +29,42 @@ export function PrimaryCell(props: CellProps<CellType>) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
setHover(false);
|
||||||
|
};
|
||||||
|
|
||||||
table.addEventListener('mousemove', onMouseMove);
|
table.addEventListener('mousemove', onMouseMove);
|
||||||
|
table.addEventListener('mouseleave', onMouseLeave);
|
||||||
return () => {
|
return () => {
|
||||||
table.removeEventListener('mousemove', onMouseMove);
|
table.removeEventListener('mousemove', onMouseMove);
|
||||||
|
table.removeEventListener('mouseleave', onMouseLeave);
|
||||||
};
|
};
|
||||||
}, [rowId]);
|
}, [rowId]);
|
||||||
|
|
||||||
|
const isMobile = useMemo(() => {
|
||||||
|
return getPlatform().isMobile;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const navigateToRow = useNavigateToRow();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'primary-cell relative flex min-h-full w-full items-center gap-2'}>
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
if (isMobile) {
|
||||||
|
navigateToRow?.(rowId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={'primary-cell relative flex min-h-full w-full items-center gap-2'}
|
||||||
|
>
|
||||||
{icon && <div className={'h-4 w-4'}>{icon}</div>}
|
{icon && <div className={'h-4 w-4'}>{icon}</div>}
|
||||||
<div className={'flex-1 overflow-x-hidden'}>
|
<div className={'flex-1 overflow-x-hidden'}>
|
||||||
<TextCell {...props} />
|
<TextCell {...props} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hover && (
|
{hover && (
|
||||||
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
|
<div className={'absolute right-0 top-1/2 min-w-0 -translate-y-1/2 transform '}>
|
||||||
<button
|
<OpenAction rowId={rowId} />
|
||||||
color={'primary'}
|
</div>
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
|
import { FieldType } from '@/application/database-yjs';
|
||||||
import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import RelationItems from '@/components/database/components/cell/relation/RelationItems';
|
import RelationItems from '@/components/database/components/cell/relation/RelationItems';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export function RelationCell({ cell, fieldId, style, placeholder }: CellProps<RelationCellType>) {
|
export function RelationCell({ cell, fieldId, style, placeholder }: CellProps<RelationCellType>) {
|
||||||
|
if (cell?.fieldType !== FieldType.Relation) return null;
|
||||||
|
|
||||||
if (!cell?.data)
|
if (!cell?.data)
|
||||||
return placeholder ? (
|
return placeholder ? (
|
||||||
<div style={style} className={'text-text-placeholder'}>
|
<div style={style} className={'text-text-placeholder'}>
|
||||||
{placeholder}
|
{placeholder}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return <RelationItems cell={cell} fieldId={fieldId} style={style} />;
|
return <RelationItems cell={cell} fieldId={fieldId} style={style} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,33 @@
|
||||||
import { YDatabaseField, YDatabaseFields, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
import { YDatabaseField, YDatabaseFields, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { parseRelationTypeOption, useFieldSelector } from '@/application/database-yjs';
|
import {
|
||||||
|
DatabaseContextState,
|
||||||
|
parseRelationTypeOption,
|
||||||
|
useDatabase,
|
||||||
|
useFieldSelector,
|
||||||
|
useNavigateToRow,
|
||||||
|
} from '@/application/database-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type';
|
||||||
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
|
||||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import * as Y from 'yjs';
|
|
||||||
|
|
||||||
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
|
||||||
const { field } = useFieldSelector(fieldId);
|
const { field } = useFieldSelector(fieldId);
|
||||||
|
const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id);
|
||||||
const workspaceId = useId()?.workspaceId;
|
const workspaceId = useId()?.workspaceId;
|
||||||
const rowIds = useMemo(() => (cell.data.toJSON() as RelationCellData) ?? [], [cell.data]);
|
const rowIds = useMemo(() => {
|
||||||
|
return (cell.data?.toJSON() as RelationCellData) ?? [];
|
||||||
|
}, [cell.data]);
|
||||||
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined;
|
||||||
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
const databaseService = useContext(AFConfigContext)?.service?.databaseService;
|
||||||
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState<string | undefined>(undefined);
|
||||||
const [rows, setRows] = useState<Y.Map<YDoc> | null>();
|
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>();
|
||||||
|
|
||||||
|
const navigateToRow = useNavigateToRow();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workspaceId || !databaseId) return;
|
if (!workspaceId || !databaseId || !rowIds.length) return;
|
||||||
void databaseService?.getDatabase(workspaceId, databaseId, rowIds).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)
|
||||||
|
@ -34,13 +44,28 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId:
|
||||||
});
|
});
|
||||||
}, [workspaceId, databaseId, databaseService, rowIds]);
|
}, [workspaceId, databaseId, databaseService, rowIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (currentDatabaseId !== databaseId && databaseId) {
|
||||||
|
void databaseService?.closeDatabase(databaseId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [currentDatabaseId, databaseId, databaseService]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style} className={'relation-cell flex w-full items-center gap-2'}>
|
<div style={style} className={'relation-cell flex w-full items-center gap-2'}>
|
||||||
{rowIds.map((rowId) => {
|
{rowIds.map((rowId) => {
|
||||||
const rowDoc = rows?.get(rowId);
|
const rowDoc = rows?.get(rowId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={rowId} className={'w-full cursor-pointer underline'}>
|
<div
|
||||||
|
key={rowId}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigateToRow?.(rowId);
|
||||||
|
}}
|
||||||
|
className={'w-full cursor-pointer underline'}
|
||||||
|
>
|
||||||
{rowDoc && databasePrimaryFieldId && (
|
{rowDoc && databasePrimaryFieldId && (
|
||||||
<RelationPrimaryValue rowDoc={rowDoc} fieldId={databasePrimaryFieldId} />
|
<RelationPrimaryValue rowDoc={rowDoc} fieldId={databasePrimaryFieldId} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -4,9 +4,24 @@ import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {
|
export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) {
|
||||||
const [text, setText] = useState<string | null>(null);
|
const [text, setText] = useState<string | null>(null);
|
||||||
|
const [row, setRow] = useState<YDatabaseRow | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
|
const data = rowDoc.getMap(YjsEditorKey.data_section);
|
||||||
|
|
||||||
|
const onRowChange = () => {
|
||||||
|
setRow(data?.get(YjsEditorKey.database_row) as YDatabaseRow);
|
||||||
|
};
|
||||||
|
|
||||||
|
onRowChange();
|
||||||
|
data?.observe(onRowChange);
|
||||||
|
return () => {
|
||||||
|
data?.unobserve(onRowChange);
|
||||||
|
};
|
||||||
|
}, [rowDoc]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!row) return;
|
||||||
const cells = row.get(YjsDatabaseKey.cells);
|
const cells = row.get(YjsDatabaseKey.cells);
|
||||||
const primaryCell = cells.get(fieldId);
|
const primaryCell = cells.get(fieldId);
|
||||||
|
|
||||||
|
@ -21,7 +36,7 @@ export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldI
|
||||||
return () => {
|
return () => {
|
||||||
primaryCell.unobserve(observeHandler);
|
primaryCell.unobserve(observeHandler);
|
||||||
};
|
};
|
||||||
}, [rowDoc, fieldId]);
|
}, [row, fieldId]);
|
||||||
|
|
||||||
return <div>{text}</div>;
|
return <div>{text}</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,15 @@ import { useReadOnly } from '@/application/database-yjs';
|
||||||
import { CellProps, TextCell as TextCellType } from '@/components/database/components/cell/cell.type';
|
import { CellProps, TextCell as TextCellType } from '@/components/database/components/cell/cell.type';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export function TextCell({ cell, style }: CellProps<TextCellType>) {
|
export function TextCell({ cell, style, placeholder }: CellProps<TextCellType>) {
|
||||||
const readOnly = useReadOnly();
|
const readOnly = useReadOnly();
|
||||||
|
|
||||||
if (!cell?.data) return null;
|
if (!cell?.data)
|
||||||
|
return placeholder ? (
|
||||||
|
<div style={style} className={'text-text-placeholder'}>
|
||||||
|
{placeholder}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
return (
|
return (
|
||||||
<div style={style} className={`text-cell w-full cursor-text leading-[1.2] ${readOnly ? 'select-text' : ''}`}>
|
<div style={style} className={`text-cell w-full cursor-text leading-[1.2] ${readOnly ? 'select-text' : ''}`}>
|
||||||
{cell?.data}
|
{cell?.data}
|
||||||
|
|
|
@ -30,9 +30,10 @@ export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
if (!isUrl || !cell) return;
|
if (!isUrl || !cell) return;
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
|
e.stopPropagation();
|
||||||
void openUrl(cell.data, '_blank');
|
void openUrl(cell.data, '_blank');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type';
|
import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs';
|
||||||
import { useDatabaseView, useFiltersSelector, useSortsSelector } from '@/application/database-yjs';
|
|
||||||
import { useConditionsContext } from '@/components/database/components/conditions/context';
|
import { useConditionsContext } from '@/components/database/components/conditions/context';
|
||||||
import { TextButton } from '@/components/database/components/tabs/TextButton';
|
import { TextButton } from '@/components/database/components/tabs/TextButton';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -7,16 +6,11 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export function DatabaseActions() {
|
export function DatabaseActions() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const view = useDatabaseView();
|
|
||||||
const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
|
||||||
const sorts = useSortsSelector();
|
const sorts = useSortsSelector();
|
||||||
const filter = useFiltersSelector();
|
const filter = useFiltersSelector();
|
||||||
const conditionsContext = useConditionsContext();
|
const conditionsContext = useConditionsContext();
|
||||||
|
|
||||||
if (layout === DatabaseViewLayout.Calendar) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex w-[120px] items-center justify-end gap-1.5'>
|
<div className='flex w-[120px] items-center justify-end gap-1.5'>
|
||||||
<TextButton
|
<TextButton
|
||||||
|
|
|
@ -3,17 +3,17 @@ import { useRowMetaSelector } from '@/application/database-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { Editor } from '@/components/editor';
|
import { Editor } from '@/components/editor';
|
||||||
import { Log } from '@/utils/log';
|
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import RecordNotFound from '@/components/_shared/not-found/RecordNotFound';
|
|
||||||
|
|
||||||
export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
||||||
const { workspaceId } = useId() || {};
|
const { workspaceId } = useId() || {};
|
||||||
const documentId = useRowMetaSelector(rowId)?.documentId;
|
const meta = useRowMetaSelector(rowId);
|
||||||
|
const documentId = meta?.documentId;
|
||||||
|
|
||||||
|
console.log('documentId', documentId);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
||||||
|
|
||||||
|
@ -23,31 +23,30 @@ export function DatabaseRowSubDocument({ rowId }: { rowId: string }) {
|
||||||
setDoc(null);
|
setDoc(null);
|
||||||
const doc = await documentService.openDocument(workspaceId, documentId);
|
const doc = await documentService.openDocument(workspaceId, documentId);
|
||||||
|
|
||||||
|
console.log('doc', doc);
|
||||||
setDoc(doc);
|
setDoc(doc);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.error(e);
|
console.error(e);
|
||||||
setNotFound(true);
|
// haven't created by client, ignore error and show empty
|
||||||
}
|
}
|
||||||
}, [documentService, workspaceId, documentId]);
|
}, [documentService, workspaceId, documentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotFound(false);
|
setLoading(true);
|
||||||
void handleOpenDocument();
|
void handleOpenDocument().then(() => setLoading(false));
|
||||||
}, [handleOpenDocument]);
|
}, [handleOpenDocument]);
|
||||||
|
|
||||||
if (notFound || !documentId) {
|
if (loading) {
|
||||||
return <RecordNotFound open={notFound} workspaceId={workspaceId} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doc) {
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-full w-full items-center justify-center'}>
|
<div className={'flex h-[260px] w-full items-center justify-center'}>
|
||||||
<CircularProgress />
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Editor doc={doc} readOnly={true} includeRoot={false} />;
|
if (!doc) return null;
|
||||||
|
|
||||||
|
return <Editor doc={doc} readOnly={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DatabaseRowSubDocument;
|
export default DatabaseRowSubDocument;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigateToRow } from '@/application/database-yjs';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
function OpenAction({ rowId }: { rowId: string }) {
|
||||||
|
const navigateToRow = useNavigateToRow();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
|
||||||
|
<button
|
||||||
|
color={'primary'}
|
||||||
|
className={'rounded border border-line-divider bg-bg-body p-1 hover:bg-fill-list-hover'}
|
||||||
|
onClick={() => {
|
||||||
|
navigateToRow?.(rowId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandMoreIcon />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenAction;
|
|
@ -14,13 +14,11 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string;
|
||||||
|
|
||||||
const isPrimary = field?.get(YjsDatabaseKey.is_primary);
|
const isPrimary = field?.get(YjsDatabaseKey.is_primary);
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
const styleProperties = {
|
const styleProperties = {};
|
||||||
fontSize: '12px',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isPrimary) {
|
if (isPrimary) {
|
||||||
Object.assign(styleProperties, {
|
Object.assign(styleProperties, {
|
||||||
fontSize: '14px',
|
fontSize: '1.25em',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { YjsDatabaseKey } from '@/application/collab.type';
|
||||||
import { FieldType } from '@/application/database-yjs/database.type';
|
import { FieldType } from '@/application/database-yjs/database.type';
|
||||||
import { Column, useFieldSelector } from '@/application/database-yjs/selector';
|
import { Column, useFieldSelector } from '@/application/database-yjs/selector';
|
||||||
import { FieldTypeIcon } from '@/components/database/components/field';
|
import { FieldTypeIcon } from '@/components/database/components/field';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
export function GridColumn({ column, index }: { column: Column; index: number }) {
|
export function GridColumn({ column, index }: { column: Column; index: number }) {
|
||||||
|
@ -16,6 +17,7 @@ export function GridColumn({ column, index }: { column: Column; index: number })
|
||||||
}, [field]);
|
}, [field]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Tooltip title={name} enterNextDelay={1000} placement={'right'}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
borderLeftWidth: index === 1 ? 0 : 1,
|
borderLeftWidth: index === 1 ? 0 : 1,
|
||||||
|
@ -29,6 +31,7 @@ export function GridColumn({ column, index }: { column: Column; index: number })
|
||||||
</div>
|
</div>
|
||||||
<div className={'flex-1'}>{name}</div>
|
<div className={'flex-1'}>{name}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
console.log(ref.current, scrollLeft);
|
|
||||||
ref.current.scrollTo({ scrollLeft });
|
ref.current.scrollTo({ scrollLeft });
|
||||||
}
|
}
|
||||||
}, [scrollLeft]);
|
}, [scrollLeft]);
|
||||||
|
|
|
@ -1,15 +1,35 @@
|
||||||
import { useCellSelector, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs';
|
import { useCellSelector, useDatabaseViewId, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs';
|
||||||
|
import { FolderContext } from '@/application/folder-yjs';
|
||||||
import Title from '@/components/database/components/header/Title';
|
import Title from '@/components/database/components/header/Title';
|
||||||
import React from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
|
|
||||||
function DatabaseRowHeader({ rowId }: { rowId: string }) {
|
function DatabaseRowHeader({ rowId }: { rowId: string }) {
|
||||||
const fieldId = usePrimaryFieldId() || '';
|
const fieldId = usePrimaryFieldId() || '';
|
||||||
|
const setCrumbs = useContext(FolderContext)?.setCrumbs;
|
||||||
|
const viewId = useDatabaseViewId();
|
||||||
|
|
||||||
const meta = useRowMetaSelector(rowId);
|
const meta = useRowMetaSelector(rowId);
|
||||||
const cell = useCellSelector({
|
const cell = useCellSelector({
|
||||||
rowId,
|
rowId,
|
||||||
fieldId,
|
fieldId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewId) return;
|
||||||
|
setCrumbs?.((prev) => {
|
||||||
|
const lastCrumb = prev[prev.length - 1];
|
||||||
|
const crumb = {
|
||||||
|
viewId,
|
||||||
|
rowId,
|
||||||
|
name: cell?.data as string,
|
||||||
|
icon: meta?.icon || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastCrumb?.rowId === rowId) return [...prev.slice(0, -1), crumb];
|
||||||
|
return [...prev, crumb];
|
||||||
|
});
|
||||||
|
}, [cell, meta, rowId, setCrumbs, viewId]);
|
||||||
|
|
||||||
return <Title icon={meta?.icon} name={cell?.data as string} />;
|
return <Title icon={meta?.icon} name={cell?.data as string} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { YjsDatabaseKey } from '@/application/collab.type';
|
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 } from '@/components/database/components/cell/cell.type';
|
||||||
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
import { CheckboxCell } from '@/components/database/components/cell/checkbox';
|
||||||
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';
|
||||||
import { RelationCell } from '@/components/database/components/cell/relation';
|
import { RelationCell } from '@/components/database/components/cell/relation';
|
||||||
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
import { SelectOptionCell } from '@/components/database/components/cell/select-option';
|
||||||
|
import { TextCell } from '@/components/database/components/cell/text';
|
||||||
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';
|
||||||
|
@ -42,6 +43,8 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string })
|
||||||
return ChecklistProperty;
|
return ChecklistProperty;
|
||||||
case FieldType.Relation:
|
case FieldType.Relation:
|
||||||
return RelationCell;
|
return RelationCell;
|
||||||
|
case FieldType.RichText:
|
||||||
|
return TextCell;
|
||||||
default:
|
default:
|
||||||
return TextProperty;
|
return TextProperty;
|
||||||
}
|
}
|
||||||
|
@ -54,10 +57,6 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string })
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fieldType === FieldType.RichText) {
|
|
||||||
return <TextProperty cell={cell as TextCell} fieldId={fieldId} rowId={rowId} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) {
|
if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) {
|
||||||
const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified;
|
const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ function PropertyWrapper({ fieldId, children }: { fieldId: string; children: Rea
|
||||||
<div className={'property-label flex h-[28px] w-[30%] items-center'}>
|
<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 items-center overflow-x-hidden pr-1'}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ export function ChecklistProperty(props: CellProps<CellType>) {
|
||||||
const selectedOptions = data?.selectedOptionIds;
|
const selectedOptions = data?.selectedOptionIds;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-col gap-2'}>
|
<div className={'flex w-full flex-col gap-2 py-2'}>
|
||||||
<ChecklistCell {...props} />
|
<ChecklistCell {...props} />
|
||||||
{options?.map((option) => {
|
{options?.map((option) => {
|
||||||
const isSelected = selectedOptions?.includes(option.id);
|
const isSelected = selectedOptions?.includes(option.id);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type';
|
import { DatabaseViewLayout, ViewLayout, YjsDatabaseKey, YjsFolderKey, YView } from '@/application/collab.type';
|
||||||
|
import { useDatabaseView } from '@/application/database-yjs';
|
||||||
import { useFolderContext } from '@/application/folder-yjs';
|
import { useFolderContext } from '@/application/folder-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { DatabaseActions } from '@/components/database/components/conditions';
|
import { DatabaseActions } from '@/components/database/components/conditions';
|
||||||
|
import { Tooltip } from '@mui/material';
|
||||||
import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react';
|
import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { ViewTabs, ViewTab } from './ViewTabs';
|
import { ViewTabs, ViewTab } from './ViewTabs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -31,6 +33,9 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||||
const objectId = useId().objectId;
|
const objectId = useId().objectId;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const folder = useFolderContext();
|
const folder = useFolderContext();
|
||||||
|
const view = useDatabaseView();
|
||||||
|
const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
|
||||||
|
|
||||||
const handleChange = (_: React.SyntheticEvent, newValue: string) => {
|
const handleChange = (_: React.SyntheticEvent, newValue: string) => {
|
||||||
setSelectedViewId?.(newValue);
|
setSelectedViewId?.(newValue);
|
||||||
};
|
};
|
||||||
|
@ -50,12 +55,21 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||||
[folder]
|
[folder]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const className = useMemo(() => {
|
||||||
|
const classList = [
|
||||||
|
'mx-16 -mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title max-md:mx-4',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (layout === DatabaseViewLayout.Calendar) {
|
||||||
|
classList.push('border-b');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classList.join(' ');
|
||||||
|
}, [layout]);
|
||||||
|
|
||||||
if (viewIds.length === 0) return null;
|
if (viewIds.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={ref} className={className}>
|
||||||
ref={ref}
|
|
||||||
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={{
|
||||||
width: 'calc(100% - 120px)',
|
width: 'calc(100% - 120px)',
|
||||||
|
@ -83,14 +97,18 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
|
||||||
icon={<Icon className={'h-4 w-4'} />}
|
icon={<Icon className={'h-4 w-4'} />}
|
||||||
iconPosition='start'
|
iconPosition='start'
|
||||||
color='inherit'
|
color='inherit'
|
||||||
label={<span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span>}
|
label={
|
||||||
|
<Tooltip title={name} placement={'right'}>
|
||||||
|
<span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
value={viewId}
|
value={viewId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ViewTabs>
|
</ViewTabs>
|
||||||
</div>
|
</div>
|
||||||
<DatabaseActions />
|
{layout !== DatabaseViewLayout.Calendar ? <DatabaseActions /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { RowsContext, useDatabase, useRowOrdersSelector, useViewId } from '@/application/database-yjs';
|
import { RowsContext, useDatabase, useRowOrdersSelector, useViewId } from '@/application/database-yjs';
|
||||||
import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid';
|
import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export function Grid() {
|
export function Grid() {
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
|
@ -11,6 +11,10 @@ export function Grid() {
|
||||||
const { fields, columnWidth } = useRenderFields();
|
const { fields, columnWidth } = useRenderFields();
|
||||||
const rowOrders = useRowOrdersSelector();
|
const rowOrders = useRowOrdersSelector();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setScrollLeft(0);
|
||||||
|
}, [viewId]);
|
||||||
|
|
||||||
if (!database || !rowOrders) {
|
if (!database || !rowOrders) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>
|
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
import { YDoc } from '@/application/collab.type';
|
import { YDoc } from '@/application/collab.type';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
|
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
||||||
|
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
import { DocumentHeader } from '@/components/document/document_header';
|
import { DocumentHeader } from '@/components/document/document_header';
|
||||||
import { Editor } from '@/components/editor';
|
import { Editor } from '@/components/editor';
|
||||||
|
import { EditorLayoutStyle } from '@/components/editor/EditorContext';
|
||||||
import { Log } from '@/utils/log';
|
import { Log } from '@/utils/log';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import React, { Suspense, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound';
|
||||||
|
|
||||||
export const Document = () => {
|
export const Document = () => {
|
||||||
const { objectId: documentId, workspaceId } = useId() || {};
|
const { objectId: documentId, workspaceId } = useId() || {};
|
||||||
const [doc, setDoc] = useState<YDoc | null>(null);
|
const [doc, setDoc] = useState<YDoc | null>(null);
|
||||||
const [notFound, setNotFound] = useState<boolean>(false);
|
const [notFound, setNotFound] = useState<boolean>(false);
|
||||||
|
const extra = usePageInfo(documentId).extra;
|
||||||
|
|
||||||
|
const layoutStyle: EditorLayoutStyle = useMemo(() => {
|
||||||
|
return {
|
||||||
|
font: extra?.font || '',
|
||||||
|
fontLayout: extra?.fontLayout,
|
||||||
|
lineHeightLayout: extra?.lineHeightLayout,
|
||||||
|
};
|
||||||
|
}, [extra]);
|
||||||
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
const documentService = useContext(AFConfigContext)?.service?.documentService;
|
||||||
|
|
||||||
const handleOpenDocument = useCallback(async () => {
|
const handleOpenDocument = useCallback(async () => {
|
||||||
|
@ -32,18 +44,64 @@ export const Document = () => {
|
||||||
void handleOpenDocument();
|
void handleOpenDocument();
|
||||||
}, [handleOpenDocument]);
|
}, [handleOpenDocument]);
|
||||||
|
|
||||||
|
const style = useMemo(() => {
|
||||||
|
const fontSizeMap = {
|
||||||
|
small: '14px',
|
||||||
|
normal: '16px',
|
||||||
|
large: '20px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fontFamily: layoutStyle.font,
|
||||||
|
fontSize: fontSizeMap[layoutStyle.fontLayout],
|
||||||
|
};
|
||||||
|
}, [layoutStyle]);
|
||||||
|
|
||||||
|
const layoutClassName = useMemo(() => {
|
||||||
|
const classList = [];
|
||||||
|
|
||||||
|
if (layoutStyle.fontLayout === 'large') {
|
||||||
|
classList.push('font-large');
|
||||||
|
} else if (layoutStyle.fontLayout === 'small') {
|
||||||
|
classList.push('font-small');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutStyle.lineHeightLayout === 'large') {
|
||||||
|
classList.push('line-height-large');
|
||||||
|
} else if (layoutStyle.lineHeightLayout === 'small') {
|
||||||
|
classList.push('line-height-small');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classList.join(' ');
|
||||||
|
}, [layoutStyle]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!layoutStyle.font) return;
|
||||||
|
void window.WebFont?.load({
|
||||||
|
google: {
|
||||||
|
families: [layoutStyle.font],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [layoutStyle.font]);
|
||||||
|
|
||||||
if (!documentId) return null;
|
if (!documentId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{doc && (
|
{doc ? (
|
||||||
<div className={'relative w-full'}>
|
<div style={style} className={`relative w-full ${layoutClassName}`}>
|
||||||
<DocumentHeader doc={doc} viewId={documentId} />
|
<DocumentHeader doc={doc} viewId={documentId} />
|
||||||
<div className={'flex w-full justify-center'}>
|
<div className={'flex w-full justify-center'}>
|
||||||
|
<Suspense fallback={<ComponentLoading />}>
|
||||||
<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} includeRoot={true} />
|
<Editor doc={doc} readOnly={true} layoutStyle={layoutStyle} />
|
||||||
|
</div>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={'flex h-full w-full items-center justify-center'}>
|
||||||
|
<CircularProgress />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import { CoverType, YDoc } from '@/application/collab.type';
|
import { DocCoverType, YDoc } from '@/application/collab.type';
|
||||||
|
import { CoverType } from '@/application/folder-yjs/folder.type';
|
||||||
|
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
|
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
||||||
import { useBlockCover } from '@/components/document/document_header/useBlockCover';
|
import { useBlockCover } from '@/components/document/document_header/useBlockCover';
|
||||||
|
import { showColorsForImage } from '@/components/document/document_header/utils';
|
||||||
import { renderColor } from '@/utils/color';
|
import { renderColor } from '@/utils/color';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import DefaultImage from './default_cover.jpg';
|
import DefaultImage from './default_cover.jpg';
|
||||||
|
|
||||||
function DocumentCover({ doc }: { doc: YDoc }) {
|
function DocumentCover({ doc, onTextColor }: { doc: YDoc; onTextColor: (color: string) => void }) {
|
||||||
|
const viewId = useId().objectId;
|
||||||
|
const { extra } = usePageInfo(viewId);
|
||||||
|
|
||||||
|
const pageCover = extra.cover;
|
||||||
const { cover } = useBlockCover(doc);
|
const { cover } = useBlockCover(doc);
|
||||||
|
|
||||||
const renderCoverColor = useCallback((color: string) => {
|
const renderCoverColor = useCallback((color: string) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -17,21 +26,47 @@ function DocumentCover({ doc }: { doc: YDoc }) {
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderCoverImage = useCallback((url: string) => {
|
const renderCoverImage = useCallback(
|
||||||
return <img draggable={false} src={url} alt={''} className={'h-full w-full object-cover'} />;
|
(url: string) => {
|
||||||
}, []);
|
return (
|
||||||
|
<img
|
||||||
|
onLoad={(e) => {
|
||||||
|
void showColorsForImage(e.currentTarget).then((res) => {
|
||||||
|
onTextColor(res);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
draggable={false}
|
||||||
|
src={url}
|
||||||
|
alt={''}
|
||||||
|
className={'h-full w-full object-cover'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[onTextColor]
|
||||||
|
);
|
||||||
|
|
||||||
const { cover_selection_type: type, cover_selection: value = '' } = cover || {};
|
if (!pageCover && !cover?.cover_selection) return null;
|
||||||
|
return (
|
||||||
return value ? (
|
<div className={`relative flex h-[255px] w-full max-sm:h-[180px]`}>
|
||||||
<div className={`relative mb-[-80px] flex h-[255px] w-full`}>
|
{pageCover ? (
|
||||||
<>
|
<>
|
||||||
{type === CoverType.Asset ? renderCoverImage(DefaultImage) : null}
|
{[CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type)
|
||||||
{type === CoverType.Color ? renderCoverColor(value) : null}
|
? renderCoverColor(pageCover.value)
|
||||||
{type === CoverType.Image ? renderCoverImage(value) : null}
|
: null}
|
||||||
|
{CoverType.BuildInImage === pageCover.type ? renderCoverImage(DefaultImage) : null}
|
||||||
|
{[CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type)
|
||||||
|
? renderCoverImage(pageCover.value)
|
||||||
|
: null}
|
||||||
</>
|
</>
|
||||||
|
) : cover?.cover_selection ? (
|
||||||
|
<>
|
||||||
|
{cover.cover_selection_type === DocCoverType.Asset ? renderCoverImage(DefaultImage) : null}
|
||||||
|
{cover.cover_selection_type === DocCoverType.Color ? renderCoverColor(cover.cover_selection) : null}
|
||||||
|
{cover.cover_selection_type === DocCoverType.Image ? renderCoverImage(cover.cover_selection) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocumentCover;
|
export default DocumentCover;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { YDoc, YjsFolderKey } from '@/application/collab.type';
|
import { YDoc, YjsFolderKey } from '@/application/collab.type';
|
||||||
import { useViewSelector } from '@/application/folder-yjs';
|
import { useViewSelector } from '@/application/folder-yjs';
|
||||||
import DocumentCover from '@/components/document/document_header/DocumentCover';
|
import DocumentCover from '@/components/document/document_header/DocumentCover';
|
||||||
import React, { memo, useMemo, useRef } from 'react';
|
import React, { memo, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) {
|
export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const { view } = useViewSelector(viewId);
|
const { view } = useViewSelector(viewId);
|
||||||
|
const [textColor, setTextColor] = useState<string>('var(--text-title)');
|
||||||
const icon = view?.get(YjsFolderKey.icon);
|
const icon = view?.get(YjsFolderKey.icon);
|
||||||
const iconObject = useMemo(() => {
|
const iconObject = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
|
@ -17,21 +17,30 @@ export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) {
|
||||||
}, [icon]);
|
}, [icon]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={'document-header select-none'}>
|
<div ref={ref} className={'document-header mb-[10px] select-none'}>
|
||||||
<div className={'flex flex-col justify-end'}>
|
<div className={'view-banner relative flex w-full flex-col overflow-hidden'}>
|
||||||
<div className={'view-banner flex w-full flex-col overflow-hidden'}>
|
<DocumentCover onTextColor={setTextColor} doc={doc} />
|
||||||
<DocumentCover doc={doc} />
|
|
||||||
|
|
||||||
<div className={`relative min-h-[65px] w-[964px] min-w-0 max-w-full px-16 pt-10 max-md:px-4`}>
|
<div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'absolute',
|
||||||
bottom: '50%',
|
bottom: '100%',
|
||||||
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
|
className={'flex items-center gap-2 px-14 pb-10 text-4xl max-md:px-2 max-md:pb-6 max-sm:text-[7vw]'}
|
||||||
>
|
>
|
||||||
<div className={`view-icon`}>{iconObject?.value}</div>
|
<div className={`view-icon`}>{iconObject?.value}</div>
|
||||||
|
<div className={'flex flex-1 items-center gap-2 overflow-hidden'}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: textColor,
|
||||||
|
}}
|
||||||
|
className={'font-bold leading-[1.5em]'}
|
||||||
|
>
|
||||||
|
{view?.get(YjsFolderKey.name)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={'py-2'}></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { PageCover, YBlocks, YDoc, YDocument, YjsEditorKey } from '@/application/collab.type';
|
import { DocCover, YBlocks, YDoc, YDocument, YjsEditorKey } from '@/application/collab.type';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
export function useBlockCover(doc: YDoc) {
|
export function useBlockCover(doc: YDoc) {
|
||||||
|
@ -22,7 +22,7 @@ export function useBlockCover(doc: YDoc) {
|
||||||
};
|
};
|
||||||
}, [doc]);
|
}, [doc]);
|
||||||
|
|
||||||
const coverObj: PageCover = useMemo(() => {
|
const coverObj: DocCover = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(cover || '');
|
return JSON.parse(cover || '');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-expect-error
|
||||||
|
import ColorThief from 'colorthief';
|
||||||
|
|
||||||
|
const colorThief = new ColorThief();
|
||||||
|
|
||||||
|
export function calculateTextColor(rgb: [number, number, number]): string {
|
||||||
|
const [r, g, b] = rgb;
|
||||||
|
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||||
|
|
||||||
|
return brightness > 125 ? 'black' : 'white';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showColorsForImage(image: HTMLImageElement) {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.crossOrigin = 'Anonymous'; // Handle CORS
|
||||||
|
img.src = image.src;
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.onerror = reject;
|
||||||
|
});
|
||||||
|
|
||||||
|
const dominantColor = colorThief.getColor(img);
|
||||||
|
|
||||||
|
return calculateTextColor(dominantColor);
|
||||||
|
}
|
|
@ -1,8 +1,5 @@
|
||||||
import { CollabOrigin, YjsFolderKey } from '@/application/collab.type';
|
import { CollabOrigin } from '@/application/collab.type';
|
||||||
import { useViewSelector } from '@/application/folder-yjs';
|
|
||||||
import { withYjs, YjsEditor } from '@/application/slate-yjs/plugins/withYjs';
|
import { withYjs, YjsEditor } from '@/application/slate-yjs/plugins/withYjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { CustomEditor } from '@/components/editor/command';
|
|
||||||
import EditorEditable from '@/components/editor/Editable';
|
import EditorEditable from '@/components/editor/Editable';
|
||||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||||
import { withPlugins } from '@/components/editor/plugins';
|
import { withPlugins } from '@/components/editor/plugins';
|
||||||
|
@ -13,10 +10,7 @@ import * as Y from 'yjs';
|
||||||
|
|
||||||
const defaultInitialValue: Descendant[] = [];
|
const defaultInitialValue: Descendant[] = [];
|
||||||
|
|
||||||
function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeRoot?: boolean }) {
|
function CollaborativeEditor({ doc }: { doc: Y.Doc }) {
|
||||||
const viewId = useId()?.objectId || '';
|
|
||||||
const { view } = useViewSelector(viewId);
|
|
||||||
const title = includeRoot ? view?.get(YjsFolderKey.name) : undefined;
|
|
||||||
const context = useEditorContext();
|
const context = useEditorContext();
|
||||||
// if readOnly, collabOrigin is Local, otherwise RemoteSync
|
// if readOnly, collabOrigin is Local, otherwise RemoteSync
|
||||||
const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync;
|
const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync;
|
||||||
|
@ -27,13 +21,12 @@ function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeR
|
||||||
withReact(
|
withReact(
|
||||||
withYjs(createEditor(), doc, {
|
withYjs(createEditor(), doc, {
|
||||||
localOrigin,
|
localOrigin,
|
||||||
includeRoot,
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
) as YjsEditor),
|
) as YjsEditor),
|
||||||
[doc, localOrigin, includeRoot]
|
[doc, localOrigin]
|
||||||
);
|
);
|
||||||
const [connected, setIsConnected] = useState(false);
|
const [, setIsConnected] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
|
@ -45,11 +38,6 @@ function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeR
|
||||||
};
|
};
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!editor || !connected || title === undefined) return;
|
|
||||||
CustomEditor.setDocumentTitle(editor, title);
|
|
||||||
}, [editor, title, connected]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Slate editor={editor} initialValue={defaultInitialValue}>
|
<Slate editor={editor} initialValue={defaultInitialValue}>
|
||||||
<EditorEditable editor={editor} />
|
<EditorEditable editor={editor} />
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
import { YDoc } from '@/application/collab.type';
|
import { YDoc } from '@/application/collab.type';
|
||||||
import CollaborativeEditor from '@/components/editor/CollaborativeEditor';
|
import CollaborativeEditor from '@/components/editor/CollaborativeEditor';
|
||||||
import { EditorContextProvider } from '@/components/editor/EditorContext';
|
import { defaultLayoutStyle, EditorContextProvider, EditorLayoutStyle } from '@/components/editor/EditorContext';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './editor.scss';
|
import './editor.scss';
|
||||||
|
|
||||||
export const Editor = ({
|
export interface EditorProps {
|
||||||
readOnly,
|
|
||||||
doc,
|
|
||||||
includeRoot = true,
|
|
||||||
}: {
|
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
doc: YDoc;
|
doc: YDoc;
|
||||||
includeRoot?: boolean;
|
layoutStyle?: EditorLayoutStyle;
|
||||||
}) => {
|
}
|
||||||
|
|
||||||
|
export const Editor = ({ readOnly, doc, layoutStyle = defaultLayoutStyle }: EditorProps) => {
|
||||||
return (
|
return (
|
||||||
<EditorContextProvider readOnly={readOnly}>
|
<EditorContextProvider layoutStyle={layoutStyle} readOnly={readOnly}>
|
||||||
<CollaborativeEditor doc={doc} includeRoot={includeRoot} />
|
<CollaborativeEditor doc={doc} />
|
||||||
</EditorContextProvider>
|
</EditorContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,26 @@
|
||||||
|
import { FontLayout, LineHeightLayout } from '@/application/collab.type';
|
||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export interface EditorLayoutStyle {
|
||||||
|
fontLayout: FontLayout;
|
||||||
|
font: string;
|
||||||
|
lineHeightLayout: LineHeightLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultLayoutStyle: EditorLayoutStyle = {
|
||||||
|
fontLayout: FontLayout.normal,
|
||||||
|
font: '',
|
||||||
|
lineHeightLayout: LineHeightLayout.normal,
|
||||||
|
};
|
||||||
|
|
||||||
interface EditorContextState {
|
interface EditorContextState {
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
|
layoutStyle: EditorLayoutStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorContext = createContext<EditorContextState>({
|
export const EditorContext = createContext<EditorContextState>({
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
layoutStyle: defaultLayoutStyle,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => {
|
export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => {
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function BoardBlock() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BoardBlock;
|
|
|
@ -1,7 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function CalendarBlock() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CalendarBlock;
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
|
||||||
|
import { useNavigateToView } from '@/application/folder-yjs';
|
||||||
import { IdProvider, useId } from '@/components/_shared/context-provider/IdProvider';
|
import { IdProvider, useId } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import { Database } from '@/components/database';
|
import { Database } from '@/components/database';
|
||||||
import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type';
|
import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type';
|
||||||
import React, { forwardRef, memo, useMemo } from 'react';
|
import { Tooltip } from '@mui/material';
|
||||||
|
import React, { forwardRef, memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BlockType } from '@/application/collab.type';
|
import { BlockType } from '@/application/collab.type';
|
||||||
|
|
||||||
|
@ -11,6 +14,8 @@ export const DatabaseBlock = memo(
|
||||||
const viewId = node.data.view_id;
|
const viewId = node.data.view_id;
|
||||||
const workspaceId = useId()?.workspaceId;
|
const workspaceId = useId()?.workspaceId;
|
||||||
const type = node.type;
|
const type = node.type;
|
||||||
|
const navigateToView = useNavigateToView();
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
const style = useMemo(() => {
|
const style = useMemo(() => {
|
||||||
const style = {};
|
const style = {};
|
||||||
|
@ -31,16 +36,45 @@ export const DatabaseBlock = memo(
|
||||||
return style;
|
return style;
|
||||||
}, [type]);
|
}, [type]);
|
||||||
|
|
||||||
|
const handleNavigateToRow = useCallback(
|
||||||
|
(viewId: string, rowId: string) => {
|
||||||
|
const url = `/view/${workspaceId}/${viewId}?r=${rowId}`;
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
},
|
||||||
|
[workspaceId]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div {...attributes} className={`relative w-full cursor-pointer py-2`}>
|
<div
|
||||||
|
{...attributes}
|
||||||
|
className={`relative w-full cursor-pointer py-2`}
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
>
|
||||||
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<div contentEditable={false} style={style} className={`container-bg flex w-full flex-col px-3`}>
|
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col px-3`}>
|
||||||
{viewId ? (
|
{viewId ? (
|
||||||
<IdProvider workspaceId={workspaceId} objectId={viewId}>
|
<IdProvider workspaceId={workspaceId} objectId={viewId}>
|
||||||
<Database />
|
<Database onNavigateToRow={handleNavigateToRow} />
|
||||||
|
{isHovering && (
|
||||||
|
<div className={'absolute right-4 top-1'}>
|
||||||
|
<Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}>
|
||||||
|
<button
|
||||||
|
color={'primary'}
|
||||||
|
className={'rounded border border-line-divider bg-bg-body p-1 hover:bg-fill-list-hover'}
|
||||||
|
onClick={() => {
|
||||||
|
navigateToView?.(viewId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandMoreIcon />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</IdProvider>
|
</IdProvider>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function GridBlock() {
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GridBlock;
|
|
|
@ -1,17 +1,17 @@
|
||||||
export function getHeadingCssProperty(level: number) {
|
export function getHeadingCssProperty(level: number) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 1:
|
case 1:
|
||||||
return 'text-3xl pt-[10px] pb-[8px] font-bold';
|
return 'text-3xl pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]';
|
||||||
case 2:
|
case 2:
|
||||||
return 'text-2xl pt-[8px] pb-[6px] font-bold';
|
return 'text-2xl pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]';
|
||||||
case 3:
|
case 3:
|
||||||
return 'text-xl pt-[4px] font-bold';
|
return 'text-xl pt-[4px] font-bold max-sm:text-[4vw]';
|
||||||
case 4:
|
case 4:
|
||||||
return 'text-lg pt-[4px] font-bold';
|
return 'text-lg pt-[4px] font-bold';
|
||||||
case 5:
|
case 5:
|
||||||
return 'text-base pt-[4px] font-bold';
|
return 'pt-[4px] font-bold';
|
||||||
case 6:
|
case 6:
|
||||||
return 'text-sm pt-[4px] font-bold';
|
return 'pt-[4px] font-bold';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
export const MathEquation = lazy(() => import('./MathEquation'));
|
||||||
// @ts-expect-error
|
|
||||||
export const MathEquation = lazy(() => import('./MathEquation?chunkName=formula'));
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { readOnly } = useEditorContext();
|
const { readOnly } = useEditorContext();
|
||||||
const editor = useSlate();
|
const editor = useSlate();
|
||||||
|
|
||||||
const selected = useSelected() && !readOnly && !!editor.selection && Range.isCollapsed(editor.selection);
|
const selected = useSelected() && !readOnly && !!editor.selection && Range.isCollapsed(editor.selection);
|
||||||
const [isComposing, setIsComposing] = useState(false);
|
const [isComposing, setIsComposing] = useState(false);
|
||||||
const block = useMemo(() => {
|
const block = useMemo(() => {
|
||||||
|
@ -33,7 +34,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin
|
||||||
const unSelectedPlaceholder = useMemo(() => {
|
const unSelectedPlaceholder = useMemo(() => {
|
||||||
switch (block?.type) {
|
switch (block?.type) {
|
||||||
case BlockType.Paragraph: {
|
case BlockType.Paragraph: {
|
||||||
if (editor.children.length === 1) {
|
if (editor.children.length === 1 && !readOnly) {
|
||||||
return t('editor.slashPlaceHolder');
|
return t('editor.slashPlaceHolder');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +74,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}, [block, t, editor.children.length]);
|
}, [readOnly, block, t, editor.children.length]);
|
||||||
|
|
||||||
const selectedPlaceholder = useMemo(() => {
|
const selectedPlaceholder = useMemo(() => {
|
||||||
switch (block?.type) {
|
switch (block?.type) {
|
||||||
|
@ -122,7 +123,7 @@ function Placeholder({ node, ...attributes }: { node: Element; className?: strin
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-placeholder={selected ? selectedPlaceholder : unSelectedPlaceholder}
|
data-placeholder={selected && !readOnly ? selectedPlaceholder : unSelectedPlaceholder}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
className={className}
|
className={className}
|
||||||
|
|
|
@ -11,13 +11,13 @@ export const Text = memo(
|
||||||
const { hasStartIcon, renderIcon } = useStartIcon(node);
|
const { hasStartIcon, renderIcon } = useStartIcon(node);
|
||||||
const editor = useSlateStatic();
|
const editor = useSlateStatic();
|
||||||
const isEmpty = editor.isEmpty(node);
|
const isEmpty = editor.isEmpty(node);
|
||||||
const className = useMemo(
|
const className = useMemo(() => {
|
||||||
() =>
|
const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-all', 'px-1'];
|
||||||
`text-element relative my-1 flex w-full whitespace-pre-wrap break-all px-1 ${classNameProp ?? ''} ${
|
|
||||||
hasStartIcon ? 'has-start-icon' : ''
|
if (classNameProp) classList.push(classNameProp);
|
||||||
}`,
|
if (hasStartIcon) classList.push('has-start-icon');
|
||||||
[classNameProp, hasStartIcon]
|
return classList.join(' ');
|
||||||
);
|
}, [classNameProp, hasStartIcon]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span {...attributes} ref={ref} className={className}>
|
<span {...attributes} ref={ref} className={className}>
|
||||||
|
|
|
@ -13,6 +13,9 @@ import { Paragraph } from '@/components/editor/components/blocks/paragraph';
|
||||||
import { Quote } from '@/components/editor/components/blocks/quote';
|
import { Quote } from '@/components/editor/components/blocks/quote';
|
||||||
import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table';
|
import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table';
|
||||||
import { Text } from '@/components/editor/components/blocks/text';
|
import { Text } from '@/components/editor/components/blocks/text';
|
||||||
|
import { ElementFallbackRender } from '@/components/error/ElementFallbackRender';
|
||||||
|
import { Skeleton } from '@mui/material';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { TodoList } from 'src/components/editor/components/blocks/todo-list';
|
import { TodoList } from 'src/components/editor/components/blocks/todo-list';
|
||||||
import { ToggleList } from 'src/components/editor/components/blocks/toggle-list';
|
import { ToggleList } from 'src/components/editor/components/blocks/toggle-list';
|
||||||
import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock';
|
import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock';
|
||||||
|
@ -20,7 +23,7 @@ import { Formula } from '@/components/editor/components/leaf/formula';
|
||||||
import { Mention } from '@/components/editor/components/leaf/mention';
|
import { Mention } from '@/components/editor/components/leaf/mention';
|
||||||
import { EditorElementProps, TextNode } from '@/components/editor/editor.type';
|
import { EditorElementProps, TextNode } from '@/components/editor/editor.type';
|
||||||
import { renderColor } from '@/utils/color';
|
import { renderColor } from '@/utils/color';
|
||||||
import React, { FC, useMemo } from 'react';
|
import React, { FC, Suspense, useMemo } from 'react';
|
||||||
import { RenderElementProps } from 'slate-react';
|
import { RenderElementProps } from 'slate-react';
|
||||||
import { DatabaseBlock } from 'src/components/editor/components/blocks/database';
|
import { DatabaseBlock } from 'src/components/editor/components/blocks/database';
|
||||||
|
|
||||||
|
@ -118,10 +121,14 @@ export const Element = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Suspense fallback={<Skeleton width={'100%'} height={24} />}>
|
||||||
|
<ErrorBoundary fallbackRender={ElementFallbackRender}>
|
||||||
<div {...attributes} data-block-type={node.type} className={className}>
|
<div {...attributes} data-block-type={node.type} className={className}>
|
||||||
<Component style={style} className={`flex w-full flex-col`} node={node}>
|
<Component style={style} className={`flex w-full flex-col`} node={node}>
|
||||||
{children}
|
{children}
|
||||||
</Component>
|
</Component>
|
||||||
</div>
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,20 +1,15 @@
|
||||||
import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type';
|
import { useNavigateToView } from '@/application/folder-yjs';
|
||||||
import { useId } from '@/components/_shared/context-provider/IdProvider';
|
|
||||||
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
import { usePageInfo } from '@/components/_shared/page/usePageInfo';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
function MentionPage({ pageId }: { pageId: string }) {
|
function MentionPage({ pageId }: { pageId: string }) {
|
||||||
const navigate = useNavigate();
|
const onNavigateToView = useNavigateToView();
|
||||||
const { workspaceId } = useId();
|
const { icon, name } = usePageInfo(pageId);
|
||||||
const { view, icon, name } = usePageInfo(pageId);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const layout = parseInt(view?.get(YjsFolderKey.layout) ?? '0') as ViewLayout;
|
onNavigateToView?.(pageId);
|
||||||
|
|
||||||
navigate(`/workspace/${workspaceId}/${layoutMap[layout]}/${pageId}`);
|
|
||||||
}}
|
}}
|
||||||
className={`mention-inline px-1 underline`}
|
className={`mention-inline px-1 underline`}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
|
|
||||||
[role=textbox] {
|
[role=textbox] {
|
||||||
.text-element {
|
.text-element {
|
||||||
|
@apply my-1;
|
||||||
&::selection {
|
&::selection {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
|
@ -209,11 +210,6 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.grid-block .grid-scroll-container::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-render {
|
.image-render {
|
||||||
.image-resizer {
|
.image-resizer {
|
||||||
@apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end;
|
@apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end;
|
||||||
|
@ -269,7 +265,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-content {
|
.mention-content {
|
||||||
@apply ml-5;
|
@apply ml-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -277,3 +273,31 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) {
|
||||||
.text-block-icon {
|
.text-block-icon {
|
||||||
@apply flex items-center justify-center;
|
@apply flex items-center justify-center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.font-small {
|
||||||
|
.text-element {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.font-large {
|
||||||
|
.text-element {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-height-large {
|
||||||
|
.text-element {
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-height-small {
|
||||||
|
.text-element {
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
import { FallbackProps } from 'react-error-boundary';
|
||||||
|
|
||||||
|
export function ElementFallbackRender({ error }: FallbackProps) {
|
||||||
|
return (
|
||||||
|
<Alert severity={'error'} variant={'standard'} className={'my-2'}>
|
||||||
|
<p>Something went wrong:</p>
|
||||||
|
<pre>{error.message}</pre>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,19 +1,15 @@
|
||||||
import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type';
|
import { useNavigateToView } from '@/application/folder-yjs';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import Page from '@/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 onNavigateToView = useNavigateToView();
|
||||||
const { pathname } = useLocation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'cursor-pointer border-b border-line-border py-4 px-2'}>
|
<div className={'cursor-pointer border-b border-line-border py-4 px-2'}>
|
||||||
<Page
|
<Page
|
||||||
onClick={(view) => {
|
onClick={() => {
|
||||||
const layout = parseInt(view?.get(YjsFolderKey.layout) ?? '0') as ViewLayout;
|
onNavigateToView?.(id);
|
||||||
|
|
||||||
navigate(`${pathname}/${layoutMap[layout]}/${id}`);
|
|
||||||
}}
|
}}
|
||||||
id={id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,10 +2,9 @@ import { downloadPage, openAppFlowySchema, openUrl } from '@/utils/url';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import Page from 'src/components/_shared/page/Page';
|
|
||||||
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||||
import Popover, { PopoverOrigin } from '@mui/material/Popover';
|
import Popover, { PopoverOrigin } from '@mui/material/Popover';
|
||||||
|
import Breadcrumb from 'src/components/layout/breadcrumb/Breadcrumb';
|
||||||
|
|
||||||
const popoverOrigin: {
|
const popoverOrigin: {
|
||||||
anchorOrigin: PopoverOrigin;
|
anchorOrigin: PopoverOrigin;
|
||||||
|
@ -22,14 +21,13 @@ const popoverOrigin: {
|
||||||
};
|
};
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
const { objectId } = useParams();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'appflowy-top-bar flex h-[64px] p-4'}>
|
<div className={'appflowy-top-bar flex h-[64px] p-4'}>
|
||||||
<div className={'flex flex-1 items-center justify-between'}>
|
<div className={'flex w-full items-center justify-between overflow-hidden'}>
|
||||||
<div className={'flex-1'}>{objectId && <Page id={objectId} />}</div>
|
<Breadcrumb />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={'border-line-border'}
|
className={'border-line-border'}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { YFolder, YjsEditorKey, YjsFolderKey } from '@/application/collab.type';
|
||||||
|
import { Crumb } from '@/application/folder-yjs';
|
||||||
|
import { AFConfigContext } from '@/components/app/AppConfig';
|
||||||
|
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function useLayout() {
|
||||||
|
const { workspaceId, objectId } = useParams();
|
||||||
|
const [search] = useSearchParams();
|
||||||
|
const folderService = useContext(AFConfigContext)?.service?.folderService;
|
||||||
|
const [folder, setFolder] = useState<YFolder | null>(null);
|
||||||
|
const views = folder?.get(YjsFolderKey.views);
|
||||||
|
const view = objectId ? views?.get(objectId) : null;
|
||||||
|
const [crumbs, setCrumbs] = useState<Crumb[]>([]);
|
||||||
|
|
||||||
|
const getFolder = useCallback(
|
||||||
|
async (workspaceId: string) => {
|
||||||
|
const folder = (await folderService?.openWorkspace(workspaceId))
|
||||||
|
?.getMap(YjsEditorKey.data_section)
|
||||||
|
.get(YjsEditorKey.folder);
|
||||||
|
|
||||||
|
if (!folder) return;
|
||||||
|
|
||||||
|
console.log(folder.toJSON());
|
||||||
|
setFolder(folder);
|
||||||
|
},
|
||||||
|
[folderService]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!workspaceId) return;
|
||||||
|
|
||||||
|
void getFolder(workspaceId);
|
||||||
|
}, [getFolder, workspaceId]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleNavigateToView = useCallback(
|
||||||
|
(viewId: string) => {
|
||||||
|
const view = folder?.get(YjsFolderKey.views)?.get(viewId);
|
||||||
|
|
||||||
|
if (!view) return;
|
||||||
|
navigate(`/view/${workspaceId}/${viewId}`);
|
||||||
|
},
|
||||||
|
[folder, navigate, workspaceId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeBreadcrumb = useCallback(() => {
|
||||||
|
if (!view) return;
|
||||||
|
const queue = [view];
|
||||||
|
let parentId = view.get(YjsFolderKey.bid);
|
||||||
|
|
||||||
|
while (parentId) {
|
||||||
|
const parent = views?.get(parentId);
|
||||||
|
|
||||||
|
if (!parent) break;
|
||||||
|
|
||||||
|
queue.unshift(parent);
|
||||||
|
parentId = parent?.get(YjsFolderKey.bid);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCrumbs(
|
||||||
|
queue
|
||||||
|
.map((view) => {
|
||||||
|
let icon = view.get(YjsFolderKey.icon);
|
||||||
|
|
||||||
|
try {
|
||||||
|
icon = JSON.parse(icon || '')?.value;
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
viewId: view.get(YjsFolderKey.id),
|
||||||
|
name: view.get(YjsFolderKey.name),
|
||||||
|
icon: icon || '',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.slice(1)
|
||||||
|
);
|
||||||
|
}, [view, views]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChangeBreadcrumb();
|
||||||
|
|
||||||
|
view?.observe(onChangeBreadcrumb);
|
||||||
|
views?.observe(onChangeBreadcrumb);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
view?.unobserve(onChangeBreadcrumb);
|
||||||
|
views?.unobserve(onChangeBreadcrumb);
|
||||||
|
};
|
||||||
|
}, [search, onChangeBreadcrumb, view, views]);
|
||||||
|
|
||||||
|
return { folder, handleNavigateToView, crumbs, setCrumbs };
|
||||||
|
}
|
|
@ -1,37 +1,23 @@
|
||||||
import { YFolder, YjsEditorKey } from '@/application/collab.type';
|
|
||||||
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider';
|
||||||
import { AFConfigContext } from '@/components/app/AppConfig';
|
|
||||||
import Header from '@/components/layout/Header';
|
import Header from '@/components/layout/Header';
|
||||||
import { AFScroller } from '@/components/_shared/scroller';
|
import { AFScroller } from '@/components/_shared/scroller';
|
||||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
import { useLayout } from '@/components/layout/Layout.hooks';
|
||||||
import { useParams } from 'react-router-dom';
|
import React from 'react';
|
||||||
import './layout.scss';
|
import './layout.scss';
|
||||||
|
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||||
|
|
||||||
function Layout({ children }: { children: React.ReactNode }) {
|
function Layout({ children }: { children: React.ReactNode }) {
|
||||||
const { workspaceId } = useParams();
|
const { folder, handleNavigateToView, crumbs, setCrumbs } = useLayout();
|
||||||
const folderService = useContext(AFConfigContext)?.service?.folderService;
|
|
||||||
const [folder, setFolder] = useState<YFolder | null>(null);
|
|
||||||
const getFolder = useCallback(
|
|
||||||
async (workspaceId: string) => {
|
|
||||||
const folder = (await folderService?.openWorkspace(workspaceId))
|
|
||||||
?.getMap(YjsEditorKey.data_section)
|
|
||||||
.get(YjsEditorKey.folder);
|
|
||||||
|
|
||||||
if (!folder) return;
|
if (!folder)
|
||||||
|
return (
|
||||||
console.log(folder.toJSON());
|
<div className={'flex h-screen w-screen items-center justify-center'}>
|
||||||
setFolder(folder);
|
<Logo className={'h-20 w-20'} />
|
||||||
},
|
</div>
|
||||||
[folderService]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workspaceId) return;
|
|
||||||
|
|
||||||
void getFolder(workspaceId);
|
|
||||||
}, [getFolder, workspaceId]);
|
|
||||||
return (
|
return (
|
||||||
<FolderProvider folder={folder}>
|
<FolderProvider setCrumbs={setCrumbs} crumbs={crumbs} onNavigateToView={handleNavigateToView} folder={folder}>
|
||||||
<Header />
|
<Header />
|
||||||
<AFScroller
|
<AFScroller
|
||||||
overflowXHidden
|
overflowXHidden
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { useCrumbs } from '@/application/folder-yjs';
|
||||||
|
import Item from '@/components/layout/breadcrumb/Item';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
export function Breadcrumb() {
|
||||||
|
const crumbs = useCrumbs();
|
||||||
|
|
||||||
|
const renderCrumb = useMemo(() => {
|
||||||
|
return crumbs?.map((crumb, index) => {
|
||||||
|
const isLast = index === crumbs.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={crumb.viewId}>
|
||||||
|
<Item crumb={crumb} disableClick={isLast} />
|
||||||
|
{!isLast && <span>/</span>}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [crumbs]);
|
||||||
|
|
||||||
|
return <div className={'flex flex-1 items-center gap-2 overflow-hidden'}>{renderCrumb}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Breadcrumb;
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Crumb, useNavigateToView } from '@/application/folder-yjs';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
function Item({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) {
|
||||||
|
const { viewId, icon, name } = crumb;
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const onNavigateToView = useNavigateToView();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center gap-1 ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (disableClick) return;
|
||||||
|
onNavigateToView?.(viewId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<span className={!disableClick ? 'underline' : 'flex-1 truncate'}>
|
||||||
|
{name || t('menuAppHeader.defaultNewPageName')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Item;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './Breadcrumb';
|
|
@ -16,28 +16,32 @@
|
||||||
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
|
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin hidden-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none; // For Firefox
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
&[data-os="windows"]:not([data-browser="firefox"]) {
|
||||||
|
.appflowy-custom-scroller {
|
||||||
|
@include hidden-scrollbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-sticky-header {
|
||||||
|
@include hidden-scrollbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.appflowy-date-picker-calendar {
|
.appflowy-date-picker-calendar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-sticky-header::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-scroll-container::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.appflowy-scroll-container {
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical {
|
.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical {
|
||||||
background-color: var(--scrollbar-thumb);
|
background-color: var(--scrollbar-thumb);
|
||||||
|
@ -46,16 +50,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database {
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.view-icon {
|
.view-icon {
|
||||||
@apply flex w-fit cursor-pointer rounded-lg py-2 text-6xl;
|
@apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em];
|
||||||
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;
|
||||||
|
|
|
@ -13,7 +13,7 @@ function LoginPage() {
|
||||||
const workspaceId = currentUser.user?.workspaceId;
|
const workspaceId = currentUser.user?.workspaceId;
|
||||||
|
|
||||||
if (!redirect || redirect === '/') {
|
if (!redirect || redirect === '/') {
|
||||||
return navigate(`/workspace/${workspaceId}`);
|
return navigate(`/view/${workspaceId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(`${redirect}`);
|
navigate(`${redirect}`);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ViewLayout } from '@/application/collab.type';
|
||||||
|
import { useViewLayout } from '@/application/folder-yjs';
|
||||||
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
import { IdProvider } from '@/components/_shared/context-provider/IdProvider';
|
||||||
import React, { lazy, useMemo } from 'react';
|
import React, { lazy, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
@ -5,30 +7,24 @@ import DocumentPage from '@/pages/DocumentPage';
|
||||||
|
|
||||||
const DatabasePage = lazy(() => import('./DatabasePage'));
|
const DatabasePage = lazy(() => import('./DatabasePage'));
|
||||||
|
|
||||||
enum URL_COLLAB_TYPE {
|
|
||||||
DOCUMENT = 'document',
|
|
||||||
GRID = 'grid',
|
|
||||||
BOARD = 'board',
|
|
||||||
CALENDAR = 'calendar',
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProductPage() {
|
function ProductPage() {
|
||||||
const { workspaceId, type, objectId } = useParams();
|
const { workspaceId, objectId } = useParams();
|
||||||
|
const type = useViewLayout();
|
||||||
|
|
||||||
const PageComponent = useMemo(() => {
|
const PageComponent = useMemo(() => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case URL_COLLAB_TYPE.DOCUMENT:
|
case ViewLayout.Document:
|
||||||
return DocumentPage;
|
return DocumentPage;
|
||||||
case URL_COLLAB_TYPE.GRID:
|
case ViewLayout.Grid:
|
||||||
case URL_COLLAB_TYPE.BOARD:
|
case ViewLayout.Board:
|
||||||
case URL_COLLAB_TYPE.CALENDAR:
|
case ViewLayout.Calendar:
|
||||||
return DatabasePage;
|
return DatabasePage;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [type]);
|
}, [type]);
|
||||||
|
|
||||||
console.log(workspaceId, type, objectId);
|
if (!workspaceId || !objectId) return null;
|
||||||
if (!workspaceId || !type || !objectId) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IdProvider workspaceId={workspaceId} objectId={objectId}>
|
<IdProvider workspaceId={workspaceId} objectId={objectId}>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue