fix: upgrade wasm package

This commit is contained in:
qinluhe 2024-07-25 12:43:21 +08:00
parent e0a70aa4d9
commit 9d3a810847
23 changed files with 575 additions and 209 deletions

View File

@ -24,7 +24,7 @@
"coverage": "pnpm run test:unit && pnpm run test:components" "coverage": "pnpm run test:unit && pnpm run test:components"
}, },
"dependencies": { "dependencies": {
"@appflowyinc/client-api-wasm": "0.1.2", "@appflowyinc/client-api-wasm": "0.1.3",
"@atlaskit/primitives": "^5.5.3", "@atlaskit/primitives": "^5.5.3",
"@emoji-mart/data": "^1.1.2", "@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",

View File

@ -1,13 +1,9 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies: dependencies:
'@appflowyinc/client-api-wasm': '@appflowyinc/client-api-wasm':
specifier: 0.1.2 specifier: 0.1.3
version: 0.1.2 version: 0.1.3
'@atlaskit/primitives': '@atlaskit/primitives':
specifier: ^5.5.3 specifier: ^5.5.3
version: 5.7.0(@types/react@18.2.66)(react@18.2.0) 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/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
/@appflowyinc/client-api-wasm@0.1.2: /@appflowyinc/client-api-wasm@0.1.3:
resolution: {integrity: sha512-+v0hs7/7BVKtgev/Bcbr0u2HLDhUuw4ZvZTaMddI+06HK8vt5S52dMaZKUcMvh1eUjVX8hjC6Mfe0X/yHqvFgA==} resolution: {integrity: sha512-M603RIBocCjDlwDx5O53j4tH2M/y6uKZSdpnBq3nCMBPwTGEhTFKBDD3tMmjSIHo8nnGx1t8gsKei55LlhtoNQ==}
dev: false dev: false
/@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): /@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==} resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
dev: true dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -14,12 +14,17 @@ import {
signInGithub, signInGithub,
signInDiscord, signInDiscord,
signInWithUrl, signInWithUrl,
getWorkspaces,
getWorkspaceFolder,
getCurrentUser,
duplicatePublishView,
} from '@/application/services/js-services/wasm/client_api'; } from '@/application/services/js-services/wasm/client_api';
import { AFService, AFServiceConfig } from '@/application/services/services.type'; import { AFService, AFServiceConfig } from '@/application/services/services.type';
import { emit, EventType } from '@/application/session'; import { emit, EventType } from '@/application/session';
import { afterAuth, AUTH_CALLBACK_URL, withSignIn } from '@/application/session/sign_in'; import { afterAuth, AUTH_CALLBACK_URL, withSignIn } from '@/application/session/sign_in';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { DuplicatePublishView } from '@/application/types';
export class AFClientService implements AFService { export class AFClientService implements AFService {
private deviceId: string = nanoid(8); private deviceId: string = nanoid(8);
@ -199,4 +204,36 @@ export class AFClientService implements AFService {
async signInDiscord(_: { redirectTo: string }) { async signInDiscord(_: { redirectTo: string }) {
return await signInDiscord(AUTH_CALLBACK_URL); 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,
});
}
} }

View File

