Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c97a31ad |
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
|
||||
|
||||
Reference in new issue
Block a user