mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'feat/support-get-encoded-collab-event' of https://github.com/AppFlowy-IO/AppFlowy into feat/support-get-encoded-collab-event
This commit is contained in:
commit
b67a576b19
@ -26,6 +26,8 @@
|
||||
/>
|
||||
<meta name="twitter:site" content="@appflowy" />
|
||||
<meta name="twitter:creator" content="@appflowy" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap" rel="stylesheet">
|
||||
|
||||
</head>
|
||||
<body id="body">
|
||||
<div id="root"></div>
|
||||
|
@ -24,7 +24,7 @@
|
||||
"coverage": "pnpm run test:unit && pnpm run test:components"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appflowyinc/client-api-wasm": "0.0.4",
|
||||
"@appflowyinc/client-api-wasm": "0.1.1",
|
||||
"@atlaskit/primitives": "^5.5.3",
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
|
@ -1,13 +1,9 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@appflowyinc/client-api-wasm':
|
||||
specifier: 0.0.4
|
||||
version: 0.0.4
|
||||
specifier: 0.1.1
|
||||
version: 0.1.1
|
||||
'@atlaskit/primitives':
|
||||
specifier: ^5.5.3
|
||||
version: 5.7.0(@types/react@18.2.66)(react@18.2.0)
|
||||
@ -451,8 +447,8 @@ packages:
|
||||
'@jridgewell/gen-mapping': 0.3.5
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
/@appflowyinc/client-api-wasm@0.0.4:
|
||||
resolution: {integrity: sha512-R8nm811vmh4oJU7LDFV7TJP89Y+WZKYrwpR1xrfL9m+PtyA/c21HvNJrvT+NssHfuz+IKj4L1cH2uXQcggnybA==}
|
||||
/@appflowyinc/client-api-wasm@0.1.1:
|
||||
resolution: {integrity: sha512-7+/TCmzMi9KrxX3HFLJv9R6ON2AO5xQavV547ii7RZM8+5bZJakuf6+pnyCzOquQX07q3ZYwJCa3MIgDvficaA==}
|
||||
dev: false
|
||||
|
||||
/@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0):
|
||||
@ -11666,3 +11662,7 @@ packages:
|
||||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||
engines: {node: '>=12.20'}
|
||||
dev: true
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
35
frontend/appflowy_web_app/scripts/merge-coverage.cjs
Normal file
35
frontend/appflowy_web_app/scripts/merge-coverage.cjs
Normal file
@ -0,0 +1,35 @@
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jestCoverageFile = path.join(__dirname, '../coverage/jest/coverage-final.json');
|
||||
const cypressCoverageFile = path.join(__dirname, '../coverage/cypress/coverage-final.json');
|
||||
const nycOutputDir = path.join(__dirname, '../coverage/.nyc_output');
|
||||
|
||||
// Ensure .nyc_output directory exists
|
||||
if (fs.existsSync(nycOutputDir)) {
|
||||
fs.rmSync(nycOutputDir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(nycOutputDir, { recursive: true });
|
||||
|
||||
if (fs.existsSync(path.join(__dirname, '../coverage/merged'))) {
|
||||
fs.rmSync(path.join(__dirname, '../coverage/merged'), { recursive: true });
|
||||
}
|
||||
// Copy Jest coverage file
|
||||
fs.copyFileSync(jestCoverageFile, path.join(nycOutputDir, 'jest-coverage.json'));
|
||||
// Copy Cypress E2E coverage file
|
||||
fs.copyFileSync(cypressCoverageFile, path.join(nycOutputDir, 'cypress-coverage.json'));
|
||||
|
||||
// Merge coverage files
|
||||
execSync('nyc merge ./coverage/.nyc_output ./coverage/merged/coverage-final.json', { stdio: 'inherit' });
|
||||
|
||||
// Move the merged result to the .nyc_output directory
|
||||
fs.rmSync(nycOutputDir, { recursive: true });
|
||||
fs.mkdirSync(nycOutputDir, { recursive: true });
|
||||
fs.copyFileSync(path.join(__dirname, '../coverage/merged/coverage-final.json'), path.join(nycOutputDir, 'out.json'));
|
||||
|
||||
// Generate final merged report
|
||||
execSync('nyc report --reporter=html --reporter=text-summary --report-dir=coverage/merged --temp-dir=coverage/.nyc_output', { stdio: 'inherit' });
|
||||
console.log(`Merged coverage report written to coverage/merged`);
|
||||
|
||||
|
||||
|
@ -56,7 +56,7 @@ const fetchMetaData = async (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
const BASE_URL = process.env.AF_BASE_URL || 'https://test.appflowy.cloud';
|
||||
const BASE_URL = process.env.AF_BASE_URL || 'https://beta.appflowy.cloud';
|
||||
const createServer = async (req) => {
|
||||
const timer = logRequestTimer(req);
|
||||
const reqUrl = new URL(req.url);
|
||||
@ -140,6 +140,7 @@ const start = () => {
|
||||
},
|
||||
});
|
||||
logger.info(`Server is running on port 3000`);
|
||||
logger.info(`Base API URL: ${process.env.AF_BASE_URL}`);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
process.exit(1);
|
||||
|
@ -636,3 +636,27 @@ export enum LineHeightLayout {
|
||||
normal = 'normal',
|
||||
large = 'large',
|
||||
}
|
||||
|
||||
export interface ViewMetaIcon {
|
||||
ty: number;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PublishViewInfo {
|
||||
view_id: string;
|
||||
name: string;
|
||||
icon: ViewMetaIcon | null;
|
||||
extra: string | null;
|
||||
layout: number;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
last_edited_time: string;
|
||||
last_edited_by: string;
|
||||
child_views: PublishViewInfo[] | null;
|
||||
}
|
||||
|
||||
export interface PublishViewMetaData {
|
||||
view: PublishViewInfo;
|
||||
child_views: PublishViewInfo[];
|
||||
ancestor_views: PublishViewInfo[];
|
||||
}
|
||||
|
@ -1,23 +1,12 @@
|
||||
import { Table } from 'dexie';
|
||||
|
||||
export interface MetaData {
|
||||
view_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
layout: number;
|
||||
extra: string | null;
|
||||
created_by: string | null;
|
||||
last_edited_by: string | null;
|
||||
last_edited_time: string;
|
||||
created_at: string;
|
||||
}
|
||||
import { PublishViewInfo } from '@/application/collab.type';
|
||||
|
||||
export type ViewMeta = {
|
||||
publish_name: string;
|
||||
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
} & MetaData;
|
||||
child_views: PublishViewInfo[];
|
||||
ancestor_views: PublishViewInfo[];
|
||||
} & PublishViewInfo;
|
||||
|
||||
export type ViewMetasTable = {
|
||||
view_metas: Table<ViewMeta>;
|
||||
|
@ -34,7 +34,7 @@ export const PublishProvider = ({
|
||||
const name = `${namespace}_${publishName}`;
|
||||
|
||||
return db.view_metas.get(name);
|
||||
});
|
||||
}, [namespace, publishName]);
|
||||
const service = useContext(AFConfigContext)?.service;
|
||||
const navigate = useNavigate();
|
||||
const toView = useCallback(
|
||||
@ -66,10 +66,12 @@ export const PublishProvider = ({
|
||||
throw new Error('View has not been published yet');
|
||||
}
|
||||
|
||||
const { namespace, publishName } = info;
|
||||
|
||||
const res = await service?.getPublishViewMeta(namespace, publishName);
|
||||
|
||||
if (!res) {
|
||||
throw new Error('View has not been published yet');
|
||||
throw new Error('View meta has not been published yet');
|
||||
}
|
||||
|
||||
return res;
|
||||
@ -77,7 +79,7 @@ export const PublishProvider = ({
|
||||
return Promise.reject(e);
|
||||
}
|
||||
},
|
||||
[namespace, publishName, service]
|
||||
[service]
|
||||
);
|
||||
|
||||
const getViewRowsMap = useCallback(
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { MetaData } from '@/application/db/tables/view_metas';
|
||||
import { CollabType, YDoc, YjsEditorKey, YSharedRoot } from '@/application/collab.type';
|
||||
import {
|
||||
CollabType,
|
||||
PublishViewInfo,
|
||||
PublishViewMetaData,
|
||||
YDoc,
|
||||
YjsEditorKey,
|
||||
YSharedRoot,
|
||||
} from '@/application/collab.type';
|
||||
import { applyYDoc } from '@/application/ydoc/apply';
|
||||
import { db, openCollabDB } from '@/application/db';
|
||||
import { Fetcher, StrategyType } from '@/application/services/js-services/cache/types';
|
||||
@ -49,11 +55,9 @@ export async function hasViewMetaCache(name: string) {
|
||||
|
||||
export async function getPublishViewMeta<
|
||||
T extends {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
view: PublishViewInfo;
|
||||
child_views: PublishViewInfo[];
|
||||
ancestor_views: PublishViewInfo[];
|
||||
}
|
||||
>(
|
||||
fetcher: Fetcher<T>,
|
||||
@ -107,11 +111,9 @@ export async function getPublishView<
|
||||
T extends {
|
||||
data: number[];
|
||||
meta: {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
view: PublishViewInfo;
|
||||
child_views: PublishViewInfo[];
|
||||
ancestor_views: PublishViewInfo[];
|
||||
};
|
||||
}
|
||||
>(
|
||||
@ -167,37 +169,31 @@ export async function getPublishView<
|
||||
|
||||
export async function revalidatePublishViewMeta<
|
||||
T extends {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
view: PublishViewInfo;
|
||||
child_views: PublishViewInfo[];
|
||||
ancestor_views: PublishViewInfo[];
|
||||
}
|
||||
>(name: string, fetcher: Fetcher<T>) {
|
||||
const { metadata } = await fetcher();
|
||||
const { view, child_views, ancestor_views } = await fetcher();
|
||||
|
||||
await db.view_metas.put(
|
||||
{
|
||||
publish_name: name,
|
||||
...metadata.view,
|
||||
child_views: metadata.child_views,
|
||||
ancestor_views: metadata.ancestor_views,
|
||||
...view,
|
||||
child_views: child_views,
|
||||
ancestor_views: ancestor_views,
|
||||
},
|
||||
name
|
||||
);
|
||||
|
||||
return db.view_metas.get(name);
|
||||
}
|
||||
|
||||
export async function revalidatePublishView<
|
||||
T extends {
|
||||
data: number[];
|
||||
rows?: Record<string, number[]>;
|
||||
meta: {
|
||||
metadata: {
|
||||
view: MetaData;
|
||||
child_views: MetaData[];
|
||||
ancestor_views: MetaData[];
|
||||
};
|
||||
};
|
||||
meta: PublishViewMetaData;
|
||||
}
|
||||
>(name: string, fetcher: Fetcher<T>, collab: YDoc) {
|
||||
const { data, meta, rows } = await fetcher();
|
||||
@ -205,17 +201,19 @@ export async function revalidatePublishView<
|
||||
await db.view_metas.put(
|
||||
{
|
||||
publish_name: name,
|
||||
...meta.metadata.view,
|
||||
child_views: meta.metadata.child_views,
|
||||
ancestor_views: meta.metadata.ancestor_views,
|
||||
...meta.view,
|
||||
child_views: meta.child_views,
|
||||
ancestor_views: meta.ancestor_views,
|
||||
},
|
||||
name
|
||||
);
|
||||
|
||||
for (const [key, value] of Object.entries(rows ?? {})) {
|
||||
const row = await openCollabDB(`${name}_${key}`);
|
||||
if (rows) {
|
||||
for (const [key, value] of Object.entries(rows)) {
|
||||
const row = await openCollabDB(`${name}_${key}`);
|
||||
|
||||
applyYDoc(row, new Uint8Array(value));
|
||||
applyYDoc(row, new Uint8Array(value));
|
||||
}
|
||||
}
|
||||
|
||||
const state = new Uint8Array(data);
|
||||
|
@ -53,6 +53,7 @@ export class AFClientService implements AFService {
|
||||
|
||||
async getPublishView(namespace: string, publishName: string) {
|
||||
const name = `${namespace}_${publishName}`;
|
||||
|
||||
const isLoaded = this.publishViewLoaded.has(name);
|
||||
const doc = await getPublishView(
|
||||
() => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ClientAPI } from '@appflowyinc/client-api-wasm';
|
||||
import { AFCloudConfig } from '@/application/services/services.type';
|
||||
import { PublishViewMetaData } from '@/application/collab.type';
|
||||
|
||||
let client: ClientAPI;
|
||||
|
||||
@ -9,6 +10,10 @@ export function initAPIService(
|
||||
clientId: string;
|
||||
}
|
||||
) {
|
||||
if (client) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.refresh_token = () => {
|
||||
//
|
||||
};
|
||||
@ -33,7 +38,12 @@ export function initAPIService(
|
||||
}
|
||||
|
||||
export async function getPublishView(publishNamespace: string, publishName: string) {
|
||||
return client.get_publish_view(publishNamespace, publishName);
|
||||
const data = await client.get_publish_view(publishNamespace, publishName);
|
||||
|
||||
return {
|
||||
data: data.data,
|
||||
meta: JSON.parse(data.meta.data) as PublishViewMetaData,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPublishInfoWithViewId(viewId: string) {
|
||||
@ -41,5 +51,8 @@ export async function getPublishInfoWithViewId(viewId: string) {
|
||||
}
|
||||
|
||||
export async function getPublishViewMeta(publishNamespace: string, publishName: string) {
|
||||
return client.get_publish_view_meta(publishNamespace, publishName);
|
||||
const data = await client.get_publish_view_meta(publishNamespace, publishName);
|
||||
const metadata = JSON.parse(data.data) as PublishViewMetaData;
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icons/ Arrow / right" opacity="0.5">
|
||||
<path id="Vector 15" d="M4.5 9.375L7.875 6L4.5 2.625" stroke="#171717" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<g id="Icons/ Arrow / right" opacity="0.5">
|
||||
<path id="Vector 15" d="M4.5 9.375L7.875 6L4.5 2.625" stroke="currentColor" stroke-width="0.9"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 291 B After Width: | Height: | Size: 326 B |
9
frontend/appflowy_web_app/src/assets/moon.svg
Normal file
9
frontend/appflowy_web_app/src/assets/moon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="vuesax/linear/moon">
|
||||
<g id="moon">
|
||||
<path id="Vector"
|
||||
d="M1.35404 8.27997C1.59404 11.7133 4.50738 14.5066 7.99404 14.66C10.454 14.7666 12.654 13.62 13.974 11.8133C14.5207 11.0733 14.2274 10.58 13.314 10.7466C12.8674 10.8266 12.4074 10.86 11.9274 10.84C8.66738 10.7066 6.00071 7.97997 5.98738 4.75997C5.98071 3.89331 6.16071 3.07331 6.48738 2.32664C6.84738 1.49997 6.41404 1.10664 5.58071 1.45997C2.94071 2.57331 1.13404 5.23331 1.35404 8.27997Z"
|
||||
stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 708 B |
8
frontend/appflowy_web_app/src/assets/more.svg
Normal file
8
frontend/appflowy_web_app/src/assets/more.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.325 9.00002C5.325 9.74561 4.72058 10.35 3.975 10.35C3.22942 10.35 2.625 9.74561 2.625 9.00002C2.625 8.25444 3.22942 7.65002 3.975 7.65002C4.72058 7.65002 5.325 8.25444 5.325 9.00002Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M10.3504 9.00002C10.3504 9.74561 9.74597 10.35 9.00039 10.35C8.25481 10.35 7.65039 9.74561 7.65039 9.00002C7.65039 8.25444 8.25481 7.65002 9.00039 7.65002C9.74597 7.65002 10.3504 8.25444 10.3504 9.00002Z"
|
||||
fill="currentColor"/>
|
||||
<path d="M15.3758 9.00002C15.3758 9.74561 14.7714 10.35 14.0258 10.35C13.2802 10.35 12.6758 9.74561 12.6758 9.00002C12.6758 8.25444 13.2802 7.65002 14.0258 7.65002C14.7714 7.65002 15.3758 8.25444 15.3758 9.00002Z"
|
||||
fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 835 B |
11
frontend/appflowy_web_app/src/assets/report.svg
Normal file
11
frontend/appflowy_web_app/src/assets/report.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group 1321314169">
|
||||
<path id="Vector" d="M7.63281 6.36667L12.8261 1.17334" stroke="currentColor" stroke-width="1.06667"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M13.331 3.70681V0.666809H10.291" stroke="currentColor" stroke-width="1.06667"
|
||||
stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3"
|
||||
d="M6.36602 0.666809H5.09935C1.93268 0.666809 0.666016 1.93348 0.666016 5.10014V8.90014C0.666016 12.0668 1.93268 13.3335 5.09935 13.3335H8.89935C12.066 13.3335 13.3327 12.0668 13.3327 8.90014V7.63348"
|
||||
stroke="currentColor" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 836 B |
@ -1,4 +1,4 @@
|
||||
import { useAppThemeMode } from '@/components/app/useAppThemeMode';
|
||||
import { ThemeModeContext, useAppThemeMode } from '@/components/app/useAppThemeMode';
|
||||
import React, { useMemo } from 'react';
|
||||
import createTheme from '@mui/material/styles/createTheme';
|
||||
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
||||
@ -8,7 +8,8 @@ import 'src/styles/tailwind.css';
|
||||
import 'src/styles/template.css';
|
||||
|
||||
function AppTheme({ children }: { children: React.ReactNode }) {
|
||||
const { isDark } = useAppThemeMode();
|
||||
const { isDark, setIsDark } = useAppThemeMode();
|
||||
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
@ -161,7 +162,16 @@ function AppTheme({ children }: { children: React.ReactNode }) {
|
||||
[isDark]
|
||||
);
|
||||
|
||||
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
||||
return (
|
||||
<ThemeModeContext.Provider
|
||||
value={{
|
||||
isDark,
|
||||
setDark: setIsDark,
|
||||
}}
|
||||
>
|
||||
<ThemeProvider theme={theme}>{children}</ThemeProvider>
|
||||
</ThemeModeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppTheme;
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, createContext } from 'react';
|
||||
|
||||
export const ThemeModeContext = createContext<
|
||||
| {
|
||||
isDark: boolean;
|
||||
setDark: (isDark: boolean) => void;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
export function useAppThemeMode() {
|
||||
const [isDark, setIsDark] = useState<boolean>(false);
|
||||
@ -8,7 +16,6 @@ export function useAppThemeMode() {
|
||||
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
setIsDark(darkModeMediaQuery.matches);
|
||||
document.documentElement.setAttribute('data-dark-mode', darkModeMediaQuery.matches ? 'true' : 'false');
|
||||
}
|
||||
|
||||
detectColorScheme();
|
||||
@ -19,7 +26,12 @@ export function useAppThemeMode() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-dark-mode', isDark ? 'true' : 'false');
|
||||
}, [isDark]);
|
||||
|
||||
return {
|
||||
isDark,
|
||||
setIsDark,
|
||||
};
|
||||
}
|
||||
|
@ -96,11 +96,13 @@ export const DatabaseBlock = memo(
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'}
|
||||
className={
|
||||
'flex h-full w-full flex-col items-center justify-center gap-2 rounded border border-line-divider bg-fill-list-active px-16 text-text-caption max-md:px-4'
|
||||
}
|
||||
>
|
||||
{notFound ? (
|
||||
<>
|
||||
<div className={'text-base font-medium'}>{t('publish.hasNotBeenPublished')}</div>
|
||||
<div className={'text-base font-medium'}>{t('publish.databaseHasNotBeenPublished')}</div>
|
||||
</>
|
||||
) : (
|
||||
<CircularProgress />
|
||||
|
@ -1,13 +1,13 @@
|
||||
export function getHeadingCssProperty(level: number) {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return 'text-3xl pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]';
|
||||
return 'text-[1.75rem] pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]';
|
||||
case 2:
|
||||
return 'text-2xl pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]';
|
||||
return 'text-[1.55rem] pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]';
|
||||
case 3:
|
||||
return 'text-xl pt-[4px] font-bold max-sm:text-[4vw]';
|
||||
return 'text-[1.25rem] pt-[4px] font-bold max-sm:text-[4vw]';
|
||||
case 4:
|
||||
return 'text-lg pt-[4px] font-bold';
|
||||
return 'text-[1rem] pt-[4px] font-bold';
|
||||
case 5:
|
||||
return 'pt-[4px] font-bold';
|
||||
case 6:
|
||||
|
@ -5,7 +5,6 @@ import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg';
|
||||
import { ViewLayout } from '@/application/collab.type';
|
||||
import { ViewMeta } from '@/application/db/tables/view_metas';
|
||||
import { useEditorContext } from '@/components/editor/EditorContext';
|
||||
import { ViewMetaIcon } from '@/components/view-meta';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -31,15 +30,7 @@ function MentionPage({ pageId }: { pageId: string }) {
|
||||
}, [loadViewMeta, pageId]);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (meta?.icon) {
|
||||
try {
|
||||
return JSON.parse(meta.icon) as ViewMetaIcon;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
return meta?.icon;
|
||||
}, [meta?.icon]);
|
||||
|
||||
const defaultIcon = useMemo(() => {
|
||||
@ -71,7 +62,7 @@ function MentionPage({ pageId }: { pageId: string }) {
|
||||
<span className={'mention-unpublished font-semibold text-text-caption'}>No Access</span>
|
||||
) : (
|
||||
<>
|
||||
<span className={'mention-icon'}>{icon?.value || defaultIcon}</span>
|
||||
<span className={'mention-icon icon'}>{icon?.value || defaultIcon}</span>
|
||||
|
||||
<span className={'mention-content'}>{meta?.name || t('menuAppHeader.defaultNewPageName')}</span>
|
||||
</>
|
||||
|
34
frontend/appflowy_web_app/src/components/error/NotFound.tsx
Normal file
34
frontend/appflowy_web_app/src/components/error/NotFound.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Container, Box, Typography, Button } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const NotFound = () => {
|
||||
return (
|
||||
<Container component='main' maxWidth='xs'>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant='h1' component='h1' color='error' gutterBottom>
|
||||
404
|
||||
</Typography>
|
||||
<Typography variant='h5' component='h2' gutterBottom>
|
||||
Page Not Found
|
||||
</Typography>
|
||||
<Typography variant='body1' color='textSecondary'>
|
||||
Sorry, the page you're looking for doesn't exist.
|
||||
</Typography>
|
||||
<Button component={Link} to='https://appflowy.io' variant='contained' color='primary' sx={{ mt: 3 }}>
|
||||
Go to AppFlowy.io
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
@ -5,6 +5,7 @@ import { AFConfigContext } from '@/components/app/AppConfig';
|
||||
import CollabView from '@/components/publish/CollabView';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { PublishViewHeader } from 'src/components/publish/header';
|
||||
import NotFound from '@/components/error/NotFound';
|
||||
|
||||
export interface PublishViewProps {
|
||||
namespace: string;
|
||||
@ -19,13 +20,10 @@ export function PublishView({ namespace, publishName }: PublishViewProps) {
|
||||
let doc;
|
||||
|
||||
setNotFound(false);
|
||||
setDoc(undefined);
|
||||
try {
|
||||
doc = await service?.getPublishView(namespace, publishName);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
setNotFound(true);
|
||||
return;
|
||||
}
|
||||
@ -37,8 +35,8 @@ export function PublishView({ namespace, publishName }: PublishViewProps) {
|
||||
void openPublishView();
|
||||
}, [openPublishView]);
|
||||
|
||||
if (notFound) {
|
||||
return <div className={'flex h-full w-full items-center justify-center'}>Not found</div>;
|
||||
if (notFound && !doc) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -53,7 +53,7 @@ function BreadcrumbItem({ crumb, disableClick = false }: { crumb: Crumb; disable
|
||||
}
|
||||
}}
|
||||
>
|
||||
{renderCrumbIcon(icon)}
|
||||
<span className={'icon'}>{renderCrumbIcon(icon)}</span>
|
||||
<span
|
||||
className={!disableClick ? 'max-w-[250px] truncate hover:text-text-title hover:underline' : 'flex-1 truncate'}
|
||||
>
|
||||
|
@ -0,0 +1,90 @@
|
||||
import { Popover } from '@/components/_shared/popover';
|
||||
import { ThemeModeContext } from '@/components/app/useAppThemeMode';
|
||||
import { openUrl } from '@/utils/url';
|
||||
import { IconButton } from '@mui/material';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { ReactComponent as MoreIcon } from '@/assets/more.svg';
|
||||
import { ReactComponent as MoonIcon } from '@/assets/moon.svg';
|
||||
import { ReactComponent as ReportIcon } from '@/assets/report.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function MoreActions() {
|
||||
const { isDark, setDark } = useContext(ThemeModeContext) || {};
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const actions = useMemo(() => {
|
||||
return [
|
||||
isDark
|
||||
? {
|
||||
Icon: MoonIcon,
|
||||
label: t('settings.appearance.themeMode.light'),
|
||||
onClick: () => {
|
||||
setDark?.(false);
|
||||
},
|
||||
}
|
||||
: {
|
||||
Icon: MoonIcon,
|
||||
label: t('settings.appearance.themeMode.dark'),
|
||||
onClick: () => {
|
||||
setDark?.(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
Icon: ReportIcon,
|
||||
label: t('publish.reportPage'),
|
||||
onClick: () => {
|
||||
void openUrl('', '_blank');
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [isDark, t, setDark]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleClick}>
|
||||
<MoreIcon className={'text-text-caption'} />
|
||||
</IconButton>
|
||||
<Popover
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className={'flex w-[240px] flex-col gap-2 px-2 py-2'}>
|
||||
{actions.map((action, index) => (
|
||||
<button
|
||||
onClick={action.onClick}
|
||||
key={index}
|
||||
className={
|
||||
'flex items-center gap-2 rounded-[8px] p-1.5 text-sm hover:bg-content-blue-50 focus:bg-content-blue-50 focus:outline-none'
|
||||
}
|
||||
>
|
||||
<action.Icon />
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoreActions;
|
@ -1,37 +1,49 @@
|
||||
import { usePublishContext } from '@/application/publish';
|
||||
import { openOrDownload } from '@/components/publish/header/utils';
|
||||
import { Divider } from '@mui/material';
|
||||
import React, { useMemo } from 'react';
|
||||
import Breadcrumb from './Breadcrumb';
|
||||
import { ReactComponent as Logo } from '@/assets/logo.svg';
|
||||
import MoreActions from './MoreActions';
|
||||
|
||||
export function PublishViewHeader() {
|
||||
const viewMeta = usePublishContext()?.viewMeta;
|
||||
const crumbs = useMemo(() => {
|
||||
const ancestors = viewMeta?.ancestor_views || [];
|
||||
let icon = viewMeta?.icon;
|
||||
const ancestors = viewMeta?.ancestor_views.slice(1) || [];
|
||||
|
||||
try {
|
||||
const extra = viewMeta?.extra ? JSON.parse(viewMeta.extra) : {};
|
||||
return ancestors.map((ancestor) => {
|
||||
let icon;
|
||||
|
||||
icon = extra.icon || icon;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const extra = ancestor?.extra ? JSON.parse(ancestor.extra) : {};
|
||||
|
||||
return ancestors.map((ancestor) => ({
|
||||
viewId: ancestor.view_id,
|
||||
name: ancestor.name,
|
||||
icon: icon || String(viewMeta?.layout),
|
||||
}));
|
||||
icon = extra.icon?.value || ancestor.icon?.value;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return {
|
||||
viewId: ancestor.view_id,
|
||||
name: ancestor.name,
|
||||
icon: icon || String(viewMeta?.layout),
|
||||
};
|
||||
});
|
||||
}, [viewMeta]);
|
||||
|
||||
return (
|
||||
<div className={'appflowy-top-bar flex h-[64px] px-5'}>
|
||||
<div className={'flex w-full items-center justify-between overflow-hidden'}>
|
||||
<Breadcrumb crumbs={crumbs} />
|
||||
<button onClick={openOrDownload}>
|
||||
<Logo className={'h-6 w-6'} />
|
||||
</button>
|
||||
<div className={'flex w-full items-center justify-between gap-2 overflow-hidden'}>
|
||||
<div className={'flex-1 overflow-hidden'}>
|
||||
<Breadcrumb crumbs={crumbs} />
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<MoreActions />
|
||||
<Divider orientation={'vertical'} className={'mx-2'} flexItem />
|
||||
<button onClick={openOrDownload}>
|
||||
<Logo className={'h-6 w-6'} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { usePublishContext } from '@/application/publish';
|
||||
import { EditorLayoutStyle } from '@/components/editor/EditorContext';
|
||||
import { ViewMetaCover, ViewMetaIcon } from '@/components/view-meta';
|
||||
import { ViewMetaCover } from '@/components/view-meta';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
|
||||
export function useViewMeta() {
|
||||
@ -63,35 +63,13 @@ export function useViewMeta() {
|
||||
});
|
||||
}, [layoutStyle.font]);
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (viewMeta?.icon) {
|
||||
try {
|
||||
return JSON.parse(viewMeta.icon) as ViewMetaIcon;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const icon = viewMeta?.icon || undefined;
|
||||
|
||||
return;
|
||||
}, [viewMeta?.icon]);
|
||||
|
||||
const cover = useMemo(() => {
|
||||
if (extra) {
|
||||
try {
|
||||
const extraObj = JSON.parse(extra);
|
||||
|
||||
return extraObj.cover as ViewMetaCover;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}, [extra]);
|
||||
const cover = extra?.cover as ViewMetaCover;
|
||||
|
||||
const viewId = viewMeta?.view_id;
|
||||
const name = viewMeta?.name;
|
||||
|
||||
|
||||
return {
|
||||
icon,
|
||||
cover,
|
||||
|
@ -22,7 +22,7 @@ function ViewCover({ coverValue, coverType }: { coverValue?: string; coverType?:
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'relative flex h-[255px] w-full max-sm:h-[180px]'}>
|
||||
<div className={'relative flex h-[198px] w-full max-sm:h-[180px]'}>
|
||||
{coverType === 'color' && renderCoverColor(coverValue)}
|
||||
{(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)}
|
||||
</div>
|
||||
|
@ -7,11 +7,7 @@ import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png';
|
||||
import ViewCover, { CoverType } from '@/components/view-meta/ViewCover';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface ViewMetaIcon {
|
||||
type: number;
|
||||
value: string;
|
||||
}
|
||||
import { ViewMetaIcon } from '@/application/collab.type';
|
||||
|
||||
export interface ViewMetaCover {
|
||||
type: CoverType;
|
||||
@ -59,21 +55,14 @@ export function ViewMetaPreview({ icon, cover, name }: ViewMetaProps) {
|
||||
return (
|
||||
<div className={'flex w-full flex-col items-center'}>
|
||||
{cover && <ViewCover coverType={coverType} coverValue={coverValue} />}
|
||||
<div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}>
|
||||
<div className={`relative mx-16 mb-6 mt-[52px] w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}>
|
||||
<div
|
||||
style={{
|
||||
position: coverValue ? 'absolute' : 'relative',
|
||||
bottom: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
className={'flex items-center gap-2 px-14 py-8 text-4xl max-md:px-2 max-sm:text-[7vw]'}
|
||||
className={
|
||||
'flex items-center gap-4 px-16 text-[2.25rem] font-bold leading-[1.5em] max-md:px-4 max-sm:text-[7vw]'
|
||||
}
|
||||
>
|
||||
<div className={`view-icon`}>{icon?.value}</div>
|
||||
<div className={'flex flex-1 items-center gap-2 overflow-hidden'}>
|
||||
<div className={'font-bold leading-[1.5em]'}>
|
||||
{name || <span className={'text-text-placeholder'}>{t('menuAppHeader.defaultNewPageName')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{name || <span className={'text-text-placeholder'}>{t('menuAppHeader.defaultNewPageName')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1 +1 @@
|
||||
export * from 'src/components/view-meta/ViewMetaPreview';
|
||||
export * from './ViewMetaPreview';
|
||||
|
@ -23,6 +23,7 @@ body {
|
||||
@apply bg-content-blue-100;
|
||||
}
|
||||
|
||||
@apply bg-bg-body text-text-title;
|
||||
&[data-os="windows"]:not([data-browser="firefox"]) {
|
||||
.appflowy-custom-scroller {
|
||||
@include mixin.hidden-scrollbar
|
||||
@ -50,10 +51,13 @@ body {
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
||||
}
|
||||
|
||||
.view-icon {
|
||||
@apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em];
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols;
|
||||
font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Twemoji Mozilla', sans-serif;
|
||||
line-height: 1em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
@ -2029,6 +2029,8 @@
|
||||
"duplicate": "Duplicate Space"
|
||||
},
|
||||
"publish": {
|
||||
"hasNotBeenPublished": "This page hasn't been published yet"
|
||||
"hasNotBeenPublished": "This page hasn't been published yet",
|
||||
"reportPage": "Report page",
|
||||
"databaseHasNotBeenPublished": "This database hasn't been published yet"
|
||||
}
|
||||
}
|
3
frontend/rust-lib/Cargo.lock
generated
3
frontend/rust-lib/Cargo.lock
generated
@ -1992,12 +1992,14 @@ dependencies = [
|
||||
"flowy-notification",
|
||||
"flowy-search-pub",
|
||||
"flowy-sqlite",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"nanoid",
|
||||
"parking_lot 0.12.1",
|
||||
"protobuf",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum_macros 0.21.1",
|
||||
@ -2018,6 +2020,7 @@ dependencies = [
|
||||
"collab-entity",
|
||||
"collab-folder",
|
||||
"lib-infra",
|
||||
"serde",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
@ -3,7 +3,7 @@ use crate::entities::{
|
||||
view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc,
|
||||
CreateViewParams, CreateWorkspaceParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB,
|
||||
MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams,
|
||||
ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB,
|
||||
ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, ViewLayoutPB
|
||||
};
|
||||
use crate::manager_observer::{
|
||||
notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change,
|
||||
@ -23,7 +23,7 @@ use collab_entity::CollabType;
|
||||
use collab_folder::error::FolderError;
|
||||
use collab_folder::{
|
||||
Folder, FolderNotify, Section, SectionItem, TrashInfo, UserId, View, ViewLayout, ViewUpdate,
|
||||
Workspace,
|
||||
Workspace
|
||||
};
|
||||
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig};
|
||||
use collab_integrate::CollabKVDB;
|
||||
|
Loading…
Reference in New Issue
Block a user