diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 22bf0cb631..274fdc46f7 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.2", + "@appflowyinc/client-api-wasm": "0.1.3", "@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 584d748c79..186c5cc29c 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.2 - version: 0.1.2 + specifier: 0.1.3 + version: 0.1.3 '@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.2: - resolution: {integrity: sha512-+v0hs7/7BVKtgev/Bcbr0u2HLDhUuw4ZvZTaMddI+06HK8vt5S52dMaZKUcMvh1eUjVX8hjC6Mfe0X/yHqvFgA==} + /@appflowyinc/client-api-wasm@0.1.3: + resolution: {integrity: sha512-M603RIBocCjDlwDx5O53j4tH2M/y6uKZSdpnBq3nCMBPwTGEhTFKBDD3tMmjSIHo8nnGx1t8gsKei55LlhtoNQ==} 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/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index 7ac7cbc256..d5f40d4cd4 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 @@ -14,12 +14,17 @@ import { signInGithub, signInDiscord, signInWithUrl, + getWorkspaces, + getWorkspaceFolder, + getCurrentUser, + duplicatePublishView, } 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 * as Y from 'yjs'; +import { DuplicatePublishView } from '@/application/types'; export class AFClientService implements AFService { private deviceId: string = nanoid(8); @@ -199,4 +204,36 @@ export class AFClientService implements AFService { async signInDiscord(_: { redirectTo: string }) { return await signInDiscord(AUTH_CALLBACK_URL); } + + async getWorkspaces() { + const data = getWorkspaces(); + + return data; + } + + async getWorkspaceFolder(workspaceId: string) { + const data = await getWorkspaceFolder(workspaceId); + + return data; + } + + async getCurrentUser() { + const data = await getCurrentUser(); + + return { + uid: data.uid, + email: data.email, + name: data.name, + avatar: data.icon_url, + }; + } + + async duplicatePublishView(params: DuplicatePublishView) { + return duplicatePublishView({ + workspace_id: params.workspaceId, + dest_view_id: params.spaceViewId, + published_view_id: params.viewId, + published_collab_type: params.collabType, + }); + } } 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 852559c3aa..6848c989a0 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,8 @@ import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token'; -import { ClientAPI } from '@appflowyinc/client-api-wasm'; +import { ClientAPI, WorkspaceFolder, DuplicatePublishViewPayload } from '@appflowyinc/client-api-wasm'; import { AFCloudConfig } from '@/application/services/services.type'; import { DatabaseId, PublishViewMetaData, RowId, ViewId, ViewLayout } from '@/application/collab.type'; +import { FolderView, Workspace } from '@/application/types'; let client: ClientAPI; @@ -115,3 +116,58 @@ export async function signInGithub(redirectTo: string) { export async function signInDiscord(redirectTo: string) { return signInProvider('discord', redirectTo); } + +export async function getWorkspaces() { + try { + const { data } = await client.get_workspaces(); + const res: Workspace[] = []; + + for (const workspace of data) { + const members = await client.get_workspace_members(workspace.workspace_id); + + res.push({ + id: workspace.workspace_id, + name: workspace.workspace_name, + icon: workspace.icon, + memberCount: members.data.length, + }); + } + + return res; + } catch (e) { + return Promise.reject(e); + } +} + +export async function getWorkspaceFolder(workspaceId: string): Promise { + try { + const data = await client.get_folder(workspaceId); + + // eslint-disable-next-line no-inner-declarations + function iterateFolder(folder: WorkspaceFolder): FolderView { + return { + id: folder.view_id, + name: folder.name, + icon: folder.icon, + isSpace: folder.is_space, + extra: folder.extra, + isPrivate: folder.is_private, + children: folder.children.map((child: WorkspaceFolder) => { + return iterateFolder(child); + }), + }; + } + + return iterateFolder(data); + } catch (e) { + return Promise.reject(e); + } +} + +export function getCurrentUser() { + return client.get_user(); +} + +export function duplicatePublishView(payload: DuplicatePublishViewPayload) { + return client.duplicate_publish_view(payload); +} 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 919cbf5306..8efca75828 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -1,6 +1,7 @@ import { YDoc } from '@/application/collab.type'; import { ViewMeta } from '@/application/db/tables/view_metas'; import * as Y from 'yjs'; +import { DuplicatePublishView, FolderView, User, Workspace } from '@/application/types'; export type AFService = PublishService; @@ -33,4 +34,9 @@ export interface PublishService { signInGoogle: (params: { redirectTo: string }) => Promise; signInGithub: (params: { redirectTo: string }) => Promise; signInDiscord: (params: { redirectTo: string }) => Promise; + + getWorkspaces: () => Promise; + getWorkspaceFolder: (workspaceId: string) => Promise; + getCurrentUser: () => Promise; + duplicatePublishView: (params: DuplicatePublishView) => Promise; } diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index e5b970e74b..b870f1ed22 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -2,6 +2,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'; +import { DuplicatePublishView, FolderView, User, Workspace } from '@/application/types'; export class AFClientService implements AFService { private deviceId: string = nanoid(8); @@ -53,4 +54,20 @@ export class AFClientService implements AFService { }> { return Promise.reject('Method not implemented'); } + + duplicatePublishView(_params: DuplicatePublishView): Promise { + return Promise.resolve(undefined); + } + + getCurrentUser(): Promise { + return Promise.reject('Method not implemented'); + } + + getWorkspaceFolder(_workspaceId: string): Promise { + return Promise.reject('Method not implemented'); + } + + getWorkspaces(): Promise { + return Promise.reject('Method not implemented'); + } } diff --git a/frontend/appflowy_web_app/src/application/types.ts b/frontend/appflowy_web_app/src/application/types.ts index c15db2ab38..e5107df068 100644 --- a/frontend/appflowy_web_app/src/application/types.ts +++ b/frontend/appflowy_web_app/src/application/types.ts @@ -1,3 +1,5 @@ +import { CollabType } from '@/application/collab.type'; + export interface Workspace { icon: string; id: string; @@ -7,6 +9,31 @@ export interface Workspace { export interface SpaceView { id: string; - extra?: string; + extra: string | null; name: string; + isPrivate: boolean; +} + +export interface FolderView { + id: string; + icon: string | null; + extra: string | null; + name: string; + isSpace: boolean; + isPrivate: boolean; + children: FolderView[]; +} + +export interface User { + email: string | null; + name: string | null; + uid: string; + avatar: string | null; +} + +export interface DuplicatePublishView { + workspaceId: string; + spaceViewId: string; + collabType: CollabType; + viewId: string; } diff --git a/frontend/appflowy_web_app/src/assets/close.svg b/frontend/appflowy_web_app/src/assets/close.svg index 8f683c2382..6eb7ce67e9 100644 --- a/frontend/appflowy_web_app/src/assets/close.svg +++ b/frontend/appflowy_web_app/src/assets/close.svg @@ -1,4 +1,6 @@ - - - + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/lock.svg b/frontend/appflowy_web_app/src/assets/lock.svg new file mode 100644 index 0000000000..2371767324 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/lock.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/_shared/modal/NormalModal.tsx b/frontend/appflowy_web_app/src/components/_shared/modal/NormalModal.tsx index c7af1d042b..ddb5b70c9d 100644 --- a/frontend/appflowy_web_app/src/components/_shared/modal/NormalModal.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/modal/NormalModal.tsx @@ -1,15 +1,21 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Dialog, DialogProps, IconButton } from '@mui/material'; +import { Button, ButtonProps, CircularProgress, Dialog, DialogProps, IconButton } from '@mui/material'; import { ReactComponent as CloseIcon } from '@/assets/close.svg'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore export interface NormalModalProps extends DialogProps { okText?: string; cancelText?: string; onOk?: () => void; onCancel?: () => void; danger?: boolean; - title?: string; + onClose?: () => void; + title: string | React.ReactNode; + okButtonProps?: ButtonProps; + cancelButtonProps?: ButtonProps; + okLoading?: boolean; } export function NormalModal({ @@ -19,7 +25,11 @@ export function NormalModal({ onOk, onCancel, danger, + onClose, children, + okButtonProps, + cancelButtonProps, + okLoading, ...dialogProps }: NormalModalProps) { const { t } = useTranslation(); @@ -31,21 +41,30 @@ export function NormalModal({
-
{title}
+
{title}
- - + +
{children}
- -
diff --git a/frontend/appflowy_web_app/src/components/_shared/notify/InfoSnackbar.tsx b/frontend/appflowy_web_app/src/components/_shared/notify/InfoSnackbar.tsx new file mode 100644 index 0000000000..1c10590f24 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/notify/InfoSnackbar.tsx @@ -0,0 +1,46 @@ +import React, { forwardRef } from 'react'; +import { Button, IconButton, Paper } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as CloseIcon } from '@/assets/close.svg'; +import { CustomContentProps, SnackbarContent } from 'notistack'; + +export interface InfoProps { + onOk?: () => void; + okText?: string; + title?: string; + message?: JSX.Element | string; + onClose?: () => void; + autoHideDuration?: number | null; +} + +export type InfoSnackbarProps = InfoProps & CustomContentProps; + +export const InfoSnackbar = forwardRef( + ({ onOk, okText, title, message, onClose }, ref) => { + const { t } = useTranslation(); + + return ( + + +
+
{title}
+
+ + + +
+
+ +
{message}
+
+ +
+
+
+ ); + } +); + +export default InfoSnackbar; diff --git a/frontend/appflowy_web_app/src/components/_shared/notify/index.ts b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts index 9b6e888f2e..71fc659117 100644 --- a/frontend/appflowy_web_app/src/components/_shared/notify/index.ts +++ b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts @@ -1,3 +1,5 @@ +import { InfoProps } from '@/components/_shared/notify/InfoSnackbar'; + export const notify = { success: (message: string) => { window.toast.success(message); @@ -11,10 +13,19 @@ export const notify = { warning: (message: string) => { window.toast.warning(message); }, - info: (message: string) => { - window.toast.info(message); + info: (props: InfoProps) => { + window.toast.info({ + ...props, + variant: 'info', + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, + }); }, clear: () => { window.toast.clear(); }, }; + +export * from './InfoSnackbar'; diff --git a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx index b4ef0e60d4..ac1ec4ce7d 100644 --- a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx @@ -6,6 +6,8 @@ import { useSnackbar } from 'notistack'; import React, { createContext, useEffect, useState } from 'react'; import { AFService, AFServiceConfig } from '@/application/services/services.type'; import { getService } from '@/application/services'; +import { InfoSnackbarProps } from '@/components/_shared/notify'; +import { User } from '@/application/types'; const baseURL = import.meta.env.AF_BASE_URL || 'https://test.appflowy.cloud'; const gotrueURL = import.meta.env.AF_GOTRUE_URL || 'https://test.appflowy.cloud/gotrue'; @@ -23,6 +25,7 @@ export const AFConfigContext = createContext< | { service: AFService | undefined; isAuthenticated: boolean; + currentUser?: User; } | undefined >(undefined); @@ -31,6 +34,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { const [appConfig] = useState(defaultConfig); const [service, setService] = useState(); const [isAuthenticated, setIsAuthenticated] = React.useState(isTokenValid()); + const [currentUser, setCurrentUser] = React.useState(); useEffect(() => { return on(EventType.SESSION_VALID, () => { @@ -38,6 +42,24 @@ function AppConfig({ children }: { children: React.ReactNode }) { }); }, []); + useEffect(() => { + if (!isAuthenticated) { + setCurrentUser(undefined); + return; + } + + void (async () => { + if (!service) return; + try { + const user = await service.getCurrentUser(); + + setCurrentUser(user); + } catch (e) { + console.error(e); + } + })(); + }, [isAuthenticated, service]); + useEffect(() => { const handleStorageChange = (event: StorageEvent) => { if (event.key === 'token') setIsAuthenticated(isTokenValid()); @@ -79,8 +101,9 @@ function AppConfig({ children }: { children: React.ReactNode }) { default: (message: string) => { enqueueSnackbar(message, { variant: 'default' }); }, - info: (message: string) => { - enqueueSnackbar(message, { variant: 'info' }); + + info: (props: InfoSnackbarProps) => { + enqueueSnackbar(props.message, props); }, clear: () => { @@ -111,6 +134,7 @@ function AppConfig({ children }: { children: React.ReactNode }) { value={{ service, isAuthenticated, + currentUser, }} > {children} diff --git a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx index cee8ff3b10..d943d89cf6 100644 --- a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx @@ -41,6 +41,9 @@ function AppTheme({ children }: { children: React.ReactNode }) { }, borderRadius: '4px', padding: '2px', + '&.MuiIconButton-colorInherit': { + color: 'var(--icon-primary)', + }, }, }, }, @@ -59,6 +62,11 @@ function AppTheme({ children }: { children: React.ReactNode }) { backgroundColor: 'var(--content-blue-600)', }, borderRadius: '8px', + '&.Mui-disabled': { + backgroundColor: 'var(--content-blue-400)', + opacity: 0.3, + color: 'var(--content-on-fill)', + }, }, outlined: { '&.MuiButton-outlinedInherit': { diff --git a/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx b/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx index 0fddafd2a5..6cc8a0d9fe 100644 --- a/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx +++ b/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx @@ -5,6 +5,7 @@ import AppConfig from '@/components/app/AppConfig'; import { Suspense } from 'react'; import { SnackbarProvider } from 'notistack'; import { styled } from '@mui/material'; +import InfoSnackbar from '../_shared/notify/InfoSnackbar'; const StyledSnackbarProvider = styled(SnackbarProvider)` &.notistack-MuiContent-default { @@ -39,6 +40,9 @@ export default function withAppWrapper(Component: React.FC): React.FC { horizontal: 'center', }} preventDuplicate + Components={{ + info: InfoSnackbar, + }} > diff --git a/frontend/appflowy_web_app/src/components/login/LoginModal.tsx b/frontend/appflowy_web_app/src/components/login/LoginModal.tsx index dea4ac8a47..a4f203cd47 100644 --- a/frontend/appflowy_web_app/src/components/login/LoginModal.tsx +++ b/frontend/appflowy_web_app/src/components/login/LoginModal.tsx @@ -9,8 +9,8 @@ export function LoginModal({ redirectTo, open, onClose }: { redirectTo: string;
- - + +
diff --git a/frontend/appflowy_web_app/src/components/publish/header/duplicate/DuplicateModal.tsx b/frontend/appflowy_web_app/src/components/publish/header/duplicate/DuplicateModal.tsx index d7c69970bb..dd3500d9df 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/duplicate/DuplicateModal.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/duplicate/DuplicateModal.tsx @@ -1,36 +1,128 @@ -import React from 'react'; +import React, { useCallback, useContext, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { NormalModal } from '@/components/_shared/modal'; import SelectWorkspace from '@/components/publish/header/duplicate/SelectWorkspace'; import { useLoadWorkspaces } from '@/components/publish/header/duplicate/useDuplicate'; import SpaceList from '@/components/publish/header/duplicate/SpaceList'; +import { downloadPage, openAppFlowySchema } from '@/utils/url'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { PublishContext } from '@/application/publish'; +import { CollabType, ViewLayout } from '@/application/collab.type'; import { notify } from '@/components/_shared/notify'; +function getCollabTypeFromViewLayout(layout: ViewLayout) { + switch (layout) { + case ViewLayout.Document: + return CollabType.Document; + case ViewLayout.Grid: + case ViewLayout.Board: + case ViewLayout.Calendar: + return CollabType.Database; + default: + return null; + } +} + function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void }) { const { t } = useTranslation(); + const service = useContext(AFConfigContext)?.service; + const viewMeta = useContext(PublishContext)?.viewMeta; + const viewId = viewMeta?.view_id; + const layout = viewMeta?.layout as ViewLayout; + const [loading, setLoading] = React.useState(false); + const [successModalOpen, setSuccessModalOpen] = React.useState(false); + const { + workspaceList, + spaceList, + setSelectedSpaceId, + setSelectedWorkspaceId, + selectedWorkspaceId, + selectedSpaceId, + workspaceLoading, + spaceLoading, + } = useLoadWorkspaces(); - const { workspaceList, spaceList, setSelectedSpaceId, setSelectedWorkspaceId, selectedWorkspaceId, selectedSpaceId } = - useLoadWorkspaces(); + useEffect(() => { + if (!open) { + setSelectedWorkspaceId(workspaceList[0]?.id || ''); + setSelectedSpaceId(''); + } + }, [open, setSelectedSpaceId, setSelectedWorkspaceId, workspaceList]); + + const handleDuplicate = useCallback(async () => { + if (!viewId) return; + const collabType = getCollabTypeFromViewLayout(layout); + + if (collabType === null) return; + + setLoading(true); + try { + await service?.duplicatePublishView({ + workspaceId: selectedWorkspaceId, + spaceViewId: selectedSpaceId, + viewId, + collabType, + }); + onClose(); + setSuccessModalOpen(true); + } catch (e) { + notify.error(t('publish.duplicateFailed')); + } finally { + setLoading(false); + } + }, [viewId, layout, service, selectedWorkspaceId, selectedSpaceId, onClose, t]); return ( - { - // submit form - notify.success(t('publish.duplicateSuccessfully')); - onClose(); - }} - > -
- - -
-
+ <> + +
+ + +
+
+ window.open(openAppFlowySchema, '_self')} + onCancel={() => { + window.open(downloadPage, '_blank'); + }} + onClose={() => setSuccessModalOpen(false)} + open={successModalOpen} + title={
{t('publish.duplicateSuccessfully')}
} + > +
+ {t('publish.duplicateSuccessfullyDescription')} +
+
+ ); } diff --git a/frontend/appflowy_web_app/src/components/publish/header/duplicate/SelectWorkspace.tsx b/frontend/appflowy_web_app/src/components/publish/header/duplicate/SelectWorkspace.tsx index 2a5042f39b..1e61b35d72 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/duplicate/SelectWorkspace.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/duplicate/SelectWorkspace.tsx @@ -1,20 +1,32 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Divider, IconButton, Tooltip } from '@mui/material'; +import { Avatar, Button, CircularProgress, Divider, Tooltip } from '@mui/material'; import { Workspace } from '@/application/types'; import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg'; import { ReactComponent as CheckIcon } from '@/assets/selected.svg'; import { Popover } from '@/components/_shared/popover'; +import { stringToColor } from '@/utils/color'; +import { AFConfigContext } from '@/components/app/AppConfig'; export interface SelectWorkspaceProps { value: string; onChange?: (value: string) => void; workspaceList: Workspace[]; + loading?: boolean; } -function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProps) { +function stringAvatar(name: string) { + return { + sx: { + bgcolor: stringToColor(name), + }, + children: `${name.split('')[0]}`, + }; +} + +function SelectWorkspace({ loading, value, onChange, workspaceList }: SelectWorkspaceProps) { const { t } = useTranslation(); - const email = 'lu@appflowy.io'; + const email = useContext(AFConfigContext)?.currentUser?.email || ''; const selectedWorkspace = useMemo(() => { return workspaceList.find((workspace) => workspace.id === value); }, [value, workspaceList]); @@ -25,7 +37,16 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp (workspace: Workspace) => { return (
-
{workspace.icon}
+ {workspace.icon ? ( +
{workspace.icon}
+ ) : ( + + )}
{workspace.name}
@@ -53,10 +74,20 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp color={'inherit'} >
-
{selectedWorkspace ? renderWorkspace(selectedWorkspace) : null}
- - - + {loading ? ( +
+ +
+ ) : ( + <> +
+ {selectedWorkspace ? renderWorkspace(selectedWorkspace) : null} +
+ + + + + )}
-
+
{email}
diff --git a/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx b/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx index 65cf7101ac..8979c0e6f0 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx +++ b/frontend/appflowy_web_app/src/components/publish/header/duplicate/SpaceList.tsx @@ -4,15 +4,17 @@ import { useTranslation } from 'react-i18next'; import { ReactComponent as CheckIcon } from '@/assets/selected.svg'; import { renderColor } from '@/utils/color'; import SpaceIcon from '@/components/publish/header/SpaceIcon'; -import { Button, Tooltip } from '@mui/material'; +import { Button, CircularProgress, Tooltip } from '@mui/material'; +import { ReactComponent as LockSvg } from '@/assets/lock.svg'; export interface SpaceListProps { value: string; onChange?: (value: string) => void; spaceList: SpaceView[]; + loading?: boolean; } -function SpaceList({ spaceList, value, onChange }: SpaceListProps) { +function SpaceList({ loading, spaceList, value, onChange }: SpaceListProps) { const { t } = useTranslation(); const getExtraObj = useCallback((extra: string) => { @@ -44,7 +46,10 @@ function SpaceList({ spaceList, value, onChange }: SpaceListProps) { > -
{space.name}
+
+ {space.name} + {space.isPrivate && } +
); }, @@ -54,29 +59,35 @@ function SpaceList({ spaceList, value, onChange }: SpaceListProps) { return (
{t('publish.addTo')}
-
- {spaceList.map((space) => { - const isSelected = value === space.id; + {loading ? ( +
+ +
+ ) : ( +
+ {spaceList.map((space) => { + const isSelected = value === space.id; - return ( - - - - ); - })} -
+ return ( + + + + ); + })} +
+ )}
); } diff --git a/frontend/appflowy_web_app/src/components/publish/header/duplicate/useDuplicate.ts b/frontend/appflowy_web_app/src/components/publish/header/duplicate/useDuplicate.ts index e6b72956d2..ab93ad253d 100644 --- a/frontend/appflowy_web_app/src/components/publish/header/duplicate/useDuplicate.ts +++ b/frontend/appflowy_web_app/src/components/publish/header/duplicate/useDuplicate.ts @@ -2,6 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { AFConfigContext } from '@/components/app/AppConfig'; import { useSearchParams } from 'react-router-dom'; import { SpaceView, Workspace } from '@/application/types'; +import { notify } from '@/components/_shared/notify'; export function useDuplicate() { const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false; @@ -46,131 +47,76 @@ export function useDuplicate() { } export function useLoadWorkspaces() { + const [spaceLoading, setSpaceLoading] = useState(false); + const [workspaceLoading, setWorkspaceLoading] = useState(false); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState('1'); const [selectedSpaceId, setSelectedSpaceId] = useState('1'); - const [workspaceList] = useState([ - { - icon: '😊', - id: '1', - name: 'AppFlowy', - memberCount: 0, - }, - { - icon: '😍', - id: '2', - name: `Kilu's Workspace`, - memberCount: 12, - }, - { - icon: '😎', - id: '3', - name: 'Workspace 3 djskh dhjsa dhsjkahdkja dshjkahd kashdjkashd askhdkjas', - memberCount: 5, - }, - { - icon: '😇', - id: '4', - name: 'Workspace 4', - memberCount: 3, - }, - { - icon: '😜', - id: '5', - name: 'Workspace 5', - memberCount: 1, - }, - { - icon: '😉', - id: '6', - name: 'Workspace 6', - memberCount: 0, - }, - ]); + const [workspaceList, setWorkspaceList] = useState([]); - const [spaceList] = useState([ - { - id: '1', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_1', - space_icon_color: 'appflowy_them_color_tint2', - }), - name: 'Space 1', - }, - { - id: '2', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_2', - space_icon_color: 'appflowy_them_color_tint1', - }), - name: 'Space 2 djisa djiso adiohjsa hdiosahdk ksahkjdhskjahdaskj', - }, - { - id: '3', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_3', - space_icon_color: 'appflowy_them_color_tint3', - }), - name: 'Space 3', - }, - { - id: '4', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_4', - space_icon_color: 'appflowy_them_color_tint4', - }), - name: 'Space 4', - }, - { - id: '5', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_5', - space_icon_color: 'appflowy_them_color_tint5', - }), - name: 'Space 5', - }, - { - id: '6', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_6', - space_icon_color: 'appflowy_them_color_tint6', - }), - name: 'Space 6', - }, - { - id: '7', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_7', - space_icon_color: 'appflowy_them_color_tint7', - }), - name: 'Space 7', - }, - { - id: '8', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_8', - space_icon_color: 'appflowy_them_color_tint8', - }), - name: 'Space 8', - }, - { - id: '9', - extra: JSON.stringify({ - is_space: true, - space_icon: 'space_icon_9', - space_icon_color: 'appflowy_them_color_tint9', - }), - name: 'Space', - }, - ]); + const [spaceList, setSpaceList] = useState([]); + + const service = useContext(AFConfigContext)?.service; + + useEffect(() => { + void (async () => { + setWorkspaceLoading(true); + try { + const workspaces = await service?.getWorkspaces(); + + if (workspaces) { + setWorkspaceList(workspaces); + setSelectedWorkspaceId(workspaces[0].id); + } else { + setWorkspaceList([]); + setSelectedWorkspaceId(''); + } + } catch (e) { + notify.error('Failed to load workspaces'); + } finally { + setWorkspaceLoading(false); + } + })(); + }, [service]); + + useEffect(() => { + if (workspaceList.length === 0 || !selectedWorkspaceId || workspaceLoading) { + setSpaceList([]); + setSelectedSpaceId(''); + return; + } + + setSpaceLoading(true); + void (async () => { + try { + const folder = await service?.getWorkspaceFolder(selectedWorkspaceId); + + if (folder) { + const spaces = []; + + for (const child of folder.children) { + if (child.isSpace) { + spaces.push({ + id: child.id, + name: child.name, + isPrivate: child.isPrivate, + extra: child.extra, + }); + } + } + + setSpaceList(spaces); + } else { + setSpaceList([]); + } + } catch (e) { + notify.error('Failed to load spaces'); + } finally { + setSelectedSpaceId(''); + setSpaceLoading(false); + } + })(); + }, [selectedWorkspaceId, service, workspaceList.length, workspaceLoading]); return { workspaceList, @@ -179,5 +125,7 @@ export function useLoadWorkspaces() { setSelectedWorkspaceId, selectedSpaceId, setSelectedSpaceId, + workspaceLoading, + spaceLoading, }; } diff --git a/frontend/appflowy_web_app/src/utils/color.ts b/frontend/appflowy_web_app/src/utils/color.ts index 9de9da1dca..91f9fe4346 100644 --- a/frontend/appflowy_web_app/src/utils/color.ts +++ b/frontend/appflowy_web_app/src/utils/color.ts @@ -72,3 +72,24 @@ export function renderColor(color: string) { return argbToRgba(color); } + +export function stringToColor(string: string) { + let hash = 0; + let i; + + /* eslint-disable no-bitwise */ + for (i = 0; i < string.length; i += 1) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = '#'; + + for (i = 0; i < 3; i += 1) { + const value = (hash >> (i * 8)) & 0xff; + + color += `00${value.toString(16)}`.slice(-2); + } + /* eslint-enable no-bitwise */ + + return color; +} diff --git a/frontend/appflowy_web_app/src/vite-env.d.ts b/frontend/appflowy_web_app/src/vite-env.d.ts index 2ec03900db..8ecd467d45 100644 --- a/frontend/appflowy_web_app/src/vite-env.d.ts +++ b/frontend/appflowy_web_app/src/vite-env.d.ts @@ -13,7 +13,8 @@ interface Window { toast: { success: (message: string) => void; error: (message: string) => void; - info: (message: string) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + info: (props: any) => void; clear: () => void; default: (message: string) => void; warning: (message: string) => void; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fc9a52feb0..537862373a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2139,9 +2139,9 @@ "duplicateTitle": "Where would you like to add", "selectWorkspace": "Select a workspace", "addTo": "Add to", - "duplicateSuccessfully": "Duplicated success", - "duplicateSuccessfullyDescription": "Open in AppFlowy's desktop app? If don't have the app, you can", - "downloadIt": "download it here", + "duplicateSuccessfully": "Duplicated success. Want to view documents?", + "duplicateSuccessfullyDescription": "Don't have the app? Your download will begin automatically after clicking the 'Download'.", + "downloadIt": "Download", "openApp": "Open in app", "duplicateFailed": "Duplicated failed", "membersCount": {