Compare commits

...
Author SHA1 Message Date
Kilu 01c97a31ad feat: support rename workspace on web 2025-01-02 16:46:29 +08:00
11 changed files with 246 additions and 46 deletions

No files matched your search

@@ -21,7 +21,7 @@ import {
CreateSpacePayload,
UpdateSpacePayload,
Role,
WorkspaceMember, QuickNote, QuickNoteEditorData, CreateWorkspacePayload,
WorkspaceMember, QuickNote, QuickNoteEditorData, CreateWorkspacePayload, UpdateWorkspacePayload,
} from '@/application/types';
import { GlobalComment, Reaction } from '@/application/comment.type';
import { initGrantService, refreshToken } from '@/application/services/js-services/http/gotrue';
@@ -253,6 +253,28 @@ export async function openWorkspace (workspaceId: string) {
return Promise.reject(response?.data);
}
export async function updateWorkspace (workspaceId: string, payload: UpdateWorkspacePayload) {
const url = `/api/workspace`;
const response = await axiosInstance?.patch<{
code: number;
data?: {
workspace_id: string;
};
message: string;
}>(url, {
workspace_id: workspaceId,
...payload,
});
const data = response?.data;
if (data?.code === 0) {
return;
}
return Promise.reject(data);
}
export async function createWorkspace (payload: CreateWorkspacePayload) {
const url = '/api/workspace';
const response = await axiosInstance?.post<{
@@ -33,7 +33,7 @@ import {
DatabaseRelations,
DuplicatePublishView, QuickNoteEditorData,
SubscriptionInterval, SubscriptionPlan,
Types, UpdatePagePayload, UpdateSpacePayload, WorkspaceMember,
Types, UpdatePagePayload, UpdateSpacePayload, UpdateWorkspacePayload, WorkspaceMember,
YjsEditorKey,
} from '@/application/types';
import { applyYDoc } from '@/application/ydoc/apply';
@@ -284,6 +284,10 @@ export class AFClientService implements AFService {
return APIService.createWorkspace(payload);
}
async updateWorkspace (workspaceId: string, payload: UpdateWorkspacePayload) {
return APIService.updateWorkspace(workspaceId, payload);
}
async getUserWorkspaceInfo () {
const workspaceInfo = await APIService.getUserWorkspaceInfo();
@@ -19,7 +19,7 @@ import {
UpdateSpacePayload,
WorkspaceMember,
QuickNoteEditorData,
QuickNote, Subscription, CreateWorkspacePayload,
QuickNote, Subscription, CreateWorkspacePayload, UpdateWorkspacePayload,
} from '@/application/types';
import { GlobalComment, Reaction } from '@/application/comment.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
@@ -48,6 +48,7 @@ export interface AFCloudConfig {
export interface WorkspaceService {
openWorkspace: (workspaceId: string) => Promise<void>;
createWorkspace: (payload: CreateWorkspacePayload) => Promise<string>;
updateWorkspace: (workspaceId: string, payload: UpdateWorkspacePayload) => Promise<void>;
leaveWorkspace: (workspaceId: string) => Promise<void>;
deleteWorkspace: (workspaceId: string) => Promise<void>;
getWorkspaceMembers: (workspaceId: string) => Promise<WorkspaceMember[]>;
@@ -1016,4 +1016,8 @@ export interface QuickNote {
export interface CreateWorkspacePayload {
workspace_name: string;
}
export interface UpdateWorkspacePayload {
workspace_name: string;
}
@@ -19,7 +19,7 @@ export interface NormalModalProps extends DialogProps {
closable?: boolean;
}
export function NormalModal({
export function NormalModal ({
okText,
title,
cancelText,
@@ -61,7 +61,7 @@ export function NormalModal({
className={'h-6 w-6'}
onClick={onClose || onCancel}
>
<CloseIcon className={'h-4 w-4'}/>
<CloseIcon className={'h-4 w-4'} />
</IconButton>
</div>}
@@ -90,11 +90,12 @@ export function NormalModal({
if (okLoading) return;
onOk?.();
}}
disabled={okLoading}
{...okButtonProps}
>
{okLoading ? <CircularProgress
color={'inherit'}
size={24}
size={16}
/> : modalOkText}
</Button>
</div>
@@ -2,11 +2,11 @@ import { View, ViewIconType, ViewLayout } from '@/application/types';
import { notify } from '@/components/_shared/notify';
import OutlineIcon from '@/components/_shared/outline/OutlineIcon';
import { Origins } from '@/components/_shared/popover';
import PageIcon from '@/components/_shared/view-icon/PageIcon';
import { useAppHandlers, useAppViewId } from '@/components/app/app.hooks';
import { Tooltip } from '@mui/material';
import React, { lazy, Suspense, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Tooltip } from '@mui/material';
import PageIcon from '@/components/_shared/view-icon/PageIcon';
const ChangeIconPopover = lazy(() => import('@/components/_shared/view-icon/ChangeIconPopover'));
@@ -21,7 +21,7 @@ const popoverProps: Origins = {
},
};
function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand, onClickView }: {
function ViewItem ({ view, width, level = 0, renderExtra, expandIds, toggleExpand, onClickView }: {
view: View;
width: number;
level?: number;
@@ -87,11 +87,14 @@ function ViewItem({ view, width, level = 0, renderExtra, expandIds, toggleExpand
}}
className={`text-[18px] mr-1 `}
>
<PageIcon view={view} className={'flex h-4 w-4 text-text-caption min-w-4 items-center justify-center'}/>
<PageIcon
view={view}
className={'flex h-4 w-4 text-text-caption min-w-4 items-center justify-center'}
/>
</div>
<Tooltip
title={view.name}
title={view.layout === ViewLayout.AIChat ? 'View the chat history in app' : view.name}
disableInteractive={true}
>
<div
@@ -3,7 +3,7 @@ import { notify } from '@/components/_shared/notify';
import { useAppHandlers } from '@/components/app/app.hooks';
import { useService } from '@/components/main/app.hooks';
import { Button, OutlinedInput } from '@mui/material';
import React, { useCallback, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as AddIcon } from '@/assets/add.svg';
@@ -37,14 +37,20 @@ function CreateWorkspace () {
}, [handleSelectedWorkspace, name, service]);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (!open) {
setName('');
}
}, [open]);
return (
<>
<Button
color={'inherit'}
className={'w-full justify-start px-3'}
className={'w-full justify-start px-3 gap-0.5'}
onClick={() => setOpen(true)}
startIcon={
<div className={'w-9 flex items-center justify-center h-9 rounded-[8px] border border-line-divider'}>
<div className={'w-[33px] flex items-center justify-center h-[33px] rounded-[8px] border border-line-divider'}>
<AddIcon />
</div>
}
@@ -54,7 +60,11 @@ function CreateWorkspace () {
<NormalModal
title={t('workspace.create')}
open={open}
onClose={() => setOpen(false)}
onClose={() => {
setOpen(false);
}}
disableRestoreFocus={true}
disableAutoFocus={false}
okLoading={loading}
onOk={handleCreate}
okText={t('button.create')}
@@ -6,9 +6,11 @@ import { Popover } from '@/components/_shared/popover';
import LeaveWorkspace from '@/components/app/workspaces/LeaveWorkspace';
import { useCurrentUser } from '@/components/main/app.hooks';
import DeleteWorkspace from '@/components/app/workspaces/DeleteWorkspace';
import RenameWorkspace from '@/components/app/workspaces/RenameWorkspace';
function MoreActions({ workspace }: {
function MoreActions ({ workspace, onUpdated }: {
workspace: Workspace;
onUpdated: (name: string) => void;
}) {
const ref = React.useRef<HTMLButtonElement | null>(null);
const [open, setOpen] = React.useState(false);
@@ -17,22 +19,49 @@ function MoreActions({ workspace }: {
return (
<>
<IconButton onClick={e => {
e.stopPropagation();
setOpen(true);
}} ref={ref} size={'small'} className={'p-1'} color={'inherit'}>
<MoreSvg className={'w-4 h-4'}/>
<IconButton
onClick={e => {
e.stopPropagation();
setOpen(true);
}}
ref={ref}
size={'small'}
className={'p-1'}
color={'inherit'}
>
<MoreSvg className={'w-4 h-4'} />
</IconButton>
<Popover
slotProps={{
paper: {
className: 'p-2 w-[260px]',
},
}} onClick={e => {
e.stopPropagation();
}} open={open} onClose={() => setOpen(false)} anchorEl={ref.current}>
{isOwner ? <DeleteWorkspace name={workspace.name} workspaceId={workspace.id}/> : <LeaveWorkspace
workspaceId={workspace.id}/>}
}}
onClick={e => {
e.stopPropagation();
}}
open={open}
onClose={() => setOpen(false)}
anchorEl={ref.current}
>
{isOwner ? (
<div className={'flex flex-col gap-2'}>
<RenameWorkspace
onUpdated={(name: string) => {
onUpdated(name);
setOpen(false);
}}
workspace={workspace}
/>
<DeleteWorkspace
name={workspace.name}
workspaceId={workspace.id}
/>
</div>
) : <LeaveWorkspace
workspaceId={workspace.id}
/>}
</Popover>
</>
);
@@ -0,0 +1,101 @@
import { Workspace } from '@/application/types';
import { NormalModal } from '@/components/_shared/modal';
import { notify } from '@/components/_shared/notify';
import { useService } from '@/components/main/app.hooks';
import { Button, OutlinedInput } from '@mui/material';
import React, { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '@/assets/edit.svg';
function RenameWorkspace ({ workspace, onUpdated }: {
workspace: Workspace;
onUpdated: (name: string) => void;
}) {
const { t } = useTranslation();
const service = useService();
const [loading, setLoading] = React.useState(false);
const [name, setName] = React.useState(workspace.name);
const [open, setOpen] = React.useState(false);
const handleUpdate = useCallback(async () => {
if (!service) return;
setLoading(true);
try {
await service.updateWorkspace(workspace.id, {
workspace_name: name,
});
setOpen(false);
onUpdated(name);
// eslint-disable-next-line
} catch (e: any) {
notify.error(e.message);
} finally {
setLoading(false);
}
}, [onUpdated, name, service, workspace.id]);
const inputRef = useRef<HTMLInputElement | null>(null);
return (
<>
<Button
color={'inherit'}
size={'small'}
className={'w-full justify-start'}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setOpen(true);
}}
startIcon={
<EditIcon />
}
>
{t('button.rename')}
</Button>
<NormalModal
disableRestoreFocus={true}
disableAutoFocus={false}
title={t('workspace.renameWorkspace')}
open={open}
onClose={() => setOpen(false)}
okLoading={loading}
onOk={handleUpdate}
okText={t('button.save')}
PaperProps={{
className: 'w-96 max-w-[70vw]',
}}
classes={{ container: 'items-start max-md:mt-auto max-md:items-center mt-[10%] ' }}
>
<OutlinedInput
autoFocus
size={'small'}
placeholder={'Enter workspace name'}
value={name}
inputRef={(input: HTMLInputElement) => {
if (!input) return;
if (!inputRef.current) {
setTimeout(() => {
input.setSelectionRange(0, input.value.length);
}, 100);
inputRef.current = input;
}
}}
onChange={e => setName(e.target.value)}
fullWidth
onKeyDown={(e) => {
if (e.key === 'Enter') {
void handleUpdate();
}
}}
/>
</NormalModal>
</>
);
}
export default RenameWorkspace;
@@ -1,7 +1,7 @@
import { Workspace } from '@/application/types';
import { getAvatarProps } from '@/components/app/workspaces/utils';
import { useService } from '@/components/main/app.hooks';
import { Avatar, Button, Tooltip } from '@mui/material';
import { Avatar, Tooltip } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@@ -13,13 +13,15 @@ function WorkspaceList ({
currentWorkspaceId,
onChange,
changeLoading,
showActions = true
showActions = true,
onUpdateCurrentWorkspace,
}: {
currentWorkspaceId?: string;
changeLoading?: string;
onChange: (selectedId: string) => void;
defaultWorkspaces?: Workspace[];
showActions?: boolean;
onUpdateCurrentWorkspace?: (name: string) => void;
}) {
const service = useService();
const { t } = useTranslation();
@@ -44,22 +46,33 @@ function WorkspaceList ({
if (changeLoading === workspace.id) return <CircularProgress size={16} />;
const hovered = hoveredWorkspaceId === workspace.id;
if (hovered) {
if (showActions) {
return <MoreActions workspace={workspace} />;
}
if (workspace.id === currentWorkspaceId && !(hovered && showActions)) return <SelectedSvg className={'w-6 text-fill-default h-6'} />;
if (showActions) {
return <div
style={{
visibility: hovered ? 'visible' : 'hidden',
}}
><MoreActions
workspace={workspace}
onUpdated={(name: string) => {
void fetchWorkspaces();
if (workspace.id === currentWorkspaceId) {
onUpdateCurrentWorkspace?.(name);
}
}}
/></div>;
}
if (workspace.id === currentWorkspaceId) return <SelectedSvg className={'w-6 text-fill-default h-6'} />;
return null;
}, [changeLoading, currentWorkspaceId, hoveredWorkspaceId, showActions]);
}, [changeLoading, currentWorkspaceId, fetchWorkspaces, hoveredWorkspaceId, onUpdateCurrentWorkspace, showActions]);
return (
<>
{workspaces.map((workspace) => {
return <Button
return <div
key={workspace.id}
className={'flex relative text-[1em] items-center justify-between gap-[10px] p-2 cursor-pointer'}
className={'flex relative hover:bg-fill-list-hover rounded-[8px] text-[1em] items-center justify-between gap-[10px] p-2 cursor-pointer'}
onClick={async () => {
if (workspace.id === currentWorkspaceId) return;
void onChange(workspace.id);
@@ -83,7 +96,7 @@ function WorkspaceList ({
</div>
</div>
{renderActions(workspace)}
</Button>;
</div>;
})}
</>
);
@@ -1,4 +1,5 @@
import { invalidToken } from '@/application/session/token';
import { Workspace } from '@/application/types';
import Import from '@/components/_shared/more-actions/importer/Import';
import { notify } from '@/components/_shared/notify';
import { Popover } from '@/components/_shared/popover';
@@ -9,7 +10,7 @@ import WorkspaceList from '@/components/app/workspaces/WorkspaceList';
import { useCurrentUser } from '@/components/main/app.hooks';
import { openUrl } from '@/utils/url';
import { Button, Divider, IconButton, Tooltip } from '@mui/material';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useEffect } from 'react';
import { ReactComponent as ArrowRightSvg } from '@/assets/arrow_right.svg';
import { ReactComponent as ImportIcon } from '@/assets/import.svg';
import { ReactComponent as TipIcon } from '@/assets/warning.svg';
@@ -39,11 +40,13 @@ export function Workspaces () {
const {
onChangeWorkspace: handleSelectedWorkspace,
} = useAppHandlers();
const [currentWorkspace, setCurrentWorkspace] = React.useState<Workspace | undefined>(undefined);
const selectedWorkspace = useMemo(() => {
return userWorkspaceInfo?.workspaces.find((workspace) => workspace.id === currentWorkspaceId);
const isOwner = currentWorkspace?.owner?.uid.toString() === currentUser?.uid.toString();
useEffect(() => {
setCurrentWorkspace(userWorkspaceInfo?.workspaces.find((workspace) => workspace.id === currentWorkspaceId));
}, [currentWorkspaceId, userWorkspaceInfo]);
const isOwner = selectedWorkspace?.owner?.uid.toString() === currentUser?.uid.toString();
const handleChange = useCallback(async (selectedId: string) => {
setChangeLoading(selectedId);
@@ -76,7 +79,7 @@ export function Workspaces () {
<div className={'flex items-center gap-1.5 text-[15px] text-text-title overflow-hidden'}>
<CurrentWorkspace
userWorkspaceInfo={userWorkspaceInfo}
selectedWorkspace={selectedWorkspace}
selectedWorkspace={currentWorkspace}
onChangeWorkspace={handleChange}
avatarSize={24}
/>
@@ -91,26 +94,35 @@ export function Workspaces () {
onClose={() => setOpen(false)}
>
<div
className={'flex text-[14px] w-[288px] flex-col gap-2 p-2 max-h-[420px] min-h-[303px] overflow-hidden'}
className={'flex text-[14px] w-[288px] flex-col gap-2 p-2 min-h-[303px] overflow-hidden'}
>
<div className={'flex p-2 text-text-caption items-center justify-between'}>
<span className={'font-medium flex-1 text-sm'}>{currentUser?.email}</span>
</div>
<div className={'flex flex-1 flex-col gap-1 overflow-y-auto appflowy-scroller'}>
<div className={'flex max-h-[236px] flex-1 flex-col gap-1 overflow-y-auto appflowy-scroller'}>
{open && <WorkspaceList
defaultWorkspaces={userWorkspaceInfo?.workspaces}
currentWorkspaceId={currentWorkspaceId}
onChange={handleChange}
changeLoading={changeLoading || undefined}
onUpdateCurrentWorkspace={(name) => {
setCurrentWorkspace(prev => {
if (!prev) return prev;
return {
...prev,
name,
};
});
}}
/>}
</div>
<CreateWorkspace />
<Divider className={'w-full mt-1'} />
{selectedWorkspace && <InviteMember
{currentWorkspace && <InviteMember
onClick={() => {
setOpen(false);
}}
workspace={selectedWorkspace}
workspace={currentWorkspace}
/>}
<Button