mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: upgrade wasm package
This commit is contained in:
parent
e0a70aa4d9
commit
9d3a810847
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 |
5
frontend/appflowy_web_app/src/assets/lock.svg
Normal file
5
frontend/appflowy_web_app/src/assets/lock.svg
Normal 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 |
@ -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>
|
||||
|
@ -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;
|
@ -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';
|
||||
|
@ -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}
|
||||
|
@ -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': {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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'}>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
3
frontend/appflowy_web_app/src/vite-env.d.ts
vendored
3
frontend/appflowy_web_app/src/vite-env.d.ts
vendored
@ -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;
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user