mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Implement workspace and account settings in sidebar (#130)
This commit is contained in:
@@ -2,8 +2,8 @@ import { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
import { accountRoutes } from '@colanode/server/api/client/routes/accounts';
|
||||
import { avatarRoutes } from '@colanode/server/api/client/routes/avatars';
|
||||
import { workspaceRoutes } from '@colanode/server/api/client/routes/workspaces';
|
||||
import { socketRoutes } from '@colanode/server/api/client/routes/sockets';
|
||||
import { workspaceRoutes } from '@colanode/server/api/client/routes/workspaces';
|
||||
|
||||
export const clientRoutes: FastifyPluginCallback = (instance, _, done) => {
|
||||
instance.register(socketRoutes, { prefix: '/sockets' });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
const copyTemplates = async () => {
|
||||
const srcDir = path.resolve(__dirname, 'src/templates');
|
||||
const destDir = path.resolve(__dirname, 'dist');
|
||||
|
||||
@@ -23,7 +23,7 @@ export type ContainerMetadata = {
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export type SidebarMenuType = 'chats' | 'spaces';
|
||||
export type SidebarMenuType = 'chats' | 'spaces' | 'settings';
|
||||
|
||||
export type SidebarMetadata = {
|
||||
menu: SidebarMenuType;
|
||||
@@ -63,3 +63,12 @@ export type WorkspaceMetadataMap = {
|
||||
'container.right': WorkspaceRightContainerMetadata;
|
||||
'container.left': WorkspaceLeftContainerMetadata;
|
||||
};
|
||||
|
||||
export enum SpecialContainerTabPath {
|
||||
Downloads = 'downloads',
|
||||
WorkspaceSettings = 'workspace/settings',
|
||||
WorkspaceUsers = 'workspace/users',
|
||||
WorkspaceDelete = 'workspace/delete',
|
||||
AccountSettings = 'account/settings',
|
||||
AccountLogout = 'account/logout',
|
||||
}
|
||||
|
||||
19
packages/ui/src/components/accounts/account-delete.tsx
Normal file
19
packages/ui/src/components/accounts/account-delete.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
|
||||
export const AccountDelete = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1 space-y-2">
|
||||
<h3 className="font-semibold">Delete account</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Account delete will be available soon.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Button variant="destructive" className="w-20" disabled>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
} from '@colanode/ui/components/ui/breadcrumb';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
export const AccountLogoutBreadcrumb = () => {
|
||||
const account = useAccount();
|
||||
|
||||
return (
|
||||
<Breadcrumb className="flex-grow">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="cursor-pointer hover:text-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
className="size-4"
|
||||
/>
|
||||
<span>{account.name} Logout</span>
|
||||
</div>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
18
packages/ui/src/components/accounts/account-logout-tab.tsx
Normal file
18
packages/ui/src/components/accounts/account-logout-tab.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
export const AccountLogoutTab = () => {
|
||||
const account = useAccount();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
size="small"
|
||||
/>
|
||||
<span>{account.name} Logout</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +1,67 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@colanode/ui/components/ui/alert-dialog';
|
||||
import { AccountLogoutBreadcrumb } from '@colanode/ui/components/accounts/account-logout-breadcrumb';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
Container,
|
||||
ContainerBody,
|
||||
ContainerHeader,
|
||||
} from '@colanode/ui/components/ui/container';
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
interface AccountLogoutProps {
|
||||
onCancel: () => void;
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export const AccountLogout = ({ onCancel, onLogout }: AccountLogoutProps) => {
|
||||
export const AccountLogout = () => {
|
||||
const account = useAccount();
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
open={true}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onCancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you sure you want logout?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
All your data will be removed from this device.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'account.logout',
|
||||
accountId: account.id,
|
||||
},
|
||||
onSuccess() {
|
||||
onLogout();
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
Logout
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<AccountLogoutBreadcrumb />
|
||||
</ContainerHeader>
|
||||
<ContainerBody className="max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Logout</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1 space-y-2">
|
||||
<h3 className="font-semibold">Sign out of your account</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All your data will be removed from this device. If there are
|
||||
pending changes, they will be lost. If you login again, all
|
||||
the data will be re-synced.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
className="w-20 cursor-pointer"
|
||||
onClick={async () => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'account.logout',
|
||||
accountId: account.id,
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
} from '@colanode/ui/components/ui/breadcrumb';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
export const AccountSettingsBreadcrumb = () => {
|
||||
const account = useAccount();
|
||||
|
||||
return (
|
||||
<Breadcrumb className="flex-grow">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="cursor-pointer hover:text-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
className="size-4"
|
||||
/>
|
||||
<span>{account.name} Settings</span>
|
||||
</div>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
18
packages/ui/src/components/accounts/account-settings-tab.tsx
Normal file
18
packages/ui/src/components/accounts/account-settings-tab.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
export const AccountSettingsTab = () => {
|
||||
const account = useAccount();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
size="small"
|
||||
/>
|
||||
<span>{account.name} Settings</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
43
packages/ui/src/components/accounts/account-settings.tsx
Normal file
43
packages/ui/src/components/accounts/account-settings.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { AccountDelete } from '@colanode/ui/components/accounts/account-delete';
|
||||
import { AccountSettingsBreadcrumb } from '@colanode/ui/components/accounts/account-settings-breadcrumb';
|
||||
import { AccountUpdate } from '@colanode/ui/components/accounts/account-update';
|
||||
import {
|
||||
Container,
|
||||
ContainerBody,
|
||||
ContainerHeader,
|
||||
} from '@colanode/ui/components/ui/container';
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
export const AccountSettings = () => {
|
||||
const account = useAccount();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<AccountSettingsBreadcrumb />
|
||||
</ContainerHeader>
|
||||
<ContainerBody className="max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<AccountUpdate account={account} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Danger Zone
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<AccountDelete />
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -162,6 +162,7 @@ export const AccountUpdate = ({ account }: { account: Account }) => {
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isUpdatingAccount || isUploadingAvatar}
|
||||
className="w-20"
|
||||
>
|
||||
{isUpdatingAccount && <Spinner className="mr-1" />}
|
||||
Save
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Account as AccountType } from '@colanode/client/types';
|
||||
import { AccountLogout } from '@colanode/ui/components/accounts/account-logout';
|
||||
import { AccountSettingsDialog } from '@colanode/ui/components/accounts/account-settings-dialog';
|
||||
import { Workspace } from '@colanode/ui/components/workspaces/workspace';
|
||||
import { WorkspaceCreate } from '@colanode/ui/components/workspaces/workspace-create';
|
||||
import { AccountContext } from '@colanode/ui/contexts/account';
|
||||
@@ -13,8 +11,6 @@ interface AccountProps {
|
||||
}
|
||||
|
||||
export const Account = ({ account }: AccountProps) => {
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const [openLogout, setOpenLogout] = useState(false);
|
||||
const [openCreateWorkspace, setOpenCreateWorkspace] = useState(false);
|
||||
|
||||
const accountMetadataListQuery = useQuery({
|
||||
@@ -59,8 +55,6 @@ export const Account = ({ account }: AccountProps) => {
|
||||
<AccountContext.Provider
|
||||
value={{
|
||||
...account,
|
||||
openSettings: () => setOpenSettings(true),
|
||||
openLogout: () => setOpenLogout(true),
|
||||
openWorkspaceCreate: () => setOpenCreateWorkspace(true),
|
||||
openWorkspace: (id) => {
|
||||
setOpenCreateWorkspace(false);
|
||||
@@ -81,20 +75,6 @@ export const Account = ({ account }: AccountProps) => {
|
||||
onCancel={handleWorkspaceCreateCancel}
|
||||
/>
|
||||
)}
|
||||
{openSettings && (
|
||||
<AccountSettingsDialog
|
||||
open={true}
|
||||
onOpenChange={() => setOpenSettings(false)}
|
||||
/>
|
||||
)}
|
||||
{openLogout && (
|
||||
<AccountLogout
|
||||
onCancel={() => setOpenLogout(false)}
|
||||
onLogout={() => {
|
||||
setOpenLogout(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AccountContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { ContainerTab } from '@colanode/client/types';
|
||||
import { ContainerTab, SpecialContainerTabPath } from '@colanode/client/types';
|
||||
import { getIdType, IdType } from '@colanode/core';
|
||||
import { AccountLogout } from '@colanode/ui/components/accounts/account-logout';
|
||||
import { AccountSettings } from '@colanode/ui/components/accounts/account-settings';
|
||||
import { ChannelContainer } from '@colanode/ui/components/channels/channel-container';
|
||||
import { ChatContainer } from '@colanode/ui/components/chats/chat-container';
|
||||
import { DatabaseContainer } from '@colanode/ui/components/databases/database-container';
|
||||
import { DownloadsList } from '@colanode/ui/components/downloads/downloads-list';
|
||||
import { DownloadsContainer } from '@colanode/ui/components/downloads/downloads-container';
|
||||
import { FileContainer } from '@colanode/ui/components/files/file-container';
|
||||
import { FolderContainer } from '@colanode/ui/components/folders/folder-container';
|
||||
import { MessageContainer } from '@colanode/ui/components/messages/message-container';
|
||||
@@ -13,14 +15,32 @@ import { PageContainer } from '@colanode/ui/components/pages/page-container';
|
||||
import { RecordContainer } from '@colanode/ui/components/records/record-container';
|
||||
import { SpaceContainer } from '@colanode/ui/components/spaces/space-container';
|
||||
import { TabsContent } from '@colanode/ui/components/ui/tabs';
|
||||
import { WorkspaceSettings } from '@colanode/ui/components/workspaces/workspace-settings';
|
||||
import { WorkspaceUsers } from '@colanode/ui/components/workspaces/workspace-users';
|
||||
|
||||
interface ContainerTabContentProps {
|
||||
tab: ContainerTab;
|
||||
}
|
||||
|
||||
const ContainerTabContentBody = ({ tab }: ContainerTabContentProps) => {
|
||||
if (tab.path === 'downloads') {
|
||||
return <DownloadsList />;
|
||||
if (tab.path === SpecialContainerTabPath.Downloads) {
|
||||
return <DownloadsContainer />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.WorkspaceSettings) {
|
||||
return <WorkspaceSettings />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.WorkspaceUsers) {
|
||||
return <WorkspaceUsers />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.AccountSettings) {
|
||||
return <AccountSettings />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.AccountLogout) {
|
||||
return <AccountLogout />;
|
||||
}
|
||||
|
||||
return match(getIdType(tab.path))
|
||||
|
||||
@@ -3,8 +3,10 @@ import { useRef } from 'react';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { ContainerTab } from '@colanode/client/types';
|
||||
import { ContainerTab, SpecialContainerTabPath } from '@colanode/client/types';
|
||||
import { getIdType, IdType } from '@colanode/core';
|
||||
import { AccountLogoutTab } from '@colanode/ui/components/accounts/account-logout-tab';
|
||||
import { AccountSettingsTab } from '@colanode/ui/components/accounts/account-settings-tab';
|
||||
import { ChannelContainerTab } from '@colanode/ui/components/channels/channel-container-tab';
|
||||
import { ChatContainerTab } from '@colanode/ui/components/chats/chat-container-tab';
|
||||
import { DatabaseContainerTab } from '@colanode/ui/components/databases/database-container-tab';
|
||||
@@ -16,6 +18,8 @@ import { PageContainerTab } from '@colanode/ui/components/pages/page-container-t
|
||||
import { RecordContainerTab } from '@colanode/ui/components/records/record-container-tab';
|
||||
import { SpaceContainerTab } from '@colanode/ui/components/spaces/space-container-tab';
|
||||
import { TabsTrigger } from '@colanode/ui/components/ui/tabs';
|
||||
import { WorkspaceSettingsTab } from '@colanode/ui/components/workspaces/workspace-settings-tab';
|
||||
import { WorkspaceUsersTab } from '@colanode/ui/components/workspaces/workspace-users-tab';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface ContainerTabTriggerProps {
|
||||
@@ -26,10 +30,26 @@ interface ContainerTabTriggerProps {
|
||||
}
|
||||
|
||||
const ContainerTabTriggerContent = ({ tab }: { tab: ContainerTab }) => {
|
||||
if (tab.path === 'downloads') {
|
||||
if (tab.path === SpecialContainerTabPath.Downloads) {
|
||||
return <DownloadsContainerTab />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.WorkspaceSettings) {
|
||||
return <WorkspaceSettingsTab />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.WorkspaceUsers) {
|
||||
return <WorkspaceUsersTab />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.AccountSettings) {
|
||||
return <AccountSettingsTab />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.AccountLogout) {
|
||||
return <AccountLogoutTab />;
|
||||
}
|
||||
|
||||
return match(getIdType(tab.path))
|
||||
.with(IdType.Space, () => <SpaceContainerTab spaceId={tab.path} />)
|
||||
.with(IdType.Channel, () => (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChatCreatePopover } from '@colanode/ui/components/chats/chat-create-popover';
|
||||
import { ChatSidebarItem } from '@colanode/ui/components/chats/chat-sidebar-item';
|
||||
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
@@ -21,14 +22,7 @@ export const SidebarChats = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col group/sidebar-chats h-full px-2">
|
||||
<div className="flex items-center justify-between h-12 pl-2 pr-1 app-drag-region">
|
||||
<p className="font-bold text-muted-foreground flex-grow app-no-drag-region">
|
||||
Chats
|
||||
</p>
|
||||
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-chats:opacity-100 flex items-center justify-center app-no-drag-region">
|
||||
<ChatCreatePopover />
|
||||
</div>
|
||||
</div>
|
||||
<SidebarHeader title="Chats" actions={<ChatCreatePopover />} />
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
{chats.map((item) => (
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
interface SidebarHeaderProps {
|
||||
title: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SidebarHeader = ({ title, actions }: SidebarHeaderProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between h-12 pl-2 pr-1 app-drag-region">
|
||||
<p className="font-bold text-muted-foreground flex-grow app-no-drag-region">
|
||||
{title}
|
||||
</p>
|
||||
{actions && (
|
||||
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-chats:opacity-100 flex items-center justify-center app-no-drag-region">
|
||||
{actions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Check, LogOut, Plus, Settings } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Check, Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { UnreadState } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
@@ -66,96 +66,56 @@ export function SidebarMenuFooter() {
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuItem key={account.id} className="p-0">
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">{account.name}</span>
|
||||
<span className="truncate text-xs">{account.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2 cursor-pointer"
|
||||
onClick={() => {
|
||||
account.openSettings();
|
||||
}}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
<p className="font-medium">Settings</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2 cursor-pointer"
|
||||
onClick={() => {
|
||||
account.openLogout();
|
||||
}}
|
||||
>
|
||||
<LogOut className="size-4" />
|
||||
<p className="font-medium">Logout</p>
|
||||
</DropdownMenuItem>
|
||||
{otherAccounts.length > 0 && (
|
||||
<Fragment>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="mb-1">Accounts</DropdownMenuLabel>
|
||||
{accounts.map((accountItem) => {
|
||||
const state = accountUnreadStates[accountItem.id] ?? {
|
||||
unreadCount: 0,
|
||||
hasUnread: false,
|
||||
};
|
||||
<DropdownMenuLabel className="mb-1">Accounts</DropdownMenuLabel>
|
||||
{accounts.map((accountItem) => {
|
||||
const state = accountUnreadStates[accountItem.id] ?? {
|
||||
unreadCount: 0,
|
||||
hasUnread: false,
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={accountItem.id}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
app.openAccount(accountItem.id);
|
||||
}}
|
||||
>
|
||||
<AccountContext.Provider
|
||||
value={{
|
||||
...accountItem,
|
||||
openSettings: () => {},
|
||||
openLogout: () => {},
|
||||
openWorkspace: () => {},
|
||||
openWorkspaceCreate: () => {},
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={accountItem.id}
|
||||
name={accountItem.name}
|
||||
avatar={accountItem.avatar}
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{accountItem.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{accountItem.email}
|
||||
</span>
|
||||
</div>
|
||||
{accountItem.id === account.id ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<UnreadBadge
|
||||
count={state.unreadCount}
|
||||
unread={state.hasUnread}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AccountContext.Provider>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
)}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={accountItem.id}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
app.openAccount(accountItem.id);
|
||||
}}
|
||||
>
|
||||
<AccountContext.Provider
|
||||
value={{
|
||||
...accountItem,
|
||||
openWorkspace: () => {},
|
||||
openWorkspaceCreate: () => {},
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={accountItem.id}
|
||||
name={accountItem.name}
|
||||
avatar={accountItem.avatar}
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{accountItem.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{accountItem.email}
|
||||
</span>
|
||||
</div>
|
||||
{accountItem.id === account.id ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<UnreadBadge
|
||||
count={state.unreadCount}
|
||||
unread={state.hasUnread}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AccountContext.Provider>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<DropdownMenuItem
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Bell, Check, Plus, Settings } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Check, Plus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
@@ -61,74 +61,42 @@ export const SidebarMenuHeader = () => {
|
||||
side="right"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuItem key={workspace.id} className="p-0">
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
/>
|
||||
<p className="flex-1 text-left text-sm leading-tight truncate font-semibold">
|
||||
{workspace.name}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2 cursor-pointer"
|
||||
onClick={() => {
|
||||
workspace.openSettings();
|
||||
}}
|
||||
>
|
||||
<Settings className="size-4" />
|
||||
<p className="font-medium">Settings</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="gap-2 p-2" disabled={true}>
|
||||
<Bell className="size-4" />
|
||||
<p className="font-medium">Notifications</p>
|
||||
</DropdownMenuItem>
|
||||
{otherWorkspaces.length > 0 && (
|
||||
<Fragment>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="mb-1">Workspaces</DropdownMenuLabel>
|
||||
{workspaces.map((workspaceItem) => {
|
||||
const workspaceUnreadState = radar.getWorkspaceState(
|
||||
workspaceItem.accountId,
|
||||
workspaceItem.id
|
||||
);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={workspaceItem.id}
|
||||
className="p-0 cursor-pointer"
|
||||
onClick={() => {
|
||||
account.openWorkspace(workspaceItem.id);
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={workspaceItem.id}
|
||||
name={workspaceItem.name}
|
||||
avatar={workspaceItem.avatar}
|
||||
/>
|
||||
<p className="flex-1 text-left text-sm leading-tight truncate font-normal">
|
||||
{workspaceItem.name}
|
||||
</p>
|
||||
{workspaceItem.id === workspace.id ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<UnreadBadge
|
||||
count={workspaceUnreadState.state.unreadCount}
|
||||
unread={workspaceUnreadState.state.hasUnread}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
)}
|
||||
<DropdownMenuLabel className="mb-1">Workspaces</DropdownMenuLabel>
|
||||
{workspaces.map((workspaceItem) => {
|
||||
const workspaceUnreadState = radar.getWorkspaceState(
|
||||
workspaceItem.accountId,
|
||||
workspaceItem.id
|
||||
);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={workspaceItem.id}
|
||||
className="p-0 cursor-pointer"
|
||||
onClick={() => {
|
||||
account.openWorkspace(workspaceItem.id);
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar
|
||||
className="h-8 w-8 rounded-lg"
|
||||
id={workspaceItem.id}
|
||||
name={workspaceItem.name}
|
||||
avatar={workspaceItem.avatar}
|
||||
/>
|
||||
<p className="flex-1 text-left text-sm leading-tight truncate font-normal">
|
||||
{workspaceItem.name}
|
||||
</p>
|
||||
{workspaceItem.id === workspace.id ? (
|
||||
<Check className="size-4" />
|
||||
) : (
|
||||
<UnreadBadge
|
||||
count={workspaceUnreadState.state.unreadCount}
|
||||
unread={workspaceUnreadState.state.hasUnread}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuSeparator className="my-1" />
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2 text-muted-foreground hover:text-foreground cursor-pointer"
|
||||
|
||||
@@ -7,6 +7,7 @@ interface SidebarMenuIconProps {
|
||||
onClick: () => void;
|
||||
isActive?: boolean;
|
||||
unreadState?: UnreadState;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SidebarMenuIcon = ({
|
||||
@@ -14,11 +15,13 @@ export const SidebarMenuIcon = ({
|
||||
onClick,
|
||||
isActive = false,
|
||||
unreadState,
|
||||
className,
|
||||
}: SidebarMenuIconProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 h-10 flex items-center justify-center cursor-pointer hover:bg-gray-200 rounded-md relative',
|
||||
className,
|
||||
isActive ? 'bg-gray-200' : ''
|
||||
)}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LayoutGrid, MessageCircle } from 'lucide-react';
|
||||
import { LayoutGrid, MessageCircle, Settings } from 'lucide-react';
|
||||
|
||||
import { SidebarMenuType } from '@colanode/client/types';
|
||||
import { SidebarMenuFooter } from '@colanode/ui/components/layouts/sidebars/sidebar-menu-footer';
|
||||
@@ -57,6 +57,14 @@ export const SidebarMenu = ({ value, onChange }: SidebarMenuProps) => {
|
||||
isActive={value === 'spaces'}
|
||||
unreadState={channelsState}
|
||||
/>
|
||||
<SidebarMenuIcon
|
||||
icon={Settings}
|
||||
onClick={() => {
|
||||
onChange('settings');
|
||||
}}
|
||||
isActive={value === 'settings'}
|
||||
className="mt-auto"
|
||||
/>
|
||||
</div>
|
||||
<SidebarMenuFooter />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface SidebarSettingsItemProps {
|
||||
title: string;
|
||||
path: string;
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
}
|
||||
|
||||
export const SidebarSettingsItem = ({
|
||||
title,
|
||||
icon: Icon,
|
||||
path,
|
||||
}: SidebarSettingsItemProps) => {
|
||||
const layout = useLayout();
|
||||
const isActive = layout.activeTab === path;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'text-sm flex h-7 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground cursor-pointer',
|
||||
isActive && 'font-semibold'
|
||||
)}
|
||||
onClick={() => {
|
||||
layout.previewLeft(path);
|
||||
}}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span className="line-clamp-1 w-full flex-grow text-left">{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { LogOut, Settings, Users } from 'lucide-react';
|
||||
|
||||
import { SpecialContainerTabPath } from '@colanode/client/types';
|
||||
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
|
||||
import { SidebarSettingsItem } from '@colanode/ui/components/layouts/sidebars/sidebar-settings-item';
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
|
||||
export const SidebarSettings = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 h-full px-2">
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
<SidebarHeader title="Workspace settings" />
|
||||
<SidebarSettingsItem
|
||||
title="General"
|
||||
icon={Settings}
|
||||
path={SpecialContainerTabPath.WorkspaceSettings}
|
||||
/>
|
||||
<SidebarSettingsItem
|
||||
title="Users"
|
||||
icon={Users}
|
||||
path={SpecialContainerTabPath.WorkspaceUsers}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
<SidebarHeader title="Account settings" />
|
||||
<SidebarSettingsItem
|
||||
title="General"
|
||||
icon={Settings}
|
||||
path={SpecialContainerTabPath.AccountSettings}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
<Separator className="my-2" />
|
||||
<SidebarSettingsItem
|
||||
title="Logout"
|
||||
icon={LogOut}
|
||||
path={SpecialContainerTabPath.AccountLogout}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
|
||||
import { SpaceCreateButton } from '@colanode/ui/components/spaces/space-create-button';
|
||||
import { SpaceSidebarItem } from '@colanode/ui/components/spaces/space-sidebar-item';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
@@ -21,16 +22,10 @@ export const SidebarSpaces = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col group/sidebar-spaces h-full px-2">
|
||||
<div className="flex items-center justify-between h-12 pl-2 pr-1 app-drag-region">
|
||||
<p className="font-bold text-muted-foreground flex-grow app-no-drag-region">
|
||||
Spaces
|
||||
</p>
|
||||
{canCreateSpace && (
|
||||
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-spaces:opacity-100 flex items-center justify-center app-no-drag-region">
|
||||
<SpaceCreateButton />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SidebarHeader
|
||||
title="Spaces"
|
||||
actions={canCreateSpace && <SpaceCreateButton />}
|
||||
/>
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
{spaces.map((space) => (
|
||||
<SpaceSidebarItem space={space} key={space.id} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SidebarMenuType } from '@colanode/client/types';
|
||||
import { SidebarChats } from '@colanode/ui/components/layouts/sidebars/sidebar-chats';
|
||||
import { SidebarMenu } from '@colanode/ui/components/layouts/sidebars/sidebar-menu';
|
||||
import { SidebarSettings } from '@colanode/ui/components/layouts/sidebars/sidebar-settings';
|
||||
import { SidebarSpaces } from '@colanode/ui/components/layouts/sidebars/sidebar-spaces';
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -15,6 +16,7 @@ export const Sidebar = ({ menu, onMenuChange }: SidebarProps) => {
|
||||
<div className="min-h-0 flex-grow overflow-auto">
|
||||
{menu === 'spaces' && <SidebarSpaces />}
|
||||
{menu === 'chats' && <SidebarChats />}
|
||||
{menu === 'settings' && <SidebarSettings />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,35 +15,33 @@ import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
onDeleted: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceDelete = ({ onDeleted }: WorkspaceDeleteProps) => {
|
||||
export const WorkspaceDelete = () => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="font-heading mb-px text-2xl font-semibold tracking-tight">
|
||||
Delete workspace
|
||||
</h3>
|
||||
<p>Deleting a workspace is permanent and cannot be undone.</p>
|
||||
<p>
|
||||
All data associated with the workspace will be deleted, including users,
|
||||
chats, messages, pages, channels, databases, records, files and more.
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1 space-y-2">
|
||||
<h3 className="font-semibold">Delete workspace</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Once you delete a workspace, there is no going back. Please be
|
||||
certain.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setShowDeleteModal(true);
|
||||
}}
|
||||
className="w-20"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<AlertDialogContent>
|
||||
@@ -70,7 +68,6 @@ export const WorkspaceDelete = ({ onDeleted }: WorkspaceDeleteProps) => {
|
||||
},
|
||||
onSuccess() {
|
||||
setShowDeleteModal(false);
|
||||
onDeleted();
|
||||
toast.success('Workspace was deleted successfully');
|
||||
},
|
||||
onError(error) {
|
||||
@@ -85,6 +82,6 @@ export const WorkspaceDelete = ({ onDeleted }: WorkspaceDeleteProps) => {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -175,7 +175,11 @@ export const WorkspaceForm = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={isPending || isSaving}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending || isSaving}
|
||||
className="w-20"
|
||||
>
|
||||
{isSaving && <Spinner className="mr-1" />}
|
||||
{saveText}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
} from '@colanode/ui/components/ui/breadcrumb';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
export const WorkspaceSettingsBreadcrumb = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<Breadcrumb className="flex-grow">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="cursor-pointer hover:text-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
className="size-4"
|
||||
/>
|
||||
<span>{workspace.name} Settings</span>
|
||||
</div>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { Info, Trash2, Users } from 'lucide-react';
|
||||
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@colanode/ui/components/ui/dialog';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@colanode/ui/components/ui/tabs';
|
||||
import { WorkspaceDelete } from '@colanode/ui/components/workspaces/workspace-delete';
|
||||
import { WorkspaceUpdate } from '@colanode/ui/components/workspaces/workspace-update';
|
||||
import { WorkspaceUsers } from '@colanode/ui/components/workspaces/workspace-users';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
interface WorkspaceSettingsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const WorkspaceSettingsDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: WorkspaceSettingsDialogProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
if (!workspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const canDelete = workspace.role === 'owner';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="md:min-h-3/4 md:max-h-3/4 p-3 md:h-3/4 md:w-3/4 md:max-w-full"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Workspace Settings</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<Tabs
|
||||
defaultValue="info"
|
||||
className="grid h-full max-h-full grid-cols-[240px_minmax(0,1fr)] overflow-hidden"
|
||||
>
|
||||
<TabsList className="flex w-full max-h-full flex-col items-start justify-start gap-1 rounded-none border-r border-r-gray-100 bg-white pr-3">
|
||||
<div className="mb-1 flex h-10 w-full items-center justify-between bg-gray-50 p-1 text-foreground/80">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
size="small"
|
||||
/>
|
||||
<span className="truncate font-semibold">{workspace.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<TabsTrigger
|
||||
key="tab-trigger-info"
|
||||
className="w-full justify-start p-2 hover:bg-gray-50 cursor-pointer"
|
||||
value="info"
|
||||
>
|
||||
<Info className="mr-2 size-4" />
|
||||
Info
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
key="tab-trigger-users"
|
||||
className="w-full justify-start p-2 hover:bg-gray-50 cursor-pointer"
|
||||
value="users"
|
||||
>
|
||||
<Users className="mr-2 size-4" />
|
||||
Users
|
||||
</TabsTrigger>
|
||||
{canDelete && (
|
||||
<TabsTrigger
|
||||
key="tab-trigger-delete"
|
||||
className="w-full justify-start p-2 hover:bg-gray-50 cursor-pointer"
|
||||
value="delete"
|
||||
>
|
||||
<Trash2 className="mr-2 size-4" />
|
||||
Delete
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<div className="overflow-auto p-4">
|
||||
<TabsContent
|
||||
key="tab-content-info"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="info"
|
||||
>
|
||||
<WorkspaceUpdate />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
key="tab-content-users"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="users"
|
||||
>
|
||||
<WorkspaceUsers />
|
||||
</TabsContent>
|
||||
{canDelete && (
|
||||
<TabsContent
|
||||
key="tab-content-delete"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="delete"
|
||||
>
|
||||
<WorkspaceDelete onDeleted={() => onOpenChange(false)} />
|
||||
</TabsContent>
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
export const WorkspaceSettingsTab = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
size="small"
|
||||
/>
|
||||
<span>{workspace.name} Settings</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
75
packages/ui/src/components/workspaces/workspace-settings.tsx
Normal file
75
packages/ui/src/components/workspaces/workspace-settings.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
Container,
|
||||
ContainerBody,
|
||||
ContainerHeader,
|
||||
} from '@colanode/ui/components/ui/container';
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { WorkspaceDelete } from '@colanode/ui/components/workspaces/workspace-delete';
|
||||
import { WorkspaceForm } from '@colanode/ui/components/workspaces/workspace-form';
|
||||
import { WorkspaceSettingsBreadcrumb } from '@colanode/ui/components/workspaces/workspace-settings-breadcrumb';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
export const WorkspaceSettings = () => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
const canEdit = workspace.role === 'owner';
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<WorkspaceSettingsBreadcrumb />
|
||||
</ContainerHeader>
|
||||
<ContainerBody className="max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<WorkspaceForm
|
||||
readOnly={!canEdit}
|
||||
values={{
|
||||
name: workspace.name,
|
||||
description: workspace.description ?? '',
|
||||
avatar: workspace.avatar ?? null,
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'workspace.update',
|
||||
id: workspace.id,
|
||||
accountId: workspace.accountId,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
avatar: values.avatar ?? null,
|
||||
},
|
||||
onSuccess() {
|
||||
toast.success('Workspace updated');
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
isSaving={isPending}
|
||||
saveText="Update"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Danger Zone
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<WorkspaceDelete />
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { WorkspaceForm } from '@colanode/ui/components/workspaces/workspace-form';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
|
||||
export const WorkspaceUpdate = () => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
const canEdit = workspace.role === 'owner';
|
||||
|
||||
return (
|
||||
<WorkspaceForm
|
||||
readOnly={!canEdit}
|
||||
values={{
|
||||
name: workspace.name,
|
||||
description: workspace.description ?? '',
|
||||
avatar: workspace.avatar ?? null,
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'workspace.update',
|
||||
id: workspace.id,
|
||||
accountId: workspace.accountId,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
avatar: values.avatar ?? null,
|
||||
},
|
||||
onSuccess() {
|
||||
toast.success('Workspace updated');
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
isSaving={isPending}
|
||||
saveText="Update"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -23,14 +23,9 @@ export const WorkspaceUserInvite = ({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div>
|
||||
<p>Invite with email</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Write the email addresses of the people you want to invite
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Write the email addresses of the people you want to invite
|
||||
</p>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="flex h-9 w-full flex-row gap-2 rounded-md border border-input bg-background p-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground">
|
||||
{emails.map((email) => (
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbList,
|
||||
} from '@colanode/ui/components/ui/breadcrumb';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
export const WorkspaceUsersBreadcrumb = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<Breadcrumb className="flex-grow">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="cursor-pointer hover:text-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
className="size-4"
|
||||
/>
|
||||
<span>{workspace.name} Users</span>
|
||||
</div>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
export const WorkspaceUsersTab = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
size="small"
|
||||
/>
|
||||
<span>{workspace.name} Users</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { InView } from 'react-intersection-observer';
|
||||
|
||||
import { UserListQueryInput } from '@colanode/client/queries';
|
||||
import { WorkspaceRole } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import {
|
||||
Container,
|
||||
ContainerBody,
|
||||
ContainerHeader,
|
||||
} from '@colanode/ui/components/ui/container';
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { WorkspaceUserInvite } from '@colanode/ui/components/workspaces/workspace-user-invite';
|
||||
import { WorkspaceUserRoleDropdown } from '@colanode/ui/components/workspaces/workspace-user-role-dropdown';
|
||||
import { WorkspaceUsersBreadcrumb } from '@colanode/ui/components/workspaces/workspace-users-breadcrumb';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
|
||||
@@ -34,57 +40,73 @@ export const WorkspaceUsers = () => {
|
||||
const hasMore = !isPending && users.length === lastPage * USERS_PER_PAGE;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
{canEditUsers && (
|
||||
<Fragment>
|
||||
<WorkspaceUserInvite workspace={workspace} />
|
||||
<Separator />
|
||||
</Fragment>
|
||||
)}
|
||||
<div>
|
||||
<p>Users</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
The list of all users on the workspaces
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{users.map((user) => {
|
||||
const name: string = user.name ?? 'Unknown';
|
||||
const email: string = user.email ?? ' ';
|
||||
const avatar: string | null | undefined = user.avatar;
|
||||
const role: WorkspaceRole = user.role;
|
||||
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={user.id} className="flex items-center space-x-3">
|
||||
<Avatar id={user.id} name={name} avatar={avatar} />
|
||||
<div className="flex-grow">
|
||||
<p className="text-sm font-medium leading-none">{name}</p>
|
||||
<p className="text-sm text-muted-foreground">{email}</p>
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<WorkspaceUsersBreadcrumb />
|
||||
</ContainerHeader>
|
||||
<ContainerBody className="max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
{canEditUsers && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Invite
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<WorkspaceUserRoleDropdown
|
||||
userId={user.id}
|
||||
value={role}
|
||||
canEdit={canEditUsers}
|
||||
/>
|
||||
<WorkspaceUserInvite workspace={workspace} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
{isPending && <Spinner />}
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Users</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
The list of all users on the workspace
|
||||
</p>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{users.map((user) => {
|
||||
const name: string = user.name ?? 'Unknown';
|
||||
const email: string = user.email ?? ' ';
|
||||
const avatar: string | null | undefined = user.avatar;
|
||||
const role: WorkspaceRole = user.role;
|
||||
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={user.id} className="flex items-center space-x-3">
|
||||
<Avatar id={user.id} name={name} avatar={avatar} />
|
||||
<div className="flex-grow">
|
||||
<p className="text-sm font-medium leading-none">{name}</p>
|
||||
<p className="text-sm text-muted-foreground">{email}</p>
|
||||
</div>
|
||||
<WorkspaceUserRoleDropdown
|
||||
userId={user.id}
|
||||
value={role}
|
||||
canEdit={canEditUsers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
{isPending && <Spinner />}
|
||||
</div>
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
}
|
||||
}}
|
||||
></InView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
}
|
||||
}}
|
||||
></InView>
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
WorkspaceMetadataKey,
|
||||
WorkspaceMetadataMap,
|
||||
Workspace as WorkspaceType,
|
||||
} from '@colanode/client/types';
|
||||
import { Layout } from '@colanode/ui/components/layouts/layout';
|
||||
import { WorkspaceSettingsDialog } from '@colanode/ui/components/workspaces/workspace-settings-dialog';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { WorkspaceContext } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
@@ -17,7 +14,6 @@ interface WorkspaceProps {
|
||||
|
||||
export const Workspace = ({ workspace }: WorkspaceProps) => {
|
||||
const account = useAccount();
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
|
||||
const workspaceMetadataListQuery = useQuery({
|
||||
type: 'workspace.metadata.list',
|
||||
@@ -33,9 +29,6 @@ export const Workspace = ({ workspace }: WorkspaceProps) => {
|
||||
<WorkspaceContext.Provider
|
||||
value={{
|
||||
...workspace,
|
||||
openSettings() {
|
||||
setOpenSettings(true);
|
||||
},
|
||||
getMetadata<K extends WorkspaceMetadataKey>(key: K) {
|
||||
const value = workspaceMetadataListQuery.data?.find(
|
||||
(m) => m.key === key
|
||||
@@ -73,12 +66,6 @@ export const Workspace = ({ workspace }: WorkspaceProps) => {
|
||||
}}
|
||||
>
|
||||
<Layout key={workspace.id} />
|
||||
{openSettings && (
|
||||
<WorkspaceSettingsDialog
|
||||
open={openSettings}
|
||||
onOpenChange={() => setOpenSettings(false)}
|
||||
/>
|
||||
)}
|
||||
</WorkspaceContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,6 @@ import { createContext, useContext } from 'react';
|
||||
import { Account } from '@colanode/client/types';
|
||||
|
||||
interface AccountContext extends Account {
|
||||
openSettings: () => void;
|
||||
openLogout: () => void;
|
||||
openWorkspace: (id: string) => void;
|
||||
openWorkspaceCreate: () => void;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from '@colanode/client/types';
|
||||
|
||||
interface WorkspaceContext extends Workspace {
|
||||
openSettings: () => void;
|
||||
getMetadata: <K extends WorkspaceMetadataKey>(
|
||||
key: K
|
||||
) => WorkspaceMetadataMap[K] | undefined;
|
||||
|
||||
Reference in New Issue
Block a user