@ -1,7 +1,8 @@
import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token'; 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 { AFCloudConfig } from '@/application/services/services.type';
import { DatabaseId, PublishViewMetaData, RowId, ViewId, ViewLayout } from '@/application/collab.type'; import { DatabaseId, PublishViewMetaData, RowId, ViewId, ViewLayout } from '@/application/collab.type';
import { FolderView, Workspace } from '@/application/types';
let client: ClientAPI; let client: ClientAPI;
@ -115,3 +116,58 @@ export async function signInGithub(redirectTo: string) {
export async function signInDiscord(redirectTo: string) { export async function signInDiscord(redirectTo: string) {
return signInProvider('discord', redirectTo); 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<FolderView> {
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);
}

View File

@ -1,6 +1,7 @@
import { YDoc } from '@/application/collab.type'; import { YDoc } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas'; import { ViewMeta } from '@/application/db/tables/view_metas';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { DuplicatePublishView, FolderView, User, Workspace } from '@/application/types';
export type AFService = PublishService; export type AFService = PublishService;
@ -33,4 +34,9 @@ export interface PublishService {
signInGoogle: (params: { redirectTo: string }) => Promise<void>; signInGoogle: (params: { redirectTo: string }) => Promise<void>;
signInGithub: (params: { redirectTo: string }) => Promise<void>; signInGithub: (params: { redirectTo: string }) => Promise<void>;
signInDiscord: (params: { redirectTo: string }) => Promise<void>; signInDiscord: (params: { redirectTo: string }) => Promise<void>;
getWorkspaces: () => Promise<Workspace[]>;
getWorkspaceFolder: (workspaceId: string) => Promise<FolderView>;
getCurrentUser: () => Promise<User>;
duplicatePublishView: (params: DuplicatePublishView) => Promise<void>;
} }

View File

@ -2,6 +2,7 @@ import { YDoc } from '@/application/collab.type';
import { AFService } from '@/application/services/services.type'; import { AFService } from '@/application/services/services.type';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { YMap } from 'yjs/dist/src/types/YMap'; import { YMap } from 'yjs/dist/src/types/YMap';
import { DuplicatePublishView, FolderView, User, Workspace } from '@/application/types';
export class AFClientService implements AFService { export class AFClientService implements AFService {
private deviceId: string = nanoid(8); private deviceId: string = nanoid(8);
@ -53,4 +54,20 @@ export class AFClientService implements AFService {
}> { }> {
return Promise.reject('Method not implemented'); return Promise.reject('Method not implemented');
} }
duplicatePublishView(_params: DuplicatePublishView): Promise<void> {
return Promise.resolve(undefined);
}
getCurrentUser(): Promise<User> {
return Promise.reject('Method not implemented');
}
getWorkspaceFolder(_workspaceId: string): Promise<FolderView> {
return Promise.reject('Method not implemented');
}
getWorkspaces(): Promise<Workspace[]> {
return Promise.reject('Method not implemented');
}
} }

View File

