feat: support publish database on web (#5748)

* fix: view name

* fix: hidden login on web

* fix: database update bugs

* feat: support render database on web

* fix: loading

* fix: calendar width on mobile

* fix: calendar boder color

* fix: replace some icons

* fix: deal with visible view ids

* fix: filter error child

* fix: hide filters and sorts

* fix: the style of relation

* fix: throw error when apply fail

* fix: upgrade yjs

* fix: eslint errors

* fix: support group by checkbox

* fix: add shortcut to clear data

* fix: relation

* fix: relation

* fix: relation

* fix: relation

* fix: view meta

* fix: view meta

* fix: view meta

* fix: empty database block

* fix: 0716 bugs

* fix: add button to url cell

* fix: jest test

* fix: unit tests

* fix: lint

* fix: reduce database space

* fix: add after payment page

* fix: add spacing

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
This commit is contained in:
Kilu.He 2024-07-22 13:37:27 +08:00 committed by GitHub
parent 432db0f6d5
commit a8b4f22703
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 1190 additions and 564 deletions

View File

@ -68,6 +68,20 @@ const createServer = async (req: Request) => {
logger.info(`Request URL: ${hostname}${reqUrl.pathname}`);
if (reqUrl.pathname === '/after-payment') {
timer();
const htmlData = fs.readFileSync(indexPath, 'utf8');
const $ = load(htmlData);
$('title').text('Payment Success | AppFlowy');
$('link[rel="icon"]').attr('href', '/appflowy.svg');
setOrUpdateMetaTag($, 'meta[name="description"]', 'name', 'Payment success on AppFlowy');
return new Response($.html(), {
headers: { 'Content-Type': 'text/html' },
});
}
const [namespace, publishName] = reqUrl.pathname.slice(1).split('/');
logger.info(`Namespace: ${namespace}, Publish Name: ${publishName}`);

View File

@ -98,7 +98,7 @@
"validator": "^13.11.0",
"vite-plugin-wasm": "^3.3.0",
"y-indexeddb": "9.0.12",
"yjs": "^13.6.14"
"yjs": "14.0.0-1"
},
"devDependencies": {
"@babel/preset-env": "^7.24.7",

View File

@ -1,5 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@appflowyinc/client-api-wasm':
specifier: 0.1.2
@ -36,7 +40,7 @@ dependencies:
version: 2.0.0(react-redux@8.1.3)(react@18.2.0)
'@slate-yjs/core':
specifier: ^1.0.2
version: 1.0.2(slate@0.101.5)(yjs@13.6.15)
version: 1.0.2(slate@0.101.5)(yjs@14.0.0-1)
'@tauri-apps/api':
specifier: ^1.5.3
version: 1.5.6
@ -222,10 +226,10 @@ dependencies:
version: 3.3.0(vite@5.2.0)
y-indexeddb:
specifier: 9.0.12
version: 9.0.12(yjs@13.6.15)
version: 9.0.12(yjs@14.0.0-1)
yjs:
specifier: ^13.6.14
version: 13.6.15
specifier: 14.0.0-1
version: 14.0.0-1
devDependencies:
'@babel/preset-env':
@ -3589,15 +3593,15 @@ packages:
dependencies:
'@sinonjs/commons': 3.0.1
/@slate-yjs/core@1.0.2(slate@0.101.5)(yjs@13.6.15):
/@slate-yjs/core@1.0.2(slate@0.101.5)(yjs@14.0.0-1):
resolution: {integrity: sha512-X0hLFJbQu9c1ItWBaNuEn0pqcXYK76KCp8C4Gvy/VaTQVMo1VgAb2WiiJ0Je/AyuIYEPPSTNVOcyrGHwgA7e6Q==}
peerDependencies:
slate: '>=0.70.0'
yjs: ^13.5.29
dependencies:
slate: 0.101.5
y-protocols: 1.0.6(yjs@13.6.15)
yjs: 13.6.15
y-protocols: 1.0.6(yjs@14.0.0-1)
yjs: 14.0.0-1
dev: false
/@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.24.3):
@ -11556,24 +11560,24 @@ packages:
engines: {node: '>=0.4'}
dev: true
/y-indexeddb@9.0.12(yjs@13.6.15):
/y-indexeddb@9.0.12(yjs@14.0.0-1):
resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
yjs: ^13.0.0
dependencies:
lib0: 0.2.94
yjs: 13.6.15
yjs: 14.0.0-1
dev: false
/y-protocols@1.0.6(yjs@13.6.15):
/y-protocols@1.0.6(yjs@14.0.0-1):
resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
peerDependencies:
yjs: ^13.0.0
dependencies:
lib0: 0.2.94
yjs: 13.6.15
yjs: 14.0.0-1
dev: false
/y18n@4.0.3:
@ -11642,9 +11646,9 @@ packages:
fd-slicer: 1.1.0
dev: true
/yjs@13.6.15:
resolution: {integrity: sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==}
engines: {node: '>=16.0.0', npm: '>=8.0.0'}
/yjs@14.0.0-1:
resolution: {integrity: sha512-w0iJlEx+XvkvPkdBH0L8pb4Da2DvTEA7UdDl/dOFCQfA0siT4cUtbJ8LfoiliH2juYFqdIoqxbScHakKBiIv0g==}
requiresBuild: true
dependencies:
lib0: 0.2.94
dev: false
@ -11662,7 +11666,3 @@ packages:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -1,3 +1,4 @@
import { ViewMeta } from '@/application/db/tables/view_metas';
import * as Y from 'yjs';
export type BlockId = string;
@ -413,7 +414,7 @@ export interface YDatabase extends Y.Map<unknown> {
get(key: YjsDatabaseKey.id): string;
}
export interface YDatabaseViews extends Y.Map<unknown> {
export interface YDatabaseViews extends Y.Map<YDatabaseView> {
get(key: ViewId): YDatabaseView;
}
@ -558,7 +559,7 @@ export interface YDatabaseMetas extends Y.Map<unknown> {
get(key: YjsDatabaseKey.iid): string;
}
export interface YDatabaseFields extends Y.Map<unknown> {
export interface YDatabaseFields extends Y.Map<YDatabaseField> {
get(key: FieldId): YDatabaseField;
}
@ -667,3 +668,9 @@ export interface PublishViewMetaData {
child_views: PublishViewInfo[];
ancestor_views: PublishViewInfo[];
}
export type GetViewRowsMap = (viewId: string) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
export type LoadView = (viewId: string) => Promise<YDoc>;
export type LoadViewMeta = (viewId: string, onChange?: (meta: ViewMeta) => void) => Promise<ViewMeta>;

View File

@ -24,10 +24,37 @@ describe('Database group', () => {
const { fields, rowMap } = withTestingData();
expect(groupByField(rows, rowMap, fields.get('text_field'))).toBeUndefined();
expect(groupByField(rows, rowMap, fields.get('number_field'))).toBeUndefined();
expect(groupByField(rows, rowMap, fields.get('checkbox_field'))).toBeUndefined();
expect(groupByField(rows, rowMap, fields.get('checklist_field'))).toBeUndefined();
});
it('should gourp by checkbox field', () => {
const { fields, rowMap } = withTestingData();
const field = fields.get('checkbox_field');
const result = groupByField(rows, rowMap, field);
const expectRes = new Map([
[
'Yes',
[
{ id: '1', height: 37 },
{ id: '3', height: 37 },
{ id: '5', height: 37 },
{ id: '7', height: 37 },
{ id: '9', height: 37 },
],
],
[
'No',
[
{ id: '2', height: 37 },
{ id: '4', height: 37 },
{ id: '6', height: 37 },
{ id: '8', height: 37 },
{ id: '10', height: 37 },
],
],
]);
expect(result).toEqual(expectRes);
});
it('should group by select option field', () => {
const { fields, rowMap } = withTestingData();
const field = fields.get('single_select_field');

View File

@ -30,7 +30,7 @@ const wrapperCreator =
(viewId: string, doc: YDoc, rowDocMap: Y.Map<YDoc>) =>
({ children }: { children: React.ReactNode }) => {
return (
<DatabaseContextProvider viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}>
<DatabaseContextProvider iidIndex={viewId} viewId={viewId} databaseDoc={doc} rowDocMap={rowDocMap} readOnly={true}>
{children}
</DatabaseContextProvider>
);

View File

@ -1,6 +1,5 @@
import {
YDatabase,
YDatabaseField,
YDatabaseFields,
YDatabaseFilters,
YDatabaseGroup,
@ -133,11 +132,13 @@ export function withTestingDatabase(viewId: string) {
const fieldOrder = new Y.Array();
const rowOrders = new Y.Array();
Array.from(fields).forEach(([fieldId, field]) => {
fields.forEach((field) => {
const setting = new Y.Map();
const fieldId = field.get(YjsDatabaseKey.id);
if (fieldId === 'text_field') {
(field as YDatabaseField).set(YjsDatabaseKey.is_primary, true);
field.set(YjsDatabaseKey.is_primary, true);
}
fieldOrder.push([fieldId]);

View File

@ -4,10 +4,11 @@ import * as Y from 'yjs';
import { v5 as uuidv5, parse as uuidParse } from 'uuid';
export const DEFAULT_ROW_HEIGHT = 36;
export const MIN_COLUMN_WIDTH = 100;
export const MIN_COLUMN_WIDTH = 150;
export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map<YDoc>) => {
const rowMeta = rowMetas.get(rowId);
const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow;
return meta?.get(YjsDatabaseKey.cells)?.get(fieldId);

View File

@ -1,18 +1,27 @@
import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import {
GetViewRowsMap,
LoadView,
LoadViewMeta,
YDatabase,
YDatabaseRow,
YDoc,
YjsDatabaseKey,
YjsEditorKey,
} from '@/application/collab.type';
import { createContext, useContext } from 'react';
import * as Y from 'yjs';
export interface DatabaseContextState {
readOnly: boolean;
databaseDoc: YDoc;
iidIndex: string;
viewId: string;
rowDocMap: Y.Map<YDoc>;
isDatabaseRowPage?: boolean;
navigateToRow?: (rowId: string) => void;
loadView?: (viewId: string) => Promise<YDoc>;
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
loadView?: LoadView;
getViewRowsMap?: GetViewRowsMap;
loadViewMeta?: LoadViewMeta;
navigateToView?: (viewId: string) => Promise<void>;
}

View File

@ -18,6 +18,8 @@ export enum FieldType {
LastEditedTime = 8,
CreatedTime = 9,
Relation = 10,
AISummaries = 11,
AITranslations = 12,
}
export enum CalculationType {

View File

@ -50,7 +50,7 @@ describe('currencyFormaterMap', () => {
test('should return the correct formatter for EUR', () => {
const formater = currencyFormaterMap[NumberFormat.EUR];
const result = ['€0', '€1', '€0.5', '€0.57', '€1,000', '€10,000', '€1,000,000', '€10,000,000', '€1,000,000'];
const result = ['€0', '€1', '€0,5', '€0,57', '€1.000', '€10.000', '€1.000.000', '€10.000.000', '€1.000.000'];
testCases.forEach((testCase, index) => {
expect(formater(testCase)).toBe(result[index]);

View File

@ -32,11 +32,18 @@ export const currencyFormaterMap: Record<NumberFormat, (n: number) => string> =
})
.format(n)
.replace('$', 'CA$'),
[NumberFormat.EUR]: (n: number) =>
new Intl.NumberFormat('en-IE', {
[NumberFormat.EUR]: (n: number) => {
const formattedAmount = new Intl.NumberFormat('de-DE', {
...commonProps,
currency: 'EUR',
}).format(n),
})
.format(n)
.replace('€', '')
.trim();
return `${formattedAmount}`;
},
[NumberFormat.Pound]: (n: number) =>
new Intl.NumberFormat('en-GB', {
...commonProps,

View File

@ -9,8 +9,31 @@ export function groupByField(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabas
const fieldType = Number(field.get(YjsDatabaseKey.type));
const isSelectOptionField = [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType);
if (!isSelectOptionField) return;
return groupBySelectOption(rows, rowMetas, field);
if (isSelectOptionField) {
return groupBySelectOption(rows, rowMetas, field);
}
if (fieldType === FieldType.Checkbox) {
return groupByCheckbox(rows, rowMetas, field);
}
return;
}
export function groupByCheckbox(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabaseField) {
const fieldId = field.get(YjsDatabaseKey.id);
const result = new Map<string, Row[]>();
rows.forEach((row) => {
const cellData = getCellData(row.id, fieldId, rowMetas);
const groupName = cellData === 'Yes' ? 'Yes' : 'No';
const group = result.get(groupName) ?? [];
group.push(row);
result.set(groupName, group);
});
return result;
}
export function groupBySelectOption(rows: Row[], rowMetas: Y.Map<YDoc>, field: YDatabaseField) {

View File

@ -1,4 +1,12 @@
import { FieldId, SortId, YDatabaseField, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import {
FieldId,
SortId,
YDatabase,
YDatabaseField,
YDoc,
YjsDatabaseKey,
YjsEditorKey,
} from '@/application/collab.type';
import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const';
import {
useDatabase,
@ -14,7 +22,7 @@ import { sortBy } from '@/application/database-yjs/sort';
import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse';
import { DateTimeCell } from '@/application/database-yjs/cell.type';
import * as dayjs from 'dayjs';
import { throttle } from 'lodash-es';
import { debounce } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Y from 'yjs';
import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type';
@ -33,7 +41,7 @@ export interface Row {
const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty];
export function useDatabaseViewsSelector(_iidIndex: string) {
export function useDatabaseViewsSelector(_iidIndex: string, visibleViewIds?: string[]) {
const database = useDatabase();
const views = database?.get(YjsDatabaseKey.views);
@ -46,7 +54,12 @@ export function useDatabaseViewsSelector(_iidIndex: string) {
if (!views) return;
const observerEvent = () => {
const viewsObj = views.toJSON();
const viewsObj = views.toJSON() as Record<
string,
{
created_at: number;
}
>;
const viewsSorted = Object.entries(viewsObj).sort((a, b) => {
const [, viewA] = a;
@ -55,7 +68,13 @@ export function useDatabaseViewsSelector(_iidIndex: string) {
return Number(viewB.created_at) - Number(viewA.created_at);
});
setViewIds(viewsSorted.map(([key]) => key));
setViewIds(
viewsSorted
.map(([key]) => key)
.filter((id) => {
return !visibleViewIds || visibleViewIds.includes(id);
})
);
};
observerEvent();
@ -64,7 +83,7 @@ export function useDatabaseViewsSelector(_iidIndex: string) {
return () => {
views.unobserve(observerEvent);
};
}, [views]);
}, [views, visibleViewIds]);
return {
childViews,
@ -85,7 +104,8 @@ export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisibl
const fieldSettings = view?.get(YjsDatabaseKey.field_settings);
const getColumns = () => {
if (!fields || !fieldsOrder || !fieldSettings) return [];
const fieldIds = fieldsOrder.toJSON().map((item) => item.id) as string[];
const fieldIds = (fieldsOrder.toJSON() as { id: string }[]).map((item) => item.id);
return fieldIds
.map((fieldId) => {
@ -160,7 +180,7 @@ export function useFiltersSelector() {
if (!filterOrders) return;
const getFilters = () => {
return filterOrders.toJSON().map((item) => item.id);
return (filterOrders.toJSON() as { id: string }[]).map((item) => item.id);
};
const observerEvent = () => setFilters(getFilters());
@ -223,7 +243,7 @@ export function useSortsSelector() {
if (!sortOrders) return;
const getSorts = () => {
return sortOrders.toJSON().map((item) => item.id);
return (sortOrders.toJSON() as { id: string }[]).map((item) => item.id);
};
const observerEvent = () => setSorts(getSorts());
@ -287,12 +307,13 @@ export function useGroupsSelector() {
useEffect(() => {
if (!viewId) return;
const view = database?.get(YjsDatabaseKey.views)?.get(viewId);
const groupOrders = view?.get(YjsDatabaseKey.groups);
if (!groupOrders) return;
const getGroups = () => {
return groupOrders.toJSON().map((item) => item.id);
return (groupOrders.toJSON() as { id: string }[]).map((item) => item.id);
};
const observerEvent = () => setGroups(getGroups());
@ -364,13 +385,13 @@ export function useRowsByGroup(groupId: string) {
const fields = useDatabaseFields();
const [notFound, setNotFound] = useState(false);
const [groupResult, setGroupResult] = useState<Map<string, Row[]>>(new Map());
const view = useDatabaseView();
const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('1');
useEffect(() => {
if (!fieldId || !rowOrders || !rows) return;
const onConditionsChange = () => {
if (rows.size < rowOrders?.length) return;
const newResult = new Map<string, Row[]>();
const field = fields.get(fieldId);
@ -399,7 +420,10 @@ export function useRowsByGroup(groupId: string) {
};
}, [fieldId, fields, rowOrders, rows]);
const visibleColumns = columns.filter((column) => column.visible);
const visibleColumns = columns.filter((column) => {
if (column.id === fieldId) return !layoutSetting?.get(YjsDatabaseKey.hide_ungrouped_column);
return column.visible;
});
return {
fieldId,
@ -450,18 +474,20 @@ export function useRowOrdersSelector() {
}, [onConditionsChange, clock]);
useEffect(() => {
const throttleChange = throttle(onConditionsChange, 200);
const throttleChange = debounce(onConditionsChange, 200);
view?.get(YjsDatabaseKey.row_orders)?.observeDeep(throttleChange);
sorts?.observeDeep(throttleChange);
filters?.observeDeep(throttleChange);
fields?.observeDeep(throttleChange);
return () => {
view?.get(YjsDatabaseKey.row_orders)?.unobserveDeep(throttleChange);
sorts?.unobserveDeep(throttleChange);
filters?.unobserveDeep(throttleChange);
fields?.unobserveDeep(throttleChange);
};
}, [onConditionsChange, fields, filters, sorts]);
}, [onConditionsChange, view, fields, filters, sorts]);
return rowOrders;
}
@ -474,17 +500,10 @@ export function useRowDocMapSelector() {
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);
});
rowMap.observeDeep(observerEvent);
return () => {
rowMap.unobserve(observerEvent);
observers.forEach((observer) => observer());
rowMap.unobserveDeep(observerEvent);
};
}, [rowMap]);
@ -514,37 +533,20 @@ export function observeDeepRow(
export function useRowDataSelector(rowId: string) {
const rowMap = useRowDocMap();
const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section);
const rowDoc = rowMap?.get(rowId);
const rowSharedRoot = rowDoc?.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 }) {
const { row } = useRowDataSelector(rowId);
const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId);
const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined));
useEffect(() => {
@ -552,10 +554,10 @@ export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: st
setCellValue(parseYDatabaseCellToCell(cell));
const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell));
cell.observe(observerEvent);
cell.observeDeep(observerEvent);
return () => {
cell.unobserve(observerEvent);
cell.unobserveDeep(observerEvent);
};
}, [cell]);
@ -656,17 +658,20 @@ export function useCalendarLayoutSetting() {
return setting;
}
export function getPrimaryFieldId(database: YDatabase) {
const fields = database?.get(YjsDatabaseKey.fields);
return Array.from(fields?.keys() || []).find((fieldId) => {
return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary);
});
}
export function usePrimaryFieldId() {
const database = useDatabase();
const [primaryFieldId, setPrimaryFieldId] = useState<string | null>(null);
useEffect(() => {
const fields = database?.get(YjsDatabaseKey.fields);
const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => {
return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary);
});
setPrimaryFieldId(primaryFieldId || null);
setPrimaryFieldId(getPrimaryFieldId(database) || null);
}, [database]);
return primaryFieldId;

View File

@ -65,7 +65,7 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole
case FieldType.URL:
return data ? data : '\uFFFF';
case FieldType.Number:
return data;
return data === 'string' && !isNaN(parseInt(data)) ? parseInt(data) : data;
case FieldType.Checkbox:
return data === 'Yes';
case FieldType.SingleSelect:

View File

@ -21,7 +21,9 @@ const openedSet = new Set<string>();
*/
export async function openCollabDB(docName: string): Promise<YDoc> {
const name = `${databasePrefix}_${docName}`;
const doc = new Y.Doc();
const doc = new Y.Doc({
guid: docName,
});
const provider = new IndexeddbPersistence(name, doc);
@ -56,3 +58,39 @@ export async function closeCollabDB(docName: string) {
await provider.destroy();
}
export async function clearData() {
try {
const databases = await indexedDB.databases();
databases.forEach((dbInfo) => {
const dbName = dbInfo.name as string;
const request = indexedDB.open(dbName);
request.onsuccess = function (event) {
const db = (event.target as IDBOpenDBRequest).result;
db.close();
const deleteRequest = indexedDB.deleteDatabase(dbName);
deleteRequest.onsuccess = function () {
console.log(`Database ${dbName} deleted successfully`);
};
deleteRequest.onerror = function (event) {
console.error(`Error deleting database ${dbName}`, event);
};
deleteRequest.onblocked = function () {
console.warn(`Delete operation blocked for database ${dbName}`);
};
};
request.onerror = function (event) {
console.error(`Error opening database ${dbName}`, event);
};
});
} catch (e) {
console.error('Error listing databases:', e);
}
}

View File

@ -8,6 +8,7 @@ export type ViewMeta = {
ancestor_views: PublishViewInfo[];
visible_view_ids: string[];
database_relations: Record<string, string>;
} & PublishViewInfo;
export type ViewMetasTable = {

View File

@ -1,21 +1,20 @@
import { YDoc } from '@/application/collab.type';
import { GetViewRowsMap, LoadView, LoadViewMeta } from '@/application/collab.type';
import { db } from '@/application/db';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { AFConfigContext } from '@/components/app/AppConfig';
import { useLiveQuery } from 'dexie-react-hooks';
import { createContext, useCallback, useContext, useEffect, useRef } from 'react';
import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import * as Y from 'yjs';
export interface PublishContextType {
namespace: string;
publishName: string;
viewMeta?: ViewMeta;
toView: (viewId: string) => Promise<void>;
loadViewMeta: (viewId: string) => Promise<ViewMeta>;
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
loadViewMeta: LoadViewMeta;
getViewRowsMap?: GetViewRowsMap;
loadView: (viewId: string) => Promise<YDoc>;
loadView: LoadView;
}
export const PublishContext = createContext<PublishContextType | null>(null);
@ -34,6 +33,39 @@ export const PublishProvider = ({
return db.view_metas.get(name);
}, [namespace, publishName]);
const [subscribers, setSubscribers] = useState<Map<string, (meta: ViewMeta) => void>>(new Map());
useEffect(() => {
return () => {
setSubscribers(new Map());
};
}, []);
useEffect(() => {
db.view_metas.hook('creating', (primaryKey, obj) => {
const subscriber = subscribers.get(primaryKey);
subscriber?.(obj);
return obj;
});
db.view_metas.hook('deleting', (primaryKey, obj) => {
const subscriber = subscribers.get(primaryKey);
subscriber?.(obj);
return;
});
db.view_metas.hook('updating', (modifications, primaryKey, obj) => {
const subscriber = subscribers.get(primaryKey);
subscriber?.({
...obj,
...modifications,
});
return modifications;
});
}, [subscribers]);
const prevViewMeta = useRef(viewMeta);
@ -59,7 +91,7 @@ export const PublishProvider = ({
);
const loadViewMeta = useCallback(
async (viewId: string) => {
async (viewId: string, callback?: (meta: ViewMeta) => void) => {
try {
const info = await service?.getPublishInfo(viewId);
@ -69,13 +101,25 @@ export const PublishProvider = ({
const { namespace, publishName } = info;
const res = await service?.getPublishViewMeta(namespace, publishName);
const name = `${namespace}_${publishName}`;
if (!res) {
throw new Error('View meta has not been published yet');
const meta = await service?.getPublishViewMeta(namespace, publishName);
if (!meta) {
return Promise.reject(new Error('View meta has not been published yet'));
}
return res;
callback?.(meta);
if (callback) {
setSubscribers((prev) => {
prev.set(name, callback);
return prev;
});
}
return meta;
} catch (e) {
return Promise.reject(e);
}
@ -84,7 +128,7 @@ export const PublishProvider = ({
);
const getViewRowsMap = useCallback(
async (viewId: string, rowIds: string[]) => {
async (viewId: string, rowIds?: string[]) => {
try {
const info = await service?.getPublishInfo(viewId);

View File

@ -1,8 +1,9 @@
import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor';
import * as Y from 'yjs';
import { AFClientService } from '../index';
import { fetchViewInfo } from '@/application/services/js-services/fetch';
import { expect, jest } from '@jest/globals';
import { getBatchCollabs, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache';
import { getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache';
jest.mock('@/application/services/js-services/wasm/client_api', () => {
return {
@ -69,18 +70,10 @@ describe('AFClientService', () => {
it('should get view', async () => {
const namespace = 'namespace';
const publishName = 'publishName';
const rowDoc = new Y.Doc();
const mockResponse = {
data: [1, 2, 3],
meta: {
metadata: {
view: {
name: 'viewName',
view_id: 'view_id',
},
child_views: [],
ancestor_views: [],
},
},
doc: withTestingYDoc('1'),
rowDocMap: rowDoc.getMap(),
};
// @ts-ignore
@ -88,7 +81,7 @@ describe('AFClientService', () => {
const result = await service.getPublishView(namespace, publishName);
expect(result).toEqual(mockResponse);
expect(result).toEqual(mockResponse.doc);
});
it('should get view info', async () => {
@ -108,21 +101,4 @@ describe('AFClientService', () => {
publishName: 'publishName',
});
});
it('getPublishDatabaseViewRows', async () => {
const namespace = 'namespace';
const publishName = 'publishName';
const rowIds = ['1', '2', '3'];
const mockResponse = [withTestingYDoc('1'), withTestingYDoc('2'), withTestingYDoc('3')];
// @ts-ignore
(getBatchCollabs as jest.Mock).mockResolvedValue(mockResponse);
const result = await service.getPublishDatabaseViewRows(namespace, publishName, rowIds);
expect(result).toEqual({
rows: expect.any(Object),
destroy: expect.any(Function),
});
});
});

View File

@ -1,15 +1,9 @@
import { CollabType } from '@/application/collab.type';
import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor';
import { expect } from '@jest/globals';
import {
collabTypeToDBType,
getPublishView,
getPublishViewMeta,
getBatchCollabs,
} from '@/application/services/js-services/cache';
import { collabTypeToDBType, getPublishView, getPublishViewMeta } from '@/application/services/js-services/cache';
import { openCollabDB, db } from '@/application/db';
import { StrategyType } from '@/application/services/js-services/cache/types';
import * as Y from 'yjs';
jest.mock('@/application/ydoc/apply', () => ({
applyYDoc: jest.fn(),
@ -59,6 +53,7 @@ describe('Cache functions', () => {
describe('getPublishView', () => {
it('should call fetcher when no cache found', async () => {
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } });
(db.view_metas.get as jest.Mock).mockResolvedValue(undefined);
await runTestWithStrategy(StrategyType.CACHE_FIRST);
@ -73,14 +68,14 @@ describe('Cache functions', () => {
(db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' });
mockFetcher.mockResolvedValue({ data: [1, 2, 3], meta: { metadata: { view: { id: '1' } } } });
await runTestWithStrategy(StrategyType.CACHE_ONLY);
expect(openCollabDB).toBeCalledTimes(1);
expect(openCollabDB).toBeCalledTimes(2);
await runTestWithStrategy(StrategyType.CACHE_FIRST);
expect(openCollabDB).toBeCalledTimes(2);
expect(openCollabDB).toBeCalledTimes(4);
expect(mockFetcher).toBeCalledTimes(0);
await runTestWithStrategy(StrategyType.CACHE_AND_NETWORK);
expect(openCollabDB).toBeCalledTimes(3);
expect(openCollabDB).toBeCalledTimes(6);
expect(mockFetcher).toBeCalledTimes(1);
});
});
@ -116,20 +111,6 @@ describe('Cache functions', () => {
expect(mockFetcher).toBeCalledTimes(1);
});
});
describe('getBatchCollabs', () => {
it('should return empty array when no cache found', async () => {
(openCollabDB as jest.Mock).mockResolvedValue(new Y.Doc());
await expect(getBatchCollabs(['1', '2', '3'])).rejects.toThrow('No cache found');
});
it('should return collabs when cache found', async () => {
(openCollabDB as jest.Mock).mockResolvedValue(normalDoc);
(db.view_metas.get as jest.Mock).mockResolvedValue({ view_id: '1' });
const collabs = await getBatchCollabs(['1', '2', '3']);
expect(collabs).toEqual([normalDoc, normalDoc, normalDoc]);
});
});
});
describe('collabTypeToDBType', () => {

View File

@ -1,7 +1,10 @@
import {
CollabType,
DatabaseId,
PublishViewInfo,
PublishViewMetaData,
RowId,
ViewId,
YDoc,
YjsEditorKey,
YSharedRoot,
@ -9,6 +12,8 @@ import {
import { applyYDoc } from '@/application/ydoc/apply';
import { closeCollabDB, db, openCollabDB } from '@/application/db';
import { Fetcher, StrategyType } from '@/application/services/js-services/cache/types';
import { IndexeddbPersistence } from 'y-indexeddb';
import * as Y from 'yjs';
export function collabTypeToDBType(type: CollabType) {
switch (type) {
@ -110,8 +115,9 @@ export async function getPublishViewMeta<
export async function getPublishView<
T extends {
data: number[];
rows?: Record<string, number[]>;
visibleViewIds?: string[];
rows?: Record<RowId, number[]>;
visibleViewIds?: ViewId[];
relations?: Record<DatabaseId, ViewId>;
meta: {
view: PublishViewInfo;
child_views: PublishViewInfo[];
@ -131,6 +137,22 @@ export async function getPublishView<
) {
const name = `${namespace}_${publishName}`;
const doc = await openCollabDB(name);
const rowMapDoc = (await openCollabDB(`${name}_rows`)) as Y.Doc;
const subdocs = Array.from(rowMapDoc.getSubdocs());
for (const subdoc of subdocs) {
const promise = new Promise((resolve) => {
const persistence = new IndexeddbPersistence(subdoc.guid, subdoc);
persistence.on('synced', () => {
resolve(true);
});
});
await promise;
}
const exist = (await hasViewMetaCache(name)) && hasCollabCache(doc);
switch (strategy) {
@ -144,7 +166,7 @@ export async function getPublishView<
case StrategyType.CACHE_FIRST: {
if (!exist) {
await revalidatePublishView(name, fetcher, doc);
await revalidatePublishView(name, fetcher, doc, rowMapDoc);
}
break;
@ -152,21 +174,21 @@ export async function getPublishView<
case StrategyType.CACHE_AND_NETWORK: {
if (!exist) {
await revalidatePublishView(name, fetcher, doc);
await revalidatePublishView(name, fetcher, doc, rowMapDoc);
} else {
void revalidatePublishView(name, fetcher, doc);
void revalidatePublishView(name, fetcher, doc, rowMapDoc);
}
break;
}
default: {
await revalidatePublishView(name, fetcher, doc);
await revalidatePublishView(name, fetcher, doc, rowMapDoc);
break;
}
}
return doc;
return { doc, rowMapDoc };
}
export async function revalidatePublishViewMeta<
@ -187,6 +209,7 @@ export async function revalidatePublishViewMeta<
child_views: child_views,
ancestor_views: ancestor_views,
visible_view_ids: dbView?.visible_view_ids ?? [],
database_relations: dbView?.database_relations ?? {},
},
name
);
@ -197,12 +220,13 @@ export async function revalidatePublishViewMeta<
export async function revalidatePublishView<
T extends {
data: number[];
rows?: Record<string, number[]>;
visibleViewIds?: string[];
rows?: Record<RowId, number[]>;
visibleViewIds?: ViewId[];
relations?: Record<DatabaseId, ViewId>;
meta: PublishViewMetaData;
}
>(name: string, fetcher: Fetcher<T>, collab: YDoc) {
const { data, meta, rows, visibleViewIds = [] } = await fetcher();
>(name: string, fetcher: Fetcher<T>, collab: YDoc, rowMapDoc: Y.Doc) {
const { data, meta, rows, visibleViewIds = [], relations = {} } = await fetcher();
await db.view_metas.put(
{
@ -211,15 +235,23 @@ export async function revalidatePublishView<
child_views: meta.child_views,
ancestor_views: meta.ancestor_views,
visible_view_ids: visibleViewIds,
database_relations: relations,
},
name
);
if (rows) {
for (const [key, value] of Object.entries(rows)) {
const row = await openCollabDB(`${name}_${key}`);
const subdoc = new Y.Doc({
guid: key,
});
const persistence = new IndexeddbPersistence(subdoc.guid, subdoc);
applyYDoc(row, new Uint8Array(value));
persistence.on('synced', () => {
applyYDoc(subdoc, new Uint8Array(value));
rowMapDoc.getMap().delete(subdoc.guid);
rowMapDoc.getMap().set(subdoc.guid, subdoc);
});
}
}
@ -228,27 +260,6 @@ export async function revalidatePublishView<
applyYDoc(collab, state);
}
export async function getBatchCollabs(names: string[]) {
const getRowDoc = async (name: string) => {
const doc = await openCollabDB(name);
const exist = hasCollabCache(doc);
if (!exist) {
return Promise.reject(new Error('No cache found'));
}
return doc;
};
const collabs = await Promise.all(
names.map((name) => {
return getRowDoc(name);
})
);
return collabs;
}
export async function deleteViewMeta(name: string) {
await db.view_metas.delete(name);
}
@ -257,4 +268,15 @@ export async function deleteView(name: string) {
console.log('deleteView', name);
await deleteViewMeta(name);
await closeCollabDB(name);
const rowMapDoc = (await openCollabDB(`${name}_rows`)) as Y.Doc;
const subdocs = Array.from(rowMapDoc.getSubdocs());
for (const subdoc of subdocs) {
const persistence = new IndexeddbPersistence(subdoc.guid, subdoc);
await persistence.destroy();
}
await closeCollabDB(`${name}_rows`);
}

View File

@ -1,7 +1,6 @@
import { YDoc } from '@/application/collab.type';
import {
deleteView,
getBatchCollabs,
getPublishView,
getPublishViewMeta,
hasViewMetaCache,
@ -52,6 +51,9 @@ export class AFClientService implements AFService {
}
async getPublishViewMeta(namespace: string, publishName: string) {
const name = `${namespace}_${publishName}`;
const isLoaded = this.publishViewLoaded.has(name);
const viewMeta = await getPublishViewMeta(
() => {
return fetchPublishViewMeta(namespace, publishName);
@ -60,7 +62,7 @@ export class AFClientService implements AFService {
namespace,
publishName,
},
StrategyType.CACHE_AND_NETWORK
isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK
);
if (!viewMeta) {
@ -75,11 +77,12 @@ export class AFClientService implements AFService {
const isLoaded = this.publishViewLoaded.has(name);
const doc = await getPublishView(
const { doc, rowMapDoc } = await getPublishView(
async () => {
try {
return await fetchPublishView(namespace, publishName);
} catch (e) {
console.error(e);
void (async () => {
if (await hasViewMetaCache(name)) {
this.publishViewLoaded.delete(name);
@ -101,39 +104,30 @@ export class AFClientService implements AFService {
this.publishViewLoaded.add(name);
}
this.cacheDatabaseRowDocMap.set(name, rowMapDoc);
return doc;
}
async getPublishDatabaseViewRows(namespace: string, publishName: string, rowIds: string[]) {
async getPublishDatabaseViewRows(namespace: string, publishName: string) {
const name = `${namespace}_${publishName}`;
if (!this.publishViewLoaded.has(name)) {
if (!this.publishViewLoaded.has(name) || !this.cacheDatabaseRowDocMap.has(name)) {
await this.getPublishView(namespace, publishName);
}
const rootRowsDoc =
this.cacheDatabaseRowDocMap.get(name) ??
new Y.Doc({
guid: name,
});
const rootRowsDoc = this.cacheDatabaseRowDocMap.get(name);
if (!this.cacheDatabaseRowDocMap.has(name)) {
this.cacheDatabaseRowDocMap.set(name, rootRowsDoc);
if (!rootRowsDoc) {
return Promise.reject(new Error('Root rows doc not found'));
}
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
const docs = await getBatchCollabs(rowIds.map((id) => `${name}_${id}`));
docs.forEach((doc, index) => {
rowsFolder.set(rowIds[index], doc);
});
console.log('getPublishDatabaseViewRows', docs);
return {
rows: rowsFolder,
destroy: () => {
this.cacheDatabaseRowDocMap.delete(name);
rootRowsDoc.destroy();
},
};
}

View File

@ -1,7 +1,7 @@
import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token';
import { ClientAPI } from '@appflowyinc/client-api-wasm';
import { AFCloudConfig } from '@/application/services/services.type';
import { PublishViewMetaData, ViewLayout } from '@/application/collab.type';
import { DatabaseId, PublishViewMetaData, RowId, ViewId, ViewLayout } from '@/application/collab.type';
let client: ClientAPI;
@ -52,19 +52,22 @@ export async function getPublishView(publishNamespace: string, publishName: stri
try {
const decoder = new TextDecoder('utf-8');
const jsonStr = decoder.decode(new Uint8Array(data.data));
const res = JSON.parse(jsonStr) as {
database_collab: number[];
database_row_collabs: Record<string, number[]>;
database_row_collabs: Record<RowId, number[]>;
database_row_document_collabs: Record<string, number[]>;
visible_database_view_ids: string[];
visible_database_view_ids: ViewId[];
database_relations: Record<DatabaseId, ViewId>;
};
console.log('getPublishView', res);
return {
data: res.database_collab,
rows: res.database_row_collabs,
visibleViewIds: res.visible_database_view_ids,
relations: res.database_relations,
meta,
};
} catch (e) {

View File

@ -22,7 +22,7 @@ export interface PublishService {
getPublishDatabaseViewRows: (
namespace: string,
publishName: string,
rowIds: string[]
rowIds?: string[]
) => Promise<{
rows: Y.Map<YDoc>;
destroy: () => void;

View File

@ -1,5 +1,7 @@
import { YDoc } from '@/application/collab.type';
import { AFService } from '@/application/services/services.type';
import { nanoid } from 'nanoid';
import { YMap } from 'yjs/dist/src/types/YMap';
export class AFClientService implements AFService {
private deviceId: string = nanoid(8);
@ -18,10 +20,6 @@ export class AFClientService implements AFService {
return Promise.reject('Method not implemented');
}
async getPublishDatabaseViewRows(_namespace: string, _publishName: string, _rowIds: string[]) {
return Promise.reject('Method not implemented');
}
getClientId(): string {
return '';
}
@ -45,4 +43,14 @@ export class AFClientService implements AFService {
signInMagicLink(_params: { email: string; redirectTo: string }): Promise<void> {
return Promise.resolve(undefined);
}
getPublishDatabaseViewRows(
_namespace: string,
_publishName: string
): Promise<{
rows: YMap<YDoc>;
destroy: () => void;
}> {
return Promise.reject('Method not implemented');
}
}

View File

@ -13,7 +13,7 @@ export interface YjsEditor extends Editor {
connect: () => void;
disconnect: () => void;
sharedRoot: YSharedRoot;
applyRemoteEvents: (events: Array<YEvent<YSharedRoot>>, transaction: Transaction) => void;
applyRemoteEvents: (events: Array<YEvent>, transaction: Transaction) => void;
flushLocalChanges: () => void;
storeLocalChange: (op: Operation) => void;
}
@ -36,7 +36,7 @@ export const YjsEditor = {
editor.disconnect();
},
applyRemoteEvents(editor: YjsEditor, events: Array<YEvent<YSharedRoot>>, transaction: Transaction): void {
applyRemoteEvents(editor: YjsEditor, events: Array<YEvent>, transaction: Transaction): void {
editor.applyRemoteEvents(events, transaction);
},
@ -90,7 +90,7 @@ export function withYjs<T extends Editor>(
apply(op);
};
e.applyRemoteEvents = (_events: Array<YEvent<YSharedRoot>>, _: Transaction) => {
e.applyRemoteEvents = (_events: Array<YEvent>, _: Transaction) => {
// Flush local changes to ensure all local changes are applied before processing remote events
YjsEditor.flushLocalChanges(e);
// Replace the apply function to avoid storing remote changes as local changes
@ -103,7 +103,7 @@ export function withYjs<T extends Editor>(
e.apply = applyIntercept;
};
const handleYEvents = (events: Array<YEvent<YSharedRoot>>, transaction: Transaction) => {
const handleYEvents = (events: Array<YEvent>, transaction: Transaction) => {
if (transaction.origin === CollabOrigin.Remote) {
YjsEditor.applyRemoteEvents(e, events, transaction);
}

View File

@ -25,10 +25,16 @@ export function yDataToSlateContent({
rootId: string;
}): Element | undefined {
function traverse(id: string) {
const block = blocks.get(id).toJSON() as BlockJson;
const block = blocks.get(id)?.toJSON() as BlockJson;
if (!block) {
console.error('Block not found', id);
return;
}
const childrenId = block.children as string;
const children = (childrenMap.get(childrenId)?.toJSON() ?? []).map(traverse) as (Element | Text)[];
const children = (childrenMap.get(childrenId)?.toJSON() ?? []).map(traverse).filter(Boolean) as (Element | Text)[];
const slateNode = blockToSlateNode(block);

View File

@ -11,7 +11,12 @@ export function applyYDoc(doc: Y.Doc, state: Uint8Array) {
Y.transact(
doc,
() => {
Y.applyUpdate(doc, state);
try {
Y.applyUpdate(doc, state);
} catch (e) {
console.error('Error applying', doc, e);
throw e;
}
},
CollabOrigin.Remote
);

View File

@ -0,0 +1,23 @@
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_2389_7357)">
<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="url(#paint0_linear_2389_7357)" shape-rendering="crispEdges"/>
<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="#806989" shape-rendering="crispEdges"/>
<path d="M11.1576 11.884H8.79963L8.42163 13H6.81063L9.09663 6.682H10.8786L13.1646 13H11.5356L11.1576 11.884ZM10.7616 10.696L9.97863 8.383L9.20463 10.696H10.7616ZM14.6794 7.456C14.4094 7.456 14.1874 7.378 14.0134 7.222C13.8454 7.06 13.7614 6.862 13.7614 6.628C13.7614 6.388 13.8454 6.19 14.0134 6.034C14.1874 5.872 14.4094 5.791 14.6794 5.791C14.9434 5.791 15.1594 5.872 15.3274 6.034C15.5014 6.19 15.5884 6.388 15.5884 6.628C15.5884 6.862 15.5014 7.06 15.3274 7.222C15.1594 7.378 14.9434 7.456 14.6794 7.456ZM15.4444 7.978V13H13.9054V7.978H15.4444Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_2389_7357" x="0.666626" y="0" width="22" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2389_7357"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2389_7357" result="shape"/>
</filter>
<linearGradient id="paint0_linear_2389_7357" x1="15.6666" y1="2.4" x2="6.86663" y2="17.2" gradientUnits="userSpaceOnUse">
<stop stop-color="#726084" stop-opacity="0.8"/>
<stop offset="1" stop-color="#5D5862"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.44667 3.33333L12.6667 6.55333V12.6667H3.33333V3.33333H9.44667ZM9.44667 2H3.33333C2.6 2 2 2.6 2 3.33333V12.6667C2 13.4 2.6 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667V6.55333C14 6.2 13.86 5.86 13.6067 5.61333L10.3867 2.39333C10.14 2.14 9.8 2 9.44667 2ZM4.66667 10H11.3333V11.3333H4.66667V10ZM4.66667 7.33333H11.3333V8.66667H4.66667V7.33333ZM4.66667 4.66667H9.33333V6H4.66667V4.66667Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 540 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.58008 10.0468L6.88675 8.3735L6.90675 8.3535C8.06675 7.06016 8.89341 5.5735 9.38008 4.00016H11.3334V2.66683H6.66675V1.3335H5.33341V2.66683H0.666748V3.9935H8.11341C7.66675 5.28016 6.96008 6.50016 6.00008 7.56683C5.38008 6.88016 4.86675 6.12683 4.46008 5.3335H3.12675C3.61341 6.42016 4.28008 7.44683 5.11341 8.3735L1.72008 11.7202L2.66675 12.6668L6.00008 9.3335L8.07341 11.4068L8.58008 10.0468ZM12.3334 6.66683H11.0001L8.00008 14.6668H9.33341L10.0801 12.6668H13.2467L14.0001 14.6668H15.3334L12.3334 6.66683ZM10.5867 11.3335L11.6667 8.44683L12.7467 11.3335H10.5867Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 714 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8C2.5 4.96243 4.96243 2.5 8 2.5C8.81896 2.5 9.59612 2.679 10.2945 3" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@ -0,0 +1,12 @@
<svg width="35" height="34" viewBox="0 0 35 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.5 21.834C25.5 21.2817 25.9477 20.834 26.5 20.834C27.0523 20.834 27.5 21.2817 27.5 21.834V22.834H28.5C29.0523 22.834 29.5 23.2817 29.5 23.834C29.5 24.3863 29.0523 24.834 28.5 24.834H26.5C25.9477 24.834 25.5 24.3863 25.5 23.834V21.834Z"
fill="currentColor"/>
<path d="M26.5 29.834C29.8137 29.834 32.5 27.1477 32.5 23.834C32.5 20.5203 29.8137 17.834 26.5 17.834C23.1863 17.834 20.5 20.5203 20.5 23.834C20.5 27.1477 23.1863 29.834 26.5 29.834ZM26.5 27.834C24.2909 27.834 22.5 26.0431 22.5 23.834C22.5 21.6248 24.2909 19.834 26.5 19.834C28.7091 19.834 30.5 21.6248 30.5 23.834C30.5 26.0431 28.7091 27.834 26.5 27.834Z"
fill="currentColor"/>
<path d="M25.5 21.834C25.5 21.2817 25.9477 20.834 26.5 20.834C27.0523 20.834 27.5 21.2817 27.5 21.834V22.834H28.5C29.0523 22.834 29.5 23.2817 29.5 23.834C29.5 24.3863 29.0523 24.834 28.5 24.834H26.5C25.9477 24.834 25.5 24.3863 25.5 23.834V21.834Z"
stroke="currentColor" stroke-width="0.342857"/>
<path d="M26.5 29.834C29.8137 29.834 32.5 27.1477 32.5 23.834C32.5 20.5203 29.8137 17.834 26.5 17.834C23.1863 17.834 20.5 20.5203 20.5 23.834C20.5 27.1477 23.1863 29.834 26.5 29.834ZM26.5 27.834C24.2909 27.834 22.5 26.0431 22.5 23.834C22.5 21.6248 24.2909 19.834 26.5 19.834C28.7091 19.834 30.5 21.6248 30.5 23.834C30.5 26.0431 28.7091 27.834 26.5 27.834Z"
stroke="currentColor" stroke-width="0.342857"/>
<path d="M12.734 6.50065V6.60065H12.834H22.1673H22.2673V6.50065C22.2673 5.91155 22.7449 5.43398 23.334 5.43398C23.9231 5.43398 24.4007 5.91155 24.4007 6.50065V6.60065H24.5007H26.834C28.0675 6.60065 29.0673 7.60055 29.0673 8.83398V15.734H8.16732H8.06732V15.834V26.334V26.434H8.16732H18.734V28.5673H8.16732C6.93388 28.5673 5.93398 27.5674 5.93398 26.334V8.83399C5.93398 7.60055 6.93384 6.60065 8.16728 6.60065H10.5007H10.6007V6.50065C10.6007 5.91155 11.0782 5.43398 11.6673 5.43398C12.2564 5.43398 12.734 5.91155 12.734 6.50065ZM26.834 13.6007H26.934V13.5007V8.83398V8.73398H26.834H24.5007H24.4007V8.83398C24.4007 9.42309 23.9231 9.90065 23.334 9.90065C22.7449 9.90065 22.2673 9.42309 22.2673 8.83398V8.73398H22.1673H12.834H12.734V8.83398C12.734 9.42309 12.2564 9.90065 11.6673 9.90065C11.0782 9.90065 10.6007 9.42309 10.6007 8.83398V8.73398H10.5007H8.16732H8.06732V8.83398V13.5007V13.6007H8.16732H26.834Z"
fill="currentColor" stroke="currentColor" stroke-width="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8889 3.5H4.11111C3.49746 3.5 3 3.94772 3 4.5V11.5C3 12.0523 3.49746 12.5 4.11111 12.5H11.8889C12.5025 12.5 13 12.0523 13 11.5V4.5C13 3.94772 12.5025 3.5 11.8889 3.5Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 2.5V4.58181" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 2.5V4.58181" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6.5H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

View File

@ -0,0 +1,4 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.734 6.50065V6.60065H12.834H22.1673H22.2673V6.50065C22.2673 5.91155 22.7449 5.43398 23.334 5.43398C23.9231 5.43398 24.4007 5.91155 24.4007 6.50065V6.60065H24.5007H26.834C28.0675 6.60065 29.0673 7.60055 29.0673 8.83398V15.734H8.16732H8.06732V15.834V26.334V26.434H8.16732H19.734V28.5673H8.16732C6.93388 28.5673 5.93398 27.5674 5.93398 26.334V8.83399C5.93398 7.60055 6.93384 6.60065 8.16728 6.60065H10.5007H10.6007V6.50065C10.6007 5.91155 11.0782 5.43398 11.6673 5.43398C12.2564 5.43398 12.734 5.91155 12.734 6.50065ZM26.834 13.6007H26.934V13.5007V8.83398V8.73398H26.834H24.5007H24.4007V8.83398C24.4007 9.42309 23.9231 9.90065 23.334 9.90065C22.7449 9.90065 22.2673 9.42309 22.2673 8.83398V8.73398H22.1673H12.834H12.734V8.83398C12.734 9.42309 12.2564 9.90065 11.6673 9.90065C11.0782 9.90065 10.6007 9.42309 10.6007 8.83398V8.73398H10.5007H8.16732H8.06732V8.83398V13.5007V13.6007H8.16732H26.834ZM27.6602 18.999C27.7937 18.7678 28.0893 18.6886 28.3205 18.8221L29.3308 19.4054C29.562 19.5389 29.6412 19.8345 29.5077 20.0657L25.1827 27.5568L23.3352 26.4901L27.6602 18.999ZM22.7054 27.7046L24.4459 28.7094L22.5708 29.9475L22.7054 27.7046Z"
fill="currentColor" stroke="currentColor" stroke-width="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 4L12.5 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 8H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 12H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="4" r="0.5" fill="#333333"/>
<circle cx="4" cy="8" r="0.5" fill="#333333"/>
<circle cx="4" cy="12" r="0.5" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="11.8274" cy="5.82739" r="1.5" stroke="#333333"/>
<path d="M10.5008 5.38471L6.24097 4.78992" stroke="#333333"/>
<path d="M4.86475 6.24121L6.02777 10.1009" stroke="#333333"/>
<circle cx="7" cy="11" r="1.5" stroke="#333333"/>
<circle cx="5" cy="5" r="1.5" stroke="#333333"/>
<path d="M10.9011 7.14258L8.1484 10.0447" stroke="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.78787 8.78787L6.51213 7.51213C6.32314 7.32314 6.45699 7 6.72426 7H9.27574C9.54301 7 9.67686 7.32314 9.48787 7.51213L8.21213 8.78787C8.09497 8.90503 7.90503 8.90503 7.78787 8.78787Z" fill="#333333"/>
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z" fill="#333333"/>
<path d="M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7.688L8.27223 12.1469C7.69304 12.6931 6.90749 13 6.0884 13C5.26931 13 4.48376 12.6931 3.90457 12.1469C3.32538 11.6006 3 10.8598 3 10.0873C3 9.31474 3.32538 8.57387 3.90457 8.02763L8.63234 3.56875C9.01847 3.20459 9.54216 3 10.0882 3C10.6343 3 11.158 3.20459 11.5441 3.56875C11.9302 3.93291 12.1472 4.42683 12.1472 4.94183C12.1472 5.45684 11.9302 5.95075 11.5441 6.31491L6.8112 10.7738C6.61814 10.9559 6.35629 11.0582 6.08326 11.0582C5.81022 11.0582 5.54838 10.9559 5.35531 10.7738C5.16225 10.5917 5.05379 10.3448 5.05379 10.0873C5.05379 9.82975 5.16225 9.58279 5.35531 9.40071L9.72297 5.28632" stroke="#333333" stroke-width="0.9989" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -16,9 +16,10 @@ export const AFScroller = React.forwardRef(
<Scrollbars
onScroll={onScroll}
autoHide
hideTracksWhenNotNeeded
ref={(el) => {
if (!el) return;
const scrollEl = el.container?.firstChild as HTMLElement;
if (!scrollEl) return;

View File

@ -1,6 +1,7 @@
import { AUTH_CALLBACK_PATH } from '@/application/session/sign_in';
import NotFound from '@/components/error/NotFound';
import LoginAuth from '@/components/login/LoginAuth';
import AfterPaymentPage from '@/pages/AfterPaymentPage';
import LoginPage from '@/pages/LoginPage';
import PublishPage from '@/pages/PublishPage';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
@ -14,6 +15,7 @@ const AppMain = withAppWrapper(() => {
<Route path={'/login'} element={<LoginPage />} />
<Route path={AUTH_CALLBACK_PATH} element={<LoginAuth />} />
<Route path='/404' element={<NotFound />} />
<Route path='/after-payment' element={<AfterPaymentPage />} />
<Route path='*' element={<NotFound />} />
</Routes>
);

View File

@ -1,3 +1,4 @@
import { clearData } from '@/application/db';
import { EventType, on } from '@/application/session';
import { isTokenValid } from '@/application/session/token';
import { useAppLanguage } from '@/components/app/useAppLanguage';
@ -88,6 +89,23 @@ function AppConfig({ children }: { children: React.ReactNode }) {
};
}, [closeSnackbar, enqueueSnackbar]);
useEffect(() => {
const handleClearData = (e: KeyboardEvent) => {
if (e.key.toLowerCase() === 'r' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
e.stopPropagation();
e.preventDefault();
void clearData().then(() => {
window.location.reload();
});
}
};
window.addEventListener('keydown', handleClearData);
return () => {
window.removeEventListener('keydown', handleClearData);
};
});
return (
<AFConfigContext.Provider
value={{

View File

@ -48,7 +48,10 @@ function AppTheme({ children }: { children: React.ReactNode }) {
styleOverrides: {
contained: {
color: 'var(--content-on-fill)',
boxShadow: 'var(--shadow)',
boxShadow: 'none',
'&:hover': {
backgroundColor: 'var(--content-blue-600)',
},
},
},
},
@ -66,7 +69,7 @@ function AppTheme({ children }: { children: React.ReactNode }) {
borderRadius: '4px',
padding: '2px',
boxShadow: 'none',
boxShadow: 'none !important',
},
},
},

View File

@ -1,86 +1,84 @@
import { YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import {
GetViewRowsMap,
LoadView,
LoadViewMeta,
YDatabase,
YDoc,
YjsDatabaseKey,
YjsEditorKey,
} from '@/application/collab.type';
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
import DatabaseHeader from '@/components/database/components/header/DatabaseHeader';
import DatabaseRow from '@/components/database/DatabaseRow';
import DatabaseViews from '@/components/database/DatabaseViews';
import { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview';
import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import React, { Suspense, useCallback, useEffect, useState } from 'react';
import * as Y from 'yjs';
import { DatabaseContextProvider } from './DatabaseContext';
export interface Database2Props extends ViewMetaProps {
export interface Database2Props {
doc: YDoc;
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
loadView?: (viewId: string) => Promise<YDoc>;
getViewRowsMap?: GetViewRowsMap;
loadView?: LoadView;
navigateToView?: (viewId: string) => Promise<void>;
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
loadViewMeta?: LoadViewMeta;
viewId: string;
iidName: string;
rowId?: string;
onChangeView: (viewId: string) => void;
onOpenRow?: (rowId: string) => void;
visibleViewIds: string[];
iidIndex: string;
}
function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, ...viewMeta }: Database2Props) {
const [search, setSearch] = useSearchParams();
function Database({
doc,
getViewRowsMap,
navigateToView,
loadViewMeta,
loadView,
viewId,
iidIndex,
iidName,
visibleViewIds,
rowId,
onChangeView,
onOpenRow,
}: Database2Props) {
const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
const viewId = search.get('v') || viewMeta.viewId;
const rowIds = useMemo(() => {
if (!viewId) return [];
const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
const rows = database.get(YjsDatabaseKey.views).get(viewId).get(YjsDatabaseKey.row_orders);
return rows.toJSON().map((row) => row.id);
}, [doc, viewId]);
const iidIndex = useMemo(() => {
const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
return database.get(YjsDatabaseKey.metas).get(YjsDatabaseKey.iid);
}, [doc]);
const view = database.get(YjsDatabaseKey.views).get(iidIndex);
const rowOrders = view.get(YjsDatabaseKey.row_orders);
const [rowDocMap, setRowDocMap] = useState<Y.Map<YDoc> | null>(null);
const handleUpdateRowDocMap = useCallback(async () => {
if (!getViewRowsMap || !iidIndex) return;
const { rows, destroy } = await getViewRowsMap(iidIndex);
setRowDocMap(rows);
return destroy;
}, [getViewRowsMap, iidIndex]);
useEffect(() => {
if (!getViewRowsMap || !rowIds.length || !iidIndex) return;
void handleUpdateRowDocMap();
void (async () => {
const { rows, destroy } = await getViewRowsMap(iidIndex, rowIds);
setRowDocMap(rows);
return destroy;
})();
}, [getViewRowsMap, rowIds, iidIndex]);
const rowId = search.get('r');
const handleChangeView = useCallback(
(viewId: string) => {
setSearch({ v: viewId });
},
[setSearch]
);
const handleNavigateToRow = useCallback(
(rowId: string) => {
setSearch({ r: rowId });
},
[setSearch]
);
rowOrders?.observe(handleUpdateRowDocMap);
return () => {
rowOrders?.unobserve(handleUpdateRowDocMap);
};
}, [handleUpdateRowDocMap, rowOrders]);
if (!rowDocMap || !viewId) {
return null;
}
return (
<div
style={{
height: 'calc(100vh - 48px)',
}}
className={'flex w-full justify-center'}
>
<div className={'flex w-full flex-1 justify-center'}>
<Suspense fallback={<ComponentLoading />}>
<DatabaseContextProvider
isDatabaseRowPage={!!rowId}
navigateToRow={handleNavigateToRow}
navigateToRow={onOpenRow}
iidIndex={iidIndex}
viewId={viewId}
databaseDoc={doc}
rowDocMap={rowDocMap}
@ -88,21 +86,20 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView,
loadView={loadView}
navigateToView={navigateToView}
loadViewMeta={loadViewMeta}
getViewRowsMap={getViewRowsMap}
>
{rowId ? (
<DatabaseRow rowId={rowId} />
) : (
<div className={'relative flex h-full w-full flex-col'}>
{viewMeta && <DatabaseHeader {...viewMeta} />}
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
<DatabaseViews
iidIndex={iidIndex}
viewName={viewMeta.name}
onChangeView={handleChangeView}
viewId={viewId}
/>
</div>
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
<DatabaseViews
visibleViewIds={visibleViewIds}
iidIndex={iidIndex}
viewName={iidName}
onChangeView={onChangeView}
viewId={viewId}
hideConditions={true}
/>
</div>
)}
</DatabaseContextProvider>

View File

@ -16,13 +16,17 @@ function DatabaseViews({
viewId,
iidIndex,
viewName,
visibleViewIds,
hideConditions = false,
}: {
onChangeView: (viewId: string) => void;
viewId: string;
iidIndex: string;
viewName?: string;
visibleViewIds?: string[];
hideConditions?: boolean;
}) {
const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex);
const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex, visibleViewIds);
const value = useMemo(() => {
return Math.max(
@ -40,10 +44,12 @@ function DatabaseViews({
return childViews[value];
}, [childViews, value]);
const view = useMemo(() => {
const layout = useMemo(() => {
if (!activeView) return null;
const layout = Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
return Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout;
}, [activeView]);
const view = useMemo(() => {
switch (layout) {
case DatabaseViewLayout.Grid:
return <Grid />;
@ -52,7 +58,7 @@ function DatabaseViews({
case DatabaseViewLayout.Calendar:
return <Calendar />;
}
}, [activeView]);
}, [layout]);
return (
<>
@ -68,8 +74,9 @@ function DatabaseViews({
selectedViewId={viewId}
setSelectedViewId={onChangeView}
viewIds={viewIds}
hideConditions={hideConditions}
/>
<DatabaseConditions />
{layout === DatabaseViewLayout.Calendar || hideConditions ? null : <DatabaseConditions />}
</DatabaseConditionsContext.Provider>
<div className={'flex h-full w-full flex-1 flex-col overflow-hidden'}>
<Suspense fallback={<ComponentLoading />}>

View File

@ -74,6 +74,7 @@ function TestDatabaseRow({
}) {
return (
<DatabaseContextProvider
iidIndex={viewId}
viewId={viewId}
readOnly={true}
isDatabaseRowPage

View File

@ -81,6 +81,7 @@ export function TestDatabase({
databaseDoc={databaseDoc}
rowDocMap={rows}
readOnly={true}
iidIndex={iidIndex}
>
<DatabaseViews iidIndex={iidIndex} viewId={activeViewId} onChangeView={handleNavigateToView} />
</DatabaseContextProvider>

View File

@ -7,6 +7,7 @@ export function Board() {
const database = useDatabase();
const groups = useGroupsSelector();
console.log('groups', database);
if (!database) {
return (
<div className={'flex w-full flex-1 flex-col items-center justify-center'}>

View File

@ -8,7 +8,7 @@ export function Calendar() {
const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup();
return (
<div className={'database-calendar h-full max-h-[960px] px-16 pt-4 max-md:px-4'}>
<div className={'database-calendar h-full max-h-[960px] pb-6 pt-4 '}>
<BigCalendar
components={{
toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />,

View File

@ -33,13 +33,16 @@ $today-highlight-bg: transparent;
.rbc-month-row {
border: 1px solid var(--line-divider);
border-top: none;
min-width: 700px;
}
@include mixin.scrollbar-style;
}
.rbc-day-bg + .rbc-day-bg {
border-left-color: var(--line-divider);
}
.rbc-month-header {
height: 40px;
@ -47,6 +50,7 @@ $today-highlight-bg: transparent;
top: 0;
background: var(--bg-body);
z-index: 50;
min-width: 700px;
.rbc-header {
border: none;
@ -72,6 +76,8 @@ $today-highlight-bg: transparent;
flex: 0 0 0 !important;
min-height: 97px !important;
height: fit-content;
table-layout: fixed;
width: 100%;
}
.event-properties {

View File

@ -43,7 +43,7 @@ export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardPro
style={{
minHeight: '38px',
}}
className='relative flex cursor-pointer flex-col rounded-lg border border-line-border p-3 text-xs shadow-sm hover:bg-fill-list-active hover:shadow'
className='relative flex flex-col gap-2 rounded-lg border border-line-border p-3 text-xs shadow-sm'
>
{showFields.map((field, index) => {
return <CardField index={index} key={field.fieldId} rowId={rowId} fieldId={field.fieldId} />;

View File

@ -1,6 +1,5 @@
import { Row } from '@/application/database-yjs';
import { AFScroller } from '@/components/_shared/scroller';
import { Tag } from '@/components/_shared/tag';
import ListItem from '@/components/database/components/board/column/ListItem';
import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn';
import { useMeasureHeight } from '@/components/database/components/cell/useMeasure';
@ -72,9 +71,7 @@ export const Column = memo(
return (
<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'>
<Tag label={header?.name} color={header?.color} />
</div>
<div className='column-header flex h-[24px] items-center text-xs font-medium'>{header}</div>
<div className={'w-full flex-1 overflow-hidden'}>
<AutoSizer>

View File

@ -1,31 +0,0 @@
import { YjsDatabaseKey } from '@/application/collab.type';
import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs';
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
import { useMemo } from 'react';
export function useRenderColumn(id: string, fieldId: string) {
const { field } = useFieldSelector(fieldId);
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
const fieldName = field?.get(YjsDatabaseKey.name) || '';
const header = useMemo(() => {
if (!field) return null;
switch (fieldType) {
case FieldType.SingleSelect:
case FieldType.MultiSelect: {
const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id);
return {
name: option?.name || `No ${fieldName}`,
color: option?.color ? SelectOptionColorMap[option?.color] : 'transparent',
};
}
default:
return null;
}
}, [field, fieldName, fieldType, id]);
return {
header,
};
}

View File

@ -0,0 +1,51 @@
import { YjsDatabaseKey } from '@/application/collab.type';
import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs';
import { Tag } from '@/components/_shared/tag';
import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg';
import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg';
export function useRenderColumn(id: string, fieldId: string) {
const { field } = useFieldSelector(fieldId);
const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType;
const fieldName = field?.get(YjsDatabaseKey.name) || '';
const { t } = useTranslation();
const header = useMemo(() => {
if (!field) return null;
if (fieldType === FieldType.Checkbox)
return (
<div className={'flex items-center gap-2'}>
{id === 'Yes' ? (
<>
<CheckboxCheckSvg className={'h-4 w-4'} />
{t('button.yes')}
</>
) : (
<>
{' '}
<CheckboxUncheckSvg className={'h-4 w-4'} />
{t('button.no')}
</>
)}
</div>
);
if ([FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType)) {
const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id);
return (
<Tag
label={option?.name || `No ${fieldName}`}
color={option?.color ? SelectOptionColorMap[option?.color] : 'transparent'}
/>
);
}
return null;
}, [field, fieldType, id, fieldName, t]);
return {
header,
};
}

View File

@ -15,7 +15,7 @@ export const Group = ({ groupId }: GroupProps) => {
if (notFound) {
return (
<div className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'}>
<div className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 text-text-caption'}>
<div className={'text-sm font-medium'}>{t('board.noGroup')}</div>
<div className={'text-xs'}>{t('board.noGroupDesc')}</div>
</div>
@ -24,7 +24,7 @@ export const Group = ({ groupId }: GroupProps) => {
if (columns.length === 0 || !fieldId) return null;
return (
<AFScroller overflowYHidden className={'relative px-16 max-md:px-4'}>
<AFScroller overflowYHidden className={'relative'}>
<div className='columns flex h-full w-fit min-w-full gap-4 border-t border-line-divider py-4'>
{columns.map((data) => (
<Column key={data.id} id={data.id} fieldId={fieldId} rows={groupResult.get(data.id)} />

View File

@ -2,14 +2,13 @@ import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs';
import { RichTooltip } from '@/components/_shared/popover';
import EventPaper from '@/components/database/components/calendar/event/EventPaper';
import CardField from '@/components/database/components/field/CardField';
import React, { useMemo } from 'react';
import React from 'react';
import { EventWrapperProps } from 'react-big-calendar';
export function Event({ event }: EventWrapperProps<CalendarEvent>) {
const { id } = event;
const [rowId, fieldId] = id.split(':');
const fields = useFieldsSelector();
const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]);
const [rowId] = id.split(':');
const showFields = useFieldsSelector();
// const navigateToRow = useNavigateToRow();
const [open, setOpen] = React.useState(false);
@ -26,21 +25,11 @@ export function Event({ event }: EventWrapperProps<CalendarEvent>) {
}
}}
className={
'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'
'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 text-xs text-xs shadow-sm hover:bg-fill-list-active hover:shadow'
}
>
{showFields.map((field) => {
return (
<div
key={field.fieldId}
style={{
fontSize: '0.85em',
}}
className={'overflow-x-hidden truncate'}
>
<CardField index={0} rowId={rowId} fieldId={field.fieldId} />
</div>
);
return <CardField key={field.fieldId} index={0} rowId={rowId} fieldId={field.fieldId} />;
})}
</div>
</RichTooltip>

View File

@ -1,7 +1,6 @@
import { CalendarEvent } from '@/application/database-yjs';
import { RichTooltip } from '@/components/_shared/popover';
import NoDateRow from '@/components/database/components/calendar/toolbar/NoDateRow';
import Button from '@mui/material/Button';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@ -11,7 +10,7 @@ function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) {
const content = useMemo(() => {
return (
<div className={'flex w-[260px] flex-col gap-3 p-2 text-xs font-medium'}>
<div className={'text-text-caption'}>{t('calendar.settings.clickToOpen')}</div>
{/*<div className={'text-text-caption'}>{t('calendar.settings.clickToOpen')}</div>*/}
{emptyEvents.map((event) => {
const rowId = event.id.split(':')[0];
@ -19,7 +18,7 @@ function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) {
})}
</div>
);
}, [emptyEvents, t]);
}, [emptyEvents]);
return (
<RichTooltip
@ -30,15 +29,12 @@ function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) {
setOpen(false);
}}
>
<Button
size={'small'}
variant={'outlined'}
className={'whitespace-nowrap rounded-md border-line-divider'}
color={'inherit'}
onClick={() => setOpen(true)}
<span
className={' whitespace-nowrap rounded-md border border-line-divider border-line-divider p-1 px-2'}
// onClick={() => setOpen(true)}
>
{`${t('calendar.settings.noDateTitle')} (${emptyEvents.length})`}
</Button>
</span>
</RichTooltip>
);
}

View File

@ -1,10 +1,10 @@
import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs';
import { useCellSelector, usePrimaryFieldId } from '@/application/database-yjs';
import { Cell } from '@/components/database/components/cell';
import React from 'react';
import { useTranslation } from 'react-i18next';
function NoDateRow({ rowId }: { rowId: string }) {
const navigateToRow = useNavigateToRow();
// const navigateToRow = useNavigateToRow();
const primaryFieldId = usePrimaryFieldId();
const cell = useCellSelector({
rowId,
@ -18,15 +18,15 @@ function NoDateRow({ rowId }: { rowId: string }) {
return (
<div
onClick={() => {
navigateToRow?.(rowId);
}}
// onClick={() => {
// navigateToRow?.(rowId);
// }}
className={'w-full hover:text-fill-default'}
>
<Cell
style={{
cursor: 'pointer',
}}
// style={{
// cursor: 'pointer',
// }}
readOnly
cell={cell}
rowId={rowId}

View File

@ -43,6 +43,7 @@ export function Toolbar({
<Button
size={'small'}
variant={'outlined'}
disabled
className={'rounded-md border-line-divider'}
color={'inherit'}
onClick={() => onNavigate('TODAY')}

View File

@ -6,10 +6,10 @@ import { CellProps, CheckboxCell as CheckboxCellType } from '@/application/datab
export function CheckboxCell({ cell, style }: CellProps<CheckboxCellType>) {
const checked = cell?.data;
if (cell?.fieldType !== FieldType.Checkbox) return null;
if (cell && cell?.fieldType !== FieldType.Checkbox) return null;
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 items-center text-lg text-fill-default'>
{checked ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />}
</div>
);

View File

@ -1,6 +1,7 @@
import { FieldType, parseChecklistData } from '@/application/database-yjs';
import { CellProps, ChecklistCell as ChecklistCellType } from '@/application/database-yjs/cell.type';
import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel';
import { isNaN } from 'lodash-es';
import React, { useMemo } from 'react';
export function ChecklistCell({ cell, style, placeholder }: CellProps<ChecklistCellType>) {
@ -19,8 +20,10 @@ export function ChecklistCell({ cell, style, placeholder }: CellProps<ChecklistC
{placeholder}
</div>
) : null;
if (isNaN(data?.percentage)) return null;
return (
<div style={style} className={'w-full cursor-pointer'}>
<div style={style} className={'w-full'}>
<LinearProgressWithLabel value={data?.percentage} count={options.length} selectedCount={selectedOptions.length} />
</div>
);

View File

@ -34,7 +34,7 @@ export function RowCreateModifiedTime({
const time = useMemo(() => {
if (!value) return null;
return getDateTimeStr(value, false);
return getDateTimeStr(value, true);
}, [value, getDateTimeStr]);
if (!time) return null;

View File

@ -35,8 +35,8 @@ export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps<Da
) : null;
return (
<div style={style} className={'flex cursor-text items-center gap-1'}>
{hasReminder && <ReminderSvg className={'h-4 w-4'} />}
{dateStr}
{hasReminder && <ReminderSvg className={'h-4 w-4'} />}
</div>
);
}

View File

@ -25,6 +25,9 @@ export function NumberCell({ cell, fieldId, style, placeholder }: CellProps<Numb
const numberFormater = currencyFormaterMap[format];
if (!numberFormater) return cell.data;
if (isNaN(parseInt(cell.data))) return '';
return numberFormater(new Decimal(cell.data).toNumber());
}, [cell, format]);

View File

@ -1,8 +1,8 @@
import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs';
import { useRowMetaSelector } from '@/application/database-yjs';
import { TextCell as CellType, CellProps } from '@/application/database-yjs/cell.type';
import { TextCell } from '@/components/database/components/cell/text';
import { getPlatform } from '@/utils/platform';
import React, { useEffect, useMemo, useState } from 'react';
// import { getPlatform } from '@/utils/platform';
import React, { useEffect, useState } from 'react';
export function PrimaryCell(props: CellProps<CellType>) {
const { rowId } = props;
@ -40,19 +40,19 @@ export function PrimaryCell(props: CellProps<CellType>) {
};
}, [rowId]);
const isMobile = useMemo(() => {
return getPlatform().isMobile;
}, []);
const navigateToRow = useNavigateToRow();
// const isMobile = useMemo(() => {
// return getPlatform().isMobile;
// }, []);
//
// const navigateToRow = useNavigateToRow();
return (
<div
onClick={() => {
if (isMobile) {
navigateToRow?.(rowId);
}
}}
// 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>}

View File

@ -1,47 +1,124 @@
import { YjsDatabaseKey } from '@/application/collab.type';
import { DatabaseContext, DatabaseContextState, useDatabase, useNavigateToRow } from '@/application/database-yjs';
import { YDatabase, YDoc, YjsEditorKey } from '@/application/collab.type';
import {
DatabaseContext,
DatabaseContextState,
getPrimaryFieldId,
parseRelationTypeOption,
useFieldSelector,
} from '@/application/database-yjs';
import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useContext, useEffect, useState } from 'react';
function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
const viewId = useContext(DatabaseContext)?.iidIndex;
const { field } = useFieldSelector(fieldId);
const relatedDatabaseId = field ? parseRelationTypeOption(field).database_id : null;
function RelationItems({ style, cell }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) {
const database = useDatabase();
const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString();
const rowIds = useMemo(() => {
return (cell.data?.toJSON() as RelationCellData) ?? [];
}, [cell.data]);
const getViewRowsMap = useContext(DatabaseContext)?.getViewRowsMap;
const loadViewMeta = useContext(DatabaseContext)?.loadViewMeta;
const loadView = useContext(DatabaseContext)?.loadView;
const [noAccess, setNoAccess] = useState(false);
const [relations, setRelations] = useState<Record<string, string> | null>();
const [rows, setRows] = useState<DatabaseContextState['rowDocMap'] | null>();
const [relatedFieldId, setRelatedFieldId] = useState<string | undefined>();
const relatedViewId = relatedDatabaseId ? relations?.[relatedDatabaseId] : null;
const navigateToRow = useNavigateToRow();
const [rowIds, setRowIds] = useState([] as string[]);
// const navigateToRow = useNavigateToRow();
useEffect(() => {
if (!viewId || !rowIds.length) return;
if (!viewId) return;
void getViewRowsMap?.(viewId, rowIds).then(({ rows }) => {
setRows(rows);
});
}, [getViewRowsMap, rowIds, viewId]);
const update = (meta: ViewMeta) => {
setRelations(meta.database_relations);
};
try {
void loadViewMeta?.(viewId, update);
} catch (e) {
console.error(e);
}
}, [loadViewMeta, viewId]);
const handleUpdateRowIds = useCallback(
(rows: DatabaseContextState['rowDocMap']) => {
const ids = (cell.data?.toJSON() as RelationCellData) ?? [];
setRowIds(ids.filter((id) => rows?.has(id)));
},
[cell.data]
);
useEffect(() => {
if (!relatedViewId || !getViewRowsMap || !relatedFieldId) return;
void (async () => {
try {
const { rows } = await getViewRowsMap(relatedViewId);
setRows(rows);
handleUpdateRowIds(rows);
} catch (e) {
console.error(e);
}
})();
}, [getViewRowsMap, relatedViewId, relatedFieldId, handleUpdateRowIds]);
useEffect(() => {
const observerHandler = () => (rows ? handleUpdateRowIds(rows) : setRowIds([]));
rows?.observe(observerHandler);
return () => rows?.unobserve(observerHandler);
}, [rows, handleUpdateRowIds]);
useEffect(() => {
if (!relatedViewId) return;
void (async () => {
try {
const viewDoc = await loadView?.(relatedViewId);
if (!viewDoc) {
throw new Error('No access');
}
const database = viewDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
const fieldId = getPrimaryFieldId(database);
setNoAccess(!fieldId);
setRelatedFieldId(fieldId);
} catch (e) {
console.error(e);
setNoAccess(true);
}
})();
}, [loadView, relatedViewId]);
return (
<div style={style} className={'relation-cell flex w-full items-center gap-2'}>
{rowIds.map((rowId) => {
const rowDoc = rows?.get(rowId);
{noAccess ? (
<div className={'text-text-caption'}>No access</div>
) : (
rowIds.map((rowId) => {
const rowDoc = rows?.get(rowId) as YDoc;
return (
<div
key={rowId}
onClick={(e) => {
e.stopPropagation();
navigateToRow?.(rowId);
}}
className={'w-full cursor-pointer underline'}
>
{rowDoc && <RelationPrimaryValue rowDoc={rowDoc} />}
</div>
);
})}
return (
<div
key={rowId}
// onClick={(e) => {
// e.stopPropagation();
// navigateToRow?.(rowId);
// }}
className={'underline'}
>
<RelationPrimaryValue fieldId={relatedFieldId} rowDoc={rowDoc} />
</div>
);
})
)}
</div>
);
}

View File

@ -15,9 +15,9 @@ export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldI
};
onRowChange();
data?.observe(onRowChange);
data?.observeDeep(onRowChange);
return () => {
data?.unobserve(onRowChange);
data?.unobserveDeep(onRowChange);
};
}, [rowDoc]);

View File

@ -31,10 +31,7 @@ export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProp
) : null;
return (
<div
style={style}
className={'select-option-cell flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}
>
<div style={style} className={'select-option-cell flex h-full w-full items-center gap-1 overflow-x-hidden'}>
{renderSelectedOptions(selectOptionIds)}
</div>
);

View File

@ -1,13 +1,20 @@
import { useReadOnly } from '@/application/database-yjs';
import { CellProps, UrlCell as UrlCellType } from '@/application/database-yjs/cell.type';
import { notify } from '@/components/_shared/notify';
import { copyTextToClipboard } from '@/utils/copy';
import { openUrl, processUrl } from '@/utils/url';
import { IconButton, Tooltip } from '@mui/material';
import React, { useMemo } from 'react';
import { ReactComponent as LinkSvg } from '@/assets/link.svg';
import { ReactComponent as CopySvg } from '@/assets/copy.svg';
import { useTranslation } from 'react-i18next';
export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
const readOnly = useReadOnly();
const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]);
const [showActions, setShowActions] = React.useState(false);
const className = useMemo(() => {
const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center'];
@ -20,6 +27,8 @@ export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
return classList.join(' ');
}, [isUrl]);
const { t } = useTranslation();
if (!cell?.data)
return placeholder ? (
<div style={style} className={'text-text-placeholder'}>
@ -30,6 +39,8 @@ export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
return (
<div
style={style}
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
onClick={(e) => {
if (!isUrl || !cell) return;
if (readOnly) {
@ -40,6 +51,37 @@ export function UrlCell({ cell, style, placeholder }: CellProps<UrlCellType>) {
className={className}
>
{cell?.data}
{showActions && isUrl && (
<div className={'absolute right-0 flex items-center gap-1 px-2'}>
<Tooltip title={t('editor.openLink')} placement={'top'}>
<IconButton
sx={{
border: '1px solid var(--line-divider)',
}}
onClick={(e) => {
e.stopPropagation();
void openUrl(cell.data, '_blank');
}}
>
<LinkSvg />
</IconButton>
</Tooltip>
<Tooltip title={t('button.copyLink')} placement={'top'}>
<IconButton
sx={{
border: '1px solid var(--line-divider)',
}}
onClick={async (e) => {
e.stopPropagation();
await copyTextToClipboard(cell.data);
notify.success(t('grid.url.copy'));
}}
>
<CopySvg />
</IconButton>
</Tooltip>
</div>
)}
</div>
);
}

View File

@ -1,10 +1,10 @@
import { YjsDatabaseKey } from '@/application/collab.type';
import { useCellSelector, useFieldSelector } from '@/application/database-yjs';
import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs';
import Cell from '@/components/database/components/cell/Cell';
import React, { useMemo } from 'react';
import React, { CSSProperties, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; index: number }) {
function CardField({ rowId, fieldId }: { rowId: string; fieldId: string; index: number }) {
const { t } = useTranslation();
const { field } = useFieldSelector(fieldId);
const cell = useCellSelector({
@ -13,8 +13,26 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string;
});
const isPrimary = field?.get(YjsDatabaseKey.is_primary);
const type = field?.get(YjsDatabaseKey.type);
const style = useMemo(() => {
const styleProperties = {};
const styleProperties: CSSProperties = {
overflow: 'hidden',
width: '100%',
textAlign: 'left',
};
if ([FieldType.Relation, FieldType.SingleSelect, FieldType.MultiSelect].includes(Number(type))) {
Object.assign(styleProperties, {
breakWord: 'break-word',
whiteSpace: 'normal',
flexWrap: 'wrap',
});
} else {
Object.assign(styleProperties, {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
}
if (isPrimary) {
Object.assign(styleProperties, {
@ -23,14 +41,8 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string;
});
}
if (index !== 0) {
Object.assign(styleProperties, {
marginTop: '8px',
});
}
return styleProperties;
}, [index, isPrimary]);
}, [isPrimary, type]);
if (isPrimary && !cell?.data) {
return (
@ -40,6 +52,17 @@ function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string;
);
}
if (Number(type) === FieldType.Checkbox) {
return (
<div className={'flex items-center gap-1'}>
<span>
<Cell readOnly cell={cell} rowId={rowId} fieldId={fieldId} />
</span>
<span>{field?.get(YjsDatabaseKey.name) || ''}</span>
</div>
);
}
return <Cell style={style} readOnly cell={cell} rowId={rowId} fieldId={fieldId} />;
}

View File

@ -1,16 +1,18 @@
import { FieldType } from '@/application/database-yjs/database.type';
import { FC, memo } from 'react';
import { ReactComponent as TextSvg } from '$icons/16x/text.svg';
import { ReactComponent as NumberSvg } from '$icons/16x/number.svg';
import { ReactComponent as DateSvg } from '$icons/16x/date.svg';
import { ReactComponent as SingleSelectSvg } from '$icons/16x/single_select.svg';
import { ReactComponent as MultiSelectSvg } from '$icons/16x/multiselect.svg';
import { ReactComponent as ChecklistSvg } from '$icons/16x/checklist.svg';
import { ReactComponent as CheckboxSvg } from '$icons/16x/checkbox.svg';
import { ReactComponent as URLSvg } from '$icons/16x/url.svg';
import { ReactComponent as LastEditedTimeSvg } from '$icons/16x/last_modified.svg';
import { ReactComponent as CreatedSvg } from '$icons/16x/created_at.svg';
import { ReactComponent as RelationSvg } from '$icons/16x/relation.svg';
import { ReactComponent as TextSvg } from '@/assets/text.svg';
import { ReactComponent as NumberSvg } from '@/assets/number.svg';
import { ReactComponent as DateSvg } from '@/assets/date.svg';
import { ReactComponent as SingleSelectSvg } from '@/assets/single_select.svg';
import { ReactComponent as MultiSelectSvg } from '@/assets/multiselect.svg';
import { ReactComponent as ChecklistSvg } from '@/assets/checklist.svg';
import { ReactComponent as CheckboxSvg } from '@/assets/checkbox.svg';
import { ReactComponent as URLSvg } from '@/assets/url.svg';
import { ReactComponent as LastEditedTimeSvg } from '@/assets/last_modified.svg';
import { ReactComponent as CreatedSvg } from '@/assets/created_at.svg';
import { ReactComponent as RelationSvg } from '@/assets/relation.svg';
import { ReactComponent as AISummariesSvg } from '@/assets/ai_summary.svg';
import { ReactComponent as AITranslationsSvg } from '@/assets/ai_translate.svg';
export const FieldTypeSvgMap: Record<FieldType, FC<React.SVGProps<SVGSVGElement>>> = {
[FieldType.RichText]: TextSvg,
@ -24,10 +26,13 @@ export const FieldTypeSvgMap: Record<FieldType, FC<React.SVGProps<SVGSVGElement>
[FieldType.LastEditedTime]: LastEditedTimeSvg,
[FieldType.CreatedTime]: CreatedSvg,
[FieldType.Relation]: RelationSvg,
[FieldType.AISummaries]: AISummariesSvg,
[FieldType.AITranslations]: AITranslationsSvg,
};
export const FieldTypeIcon: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => {
const Svg = FieldTypeSvgMap[type];
if (!Svg) return null;
return <Svg {...props} />;
});

View File

@ -4,6 +4,7 @@ import { Column, useFieldSelector } from '@/application/database-yjs/selector';
import { FieldTypeIcon } from '@/components/database/components/field';
import { Tooltip } from '@mui/material';
import React, { useMemo } from 'react';
import { ReactComponent as AIIndicatorSvg } from '@/assets/ai_indicator.svg';
export function GridColumn({ column, index }: { column: Column; index: number }) {
const { field } = useFieldSelector(column.fieldId);
@ -16,20 +17,23 @@ export function GridColumn({ column, index }: { column: Column; index: number })
return parseInt(type) as FieldType;
}, [field]);
const isAIField = [FieldType.AISummaries, FieldType.AITranslations].includes(type);
return (
<Tooltip title={name} enterNextDelay={1000} placement={'right'}>
<div
style={{
borderLeftWidth: index === 1 ? 0 : 1,
borderLeftWidth: index === 0 ? 0 : 1,
}}
className={
'flex h-full w-full cursor-pointer items-center overflow-hidden whitespace-nowrap border-t border-b border-l border-line-divider px-1.5 text-xs font-medium hover:bg-fill-list-active'
'flex h-full w-full items-center overflow-hidden whitespace-nowrap border-t border-b border-l border-line-divider px-1.5 text-xs font-medium hover:bg-fill-list-active'
}
>
<div className={'w-5'}>
<FieldTypeIcon type={type} className={'mr-1 h-4 w-4'} />
</div>
<div className={'flex-1'}>{name}</div>
{isAIField && <AIIndicatorSvg className={'text-xl'} />}
</div>
</Tooltip>
);

View File

@ -27,19 +27,19 @@ export function useRenderFields() {
}));
return [
{
type: GridColumnType.Action,
width: 64,
},
// {
// type: GridColumnType.Action,
// width: 64,
// },
...data,
{
type: GridColumnType.NewProperty,
width: 150,
},
{
type: GridColumnType.Action,
width: 64,
},
// {
// type: GridColumnType.Action,
// width: 64,
// },
].filter(Boolean) as RenderColumn[];
}, [fields]);

View File

@ -1,4 +1,4 @@
import React, { memo, useEffect, useRef } from 'react';
import React, { memo, useCallback, useEffect, useRef } from 'react';
import { areEqual, GridChildComponentProps, VariableSizeGrid } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GridColumnType, RenderColumn, GridColumn } from '../grid-column';
@ -36,15 +36,17 @@ export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: G
}
}, [scrollLeft]);
const resetGrid = useCallback(() => {
ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 });
}, []);
useEffect(() => {
if (ref.current) {
ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 });
}
}, [columns]);
resetGrid();
}, [columns, resetGrid]);
return (
<div className={'h-[36px] w-full'}>
<AutoSizer>
<AutoSizer onResize={resetGrid}>
{({ height, width }: { height: number; width: number }) => {
return (
<VariableSizeGrid
@ -54,7 +56,9 @@ export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: G
rowHeight={() => 36}
rowCount={1}
columnCount={columns.length}
columnWidth={(index) => columnWidth(index, width)}
columnWidth={(index) => {
return columnWidth(index, width);
}}
ref={ref}
onScroll={(props) => {
onScrollLeft(props.scrollLeft);

View File

@ -2,38 +2,44 @@ import { YjsDatabaseKey } from '@/application/collab.type';
import { useDatabaseView } from '@/application/database-yjs';
import { CalculationType } from '@/application/database-yjs/database.type';
import { CalculationCell, ICalculationCell } from '../grid-calculation-cell';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
export interface GridCalculateRowCellProps {
fieldId: string;
}
export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) {
const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations);
const databaseView = useDatabaseView();
const [calculation, setCalculation] = useState<ICalculationCell>();
useEffect(() => {
const handleObserver = useCallback(() => {
const calculations = databaseView?.get(YjsDatabaseKey.calculations);
if (!calculations) return;
calculations.forEach((calculation) => {
if (calculation.get(YjsDatabaseKey.field_id) === fieldId) {
setCalculation({
id: calculation.get(YjsDatabaseKey.id),
fieldId: calculation.get(YjsDatabaseKey.field_id),
value: calculation.get(YjsDatabaseKey.calculation_value),
type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType,
});
}
});
}, [databaseView, fieldId]);
useEffect(() => {
const observerHandle = () => {
calculations.forEach((calculation) => {
if (calculation.get(YjsDatabaseKey.field_id) === fieldId) {
setCalculation({
id: calculation.get(YjsDatabaseKey.id),
fieldId: calculation.get(YjsDatabaseKey.field_id),
value: calculation.get(YjsDatabaseKey.calculation_value),
type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType,
});
}
});
handleObserver();
};
observerHandle();
calculations.observeDeep(observerHandle);
databaseView?.observeDeep(handleObserver);
return () => {
calculations.unobserveDeep(observerHandle);
databaseView?.observeDeep(handleObserver);
};
}, [calculations, fieldId]);
}, [databaseView, fieldId, handleObserver]);
return <CalculationCell cell={calculation} />;
}

View File

@ -31,11 +31,13 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
}
}, [scrollLeft]);
const resetGrid = useCallback(() => {
ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 });
}, []);
useEffect(() => {
if (ref.current) {
ref.current.resetAfterIndices({ columnIndex: 0, rowIndex: 0 });
}
}, [columns]);
resetGrid();
}, [columns, resetGrid]);
const getItemKey = useCallback(
({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => {
@ -85,7 +87,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
<div
data-row-id={row.rowId}
className={classList.join(' ')}
style={{ ...style, borderLeftWidth: columnIndex === 1 || column.type === GridColumnType.Action ? 0 : 1 }}
style={{ ...style, borderLeftWidth: columnIndex === 0 || column.type === GridColumnType.Action ? 0 : 1 }}
>
<GridRowCell
onResize={onResize}
@ -113,7 +115,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
);
return (
<AutoSizer>
<AutoSizer onResize={resetGrid}>
{({ height, width }: { height: number; width: number }) => (
<VariableSizeGrid
ref={ref}
@ -124,7 +126,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr
columnCount={columns.length}
columnWidth={(index) => columnWidth(index, width)}
rowHeight={rowHeight}
className={'grid-table'}
className={'grid-table pb-6'}
overscanRowCount={5}
overscanColumnCount={5}
style={{

View File

@ -18,7 +18,7 @@ function DatabaseHeader({
return (
<div
className={
'my-10 flex w-full items-center gap-4 overflow-hidden whitespace-pre-wrap break-words break-all px-16 text-[2.25rem] font-bold leading-[1.5em] max-md:px-4 max-sm:text-[7vw]'
'my-10 flex w-full items-center gap-4 overflow-hidden whitespace-pre-wrap break-words break-all text-[2.25rem] font-bold leading-[1.5em] max-sm:text-[7vw]'
}
>
<div className={'relative'}>

View File

@ -44,6 +44,8 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string })
case FieldType.Relation:
return RelationCell;
case FieldType.RichText:
case FieldType.AISummaries:
case FieldType.AITranslations:
return TextCell;
default:
return TextProperty;

View File

@ -4,10 +4,10 @@ import React from 'react';
function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) {
return (
<div className={'flex min-h-[28px] w-full gap-2'}>
<div className={'property-label flex h-[28px] w-[30%] items-center'}>
<div className={'property-label flex h-auto w-[30%] items-center py-2'}>
<FieldDisplay fieldId={fieldId} />
</div>
<div className={'flex flex-1 flex-wrap items-center overflow-x-hidden pr-1'}>{children}</div>
<div className={'flex h-fit flex-1 flex-wrap items-center overflow-x-hidden py-2 pr-1'}>{children}</div>
</div>
);
}

View File

@ -6,9 +6,9 @@ import { forwardRef, FunctionComponent, SVGProps, useMemo } from 'react';
import { ViewTabs, ViewTab } from './ViewTabs';
import { useTranslation } from 'react-i18next';
import { ReactComponent as GridSvg } from '$icons/16x/grid.svg';
import { ReactComponent as BoardSvg } from '$icons/16x/board.svg';
import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg';
import { ReactComponent as GridSvg } from '@/assets/grid.svg';
import { ReactComponent as BoardSvg } from '@/assets/board.svg';
import { ReactComponent as CalendarSvg } from '@/assets/calendar.svg';
export interface DatabaseTabBarProps {
viewIds: string[];
@ -16,6 +16,7 @@ export interface DatabaseTabBarProps {
setSelectedViewId?: (viewId: string) => void;
viewName?: string;
iidIndex: string;
hideConditions?: boolean;
}
const DatabaseIcons: {
@ -27,7 +28,7 @@ const DatabaseIcons: {
};
export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
({ viewIds, viewName, iidIndex, selectedViewId, setSelectedViewId }, ref) => {
({ viewIds, viewName, hideConditions, iidIndex, selectedViewId, setSelectedViewId }, ref) => {
const { t } = useTranslation();
const view = useDatabaseView();
const views = useDatabase().get(YjsDatabaseKey.views);
@ -38,9 +39,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
};
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',
];
const classList = ['-mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title'];
if (layout === DatabaseViewLayout.Calendar) {
classList.push('border-b');
@ -91,7 +90,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>(
})}
</ViewTabs>
</div>
{layout !== DatabaseViewLayout.Calendar ? <DatabaseActions /> : null}
{!hideConditions && layout !== DatabaseViewLayout.Calendar ? <DatabaseActions /> : null}
</div>
);
}

View File

@ -1,27 +1,19 @@
import { YDoc } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { GetViewRowsMap, LoadView, LoadViewMeta, YDoc } from '@/application/collab.type';
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
import { Editor } from '@/components/editor';
import React, { Suspense } from 'react';
import ViewMetaPreview, { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview';
import Y from 'yjs';
export interface DocumentProps extends ViewMetaProps {
export interface DocumentProps {
doc: YDoc;
navigateToView?: (viewId: string) => Promise<void>;
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
loadView?: (viewId: string) => Promise<YDoc>;
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
loadViewMeta?: LoadViewMeta;
loadView?: LoadView;
getViewRowsMap?: GetViewRowsMap;
viewMeta: ViewMetaProps;
}
export const Document = ({
doc,
loadView,
navigateToView,
loadViewMeta,
getViewRowsMap,
...viewMeta
}: DocumentProps) => {
export const Document = ({ doc, loadView, navigateToView, loadViewMeta, getViewRowsMap, viewMeta }: DocumentProps) => {
return (
<div className={'mb-16 flex h-full w-full flex-col items-center justify-center'}>
<ViewMetaPreview {...viewMeta} />

View File

@ -1,7 +1,5 @@
import { FontLayout, LineHeightLayout, YDoc } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { FontLayout, GetViewRowsMap, LineHeightLayout, LoadView, LoadViewMeta } from '@/application/collab.type';
import { createContext, useContext } from 'react';
import Y from 'yjs';
export interface EditorLayoutStyle {
fontLayout: FontLayout;
@ -21,9 +19,9 @@ export interface EditorContextState {
codeGrammars?: Record<string, string>;
addCodeGrammars?: (blockId: string, grammar: string) => void;
navigateToView?: (viewId: string) => Promise<void>;
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
loadView?: (viewId: string) => Promise<YDoc>;
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
loadViewMeta?: LoadViewMeta;
loadView?: LoadView;
getViewRowsMap?: GetViewRowsMap;
}
export const EditorContext = createContext<EditorContextState>({

View File

@ -1,4 +1,5 @@
import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { useEditorContext } from '@/components/editor/EditorContext';
import { Database } from '@/components/database';
import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type';
@ -27,7 +28,7 @@ export const DatabaseBlock = memo(
switch (type) {
case BlockType.GridBlock:
Object.assign(style, {
height: 360,
height: 400,
});
break;
case BlockType.CalendarBlock:
@ -57,6 +58,37 @@ export const DatabaseBlock = memo(
})();
}, [viewId, loadView]);
const [selectedViewId, setSelectedViewId] = useState<string>();
const [visibleViewIds, setVisibleViewIds] = useState<string[]>([]);
const [iidName, setIidName] = useState<string>('');
useEffect(() => {
const updateVisibleViewIds = async (meta: ViewMeta) => {
const viewIds = meta.visible_view_ids || [];
if (!viewIds.includes(viewId)) {
setSelectedViewId(meta.visible_view_ids[0]);
} else {
setSelectedViewId(viewId);
}
setIidName(meta.name);
setVisibleViewIds(viewIds);
};
void (async () => {
try {
const meta = await loadViewMeta?.(viewId, updateVisibleViewIds);
if (meta) {
await updateVisibleViewIds(meta);
}
} catch (e) {
setNotFound(true);
}
})();
}, [loadViewMeta, viewId]);
return (
<>
<div
@ -68,15 +100,20 @@ export const DatabaseBlock = memo(
<div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}>
{children}
</div>
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col px-3`}>
{viewId && doc ? (
<div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col`}>
{selectedViewId && doc ? (
<>
<Database
doc={doc}
iidIndex={viewId}
viewId={selectedViewId}
getViewRowsMap={getViewRowsMap}
loadView={loadView}
navigateToView={navigateToView}
loadViewMeta={loadViewMeta}
iidName={iidName}
visibleViewIds={visibleViewIds}
onChangeView={setSelectedViewId}
/>
{isHovering && (
<div className={'absolute right-4 top-1'}>
@ -102,7 +139,7 @@ export const DatabaseBlock = memo(
>
{notFound ? (
<>
<div className={'text-base font-medium'}>{t('publish.databaseHasNotBeenPublished')}</div>
<div className={'text-base font-medium'}>{t('publish.hasNotBeenPublished')}</div>
</>
) : (
<CircularProgress />

View File

@ -1,6 +1,6 @@
import { renderDate } from '@/utils/time';
import React, { useMemo } from 'react';
import { ReactComponent as DateSvg } from '$icons/16x/date.svg';
import { ReactComponent as DateSvg } from '@/assets/date.svg';
import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg';
function MentionDate({ date, reminder }: { date: string; reminder?: { id: string; option: string } }) {

View File

@ -7,15 +7,16 @@ import { useTranslation } from 'react-i18next';
function MentionPage({ pageId }: { pageId: string }) {
const context = useEditorContext();
const { navigateToView, loadViewMeta } = context;
const { navigateToView, loadViewMeta, loadView } = context;
const [unPublished, setUnPublished] = useState(false);
const [meta, setMeta] = useState<ViewMeta | null>(null);
useEffect(() => {
void (async () => {
if (loadViewMeta) {
if (loadViewMeta && loadView) {
setUnPublished(false);
try {
await loadView(pageId);
const meta = await loadViewMeta(pageId);
setMeta(meta);
@ -24,7 +25,7 @@ function MentionPage({ pageId }: { pageId: string }) {
}
}
})();
}, [loadViewMeta, pageId]);
}, [loadViewMeta, pageId, loadView]);
const icon = useMemo(() => {
return meta?.icon;

View File

@ -10,11 +10,11 @@ import { useSearchParams } from 'react-router-dom';
export function Login() {
const { t } = useTranslation();
const [search] = useSearchParams();
const redirectTo = search.get('redirectTo') || window.location.href;
const redirectTo = search.get('redirectTo') || '';
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
useEffect(() => {
if (isAuthenticated && encodeURIComponent(redirectTo) !== window.location.href) {
if (isAuthenticated && redirectTo && encodeURIComponent(redirectTo) !== window.location.href) {
window.location.href = redirectTo;
}
}, [isAuthenticated, redirectTo]);

View File

@ -1,22 +1,19 @@
import { ViewLayout, YDoc } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import { GetViewRowsMap, LoadView, LoadViewMeta, ViewLayout, YDoc } from '@/application/collab.type';
import { usePublishContext } from '@/application/publish';
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
import { useAppThemeMode } from '@/components/app/useAppThemeMode';
import { Database } from '@/components/database';
import { Document } from '@/components/document';
import DatabaseView from '@/components/publish/DatabaseView';
import { useViewMeta } from '@/components/publish/useViewMeta';
import React, { useMemo } from 'react';
import { ViewMetaProps } from 'src/components/view-meta';
import Y from 'yjs';
import { ViewMetaProps } from '@/components/view-meta';
export interface CollabViewProps {
doc?: YDoc;
}
function CollabView({ doc }: CollabViewProps) {
const visibleViewIds = usePublishContext()?.viewMeta?.visible_view_ids;
const { viewId, layout, icon, cover, layoutClassName, style, name } = useViewMeta();
const { isDark } = useAppThemeMode();
const View = useMemo(() => {
switch (layout) {
case ViewLayout.Document:
@ -24,20 +21,18 @@ function CollabView({ doc }: CollabViewProps) {
case ViewLayout.Grid:
case ViewLayout.Board:
case ViewLayout.Calendar:
return Database;
return DatabaseView;
default:
return null;
}
}, [layout]) as React.FC<
{
doc: YDoc;
isDark: boolean;
navigateToView?: (viewId: string) => Promise<void>;
loadViewMeta?: (viewId: string) => Promise<ViewMeta>;
getViewRowsMap?: (viewId: string, rowIds: string[]) => Promise<{ rows: Y.Map<YDoc>; destroy: () => void }>;
loadView?: (id: string) => Promise<YDoc>;
} & ViewMetaProps
>;
}, [layout]) as React.FC<{
doc: YDoc;
navigateToView?: (viewId: string) => Promise<void>;
loadViewMeta?: LoadViewMeta;
getViewRowsMap?: GetViewRowsMap;
loadView?: LoadView;
viewMeta: ViewMetaProps;
}>;
const navigateToView = usePublishContext()?.toView;
const loadViewMeta = usePublishContext()?.loadViewMeta;
@ -56,12 +51,14 @@ function CollabView({ doc }: CollabViewProps) {
getViewRowsMap={getViewRowsMap}
navigateToView={navigateToView}
loadView={loadView}
icon={icon}
cover={cover}
viewId={viewId}
name={name}
isDark={isDark}
layout={layout || ViewLayout.Document}
viewMeta={{
icon,
cover,
viewId,
name,
layout: layout || ViewLayout.Document,
visibleViewIds: visibleViewIds || [],
}}
/>
</div>
);

View File

@ -0,0 +1,69 @@
import { GetViewRowsMap, LoadView, LoadViewMeta, YDoc } from '@/application/collab.type';
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
import { Database } from '@/components/database';
import DatabaseHeader from '@/components/database/components/header/DatabaseHeader';
import { ViewMetaProps } from '@/components/view-meta';
import React, { Suspense, useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
export interface DatabaseProps {
doc: YDoc;
getViewRowsMap?: GetViewRowsMap;
loadView?: LoadView;
navigateToView?: (viewId: string) => Promise<void>;
loadViewMeta?: LoadViewMeta;
viewMeta: ViewMetaProps;
}
function DatabaseView({ viewMeta, ...props }: DatabaseProps) {
const [search, setSearch] = useSearchParams();
const visibleViewIds = useMemo(() => viewMeta.visibleViewIds || [], [viewMeta]);
const iidIndex = viewMeta.viewId;
const viewId = useMemo(() => {
return search.get('v') || iidIndex;
}, [search, iidIndex]);
const handleChangeView = useCallback(
(viewId: string) => {
setSearch({ v: viewId });
},
[setSearch]
);
const handleNavigateToRow = useCallback(
(rowId: string) => {
setSearch({ r: rowId });
},
[setSearch]
);
const rowId = search.get('r') || undefined;
if (!viewId) return null;
return (
<div
style={{
height: 'calc(100vh - 48px)',
}}
className={'relative flex h-full w-full flex-col px-16 max-md:px-4'}
>
<DatabaseHeader {...viewMeta} />
<Suspense fallback={<ComponentLoading />}>
<Database
iidName={viewMeta.name || ''}
iidIndex={iidIndex || ''}
{...props}
viewId={viewId}
rowId={rowId}
visibleViewIds={visibleViewIds}
onChangeView={handleChangeView}
onOpenRow={handleNavigateToRow}
/>
</Suspense>
</div>
);
}
export default DatabaseView;

View File

@ -22,7 +22,12 @@ function ViewCover({ coverValue, coverType }: { coverValue?: string; coverType?:
}
return (
<div className={'relative flex h-[208px] w-full max-sm:h-[180px]'}>
<div
style={{
height: '40vh',
}}
className={'relative flex max-h-[288px] min-h-[88px] w-full max-sm:h-[180px]'}
>
{coverType === 'color' && renderCoverColor(coverValue)}
{(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)}
</div>

View File

@ -20,6 +20,7 @@ export interface ViewMetaProps {
name?: string;
viewId?: string;
layout?: ViewLayout;
visibleViewIds?: string[];
}
export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) {

View File

@ -0,0 +1,41 @@
import { openOrDownload } from '@/components/publish/header/utils';
import { Button, Typography } from '@mui/material';
import React from 'react';
import { ReactComponent as Logo } from '@/assets/logo.svg';
import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg';
function AfterPaymentPage() {
return (
<div className={'m-0 flex h-screen w-screen items-center justify-center bg-bg-body p-6'}>
<div className={'flex max-w-[560px] flex-col items-center gap-1 text-center'}>
<Typography variant='h3' className={'mb-[27px] flex items-center gap-4 text-text-title'} gutterBottom>
<>
<Logo className={'w-9'} />
<AppflowyLogo className={'w-32'} />
</>
</Typography>
<div className={'mb-[16px] text-[52px] font-semibold leading-[128%] text-text-title'}>
Explore features in your new plan
</div>
<div className={'flex flex-col items-center justify-center text-[20px] leading-[152%]'}>
<div>
Congratulations! You just unlocked more workspace members and <span className={''}>unlimited</span> AI
responses. 🎉
</div>
</div>
<Button
onClick={openOrDownload}
variant='contained'
color='primary'
className={
'mt-[32px] mb-[48px] h-[68px] rounded-[20px] px-[44px] py-[18px] text-[20px] font-medium leading-[120%] text-content-on-fill'
}
>
Back to AppFlowy
</Button>
</div>
</div>
);
}
export default AfterPaymentPage;

View File

@ -9,8 +9,8 @@
@mixin scrollbar-style {
::-webkit-scrollbar, &::-webkit-scrollbar {
width: 4px;
height: 4px;
width: 8px;
height: 8px;
}
&:hover {

View File

@ -20,6 +20,14 @@ export function processUrl(input: string) {
return processedUrl;
}
if (input.startsWith('http')) {
return processedUrl;
}
if (input.startsWith('localhost')) {
return `http://${input}`;
}
const domain = input.split('/')[0];
if (isIP(domain) || isFQDN(domain)) {

View File

@ -327,6 +327,7 @@
"helpCenter": "Help Center",
"add": "Add",
"yes": "Yes",
"no": "No",
"clear": "Clear",
"remove": "Remove",
"dontRemove": "Don't remove",