From 1174052be528eeffd56f05e8856094a85d04bd17 Mon Sep 17 00:00:00 2001 From: Kilu Date: Mon, 8 Jul 2024 13:48:08 +0800 Subject: [PATCH 01/15] feat: support sign-in or sign-up on web --- .../application/services/js-services/index.ts | 48 ++++++++++++- .../services/js-services/wasm/client_api.ts | 43 +++++++++-- .../src/application/services/services.type.ts | 7 ++ .../src/application/session/event.ts | 24 +++++++ .../src/application/session/index.ts | 1 + .../src/application/session/sign_in.ts | 51 +++++++++++++ .../src/application/session/token.ts | 20 ++++++ .../appflowy_web_app/src/assets/login.svg | 12 ++++ .../src/assets/login/discord.svg | 4 ++ .../src/assets/login/github.svg | 4 ++ .../src/assets/login/google.svg | 17 +++++ .../src/components/app/App.tsx | 5 ++ .../src/components/app/AppConfig.tsx | 26 +++++++ .../src/components/login/Login.tsx | 53 ++++++++++++++ .../src/components/login/LoginAuth.tsx | 28 ++++++++ .../src/components/login/LoginProvider.tsx | 72 +++++++++++++++++++ .../src/components/login/MagicLink.tsx | 67 +++++++++++++++++ .../src/components/login/index.ts | 1 + .../components/publish/header/MoreActions.tsx | 21 +++++- .../appflowy_web_app/src/pages/LoginPage.tsx | 12 ++++ frontend/resources/translations/en.json | 23 ++++-- 21 files changed, 525 insertions(+), 14 deletions(-) create mode 100644 frontend/appflowy_web_app/src/application/session/event.ts create mode 100644 frontend/appflowy_web_app/src/application/session/index.ts create mode 100644 frontend/appflowy_web_app/src/application/session/sign_in.ts create mode 100644 frontend/appflowy_web_app/src/application/session/token.ts create mode 100644 frontend/appflowy_web_app/src/assets/login.svg create mode 100644 frontend/appflowy_web_app/src/assets/login/discord.svg create mode 100644 frontend/appflowy_web_app/src/assets/login/github.svg create mode 100644 frontend/appflowy_web_app/src/assets/login/google.svg create mode 100644 frontend/appflowy_web_app/src/components/login/Login.tsx create mode 100644 frontend/appflowy_web_app/src/components/login/LoginAuth.tsx create mode 100644 frontend/appflowy_web_app/src/components/login/LoginProvider.tsx create mode 100644 frontend/appflowy_web_app/src/components/login/MagicLink.tsx create mode 100644 frontend/appflowy_web_app/src/components/login/index.ts create mode 100644 frontend/appflowy_web_app/src/pages/LoginPage.tsx diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index 9e88466b83ad8..afa0b43b1e154 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -8,9 +8,18 @@ import { } from '@/application/services/js-services/cache'; import { StrategyType } from '@/application/services/js-services/cache/types'; import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '@/application/services/js-services/fetch'; +import { + initAPIService, + signInGoogle, + signInWithMagicLink, + signInGithub, + signInDiscord, + signInWithUrl, +} from '@/application/services/js-services/wasm/client_api'; import { AFService, AFServiceConfig } from '@/application/services/services.type'; +import { emit, EventType } from '@/application/session'; +import { afterAuth, AUTH_CALLBACK_URL, withSignIn } from '@/application/session/sign_in'; import { nanoid } from 'nanoid'; -import { initAPIService } from '@/application/services/js-services/wasm/client_api'; import * as Y from 'yjs'; export class AFClientService implements AFService { @@ -38,6 +47,10 @@ export class AFClientService implements AFService { }); } + getClientId() { + return this.clientId; + } + async getPublishViewMeta(namespace: string, publishName: string) { const viewMeta = await getPublishViewMeta( () => { @@ -149,4 +162,37 @@ export class AFClientService implements AFService { return data; } + + async loginAuth(url: string) { + try { + console.log('loginAuth', url); + await signInWithUrl(url); + emit(EventType.SESSION_VALID); + afterAuth(); + return; + } catch (e) { + emit(EventType.SESSION_INVALID); + return Promise.reject(e); + } + } + + @withSignIn() + async signInMagicLink({ email }: { email: string; redirectTo: string }) { + return await signInWithMagicLink(email, AUTH_CALLBACK_URL); + } + + @withSignIn() + async signInGoogle(_: { redirectTo: string }) { + return await signInGoogle(AUTH_CALLBACK_URL); + } + + @withSignIn() + async signInGithub(_: { redirectTo: string }) { + return await signInGithub(AUTH_CALLBACK_URL); + } + + @withSignIn() + async signInDiscord(_: { redirectTo: string }) { + return await signInDiscord(AUTH_CALLBACK_URL); + } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index 9ee174b6c6cd3..a2fdd707bd46b 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -1,3 +1,4 @@ +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 } from '@/application/collab.type'; @@ -14,13 +15,9 @@ export function initAPIService( return; } - window.refresh_token = () => { - // - }; + window.refresh_token = refreshToken; - window.invalid_token = () => { - // invalidToken(); - }; + window.invalid_token = invalidToken; client = ClientAPI.new({ base_url: config.baseURL, @@ -34,6 +31,10 @@ export function initAPIService( }, }); + if (isTokenValid()) { + client.restore_token(getToken() || ''); + } + client.subscribe(); } @@ -56,3 +57,33 @@ export async function getPublishViewMeta(publishNamespace: string, publishName: return metadata; } + +export async function signInWithUrl(url: string) { + return client.sign_in_with_url(url); +} + +export async function signInWithMagicLink(email: string, redirectTo: string) { + return client.sign_in_with_magic_link(email, redirectTo); +} + +export async function signInGoogle(redirectTo: string) { + return signInProvider('google', redirectTo); +} + +export async function signInProvider(provider: string, redirectTo: string) { + try { + const { url } = await client.generate_oauth_url_with_provider(provider, redirectTo); + + window.open(url, '_current'); + } catch (e) { + return Promise.reject(e); + } +} + +export async function signInGithub(redirectTo: string) { + return signInProvider('github', redirectTo); +} + +export async function signInDiscord(redirectTo: string) { + return signInProvider('discord', redirectTo); +} diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index 99076175a6f7a..1eea995246c67 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -15,6 +15,7 @@ export interface AFCloudConfig { } export interface PublishService { + getClientId: () => string; getPublishViewMeta: (namespace: string, publishName: string) => Promise; getPublishView: (namespace: string, publishName: string) => Promise; getPublishInfo: (viewId: string) => Promise<{ namespace: string; publishName: string }>; @@ -26,4 +27,10 @@ export interface PublishService { rows: Y.Map; destroy: () => void; }>; + + loginAuth: (url: string) => Promise; + signInMagicLink: (params: { email: string; redirectTo: string }) => Promise; + signInGoogle: (params: { redirectTo: string }) => Promise; + signInGithub: (params: { redirectTo: string }) => Promise; + signInDiscord: (params: { redirectTo: string }) => Promise; } diff --git a/frontend/appflowy_web_app/src/application/session/event.ts b/frontend/appflowy_web_app/src/application/session/event.ts new file mode 100644 index 0000000000000..4b562baa78c96 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/event.ts @@ -0,0 +1,24 @@ +import { EventEmitter } from 'events'; + +const event = new EventEmitter(); + +export enum EventType { + SESSION_EXPIRED = 'session_expired', + SESSION_REFRESH = 'session_refresh', + SESSION_INVALID = 'session_invalid', + SESSION_VALID = 'session_valid', +} + +export type Listener = (data: T) => void; + +export function on(eventType: EventType, listener: Listener) { + event.on(eventType, listener); + + return () => { + event.off(eventType, listener); + }; +} + +export function emit(eventType: EventType, data?: T) { + event.emit(eventType, data); +} diff --git a/frontend/appflowy_web_app/src/application/session/index.ts b/frontend/appflowy_web_app/src/application/session/index.ts new file mode 100644 index 0000000000000..0ef93842ad5b7 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/index.ts @@ -0,0 +1 @@ +export * from './event'; diff --git a/frontend/appflowy_web_app/src/application/session/sign_in.ts b/frontend/appflowy_web_app/src/application/session/sign_in.ts new file mode 100644 index 0000000000000..a78c3229a638d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/sign_in.ts @@ -0,0 +1,51 @@ +export function saveRedirectTo(redirectTo: string) { + localStorage.setItem('redirectTo', redirectTo); +} + +export function getRedirectTo() { + return localStorage.getItem('redirectTo'); +} + +export function clearRedirectTo() { + localStorage.removeItem('redirectTo'); +} + +export const AUTH_CALLBACK_PATH = '/auth/callback'; +export const AUTH_CALLBACK_URL = `${window.location.origin}${AUTH_CALLBACK_PATH}`; + +export function withSignIn() { + return function ( + // eslint-disable-next-line + _target: any, + _propertyKey: string, + descriptor: PropertyDescriptor + ) { + const originalMethod = descriptor.value; + + // eslint-disable-next-line + descriptor.value = async function (args: { redirectTo: string }) { + const redirectTo = args.redirectTo; + + saveRedirectTo(redirectTo); + + console.log('=====saveRedirectTo', redirectTo); + try { + await originalMethod.apply(this, [args]); + } catch (e) { + console.error(e); + return Promise.reject(e); + } + }; + + return descriptor; + }; +} + +export function afterAuth() { + const redirectTo = getRedirectTo(); + + if (redirectTo) { + clearRedirectTo(); + window.location.href = redirectTo; + } +} diff --git a/frontend/appflowy_web_app/src/application/session/token.ts b/frontend/appflowy_web_app/src/application/session/token.ts new file mode 100644 index 0000000000000..174e350490718 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/session/token.ts @@ -0,0 +1,20 @@ +import { emit, EventType } from '@/application/session/event'; + +export function refreshToken(token: string) { + localStorage.removeItem('token'); + localStorage.setItem('token', token); + emit(EventType.SESSION_REFRESH, token); +} + +export function invalidToken() { + localStorage.removeItem('token'); + emit(EventType.SESSION_INVALID); +} + +export function isTokenValid() { + return !!localStorage.getItem('token'); +} + +export function getToken() { + return localStorage.getItem('token'); +} diff --git a/frontend/appflowy_web_app/src/assets/login.svg b/frontend/appflowy_web_app/src/assets/login.svg new file mode 100644 index 0000000000000..348b76aa69daf --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/login/discord.svg b/frontend/appflowy_web_app/src/assets/login/discord.svg new file mode 100644 index 0000000000000..901b2fd9e0d72 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login/discord.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/login/github.svg b/frontend/appflowy_web_app/src/assets/login/github.svg new file mode 100644 index 0000000000000..0a73a316ab467 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login/github.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/login/google.svg b/frontend/appflowy_web_app/src/assets/login/google.svg new file mode 100644 index 0000000000000..84bee691e0c91 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/login/google.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index c66556d8f7a48..e029a9f207739 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -1,4 +1,7 @@ +import { AUTH_CALLBACK_PATH } from '@/application/session/sign_in'; import NotFound from '@/components/error/NotFound'; +import LoginAuth from '@/components/login/LoginAuth'; +import LoginPage from '@/pages/LoginPage'; import PublishPage from '@/pages/PublishPage'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import withAppWrapper from '@/components/app/withAppWrapper'; @@ -8,6 +11,8 @@ const AppMain = withAppWrapper(() => { return ( } /> + } /> + } /> } /> } /> diff --git a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx index 7c215bd530576..8d5c6c2b64162 100644 --- a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx @@ -1,3 +1,5 @@ +import { EventType, on } from '@/application/session'; +import { isTokenValid } from '@/application/session/token'; import { useAppLanguage } from '@/components/app/useAppLanguage'; import { useSnackbar } from 'notistack'; import React, { createContext, useEffect, useState } from 'react'; @@ -19,6 +21,7 @@ const defaultConfig: AFServiceConfig = { export const AFConfigContext = createContext< | { service: AFService | undefined; + isAuthenticated: boolean; } | undefined >(undefined); @@ -26,7 +29,29 @@ export const AFConfigContext = createContext< function AppConfig({ children }: { children: React.ReactNode }) { const [appConfig] = useState(defaultConfig); const [service, setService] = useState(); + const [isAuthenticated, setIsAuthenticated] = React.useState(isTokenValid()); + useEffect(() => { + return on(EventType.SESSION_VALID, () => { + setIsAuthenticated(true); + }); + }, []); + + useEffect(() => { + const handleStorageChange = (event: StorageEvent) => { + if (event.key === 'token') setIsAuthenticated(isTokenValid()); + }; + + window.addEventListener('storage', handleStorageChange); + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, []); + useEffect(() => { + return on(EventType.SESSION_INVALID, () => { + setIsAuthenticated(false); + }); + }, []); useAppLanguage(); useEffect(() => { @@ -67,6 +92,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { {children} diff --git a/frontend/appflowy_web_app/src/components/login/Login.tsx b/frontend/appflowy_web_app/src/components/login/Login.tsx new file mode 100644 index 0000000000000..76b8e9faa52d3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/Login.tsx @@ -0,0 +1,53 @@ +import { AFConfigContext } from '@/components/app/AppConfig'; +import LoginProvider from '@/components/login/LoginProvider'; +import MagicLink from '@/components/login/MagicLink'; +import { Divider } from '@mui/material'; +import React, { useContext, useEffect } from 'react'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { useTranslation } from 'react-i18next'; +import { useSearchParams } from 'react-router-dom'; + +export function Login() { + const { t } = useTranslation(); + const [search] = useSearchParams(); + const redirectTo = search.get('redirectTo') || window.location.href; + const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false; + + useEffect(() => { + if (isAuthenticated && encodeURIComponent(redirectTo) !== window.location.href) { + window.location.href = redirectTo; + } + }, [isAuthenticated, redirectTo]); + return ( +
+
+ +
{t('welcomeTo')} AppFlowy
+
+ +
+ + {t('web.or')} + +
+ +
+ {t('web.signInAgreement')} + + {t('web.termOfUse')} + {' '} + {t('web.and')}{' '} + + {t('web.privacyPolicy')} + + . +
+
+ ); +} + +export default Login; diff --git a/frontend/appflowy_web_app/src/components/login/LoginAuth.tsx b/frontend/appflowy_web_app/src/components/login/LoginAuth.tsx new file mode 100644 index 0000000000000..b22f51083e4af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/LoginAuth.tsx @@ -0,0 +1,28 @@ +import { AFConfigContext } from '@/components/app/AppConfig'; +import { CircularProgress } from '@mui/material'; +import { useContext, useEffect, useState } from 'react'; + +function LoginAuth() { + const service = useContext(AFConfigContext)?.service; + const [loading, setLoading] = useState(false); + + useEffect(() => { + void (async () => { + setLoading(true); + try { + await service?.loginAuth(window.location.href); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + })(); + }, [service]); + return loading ? ( +
+ +
+ ) : null; +} + +export default LoginAuth; diff --git a/frontend/appflowy_web_app/src/components/login/LoginProvider.tsx b/frontend/appflowy_web_app/src/components/login/LoginProvider.tsx new file mode 100644 index 0000000000000..9e531f2f24158 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/LoginProvider.tsx @@ -0,0 +1,72 @@ +import { notify } from '@/components/_shared/notify'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Button } from '@mui/material'; +import React, { useContext, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as GoogleSvg } from '@/assets/login/google.svg'; +import { ReactComponent as GithubSvg } from '@/assets/login/github.svg'; +import { ReactComponent as DiscordSvg } from '@/assets/login/discord.svg'; + +function LoginProvider({ redirectTo }: { redirectTo: string }) { + const { t } = useTranslation(); + const options = useMemo( + () => [ + { + label: t('web.continueWithGoogle'), + Icon: GoogleSvg, + value: 'google', + }, + { + label: t('web.continueWithGithub'), + value: 'github', + Icon: GithubSvg, + }, + { + label: t('web.continueWithDiscord'), + value: 'discord', + Icon: DiscordSvg, + }, + ], + [t] + ); + const service = useContext(AFConfigContext)?.service; + + const handleClick = async (option: string) => { + try { + switch (option) { + case 'google': + await service?.signInGoogle({ redirectTo }); + break; + case 'github': + await service?.signInGithub({ redirectTo }); + break; + case 'discord': + await service?.signInDiscord({ redirectTo }); + break; + } + } catch (e) { + notify.error(t('web.signInError')); + } + }; + + return ( +
+ {options.map((option) => ( + + ))} +
+ ); +} + +export default LoginProvider; diff --git a/frontend/appflowy_web_app/src/components/login/MagicLink.tsx b/frontend/appflowy_web_app/src/components/login/MagicLink.tsx new file mode 100644 index 0000000000000..7ba078d8fca7f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/MagicLink.tsx @@ -0,0 +1,67 @@ +import { notify } from '@/components/_shared/notify'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Button, CircularProgress, OutlinedInput } from '@mui/material'; +import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import validator from 'validator'; + +function MagicLink({ redirectTo }: { redirectTo: string }) { + const { t } = useTranslation(); + const [email, setEmail] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const service = useContext(AFConfigContext)?.service; + const handleSubmit = async () => { + const isValidEmail = validator.isEmail(email); + + if (!isValidEmail) { + notify.error(t('signIn.invalidEmail')); + return; + } + + setLoading(true); + + try { + await service?.signInMagicLink({ + email, + redirectTo, + }); + notify.success(t('signIn.magicLinkSent')); + } catch (e) { + notify.error(t('web.signInError')); + } finally { + setLoading(false); + } + }; + + return ( +
+ setEmail(e.target.value)} + /> + +
+ ); +} + +export default MagicLink; diff --git a/frontend/appflowy_web_app/src/components/login/index.ts b/frontend/appflowy_web_app/src/components/login/index.ts new file mode 100644 index 0000000000000..a10c3a83ac04a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/login/index.ts @@ -0,0 +1 @@ +export * from './Login'; diff --git a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx index 4497eb631439f..dd5ff8b5aec11 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/MoreActions.tsx @@ -1,15 +1,19 @@ +import { invalidToken } from '@/application/session/token'; import { Popover } from '@/components/_shared/popover'; +import { AFConfigContext } from '@/components/app/AppConfig'; import { ThemeModeContext } from '@/components/app/useAppThemeMode'; import { openUrl } from '@/utils/url'; import { IconButton } from '@mui/material'; -import React, { useContext, useMemo } from 'react'; +import React, { useCallback, useContext, useMemo } from 'react'; import { ReactComponent as MoreIcon } from '@/assets/more.svg'; import { ReactComponent as MoonIcon } from '@/assets/moon.svg'; import { ReactComponent as SunIcon } from '@/assets/sun.svg'; +import { ReactComponent as LoginIcon } from '@/assets/login.svg'; import { ReactComponent as ReportIcon } from '@/assets/report.svg'; import { useTranslation } from 'react-i18next'; import { ReactComponent as Logo } from '@/assets/logo.svg'; import { ReactComponent as AppflowyLogo } from '@/assets/appflowy.svg'; +import { useNavigate } from 'react-router-dom'; function MoreActions() { const { isDark, setDark } = useContext(ThemeModeContext) || {}; @@ -26,8 +30,21 @@ function MoreActions() { const { t } = useTranslation(); + const navigate = useNavigate(); + + const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false; + + const handleLogin = useCallback(() => { + invalidToken(); + navigate('/login?redirectTo=' + encodeURIComponent(window.location.href)); + }, [navigate]); const actions = useMemo(() => { return [ + { + Icon: LoginIcon, + label: isAuthenticated ? t('button.logout') : t('web.login'), + onClick: handleLogin, + }, isDark ? { Icon: SunIcon, @@ -51,7 +68,7 @@ function MoreActions() { }, }, ]; - }, [isDark, t, setDark]); + }, [isAuthenticated, t, isDark, handleLogin, setDark]); return ( <> diff --git a/frontend/appflowy_web_app/src/pages/LoginPage.tsx b/frontend/appflowy_web_app/src/pages/LoginPage.tsx new file mode 100644 index 0000000000000..52bfd9d9634e9 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/LoginPage.tsx @@ -0,0 +1,12 @@ +import { Login } from '@/components/login'; +import React from 'react'; + +function LoginPage() { + return ( +
+ +
+ ); +} + +export default LoginPage; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 17c7b750b3146..5c90eb2f3a456 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -47,15 +47,15 @@ "unmatchedPasswordError": "Repeat password is not the same as password", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", "or": "OR", - "signInWithGoogle": "Log in with Google", - "signInWithGithub": "Log in with Github", + "signInWithGoogle": "Continue with Google", + "signInWithGithub": "Continue with Github", "signInWithDiscord": "Log in with Discord", "signUpWithGoogle": "Sign up with Google", "signUpWithGithub": "Sign up with Github", "signUpWithDiscord": "Sign up with Discord", - "signInWith": "Sign in with:", - "signInWithEmail": "Sign in with Email", - "signInWithMagicLink": "Log in with Magic Link", + "signInWith": "Continue with:", + "signInWithEmail": "Continue with Email", + "signInWithMagicLink": "Continue", "signUpWithMagicLink": "Sign up with Magic Link", "pleaseInputYourEmail": "Please enter your email address", "settings": "Settings", @@ -2063,5 +2063,18 @@ "createWithAppFlowy": "Create a website with AppFlowy", "fastWithAI": "Fast and easy with AI.", "tryItNow": "Try it now" + }, + "web": { + "continue": "Continue", + "or": "or", + "continueWithGoogle": "Continue with Google", + "continueWithGithub": "Continue with GitHub", + "continueWithDiscord": "Continue with Discord", + "signInAgreement": "By clicking \"Continue\" above, you confirm that you have read, understood, and agreed to AppFlowy's", + "and": "and", + "termOfUse": "Terms", + "privacyPolicy": "Privacy Policy", + "signInError": "Sign in error", + "login": "Sign up or log in" } } From bb275ede1cf919848aba853a30007e2256c863bf Mon Sep 17 00:00:00 2001 From: qinluhe Date: Tue, 9 Jul 2024 16:20:16 +0800 Subject: [PATCH 02/15] fix: modified sign in on flutter --- .../desktop_sign_in_screen.dart | 12 +----- .../sign_in_screen/mobile_sign_in_screen.dart | 11 +----- .../widgets/sign_in_agreement.dart | 37 +++++++++++++++++++ .../sign_in_screen/widgets/widgets.dart | 4 +- frontend/appflowy_web_app/package.json | 2 +- frontend/appflowy_web_app/pnpm-lock.yaml | 16 ++++---- frontend/resources/translations/en.json | 9 ++--- 7 files changed, 56 insertions(+), 35 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 7b0cf7c5fe111..89b782d2317be 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -54,16 +54,8 @@ class DesktopSignInScreen extends StatelessWidget { const SignInAnonymousButtonV2(), const VSpace(10), - SwitchSignInSignUpButton( - onTap: () { - final type = state.loginType == LoginType.signIn - ? LoginType.signUp - : LoginType.signIn; - context - .read() - .add(SignInEvent.switchLoginType(type)); - }, - ), + // sign in agreement + const SignInAgreement(), // loading status const VSpace(indicatorMinHeight), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 2e2e1e8f39690..ce0e959aca4a9 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -39,16 +39,7 @@ class MobileSignInScreen extends StatelessWidget { const VSpace(spacing), const SignInAnonymousButtonV2(), const VSpace(spacing), - SwitchSignInSignUpButton( - onTap: () { - final type = state.loginType == LoginType.signIn - ? LoginType.signUp - : LoginType.signIn; - context.read().add( - SignInEvent.switchLoginType(type), - ); - }, - ), + const SignInAgreement(), const VSpace(spacing), _buildSettingsButton(context), if (!isAuthEnabled) const Spacer(flex: 2), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart new file mode 100644 index 0000000000000..b34f20d3c2462 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -0,0 +1,37 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; + +class SignInAgreement extends StatelessWidget { + const SignInAgreement({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.web_signInAgreement.tr()} ', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + TextSpan( + text: '${LocaleKeys.web_termOfUse.tr()} ', + style: const TextStyle(color: Colors.blue, fontSize: 12), + ), + TextSpan( + text: '${LocaleKeys.web_and.tr()} ', + style: const TextStyle(color: Colors.grey, fontSize: 12), + ), + TextSpan( + text: LocaleKeys.web_privacyPolicy.tr(), + style: const TextStyle(color: Colors.blue, fontSize: 12), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart index 974e2b5927aff..c1c2bfecf9286 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart @@ -1,5 +1,7 @@ export 'magic_link_sign_in_buttons.dart'; export 'sign_in_anonymous_button.dart'; export 'sign_in_or_logout_button.dart'; -export 'switch_sign_in_sign_up_button.dart'; + +// export 'switch_sign_in_sign_up_button.dart'; export 'third_party_sign_in_buttons.dart'; +export 'sign_in_agreement.dart'; diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 8a9be42420c6a..0f7c566a6fd07 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -24,7 +24,7 @@ "coverage": "pnpm run test:unit && pnpm run test:components" }, "dependencies": { - "@appflowyinc/client-api-wasm": "0.1.1", + "@appflowyinc/client-api-wasm": "0.1.2", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index c7c9daa7952e8..cbee7177c7bb3 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -1,13 +1,9 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@appflowyinc/client-api-wasm': - specifier: 0.1.1 - version: 0.1.1 + specifier: 0.1.2 + version: 0.1.2 '@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.1.1: - resolution: {integrity: sha512-7+/TCmzMi9KrxX3HFLJv9R6ON2AO5xQavV547ii7RZM8+5bZJakuf6+pnyCzOquQX07q3ZYwJCa3MIgDvficaA==} + /@appflowyinc/client-api-wasm@0.1.2: + resolution: {integrity: sha512-+v0hs7/7BVKtgev/Bcbr0u2HLDhUuw4ZvZTaMddI+06HK8vt5S52dMaZKUcMvh1eUjVX8hjC6Mfe0X/yHqvFgA==} 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 diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5c90eb2f3a456..f4e38dae8d4b6 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -49,7 +49,7 @@ "or": "OR", "signInWithGoogle": "Continue with Google", "signInWithGithub": "Continue with Github", - "signInWithDiscord": "Log in with Discord", + "signInWithDiscord": "Continue with Discord", "signUpWithGoogle": "Sign up with Google", "signUpWithGithub": "Sign up with Github", "signUpWithDiscord": "Sign up with Discord", @@ -335,9 +335,9 @@ "logout": "Log out", "deleteAccount": "Delete account", "back": "Back", - "signInGoogle": "Sign in with Google", - "signInGithub": "Sign in with Github", - "signInDiscord": "Sign in with Discord", + "signInGoogle": "Continue with Google", + "signInGithub": "Continue with Github", + "signInDiscord": "Continue with Discord", "more": "More", "create": "Create", "close": "Close" @@ -2038,7 +2038,6 @@ "upgrade": "Update", "upgradeYourSpace": "Create multiple Spaces", "quicklySwitch": "Quickly switch to the next space", - "duplicate": "Duplicate Space", "movePageToSpace": "Move page to space", "switchSpace": "Switch space" From fd4923aec438c2f88013d7f9625fc1e992895e19 Mon Sep 17 00:00:00 2001 From: qinluhe Date: Tue, 9 Jul 2024 18:06:30 +0800 Subject: [PATCH 03/15] fix: sign in --- .../sign_in_screen/widgets/magic_link_sign_in_buttons.dart | 1 + frontend/resources/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index b6d5639ee0cb4..035933ef09600 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -61,6 +61,7 @@ class _SignInWithMagicLinkButtonsState } context.read().add(SignInEvent.signedWithMagicLink(email)); + showSnackBarMessage( context, LocaleKeys.signIn_magicLinkSent.tr(), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index f4e38dae8d4b6..bb73ee342c4e2 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -34,7 +34,7 @@ "signIn": { "loginTitle": "Login to @:appName", "loginButtonText": "Login", - "loginStartWithAnonymous": "Start with an anonymous session", + "loginStartWithAnonymous": "Continue with an anonymous session", "continueAnonymousUser": "Continue with an anonymous session", "buttonText": "Sign In", "signingInText": "Signing in...", @@ -2069,7 +2069,7 @@ "continueWithGoogle": "Continue with Google", "continueWithGithub": "Continue with GitHub", "continueWithDiscord": "Continue with Discord", - "signInAgreement": "By clicking \"Continue\" above, you confirm that you have read, understood, and agreed to AppFlowy's", + "signInAgreement": "By clicking \"Continue\" above, you confirm that\nyou have read, understood, and agreed to\nAppFlowy's", "and": "and", "termOfUse": "Terms", "privacyPolicy": "Privacy Policy", From ebd28dfad5d7dd9b9e805cd63369c1d408fd1b55 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Jul 2024 19:14:18 +0800 Subject: [PATCH 04/15] fix: no spacing between page name and delete button --- frontend/appflowy_flutter/ios/Podfile.lock | 4 ++-- .../presentation/home/menu/sidebar/space/shared_widget.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 10aa1f9df1ab8..93a8eb77e180c 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -170,7 +170,7 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 @@ -191,4 +191,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.11.3 +COCOAPODS: 1.15.2 diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index d82e81f48a98e..e823ae0ca6ee3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -264,7 +264,7 @@ class _ConfirmDeletionPopupState extends State { children: [ Row( children: [ - Flexible( + Expanded( child: FlowyText( widget.title, fontSize: 14.0, From cab2fd8cf406d6aca62c993df1084300fd89df30 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 9 Jul 2024 19:26:00 +0800 Subject: [PATCH 05/15] feat: add popup with ok button style --- .../widgets/sign_in_agreement.dart | 1 - .../menu/sidebar/space/shared_widget.dart | 147 +++++++++++++----- .../presentation/widgets/dialogs.dart | 2 +- 3 files changed, 110 insertions(+), 40 deletions(-) diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart index b34f20d3c2462..fc1d9501bc59f 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; class SignInAgreement extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index e823ae0ca6ee3..6b1ddc0917f97 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -223,9 +223,55 @@ class SpaceCancelOrConfirmButton extends StatelessWidget { } } -class ConfirmDeletionPopup extends StatefulWidget { - const ConfirmDeletionPopup({ +class SpaceOkButton extends StatelessWidget { + const SpaceOkButton({ super.key, + required this.onConfirm, + required this.confirmButtonName, + this.confirmButtonColor, + }); + + final VoidCallback onConfirm; + final String confirmButtonName; + final Color? confirmButtonColor; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + DecoratedBox( + decoration: ShapeDecoration( + color: confirmButtonColor ?? Theme.of(context).colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), + radius: BorderRadius.circular(8), + text: FlowyText.regular( + confirmButtonName, + color: Colors.white, + ), + onTap: onConfirm, + ), + ), + ], + ); + } +} + +enum ConfirmPopupStyle { + onlyOk, + cancelAndOk, +} + +class ConfirmPopup extends StatefulWidget { + const ConfirmPopup({ + super.key, + this.style = ConfirmPopupStyle.cancelAndOk, required this.title, required this.description, required this.onConfirm, @@ -234,12 +280,13 @@ class ConfirmDeletionPopup extends StatefulWidget { final String title; final String description; final VoidCallback onConfirm; + final ConfirmPopupStyle style; @override - State createState() => _ConfirmDeletionPopupState(); + State createState() => _ConfirmPopupState(); } -class _ConfirmDeletionPopupState extends State { +class _ConfirmPopupState extends State { final focusNode = FocusNode(); @override @@ -262,46 +309,70 @@ class _ConfirmDeletionPopupState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: FlowyText( - widget.title, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6.0), - FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.upgrade_close_s), - onTap: () => Navigator.of(context).pop(), - ), - ], - ), - const VSpace(8.0), - FlowyText.regular( - widget.description, - fontSize: 12.0, - color: Theme.of(context).hintColor, - maxLines: 3, - lineHeight: 1.4, - ), + _buildTitle(), + const VSpace(6.0), + _buildDescription(), const VSpace(20.0), - SpaceCancelOrConfirmButton( - onCancel: () => Navigator.of(context).pop(), - onConfirm: () { - widget.onConfirm(); - Navigator.of(context).pop(); - }, - confirmButtonName: LocaleKeys.space_delete.tr(), - confirmButtonColor: Theme.of(context).colorScheme.error, - ), + _buildStyledButton(context), ], ), ), ); } + + Widget _buildTitle() { + return Row( + children: [ + Expanded( + child: FlowyText( + widget.title, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(6.0), + FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.upgrade_close_s), + onTap: () => Navigator.of(context).pop(), + ), + ], + ); + } + + Widget _buildDescription() { + return FlowyText.regular( + widget.description, + fontSize: 12.0, + color: Theme.of(context).hintColor, + maxLines: 3, + lineHeight: 1.4, + ); + } + + Widget _buildStyledButton(BuildContext context) { + switch (widget.style) { + case ConfirmPopupStyle.onlyOk: + return SpaceOkButton( + onConfirm: () { + widget.onConfirm(); + Navigator.of(context).pop(); + }, + confirmButtonName: LocaleKeys.space_delete.tr(), + confirmButtonColor: Theme.of(context).colorScheme.error, + ); + case ConfirmPopupStyle.cancelAndOk: + return SpaceCancelOrConfirmButton( + onCancel: () => Navigator.of(context).pop(), + onConfirm: () { + widget.onConfirm(); + Navigator.of(context).pop(); + }, + confirmButtonName: LocaleKeys.space_delete.tr(), + confirmButtonColor: Theme.of(context).colorScheme.error, + ); + } + } } class SpacePopup extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 8d1196b503e8b..b410924e123e4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -329,7 +329,7 @@ Future showConfirmDeletionDialog({ ), child: SizedBox( width: 440, - child: ConfirmDeletionPopup( + child: ConfirmPopup( title: title, description: description, onConfirm: onConfirm, From d5cb304992183a15d88127af74abade3bdf60762 Mon Sep 17 00:00:00 2001 From: Kilu Date: Tue, 9 Jul 2024 19:28:08 +0800 Subject: [PATCH 06/15] fix: database render height --- .../src/application/db/tables/view_metas.ts | 2 + .../services/js-services/cache/index.ts | 9 ++++- .../application/services/js-services/index.ts | 1 + .../services/js-services/wasm/client_api.ts | 35 ++++++++++++++--- .../src/components/database/Database.tsx | 14 +++++-- .../components/cell/primary/PrimaryCell.tsx | 13 +++---- .../components/grid/grid-table/GridTable.tsx | 1 + .../components/header/DatabaseHeader.tsx | 38 +++++++++++++++++++ .../src/components/error/NotFound.tsx | 27 ++++++------- .../src/components/publish/CollabView.tsx | 7 ++-- .../components/view-meta/ViewMetaPreview.tsx | 3 +- 11 files changed, 114 insertions(+), 36 deletions(-) create mode 100644 frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx diff --git a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts index 1f39ac19f852c..8cc3573038210 100644 --- a/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts +++ b/frontend/appflowy_web_app/src/application/db/tables/view_metas.ts @@ -6,6 +6,8 @@ export type ViewMeta = { child_views: PublishViewInfo[]; ancestor_views: PublishViewInfo[]; + + visible_view_ids: string[]; } & PublishViewInfo; export type ViewMetasTable = { diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts index a4db11463035e..467f5653ee387 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts @@ -110,6 +110,8 @@ export async function getPublishViewMeta< export async function getPublishView< T extends { data: number[]; + rows?: Record; + visibleViewIds?: string[]; meta: { view: PublishViewInfo; child_views: PublishViewInfo[]; @@ -176,12 +178,15 @@ export async function revalidatePublishViewMeta< >(name: string, fetcher: Fetcher) { const { view, child_views, ancestor_views } = await fetcher(); + const dbView = await db.view_metas.get(name); + await db.view_metas.put( { publish_name: name, ...view, child_views: child_views, ancestor_views: ancestor_views, + visible_view_ids: dbView?.visible_view_ids ?? [], }, name ); @@ -193,10 +198,11 @@ export async function revalidatePublishView< T extends { data: number[]; rows?: Record; + visibleViewIds?: string[]; meta: PublishViewMetaData; } >(name: string, fetcher: Fetcher, collab: YDoc) { - const { data, meta, rows } = await fetcher(); + const { data, meta, rows, visibleViewIds = [] } = await fetcher(); await db.view_metas.put( { @@ -204,6 +210,7 @@ export async function revalidatePublishView< ...meta.view, child_views: meta.child_views, ancestor_views: meta.ancestor_views, + visible_view_ids: visibleViewIds, }, name ); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index afa0b43b1e154..697a050168a74 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -128,6 +128,7 @@ export class AFClientService implements AFService { rowsFolder.set(rowIds[index], doc); }); + console.log('getPublishDatabaseViewRows', rowsFolder); return { rows: rowsFolder, destroy: () => { diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index a2fdd707bd46b..b3865b76713cd 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -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 } from '@/application/collab.type'; +import { PublishViewMetaData, ViewLayout } from '@/application/collab.type'; let client: ClientAPI; @@ -41,10 +41,35 @@ export function initAPIService( export async function getPublishView(publishNamespace: string, publishName: string) { const data = await client.get_publish_view(publishNamespace, publishName); - return { - data: data.data, - meta: JSON.parse(data.meta.data) as PublishViewMetaData, - }; + const meta = JSON.parse(data.meta.data) as PublishViewMetaData; + + if (meta.view.layout === ViewLayout.Document) { + return { + data: data.data, + meta, + }; + } + + 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; + database_row_document_collabs: Record; + visible_database_view_ids: string[]; + }; + + console.log('getPublishView', res); + return { + data: res.database_collab, + rows: res.database_row_collabs, + visibleViewIds: res.visible_database_view_ids, + meta, + }; + } catch (e) { + return Promise.reject(e); + } } export async function getPublishInfoWithViewId(viewId: string) { diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 4b3964e240bb4..154d8c85044d5 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -1,9 +1,10 @@ import { YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; import { ViewMeta } from '@/application/db/tables/view_metas'; 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 { ViewMetaPreview, ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; +import { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview'; import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import * as Y from 'yjs'; @@ -27,7 +28,7 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, 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.toArray().map((row) => row.get(YjsDatabaseKey.id)); + return rows.toJSON().map((row) => row.id); }, [doc, viewId]); const iidIndex = useMemo(() => { @@ -70,7 +71,12 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView, } return ( -
+
}> ) : (
- {viewMeta && } + {viewMeta && }
diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx index 7c2eb6c648b97..102e9eb0cc294 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -1,7 +1,6 @@ import { useNavigateToRow, 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 OpenAction from '@/components/database/components/database-row/OpenAction'; import { getPlatform } from '@/utils/platform'; import React, { useEffect, useMemo, useState } from 'react'; @@ -10,7 +9,7 @@ export function PrimaryCell(props: CellProps) { const meta = useRowMetaSelector(rowId); const icon = meta?.icon; - const [hover, setHover] = useState(false); + const [, setHover] = useState(false); useEffect(() => { const table = document.querySelector('.grid-table'); @@ -61,11 +60,11 @@ export function PrimaryCell(props: CellProps) {
- {hover && ( -
- -
- )} + {/*{hover && (*/} + {/*
*/} + {/* */} + {/*
*/} + {/*)}*/}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index 41e52c508609e..ce09cb500fa0f 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -18,6 +18,7 @@ export interface GridTableProps { export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => { const ref = useRef(null); const { rows } = useRenderRows(); + const forceUpdate = useCallback((index: number) => { ref.current?.resetAfterRowIndex(index, true); }, []); diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx new file mode 100644 index 0000000000000..5f99969abfe59 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx @@ -0,0 +1,38 @@ +import { ViewLayout, ViewMetaIcon } from '@/application/collab.type'; +import { ViewIcon } from '@/components/_shared/view-icon'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function DatabaseHeader({ + icon, + name, + layout, +}: { + icon?: ViewMetaIcon; + name?: string; + viewId?: string; + layout?: ViewLayout; +}) { + const { t } = useTranslation(); + + return ( +
+
+ {icon?.value ? ( +
{icon?.value}
+ ) : ( + + )} +
+
+ {name || {t('menuAppHeader.defaultNewPageName')}} +
+
+ ); +} + +export default DatabaseHeader; diff --git a/frontend/appflowy_web_app/src/components/error/NotFound.tsx b/frontend/appflowy_web_app/src/components/error/NotFound.tsx index 1d2810fdc0a39..95410cef98cd9 100644 --- a/frontend/appflowy_web_app/src/components/error/NotFound.tsx +++ b/frontend/appflowy_web_app/src/components/error/NotFound.tsx @@ -11,25 +11,22 @@ const NotFound = () => { return (
- - - + + <> + + + - +
{t('publish.noAccessToVisit')} - - -
{t('publish.createWithAppFlowy')}
+
+
+
{t('publish.createWithAppFlowy')}
- {t('publish.fastWithAI')} - {t('publish.tryItNow')} +
{t('publish.fastWithAI')}
+
{t('publish.tryItNow')}
- +