@ -1,3 +1,5 @@
import { CollabType } from '@/application/collab.type';
export interface Workspace { export interface Workspace {
icon: string; icon: string;
id: string; id: string;
@ -7,6 +9,31 @@ export interface Workspace {
export interface SpaceView { export interface SpaceView {
id: string; id: string;
extra?: string; extra: string | null;
name: string; 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;
} }

View File

@ -1,4 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="10.4751" y="4.81812" width="1" height="8" rx="0.5" transform="rotate(45 10.4751 4.81812)" fill="#333333"/> <g opacity="0.7">
<rect x="11.1821" y="10.4749" width="1" height="8" rx="0.5" transform="rotate(135 11.1821 10.4749)" fill="#333333"/> <path d="M15.1924 15.1924C15.4853 14.8995 15.4875 14.4268 15.1946 14.1339L10.061 9.00027L15.1945 3.86672C15.4874 3.57382 15.4853 3.10107 15.1924 2.80818C14.8995 2.51529 14.4268 2.51316 14.1339 2.80606C13.841 3.09895 9.00031 7.93961 9.00031 7.93961L3.86671 2.80601C3.57382 2.51312 3.10107 2.51524 2.80817 2.80814C2.51528 3.10103 2.51316 3.57378 2.80605 3.86667L7.93965 9.00027L2.80601 14.1339C2.51312 14.4268 2.51524 14.8996 2.80814 15.1924C3.10103 15.4853 3.57378 15.4875 3.86667 15.1946L9.00031 10.0609L14.1339 15.1945C14.4268 15.4874 14.8995 15.4853 15.1924 15.1924Z"
fill="currentColor"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 343 B

After

Width:  |  Height:  |  Size: 747 B

View File

@ -0,0 +1,5 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd"
d="M8.78714 4.75039V4.16515C8.78714 3.42984 8.6 1.25 5.99904 1.25C3.3 1.25 3.21094 3.42984 3.21094 4.16515V4.75039L3.38817 4.73529L3.2119 4.75123C2.30484 4.99873 2 5.7229 2 7.29041V8.20709C2 10.2238 2.50558 10.9996 4.14127 10.9996H7.85873C9.49442 10.9996 10 10.2238 10 8.20709V7.29041C10 5.7229 9.69516 4.99873 8.7881 4.75123L8.64331 4.73813L8.78714 4.75039ZM4.32618 4.16515V4.65048L7.6719 4.65039V4.16515C7.6719 3.0108 7.3 2.3002 5.99904 2.3002C4.65 2.3002 4.32618 3.0108 4.32618 4.16515ZM6.00078 8.95078C6.66352 8.95078 7.20078 8.41352 7.20078 7.75078C7.20078 7.08804 6.66352 6.55078 6.00078 6.55078C5.33804 6.55078 4.80078 7.08804 4.80078 7.75078C4.80078 8.41352 5.33804 8.95078 6.00078 8.95078Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 908 B

View File

@ -1,15 +1,21 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; 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'; import { ReactComponent as CloseIcon } from '@/assets/close.svg';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export interface NormalModalProps extends DialogProps { export interface NormalModalProps extends DialogProps {
okText?: string; okText?: string;
cancelText?: string; cancelText?: string;
onOk?: () => void; onOk?: () => void;
onCancel?: () => void; onCancel?: () => void;
danger?: boolean; danger?: boolean;
title?: string; onClose?: () => void;
title: string | React.ReactNode;
okButtonProps?: ButtonProps;
cancelButtonProps?: ButtonProps;
okLoading?: boolean;
} }
export function NormalModal({ export function NormalModal({
@ -19,7 +25,11 @@ export function NormalModal({
onOk, onOk,
onCancel, onCancel,
danger, danger,
onClose,
children, children,
okButtonProps,
cancelButtonProps,
okLoading,
...dialogProps ...dialogProps
}: NormalModalProps) { }: NormalModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -31,21 +41,30 @@ export function NormalModal({
<Dialog {...dialogProps}> <Dialog {...dialogProps}>
<div className={'relative flex flex-col gap-4 p-5'}> <div className={'relative flex flex-col gap-4 p-5'}>
<div className={'flex w-full items-center justify-between text-base font-medium'}> <div className={'flex w-full items-center justify-between text-base font-medium'}>
<div className={'flex-1 text-center '}>{title}</div> <div className={'flex-1 text-center '}>{title}</div>
<div className={'relative -right-1.5'}> <div className={'relative -right-1.5'}>
<IconButton size={'small'} color={'inherit'} className={'h-8 w-8'} onClick={onCancel}> <IconButton size={'small'} color={'inherit'} className={'h-6 w-6'} onClick={onClose || onCancel}>
<CloseIcon className={'h-8 w-8'} /> <CloseIcon className={'h-4 w-4'} />
</IconButton> </IconButton>
</div> </div>
</div> </div>
<div className={'flex-1'}>{children}</div> <div className={'flex-1'}>{children}</div>
<div className={'flex w-full justify-end gap-4'}> <div className={'flex w-full justify-end gap-4'}>
<Button color={'inherit'} variant={'outlined'} onClick={onCancel}> <Button color={'inherit'} variant={'outlined'} onClick={onCancel} {...cancelButtonProps}>
{modalCancelText} {modalCancelText}
</Button> </Button>
<Button color={'primary'} variant={'contained'} style={{ backgroundColor: buttonColor }} onClick={onOk}> <Button
{modalOkText} color={'primary'}
variant={'contained'}
style={{ backgroundColor: buttonColor }}
onClick={() => {
if (okLoading) return;
onOk?.();
}}
{...okButtonProps}
>
{okLoading ? <CircularProgress size={24} /> : modalOkText}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -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<HTMLDivElement, InfoSnackbarProps>(
({ onOk, okText, title, message, onClose }, ref) => {
const { t } = useTranslation();
return (
<SnackbarContent ref={ref}>
<Paper className={'relative flex flex-col gap-4 p-5'}>
<div className={'flex w-full items-center justify-between text-base font-medium'}>
<div className={'flex-1 text-left '}>{title}</div>
<div className={'relative -right-1.5'}>
<IconButton size={'small'} color={'inherit'} className={'h-6 w-6'} onClick={onClose}>
<CloseIcon className={'h-4 w-4'} />
</IconButton>
</div>
</div>
<div className={'flex-1'}>{message}</div>
<div className={'flex w-full justify-end gap-4'}>
<Button color={'primary'} variant={'contained'} onClick={onOk}>
{okText || t('button.ok')}
</Button>
</div>
</Paper>
</SnackbarContent>
);
}
);
export default InfoSnackbar;

View File

@ -1,3 +1,5 @@
import { InfoProps } from '@/components/_shared/notify/InfoSnackbar';
export const notify = { export const notify = {
success: (message: string) => { success: (message: string) => {
window.toast.success(message); window.toast.success(message);
@ -11,10 +13,19 @@ export const notify = {
warning: (message: string) => { warning: (message: string) => {
window.toast.warning(message); window.toast.warning(message);
}, },
info: (message: string) => { info: (props: InfoProps) => {
window.toast.info(message); window.toast.info({
...props,
variant: 'info',
anchorOrigin: {
vertical: 'bottom',
horizontal: 'center',
},
});
}, },
clear: () => { clear: () => {
window.toast.clear(); window.toast.clear();
}, },
}; };
export * from './InfoSnackbar';

View File

@ -6,6 +6,8 @@ import { useSnackbar } from 'notistack';
import React, { createContext, useEffect, useState } from 'react'; import React, { createContext, useEffect, useState } from 'react';
import { AFService, AFServiceConfig } from '@/application/services/services.type'; import { AFService, AFServiceConfig } from '@/application/services/services.type';
import { getService } from '@/application/services'; 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 baseURL = import.meta.env.AF_BASE_URL || 'https://test.appflowy.cloud';
const gotrueURL = import.meta.env.AF_GOTRUE_URL || 'https://test.appflowy.cloud/gotrue'; const gotrueURL = import.meta.env.AF_GOTRUE_URL || 'https://test.appflowy.cloud/gotrue';
@ -23,6 +25,7 @@ export const AFConfigContext = createContext<
| { | {
service: AFService | undefined; service: AFService | undefined;
isAuthenticated: boolean; isAuthenticated: boolean;
currentUser?: User;
} }
| undefined | undefined
>(undefined); >(undefined);
@ -31,6 +34,7 @@ function AppConfig({ children }: { children: React.ReactNode }) {
const [appConfig] = useState<AFServiceConfig>(defaultConfig); const [appConfig] = useState<AFServiceConfig>(defaultConfig);
const [service, setService] = useState<AFService>(); const [service, setService] = useState<AFService>();
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(isTokenValid()); const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(isTokenValid());
const [currentUser, setCurrentUser] = React.useState<User>();
useEffect(() => { useEffect(() => {
return on(EventType.SESSION_VALID, () => { 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(() => { useEffect(() => {
const handleStorageChange = (event: StorageEvent) => { const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'token') setIsAuthenticated(isTokenValid()); if (event.key === 'token') setIsAuthenticated(isTokenValid());
@ -79,8 +101,9 @@ function AppConfig({ children }: { children: React.ReactNode }) {
default: (message: string) => { default: (message: string) => {
enqueueSnackbar(message, { variant: 'default' }); enqueueSnackbar(message, { variant: 'default' });
}, },
info: (message: string) => {
enqueueSnackbar(message, { variant: 'info' }); info: (props: InfoSnackbarProps) => {
enqueueSnackbar(props.message, props);
}, },
clear: () => { clear: () => {
@ -111,6 +134,7 @@ function AppConfig({ children }: { children: React.ReactNode }) {
value={{ value={{
service, service,
isAuthenticated, isAuthenticated,
currentUser,
}} }}
> >
{children} {children}

View File

@ -41,6 +41,9 @@ function AppTheme({ children }: { children: React.ReactNode }) {
}, },
borderRadius: '4px', borderRadius: '4px',
padding: '2px', padding: '2px',
'&.MuiIconButton-colorInherit': {
color: 'var(--icon-primary)',
},
}, },
}, },
}, },
@ -59,6 +62,11 @@ function AppTheme({ children }: { children: React.ReactNode }) {
backgroundColor: 'var(--content-blue-600)', backgroundColor: 'var(--content-blue-600)',
}, },
borderRadius: '8px', borderRadius: '8px',
'&.Mui-disabled': {
backgroundColor: 'var(--content-blue-400)',
opacity: 0.3,
color: 'var(--content-on-fill)',
},
}, },
outlined: { outlined: {
'&.MuiButton-outlinedInherit': { '&.MuiButton-outlinedInherit': {

View File

@ -5,6 +5,7 @@ import AppConfig from '@/components/app/AppConfig';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import InfoSnackbar from '../_shared/notify/InfoSnackbar';
const StyledSnackbarProvider = styled(SnackbarProvider)` const StyledSnackbarProvider = styled(SnackbarProvider)`
&.notistack-MuiContent-default { &.notistack-MuiContent-default {
@ -39,6 +40,9 @@ export default function withAppWrapper(Component: React.FC): React.FC {
horizontal: 'center', horizontal: 'center',
}} }}
preventDuplicate preventDuplicate
Components={{
info: InfoSnackbar,
}}
> >
<AppConfig> <AppConfig>
<Suspense> <Suspense>

View File

@ -9,8 +9,8 @@ export function LoginModal({ redirectTo, open, onClose }: { redirectTo: string;
<div className={'relative px-6'}> <div className={'relative px-6'}>
<Login redirectTo={redirectTo} /> <Login redirectTo={redirectTo} />
<div className={'absolute top-2 right-2'}> <div className={'absolute top-2 right-2'}>
<IconButton color={'inherit'} onClick={onClose}> <IconButton size={'small'} color={'inherit'} className={'h-6 w-6'} onClick={onClose}>
<CloseIcon className={'h-8 w-8'} /> <CloseIcon className={'h-4 w-4'} />
</IconButton> </IconButton>
</div> </div>
</div> </div>

View File

@ -1,36 +1,128 @@
import React from 'react'; import React, { useCallback, useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { NormalModal } from '@/components/_shared/modal'; import { NormalModal } from '@/components/_shared/modal';
import SelectWorkspace from '@/components/publish/header/duplicate/SelectWorkspace'; import SelectWorkspace from '@/components/publish/header/duplicate/SelectWorkspace';
import { useLoadWorkspaces } from '@/components/publish/header/duplicate/useDuplicate'; import { useLoadWorkspaces } from '@/components/publish/header/duplicate/useDuplicate';
import SpaceList from '@/components/publish/header/duplicate/SpaceList'; 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'; 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 }) { function DuplicateModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const { t } = useTranslation(); 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<boolean>(false);
const [successModalOpen, setSuccessModalOpen] = React.useState<boolean>(false);
const {
workspaceList,
spaceList,
setSelectedSpaceId,
setSelectedWorkspaceId,
selectedWorkspaceId,
selectedSpaceId,
workspaceLoading,
spaceLoading,
} = useLoadWorkspaces();
const { workspaceList, spaceList, setSelectedSpaceId, setSelectedWorkspaceId, selectedWorkspaceId, selectedSpaceId } = useEffect(() => {
useLoadWorkspaces(); 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 ( return (
<NormalModal <>
onCancel={onClose} <NormalModal
okText={t('button.add')} okButtonProps={{
title={t('publish.duplicateTitle')} disabled: !selectedWorkspaceId || !selectedSpaceId,
open={open} }}
onClose={onClose} onCancel={onClose}
classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%] ' }} okText={t('button.add')}
onOk={async () => { title={t('publish.duplicateTitle')}
// submit form open={open}
notify.success(t('publish.duplicateSuccessfully')); onClose={onClose}
onClose(); classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%] ' }}
}} onOk={handleDuplicate}
> okLoading={loading}
<div className={'flex flex-col gap-4'}> >
<SelectWorkspace workspaceList={workspaceList} value={selectedWorkspaceId} onChange={setSelectedWorkspaceId} /> <div className={'flex flex-col gap-4'}>
<SpaceList spaceList={spaceList} value={selectedSpaceId} onChange={setSelectedSpaceId} /> <SelectWorkspace
</div> loading={workspaceLoading}
</NormalModal> workspaceList={workspaceList}
value={selectedWorkspaceId}
onChange={setSelectedWorkspaceId}
/>
<SpaceList
loading={spaceLoading}
spaceList={spaceList}
value={selectedSpaceId}
onChange={setSelectedSpaceId}
/>
</div>
</NormalModal>
<NormalModal
PaperProps={{
sx: {
maxWidth: 420,
},
}}
okText={t('publish.openApp')}
cancelText={t('publish.downloadIt')}
onOk={() => window.open(openAppFlowySchema, '_self')}
onCancel={() => {
window.open(downloadPage, '_blank');
}}
onClose={() => setSuccessModalOpen(false)}
open={successModalOpen}
title={<div className={'text-left'}>{t('publish.duplicateSuccessfully')}</div>}
>
<div className={'w-full whitespace-pre-wrap break-words pb-1 text-text-caption'}>
{t('publish.duplicateSuccessfullyDescription')}
</div>
</NormalModal>
</>
); );
} }

View File

@ -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 { 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 { Workspace } from '@/application/types';
import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg'; import { ReactComponent as RightIcon } from '@/assets/arrow_right.svg';
import { ReactComponent as CheckIcon } from '@/assets/selected.svg'; import { ReactComponent as CheckIcon } from '@/assets/selected.svg';
import { Popover } from '@/components/_shared/popover'; import { Popover } from '@/components/_shared/popover';
import { stringToColor } from '@/utils/color';
import { AFConfigContext } from '@/components/app/AppConfig';
export interface SelectWorkspaceProps { export interface SelectWorkspaceProps {
value: string; value: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
workspaceList: Workspace[]; 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 { t } = useTranslation();
const email = 'lu@appflowy.io'; const email = useContext(AFConfigContext)?.currentUser?.email || '';
const selectedWorkspace = useMemo(() => { const selectedWorkspace = useMemo(() => {
return workspaceList.find((workspace) => workspace.id === value); return workspaceList.find((workspace) => workspace.id === value);
}, [value, workspaceList]); }, [value, workspaceList]);
@ -25,7 +37,16 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp
(workspace: Workspace) => { (workspace: Workspace) => {
return ( return (
<div className={'flex items-center gap-[10px] overflow-hidden'}> <div className={'flex items-center gap-[10px] overflow-hidden'}>
<div className={'h-8 w-8 text-2xl'}>{workspace.icon}</div> {workspace.icon ? (
<div className={'h-10 w-10 text-2xl'}>{workspace.icon}</div>
) : (
<Avatar
className={'border border-line-border'}
sizes={'24px'}
variant={'rounded'}
{...stringAvatar(workspace.name)}
/>
)}
<div className={'flex flex-1 flex-col items-start gap-0.5 overflow-hidden'}> <div className={'flex flex-1 flex-col items-start gap-0.5 overflow-hidden'}>
<div className={'w-full truncate text-left text-sm font-medium'}>{workspace.name}</div> <div className={'w-full truncate text-left text-sm font-medium'}>{workspace.name}</div>
<div className={'text-xs text-text-caption'}> <div className={'text-xs text-text-caption'}>
@ -53,10 +74,20 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp
color={'inherit'} color={'inherit'}
> >
<div className={'flex w-full items-center gap-[10px]'}> <div className={'flex w-full items-center gap-[10px]'}>
<div className={'flex-1 overflow-hidden'}>{selectedWorkspace ? renderWorkspace(selectedWorkspace) : null}</div> {loading ? (
<IconButton size={'small'} className={`h-6 w-6 ${selectOpen ? '-rotate-90' : 'rotate-90'} transform`}> <div className={'flex w-full items-center justify-center'}>
<RightIcon className={'h-6 w-6'} /> <CircularProgress size={24} />
</IconButton> </div>
) : (
<>
<div className={'flex-1 overflow-hidden'}>
{selectedWorkspace ? renderWorkspace(selectedWorkspace) : null}
</div>
<span className={`h-4 w-4 ${selectOpen ? '-rotate-90' : 'rotate-90'} transform`}>
<RightIcon className={'h-full w-full'} />
</span>
</>
)}
</div> </div>
</Button> </Button>
<Popover <Popover
@ -70,7 +101,7 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp
setSelectOpen(false); setSelectOpen(false);
}} }}
> >
<div className={'flex max-h-[360px] w-[360px] flex-col gap-1 p-2 max-sm:w-full'}> <div className={'flex max-h-[340px] w-[360px] flex-col gap-1 p-2 max-sm:w-full'}>
<div className={'w-full px-3 py-2 text-sm font-medium text-text-caption'}>{email}</div> <div className={'w-full px-3 py-2 text-sm font-medium text-text-caption'}>{email}</div>
<Divider /> <Divider />
<div className={'appflowy-scroller flex flex-1 flex-col overflow-y-auto overflow-x-hidden'}> <div className={'appflowy-scroller flex flex-1 flex-col overflow-y-auto overflow-x-hidden'}>

View File

@ -4,15 +4,17 @@ import { useTranslation } from 'react-i18next';
import { ReactComponent as CheckIcon } from '@/assets/selected.svg'; import { ReactComponent as CheckIcon } from '@/assets/selected.svg';
import { renderColor } from '@/utils/color'; import { renderColor } from '@/utils/color';
import SpaceIcon from '@/components/publish/header/SpaceIcon'; 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 { export interface SpaceListProps {
value: string; value: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
spaceList: SpaceView[]; spaceList: SpaceView[];
loading?: boolean;
} }
function SpaceList({ spaceList, value, onChange }: SpaceListProps) { function SpaceList({ loading, spaceList, value, onChange }: SpaceListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const getExtraObj = useCallback((extra: string) => { const getExtraObj = useCallback((extra: string) => {
@ -44,7 +46,10 @@ function SpaceList({ spaceList, value, onChange }: SpaceListProps) {
> >
<SpaceIcon value={extraObj.space_icon || ''} /> <SpaceIcon value={extraObj.space_icon || ''} />
</span> </span>
<div className={'flex-1 truncate'}>{space.name}</div> <div className={'flex flex-1 items-center gap-2 truncate'}>
{space.name}
{space.isPrivate && <LockSvg className={'h-3.5 w-3.5 text-icon-primary'} />}
</div>
</div> </div>
); );
}, },
@ -54,29 +59,35 @@ function SpaceList({ spaceList, value, onChange }: SpaceListProps) {
return ( return (
<div className={'flex max-h-[280px] w-[360px] flex-col gap-2 overflow-hidden max-sm:w-full'}> <div className={'flex max-h-[280px] w-[360px] flex-col gap-2 overflow-hidden max-sm:w-full'}>
<div className={'text-sm text-text-caption'}>{t('publish.addTo')}</div> <div className={'text-sm text-text-caption'}>{t('publish.addTo')}</div>
<div className={'appflowy-scroller flex w-full flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden'}> {loading ? (
{spaceList.map((space) => { <div className={'flex w-full items-center justify-center'}>
const isSelected = value === space.id; <CircularProgress size={24} />
</div>
) : (
<div className={'appflowy-scroller flex w-full flex-1 flex-col gap-1 overflow-y-auto overflow-x-hidden'}>
{spaceList.map((space) => {
const isSelected = value === space.id;
return ( return (
<Tooltip title={space.name} key={space.id} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}> <Tooltip title={space.name} key={space.id} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
<Button <Button
variant={'text'} variant={'text'}
color={'inherit'} color={'inherit'}
className={'flex items-center p-1 font-normal'} className={'flex items-center p-1 font-normal'}
onClick={() => { onClick={() => {
onChange?.(space.id); onChange?.(space.id);
}} }}
> >
<div className={'flex-1 overflow-hidden text-left'}>{renderSpace(space)}</div> <div className={'flex-1 overflow-hidden text-left'}>{renderSpace(space)}</div>
<div className={'h-6 w-6'}> <div className={'h-6 w-6'}>
{isSelected && <CheckIcon className={'h-6 w-6 text-content-blue-400'} />} {isSelected && <CheckIcon className={'h-6 w-6 text-content-blue-400'} />}
</div> </div>
</Button> </Button>
</Tooltip> </Tooltip>
); );
})} })}
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -2,6 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react';
import { AFConfigContext } from '@/components/app/AppConfig'; import { AFConfigContext } from '@/components/app/AppConfig';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { SpaceView, Workspace } from '@/application/types'; import { SpaceView, Workspace } from '@/application/types';
import { notify } from '@/components/_shared/notify';
export function useDuplicate() { export function useDuplicate() {
const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false; const isAuthenticated = useContext(AFConfigContext)?.isAuthenticated || false;
@ -46,131 +47,76 @@ export function useDuplicate() {
} }
export function useLoadWorkspaces() { export function useLoadWorkspaces() {
const [spaceLoading, setSpaceLoading] = useState<boolean>(false);
const [workspaceLoading, setWorkspaceLoading] = useState<boolean>(false);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>('1'); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>('1');
const [selectedSpaceId, setSelectedSpaceId] = useState<string>('1'); const [selectedSpaceId, setSelectedSpaceId] = useState<string>('1');
const [workspaceList] = useState<Workspace[]>([ const [workspaceList, setWorkspaceList] = useState<Workspace[]>([]);
{
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 [spaceList] = useState<SpaceView[]>([ const [spaceList, setSpaceList] = useState<SpaceView[]>([]);
{
id: '1', const service = useContext(AFConfigContext)?.service;
extra: JSON.stringify({
is_space: true, useEffect(() => {
space_icon: 'space_icon_1', void (async () => {
space_icon_color: 'appflowy_them_color_tint2', setWorkspaceLoading(true);
}), try {
name: 'Space 1', const workspaces = await service?.getWorkspaces();
},
{ if (workspaces) {
id: '2', setWorkspaceList(workspaces);
extra: JSON.stringify({ setSelectedWorkspaceId(workspaces[0].id);
is_space: true, } else {
space_icon: 'space_icon_2', setWorkspaceList([]);
space_icon_color: 'appflowy_them_color_tint1', setSelectedWorkspaceId('');
}), }
name: 'Space 2 djisa djiso adiohjsa hdiosahdk ksahkjdhskjahdaskj', } catch (e) {
}, notify.error('Failed to load workspaces');
{ } finally {
id: '3', setWorkspaceLoading(false);
extra: JSON.stringify({ }
is_space: true, })();
space_icon: 'space_icon_3', }, [service]);
space_icon_color: 'appflowy_them_color_tint3',
}), useEffect(() => {
name: 'Space 3', if (workspaceList.length === 0 || !selectedWorkspaceId || workspaceLoading) {
}, setSpaceList([]);
{ setSelectedSpaceId('');
id: '4', return;
extra: JSON.stringify({ }
is_space: true,
space_icon: 'space_icon_4', setSpaceLoading(true);
space_icon_color: 'appflowy_them_color_tint4', void (async () => {
}), try {
name: 'Space 4', const folder = await service?.getWorkspaceFolder(selectedWorkspaceId);
},
{ if (folder) {
id: '5', const spaces = [];
extra: JSON.stringify({
is_space: true, for (const child of folder.children) {
space_icon: 'space_icon_5', if (child.isSpace) {
space_icon_color: 'appflowy_them_color_tint5', spaces.push({
}), id: child.id,
name: 'Space 5', name: child.name,
}, isPrivate: child.isPrivate,
{ extra: child.extra,
id: '6', });
extra: JSON.stringify({ }
is_space: true, }
space_icon: 'space_icon_6',
space_icon_color: 'appflowy_them_color_tint6', setSpaceList(spaces);
}), } else {
name: 'Space 6', setSpaceList([]);
}, }
{ } catch (e) {
id: '7', notify.error('Failed to load spaces');
extra: JSON.stringify({ } finally {
is_space: true, setSelectedSpaceId('');
space_icon: 'space_icon_7', setSpaceLoading(false);
space_icon_color: 'appflowy_them_color_tint7', }
}), })();
name: 'Space 7', }, [selectedWorkspaceId, service, workspaceList.length, workspaceLoading]);
},
{
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',
},
]);
return { return {
workspaceList, workspaceList,
@ -179,5 +125,7 @@ export function useLoadWorkspaces() {
setSelectedWorkspaceId, setSelectedWorkspaceId,
selectedSpaceId, selectedSpaceId,
setSelectedSpaceId, setSelectedSpaceId,
workspaceLoading,
spaceLoading,
}; };
} }

View File

@ -72,3 +72,24 @@ export function renderColor(color: string) {
return argbToRgba(color); 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;
}

View File

@ -13,7 +13,8 @@ interface Window {
toast: { toast: {
success: (message: string) => void; success: (message: string) => void;
error: (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; clear: () => void;
default: (message: string) => void; default: (message: string) => void;
warning: (message: string) => void; warning: (message: string) => void;

View File

@ -2139,9 +2139,9 @@
"duplicateTitle": "Where would you like to add", "duplicateTitle": "Where would you like to add",
"selectWorkspace": "Select a workspace", "selectWorkspace": "Select a workspace",
"addTo": "Add to", "addTo": "Add to",
"duplicateSuccessfully": "Duplicated success", "duplicateSuccessfully": "Duplicated success. Want to view documents?",
"duplicateSuccessfullyDescription": "Open in AppFlowy's desktop app? If don't have the app, you can", "duplicateSuccessfullyDescription": "Don't have the app? Your download will begin automatically after clicking the 'Download'.",
"downloadIt": "download it here", "downloadIt": "Download",
"openApp": "Open in app", "openApp": "Open in app",
"duplicateFailed": "Duplicated failed", "duplicateFailed": "Duplicated failed",
"membersCount": { "membersCount": {