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"
},
"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",

View File

@ -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

View File

@ -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,
});
}
}

View File

@ -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<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 { 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<void>;
signInGithub: (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 { 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<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 {
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;
}

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">
<rect x="10.4751" y="4.81812" width="1" height="8" rx="0.5" transform="rotate(45 10.4751 4.81812)" fill="#333333"/>
<rect x="11.1821" y="10.4749" width="1" height="8" rx="0.5" transform="rotate(135 11.1821 10.4749)" fill="#333333"/>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.7">
<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>

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 { 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({
<Dialog {...dialogProps}>
<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-1 text-center '}>{title}</div>
<div className={'flex-1 text-center '}>{title}</div>
<div className={'relative -right-1.5'}>
<IconButton size={'small'} color={'inherit'} className={'h-8 w-8'} onClick={onCancel}>
<CloseIcon className={'h-8 w-8'} />
<IconButton size={'small'} color={'inherit'} className={'h-6 w-6'} onClick={onClose || onCancel}>
<CloseIcon className={'h-4 w-4'} />
</IconButton>
</div>
</div>
<div className={'flex-1'}>{children}</div>
<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}
</Button>
<Button color={'primary'} variant={'contained'} style={{ backgroundColor: buttonColor }} onClick={onOk}>
{modalOkText}
<Button
color={'primary'}
variant={'contained'}
style={{ backgroundColor: buttonColor }}
onClick={() => {
if (okLoading) return;
onOk?.();
}}
{...okButtonProps}
>
{okLoading ? <CircularProgress size={24} /> : modalOkText}
</Button>
</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 = {
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';

View File

@ -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<AFServiceConfig>(defaultConfig);
const [service, setService] = useState<AFService>();
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(isTokenValid());
const [currentUser, setCurrentUser] = React.useState<User>();
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}

View File

@ -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': {

View File

@ -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,
}}
>
<AppConfig>
<Suspense>

View File

@ -9,8 +9,8 @@ export function LoginModal({ redirectTo, open, onClose }: { redirectTo: string;
<div className={'relative px-6'}>
<Login redirectTo={redirectTo} />
<div className={'absolute top-2 right-2'}>
<IconButton color={'inherit'} onClick={onClose}>
<CloseIcon className={'h-8 w-8'} />
<IconButton size={'small'} color={'inherit'} className={'h-6 w-6'} onClick={onClose}>
<CloseIcon className={'h-4 w-4'} />
</IconButton>
</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 { 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<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 } =
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 (
<NormalModal
onCancel={onClose}
okText={t('button.add')}
title={t('publish.duplicateTitle')}
open={open}
onClose={onClose}
classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%] ' }}
onOk={async () => {
// submit form
notify.success(t('publish.duplicateSuccessfully'));
onClose();
}}
>
<div className={'flex flex-col gap-4'}>
<SelectWorkspace workspaceList={workspaceList} value={selectedWorkspaceId} onChange={setSelectedWorkspaceId} />
<SpaceList spaceList={spaceList} value={selectedSpaceId} onChange={setSelectedSpaceId} />
</div>
</NormalModal>
<>
<NormalModal
okButtonProps={{
disabled: !selectedWorkspaceId || !selectedSpaceId,
}}
onCancel={onClose}
okText={t('button.add')}
title={t('publish.duplicateTitle')}
open={open}
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
loading={workspaceLoading}
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 { 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 (
<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={'w-full truncate text-left text-sm font-medium'}>{workspace.name}</div>
<div className={'text-xs text-text-caption'}>
@ -53,10 +74,20 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp
color={'inherit'}
>
<div className={'flex w-full items-center gap-[10px]'}>
<div className={'flex-1 overflow-hidden'}>{selectedWorkspace ? renderWorkspace(selectedWorkspace) : null}</div>
<IconButton size={'small'} className={`h-6 w-6 ${selectOpen ? '-rotate-90' : 'rotate-90'} transform`}>
<RightIcon className={'h-6 w-6'} />
</IconButton>
{loading ? (
<div className={'flex w-full items-center justify-center'}>
<CircularProgress size={24} />
</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>
</Button>
<Popover
@ -70,7 +101,7 @@ function SelectWorkspace({ value, onChange, workspaceList }: SelectWorkspaceProp
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>
<Divider />
<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 { 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) {
>
<SpaceIcon value={extraObj.space_icon || ''} />
</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>
);
},
@ -54,29 +59,35 @@ function SpaceList({ spaceList, value, onChange }: SpaceListProps) {
return (
<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={'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;
{loading ? (
<div className={'flex w-full items-center justify-center'}>
<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 (
<Tooltip title={space.name} key={space.id} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
<Button
variant={'text'}
color={'inherit'}
className={'flex items-center p-1 font-normal'}
onClick={() => {
onChange?.(space.id);
}}
>
<div className={'flex-1 overflow-hidden text-left'}>{renderSpace(space)}</div>
<div className={'h-6 w-6'}>
{isSelected && <CheckIcon className={'h-6 w-6 text-content-blue-400'} />}
</div>
</Button>
</Tooltip>
);
})}
</div>
return (
<Tooltip title={space.name} key={space.id} placement={'bottom'} enterDelay={1000} enterNextDelay={1000}>
<Button
variant={'text'}
color={'inherit'}
className={'flex items-center p-1 font-normal'}
onClick={() => {
onChange?.(space.id);
}}
>
<div className={'flex-1 overflow-hidden text-left'}>{renderSpace(space)}</div>
<div className={'h-6 w-6'}>
{isSelected && <CheckIcon className={'h-6 w-6 text-content-blue-400'} />}
</div>
</Button>
</Tooltip>
);
})}
</div>
)}
</div>
);
}

View File

@ -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<boolean>(false);
const [workspaceLoading, setWorkspaceLoading] = useState<boolean>(false);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>('1');
const [selectedSpaceId, setSelectedSpaceId] = useState<string>('1');
const [workspaceList] = 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 [workspaceList, setWorkspaceList] = useState<Workspace[]>([]);
const [spaceList] = useState<SpaceView[]>([
{
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<SpaceView[]>([]);
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,
};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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": {