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"
|
"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",
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 |
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 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>
|
||||||
|
@ -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 = {
|
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';
|
||||||
|
@ -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}
|
||||||
|
@ -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': {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'}>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
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: {
|
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;
|
||||||
|
@ -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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user