mirror of
https://github.com/colanode/colanode.git
synced 2025-12-29 00:25:03 +01:00
Workspace storage limits (#140)
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,10 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { LogOut } from 'lucide-react';
|
||||
|
||||
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>
|
||||
<LogOut className="size-4" />
|
||||
<span>Logout</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
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 { Container, ContainerBody } 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';
|
||||
@@ -18,46 +13,41 @@ export const AccountLogout = () => {
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<ContainerBody className="max-w-4xl 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 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 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>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||
import { Info, Trash2 } from 'lucide-react';
|
||||
|
||||
import { AccountUpdate } from '@colanode/ui/components/accounts/account-update';
|
||||
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 { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
interface AccountSettingsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const AccountSettingsDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AccountSettingsDialogProps) => {
|
||||
const account = useAccount();
|
||||
|
||||
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={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
size="small"
|
||||
/>
|
||||
<span className="truncate font-semibold">{account.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-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"
|
||||
>
|
||||
<AccountUpdate account={account} />
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
key="tab-content-delete"
|
||||
className="focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
value="delete"
|
||||
>
|
||||
<p>Coming soon.</p>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,10 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { Settings } from 'lucide-react';
|
||||
|
||||
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>
|
||||
<Settings className="size-4" />
|
||||
<span>Account Settings</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
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 { Container, ContainerBody } from '@colanode/ui/components/ui/container';
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
|
||||
@@ -14,28 +9,23 @@ export const AccountSettings = () => {
|
||||
|
||||
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} />
|
||||
<ContainerBody className="max-w-4xl 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 className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Danger Zone
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<AccountDelete />
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Account as AccountType } from '@colanode/client/types';
|
||||
import { Workspace } from '@colanode/ui/components/workspaces/workspace';
|
||||
import { WorkspaceCreate } from '@colanode/ui/components/workspaces/workspace-create';
|
||||
import { AccountContext } from '@colanode/ui/contexts/account';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface AccountProps {
|
||||
account: AccountType;
|
||||
@@ -13,12 +13,12 @@ interface AccountProps {
|
||||
export const Account = ({ account }: AccountProps) => {
|
||||
const [openCreateWorkspace, setOpenCreateWorkspace] = useState(false);
|
||||
|
||||
const accountMetadataListQuery = useQuery({
|
||||
const accountMetadataListQuery = useLiveQuery({
|
||||
type: 'account.metadata.list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
const workspaceListQuery = useQuery({
|
||||
const workspaceListQuery = useLiveQuery({
|
||||
type: 'workspace.list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ const GoogleLoginButton = ({ context, onSuccess }: GoogleLoginProps) => {
|
||||
|
||||
const login = useGoogleLogin({
|
||||
onSuccess: async (response) => {
|
||||
console.log('response', response);
|
||||
mutate({
|
||||
input: {
|
||||
type: 'google.login',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LoginForm } from '@colanode/ui/components/accounts/login-form';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const Login = () => {
|
||||
const accountListQuery = useQuery({
|
||||
const accountListQuery = useLiveQuery({
|
||||
type: 'account.list',
|
||||
});
|
||||
|
||||
const serverListQuery = useQuery({
|
||||
const serverListQuery = useLiveQuery({
|
||||
type: 'server.list',
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { RadarProvider } from '@colanode/ui/components/radar-provider';
|
||||
import { ServerProvider } from '@colanode/ui/components/servers/server-provider';
|
||||
import { DelayedComponent } from '@colanode/ui/components/ui/delayed-component';
|
||||
import { AppContext } from '@colanode/ui/contexts/app';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface AppProps {
|
||||
type: AppType;
|
||||
@@ -18,11 +18,11 @@ export const App = ({ type }: AppProps) => {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [openLogin, setOpenLogin] = useState(false);
|
||||
|
||||
const appMetadataListQuery = useQuery({
|
||||
const appMetadataListQuery = useLiveQuery({
|
||||
type: 'app.metadata.list',
|
||||
});
|
||||
|
||||
const accountListQuery = useQuery({
|
||||
const accountListQuery = useLiveQuery({
|
||||
type: 'account.list',
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
|
||||
import { AvatarFallback } from '@colanode/ui/components/avatars/avatar-fallback';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { AvatarProps, getAvatarSizeClasses } from '@colanode/ui/lib/avatars';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const AvatarImage = (props: AvatarProps) => {
|
||||
const account = useAccount();
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
const { data, isPending } = useQuery(
|
||||
const { data, isPending } = useLiveQuery(
|
||||
{
|
||||
type: 'avatar.url.get',
|
||||
accountId: account.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
|
||||
import { useRadar } from '@colanode/ui/contexts/radar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface ChannelContainerTabProps {
|
||||
channelId: string;
|
||||
@@ -17,7 +17,7 @@ export const ChannelContainerTab = ({
|
||||
const workspace = useWorkspace();
|
||||
const radar = useRadar();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: channelId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalChatNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface ChatBreadcrumbItemProps {
|
||||
chat: LocalChatNode;
|
||||
@@ -17,7 +17,7 @@ export const ChatBreadcrumbItem = ({ chat }: ChatBreadcrumbItemProps) => {
|
||||
) ?? '')
|
||||
: '';
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
|
||||
import { useRadar } from '@colanode/ui/contexts/radar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface ChatContainerTabProps {
|
||||
chatId: string;
|
||||
@@ -17,7 +17,7 @@ export const ChatContainerTab = ({
|
||||
const workspace = useWorkspace();
|
||||
const radar = useRadar();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: chatId,
|
||||
accountId: workspace.accountId,
|
||||
@@ -31,7 +31,7 @@ export const ChatContainerTab = ({
|
||||
) ?? '')
|
||||
: '';
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useRadar } from '@colanode/ui/contexts/radar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface ChatSidebarItemProps {
|
||||
@@ -23,7 +23,7 @@ export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
|
||||
(id) => id !== workspace.userId
|
||||
) ?? '';
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { timeAgo } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface NodeCollaboratorAuditProps {
|
||||
collaboratorId: string;
|
||||
@@ -14,7 +14,7 @@ export const NodeCollaboratorAudit = ({
|
||||
}: NodeCollaboratorAuditProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@colanode/ui/components/ui/popover';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface NodeCollaboratorSearchProps {
|
||||
excluded: string[];
|
||||
@@ -37,7 +37,7 @@ export const NodeCollaboratorSearch = ({
|
||||
const [query, setQuery] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const userSearchQuery = useQuery({
|
||||
const userSearchQuery = useLiveQuery({
|
||||
type: 'user.search',
|
||||
searchQuery: query,
|
||||
exclude: excluded,
|
||||
|
||||
@@ -5,8 +5,8 @@ import { NodeRole } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { NodeCollaboratorRoleDropdown } from '@colanode/ui/components/collaborators/node-collaborator-role-dropdown';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
|
||||
interface NodeCollaboratorProps {
|
||||
nodeId: string;
|
||||
@@ -26,7 +26,7 @@ export const NodeCollaborator = ({
|
||||
const workspace = useWorkspace();
|
||||
const { mutate } = useMutation();
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface BoardViewColumnsCollaboratorProps {
|
||||
field: CollaboratorFieldAttributes;
|
||||
@@ -26,7 +26,7 @@ export const BoardViewColumnsCollaborator = ({
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
const collaboratorCountQuery = useQuery({
|
||||
const collaboratorCountQuery = useLiveQuery({
|
||||
type: 'record.field.value.count',
|
||||
databaseId: database.id,
|
||||
filters: view.filters,
|
||||
@@ -200,7 +200,7 @@ const BoardViewColumnCollaboratorHeader = ({
|
||||
}: BoardViewColumnCollaboratorHeaderProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const userQuery = useQuery(
|
||||
const userQuery = useLiveQuery(
|
||||
{
|
||||
type: 'user.get',
|
||||
userId: collaborator ?? '',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface BoardViewColumnsCreatedByProps {
|
||||
field: CreatedByFieldAttributes;
|
||||
@@ -24,7 +24,7 @@ export const BoardViewColumnsCreatedBy = ({
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
const createdByCountQuery = useQuery({
|
||||
const createdByCountQuery = useLiveQuery({
|
||||
type: 'record.field.value.count',
|
||||
databaseId: database.id,
|
||||
filters: view.filters,
|
||||
@@ -89,7 +89,7 @@ const BoardViewColumnCreatedByHeader = ({
|
||||
}: BoardViewColumnCreatedByHeaderProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const userQuery = useQuery({
|
||||
const userQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
userId: createdBy,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases';
|
||||
|
||||
interface BoardViewColumnsMultiSelectProps {
|
||||
@@ -27,7 +27,7 @@ export const BoardViewColumnsMultiSelect = ({
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
const selectOptionCountQuery = useQuery({
|
||||
const selectOptionCountQuery = useLiveQuery({
|
||||
type: 'record.field.value.count',
|
||||
databaseId: database.id,
|
||||
filters: view.filters,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases';
|
||||
|
||||
interface BoardViewColumnsSelectProps {
|
||||
@@ -26,7 +26,7 @@ export const BoardViewColumnsSelect = ({
|
||||
const database = useDatabase();
|
||||
const view = useDatabaseView();
|
||||
|
||||
const selectOptionCountQuery = useQuery({
|
||||
const selectOptionCountQuery = useLiveQuery({
|
||||
type: 'record.field.value.count',
|
||||
databaseId: database.id,
|
||||
filters: view.filters,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface CalendarViewNoValueCountProps {
|
||||
field: FieldAttributes;
|
||||
@@ -36,7 +36,7 @@ export const CalendarViewNoValueCount = ({
|
||||
},
|
||||
];
|
||||
|
||||
const noValueCountQuery = useQuery({
|
||||
const noValueCountQuery = useLiveQuery({
|
||||
type: 'record.field.value.count',
|
||||
databaseId: database.id,
|
||||
filters: filters,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalDatabaseNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface DatabaseContainerTabProps {
|
||||
databaseId: string;
|
||||
@@ -12,7 +12,7 @@ export const DatabaseContainerTab = ({
|
||||
}: DatabaseContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: databaseId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@colanode/ui/components/ui/popover';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface DatabaseSelectProps {
|
||||
@@ -29,7 +29,7 @@ export const DatabaseSelect = ({ id, onChange }: DatabaseSelectProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const databaseListQuery = useQuery({
|
||||
const databaseListQuery = useLiveQuery({
|
||||
type: 'database.list',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -6,14 +6,14 @@ import { ScrollBar } from '@colanode/ui/components/ui/scroll-area';
|
||||
import { useDatabase } from '@colanode/ui/contexts/database';
|
||||
import { DatabaseViewsContext } from '@colanode/ui/contexts/database-views';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const DatabaseViews = () => {
|
||||
const workspace = useWorkspace();
|
||||
const database = useDatabase();
|
||||
const [activeViewId, setActiveViewId] = useState<string | null>(null);
|
||||
|
||||
const databaseViewListQuery = useQuery({
|
||||
const databaseViewListQuery = useLiveQuery({
|
||||
type: 'database.view.list',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { UserSearch } from '@colanode/ui/components/users/user-search';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
import {
|
||||
collaboratorFieldFilterOperators,
|
||||
createdByFieldFilterOperators,
|
||||
@@ -71,7 +71,7 @@ export const ViewCollaboratorFieldFilter = ({
|
||||
) ?? collaboratorFieldFilterOperators[0]!;
|
||||
|
||||
const collaboratorIds = (filter.value as string[]) ?? [];
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
collaboratorIds.map((id) => ({
|
||||
type: 'user.get',
|
||||
userId: id,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { UserSearch } from '@colanode/ui/components/users/user-search';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
import { createdByFieldFilterOperators } from '@colanode/ui/lib/databases';
|
||||
|
||||
interface ViewCreatedByFieldFilterProps {
|
||||
@@ -63,7 +63,7 @@ export const ViewCreatedByFieldFilter = ({
|
||||
) ?? createdByFieldFilterOperators[0]!;
|
||||
|
||||
const collaboratorIds = (filter.value as string[]) ?? [];
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
collaboratorIds.map((id) => ({
|
||||
type: 'user.get',
|
||||
userId: id,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { UserSearch } from '@colanode/ui/components/users/user-search';
|
||||
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
import { updatedByFieldFilterOperators } from '@colanode/ui/lib/databases';
|
||||
|
||||
interface ViewUpdatedByFieldFilterProps {
|
||||
@@ -68,7 +68,7 @@ export const ViewUpdatedByFieldFilter = ({
|
||||
) ?? updatedByFieldFilterOperators[0]!;
|
||||
|
||||
const collaboratorIds = (filter.value as string[]) ?? [];
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
collaboratorIds.map((id) => ({
|
||||
type: 'user.get',
|
||||
userId: id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { FocusPosition } from '@tiptap/core';
|
||||
import { LocalNode } from '@colanode/client/types';
|
||||
import { DocumentEditor } from '@colanode/ui/components/documents/document-editor';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface DocumentProps {
|
||||
node: LocalNode;
|
||||
@@ -14,14 +14,14 @@ interface DocumentProps {
|
||||
export const Document = ({ node, canEdit, autoFocus }: DocumentProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const documentStateQuery = useQuery({
|
||||
const documentStateQuery = useLiveQuery({
|
||||
type: 'document.state.get',
|
||||
documentId: node.id,
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const documentUpdatesQuery = useQuery({
|
||||
const documentUpdatesQuery = useLiveQuery({
|
||||
type: 'document.updates.list',
|
||||
documentId: node.id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -2,12 +2,12 @@ import { Download } from 'lucide-react';
|
||||
|
||||
import { SaveStatus } from '@colanode/client/types';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const DownloadsContainerTab = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const fileSaveListQuery = useQuery({
|
||||
const fileSaveListQuery = useLiveQuery({
|
||||
type: 'file.save.list',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -10,12 +10,12 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@colanode/ui/components/ui/tooltip';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const DownloadsList = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const fileSaveListQuery = useQuery({
|
||||
const fileSaveListQuery = useLiveQuery({
|
||||
type: 'file.save.list',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EmojiPickerItemsRow } from '@colanode/client/types';
|
||||
import { EmojiPickerItem } from '@colanode/ui/components/emojis/emoji-picker-item';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface EmojiBrowserItemsProps {
|
||||
row: EmojiPickerItemsRow;
|
||||
@@ -8,7 +8,7 @@ interface EmojiBrowserItemsProps {
|
||||
}
|
||||
|
||||
export const EmojiBrowserItems = ({ row, style }: EmojiBrowserItemsProps) => {
|
||||
const emojiListQuery = useQuery({
|
||||
const emojiListQuery = useLiveQuery({
|
||||
type: 'emoji.list',
|
||||
category: row.category,
|
||||
page: row.page,
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useMemo, useRef } from 'react';
|
||||
import { EmojiPickerRowData } from '@colanode/client/types';
|
||||
import { EmojiBrowserCategory } from '@colanode/ui/components/emojis/emoji-browser-category';
|
||||
import { EmojiBrowserItems } from '@colanode/ui/components/emojis/emoji-browser-items';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
const EMOJIS_PER_ROW = 10;
|
||||
|
||||
export const EmojiBrowser = () => {
|
||||
const emojiCategoryListQuery = useQuery({
|
||||
const emojiCategoryListQuery = useLiveQuery({
|
||||
type: 'emoji.category.list',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EmojiPickerItem } from '@colanode/ui/components/emojis/emoji-picker-item';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface EmojiSearchProps {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export const EmojiSearch = ({ query }: EmojiSearchProps) => {
|
||||
const emojiSearchQuery = useQuery({
|
||||
const emojiSearchQuery = useLiveQuery({
|
||||
type: 'emoji.search',
|
||||
query,
|
||||
count: 100,
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@colanode/ui/components/ui/popover';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { defaultEmojis } from '@colanode/ui/lib/assets';
|
||||
|
||||
interface EmojiSkinToneSelectorProps {
|
||||
@@ -20,7 +20,7 @@ export const EmojiSkinToneSelector = ({
|
||||
}: EmojiSkinToneSelectorProps) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const emojiGetQuery = useQuery({
|
||||
const emojiGetQuery = useLiveQuery({
|
||||
type: 'emoji.get',
|
||||
id: defaultEmojis.hand,
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { LocalFileNode } from '@colanode/client/types';
|
||||
import { FilePreview } from '@colanode/ui/components/files/file-preview';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
|
||||
interface FileBlockProps {
|
||||
id: string;
|
||||
@@ -13,7 +13,7 @@ export const FileBlock = ({ id }: FileBlockProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const layout = useLayout();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalFileNode } from '@colanode/client/types';
|
||||
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface FileContainerTabProps {
|
||||
fileId: string;
|
||||
@@ -10,7 +10,7 @@ interface FileContainerTabProps {
|
||||
export const FileContainerTab = ({ fileId }: FileContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: fileId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -7,8 +7,8 @@ import { FilePreviewAudio } from '@colanode/ui/components/files/previews/file-pr
|
||||
import { FilePreviewImage } from '@colanode/ui/components/files/previews/file-preview-image';
|
||||
import { FilePreviewVideo } from '@colanode/ui/components/files/previews/file-preview-video';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
|
||||
interface FilePreviewProps {
|
||||
file: LocalFileNode;
|
||||
@@ -18,7 +18,7 @@ export const FilePreview = ({ file }: FilePreviewProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const mutation = useMutation();
|
||||
|
||||
const fileStateQuery = useQuery({
|
||||
const fileStateQuery = useLiveQuery({
|
||||
type: 'file.state.get',
|
||||
id: file.id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { useApp } from '@colanode/ui/contexts/app';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
|
||||
interface FileSaveButtonProps {
|
||||
file: LocalFileNode;
|
||||
@@ -22,7 +22,7 @@ export const FileSaveButton = ({ file }: FileSaveButtonProps) => {
|
||||
const layout = useLayout();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const fileStateQuery = useQuery({
|
||||
const fileStateQuery = useLiveQuery({
|
||||
type: 'file.state.get',
|
||||
id: file.id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { formatBytes, formatDate } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface FileSidebarProps {
|
||||
file: LocalFileNode;
|
||||
@@ -23,7 +23,7 @@ const FileMeta = ({ title, value }: { title: string; value: string }) => {
|
||||
export const FileSidebar = ({ file }: FileSidebarProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalFileNode } from '@colanode/client/types';
|
||||
import { FileIcon } from '@colanode/ui/components/files/file-icon';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface FileThumbnailProps {
|
||||
@@ -12,7 +12,7 @@ interface FileThumbnailProps {
|
||||
export const FileThumbnail = ({ file, className }: FileThumbnailProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const fileStateGetQuery = useQuery({
|
||||
const fileStateGetQuery = useLiveQuery({
|
||||
type: 'file.state.get',
|
||||
id: file.id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalFolderNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface FolderContainerTabProps {
|
||||
folderId: string;
|
||||
@@ -10,7 +10,7 @@ interface FolderContainerTabProps {
|
||||
export const FolderContainerTab = ({ folderId }: FolderContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: folderId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ListLayout } from '@colanode/ui/components/folders/lists/list-layout';
|
||||
import { FolderContext } from '@colanode/ui/contexts/folder';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
|
||||
const FILES_PER_PAGE = 100;
|
||||
|
||||
@@ -39,7 +39,7 @@ export const FolderFiles = ({
|
||||
page: i + 1,
|
||||
}));
|
||||
|
||||
const result = useQueries(inputs);
|
||||
const result = useLiveQueries(inputs);
|
||||
const files = result.flatMap((data) => data.data ?? []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconPickerItemsRow } from '@colanode/client/types';
|
||||
import { IconPickerItem } from '@colanode/ui/components/icons/icon-picker-item';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface IconBrowserItemsProps {
|
||||
row: IconPickerItemsRow;
|
||||
@@ -8,7 +8,7 @@ interface IconBrowserItemsProps {
|
||||
}
|
||||
|
||||
export const IconBrowserItems = ({ row, style }: IconBrowserItemsProps) => {
|
||||
const iconListQuery = useQuery({
|
||||
const iconListQuery = useLiveQuery({
|
||||
type: 'icon.list',
|
||||
category: row.category,
|
||||
page: row.page,
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useMemo, useRef } from 'react';
|
||||
import { IconPickerRowData } from '@colanode/client/types';
|
||||
import { IconBrowserCategory } from '@colanode/ui/components/icons/icon-browser-category';
|
||||
import { IconBrowserItems } from '@colanode/ui/components/icons/icon-browser-items';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
const ICONS_PER_ROW = 10;
|
||||
|
||||
export const IconBrowser = () => {
|
||||
const iconCategoryListQuery = useQuery({
|
||||
const iconCategoryListQuery = useLiveQuery({
|
||||
type: 'icon.category.list',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { IconPickerItem } from '@colanode/ui/components/icons/icon-picker-item';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface IconSearchProps {
|
||||
query: string;
|
||||
}
|
||||
|
||||
export const IconSearch = ({ query }: IconSearchProps) => {
|
||||
const iconSearchQuery = useQuery({
|
||||
const iconSearchQuery = useLiveQuery({
|
||||
type: 'icon.search',
|
||||
query,
|
||||
count: 100,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { RecordContainer } from '@colanode/ui/components/records/record-containe
|
||||
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 { WorkspaceStorage } from '@colanode/ui/components/workspaces/workspace-storage';
|
||||
import { WorkspaceUsers } from '@colanode/ui/components/workspaces/workspace-users';
|
||||
|
||||
interface ContainerTabContentProps {
|
||||
@@ -43,6 +44,10 @@ const ContainerTabContentBody = ({ tab }: ContainerTabContentProps) => {
|
||||
return <AccountLogout />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.WorkspaceStorage) {
|
||||
return <WorkspaceStorage />;
|
||||
}
|
||||
|
||||
return match(getIdType(tab.path))
|
||||
.with(IdType.Space, () => <SpaceContainer spaceId={tab.path} />)
|
||||
.with(IdType.Channel, () => <ChannelContainer channelId={tab.path} />)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { RecordContainerTab } from '@colanode/ui/components/records/record-conta
|
||||
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 { WorkspaceStorageTab } from '@colanode/ui/components/workspaces/workspace-storage-tab';
|
||||
import { WorkspaceUsersTab } from '@colanode/ui/components/workspaces/workspace-users-tab';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
@@ -50,6 +51,10 @@ const ContainerTabTriggerContent = ({ tab }: { tab: ContainerTab }) => {
|
||||
return <AccountLogoutTab />;
|
||||
}
|
||||
|
||||
if (tab.path === SpecialContainerTabPath.WorkspaceStorage) {
|
||||
return <WorkspaceStorageTab />;
|
||||
}
|
||||
|
||||
return match(getIdType(tab.path))
|
||||
.with(IdType.Space, () => <SpaceContainerTab spaceId={tab.path} />)
|
||||
.with(IdType.Channel, () => (
|
||||
|
||||
@@ -3,14 +3,14 @@ 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';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
export const SidebarChats = () => {
|
||||
const workspace = useWorkspace();
|
||||
const layout = useLayout();
|
||||
|
||||
const chatListQuery = useQuery({
|
||||
const chatListQuery = useLiveQuery({
|
||||
type: 'chat.list',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
|
||||
import { AccountContext, useAccount } from '@colanode/ui/contexts/account';
|
||||
import { useApp } from '@colanode/ui/contexts/app';
|
||||
import { useRadar } from '@colanode/ui/contexts/radar';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export function SidebarMenuFooter() {
|
||||
const app = useApp();
|
||||
@@ -23,7 +23,7 @@ export function SidebarMenuFooter() {
|
||||
const radar = useRadar();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const accountListQuery = useQuery({
|
||||
const accountListQuery = useLiveQuery({
|
||||
type: 'account.list',
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { useRadar } from '@colanode/ui/contexts/radar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const SidebarMenuHeader = () => {
|
||||
const workspace = useWorkspace();
|
||||
@@ -22,7 +22,7 @@ export const SidebarMenuHeader = () => {
|
||||
const radar = useRadar();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const workspaceListQuery = useQuery({
|
||||
const workspaceListQuery = useLiveQuery({
|
||||
type: 'workspace.list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogOut, Settings, Users } from 'lucide-react';
|
||||
import { Cylinder, LogOut, Settings, Users } from 'lucide-react';
|
||||
|
||||
import { SpecialContainerTabPath } from '@colanode/client/types';
|
||||
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
|
||||
@@ -20,6 +20,11 @@ export const SidebarSettings = () => {
|
||||
icon={Users}
|
||||
path={SpecialContainerTabPath.WorkspaceUsers}
|
||||
/>
|
||||
<SidebarSettingsItem
|
||||
title="Storage"
|
||||
icon={Cylinder}
|
||||
path={SpecialContainerTabPath.WorkspaceStorage}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||
<SidebarHeader title="Account settings" />
|
||||
|
||||
@@ -2,14 +2,14 @@ import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-
|
||||
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';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const SidebarSpaces = () => {
|
||||
const workspace = useWorkspace();
|
||||
const canCreateSpace =
|
||||
workspace.role !== 'guest' && workspace.role !== 'none';
|
||||
|
||||
const spaceListQuery = useQuery({
|
||||
const spaceListQuery = useLiveQuery({
|
||||
type: 'space.list',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalMessageNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface MessageAuthorAvatarProps {
|
||||
message: LocalMessageNode;
|
||||
@@ -13,7 +13,7 @@ export const MessageAuthorAvatar = ({
|
||||
className,
|
||||
}: MessageAuthorAvatarProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LocalMessageNode } from '@colanode/client/types';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface MessageAuthorNameProps {
|
||||
@@ -14,7 +14,7 @@ export const MessageAuthorName = ({
|
||||
}: MessageAuthorNameProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { compareString } from '@colanode/core';
|
||||
import { Message } from '@colanode/ui/components/messages/message';
|
||||
import { useConversation } from '@colanode/ui/contexts/conversation';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
|
||||
const MESSAGES_PER_PAGE = 50;
|
||||
|
||||
@@ -28,7 +28,7 @@ export const MessageList = () => {
|
||||
count: MESSAGES_PER_PAGE,
|
||||
}));
|
||||
|
||||
const result = useQueries(inputs);
|
||||
const result = useLiveQueries(inputs);
|
||||
const messages = result
|
||||
.flatMap((data) => data.data ?? [])
|
||||
.sort((a, b) => compareString(a.id, b.id));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EmojiElement } from '@colanode/ui/components/emojis/emoji-element';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface MessageQuickReactionProps {
|
||||
emoji: string;
|
||||
@@ -10,7 +10,7 @@ export const MessageQuickReaction = ({
|
||||
emoji,
|
||||
onClick,
|
||||
}: MessageQuickReactionProps) => {
|
||||
const emojiGetQuery = useQuery({
|
||||
const emojiGetQuery = useLiveQuery({
|
||||
type: 'emoji.get',
|
||||
id: emoji,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NodeReactionCount, LocalMessageNode } from '@colanode/client/types';
|
||||
import { EmojiElement } from '@colanode/ui/components/emojis/emoji-element';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface MessageReactionCountTooltipContentProps {
|
||||
message: LocalMessageNode;
|
||||
@@ -15,12 +15,12 @@ export const MessageReactionCountTooltipContent = ({
|
||||
}: MessageReactionCountTooltipContentProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const emojiGetQuery = useQuery({
|
||||
const emojiGetQuery = useLiveQuery({
|
||||
type: 'emoji.get.by.skin.id',
|
||||
id: reactionCount.reaction,
|
||||
});
|
||||
|
||||
const nodeReactionListQuery = useQuery({
|
||||
const nodeReactionListQuery = useLiveQuery({
|
||||
type: 'node.reaction.list',
|
||||
nodeId: message.id,
|
||||
reaction: reactionCount.reaction,
|
||||
@@ -34,7 +34,7 @@ export const MessageReactionCountTooltipContent = ({
|
||||
nodeReactionListQuery.data?.map((reaction) => reaction.collaboratorId) ??
|
||||
[];
|
||||
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
userIds.map((userId) => ({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { NodeReactionListQueryInput } from '@colanode/client/queries';
|
||||
import { NodeReactionCount, LocalMessageNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
|
||||
const REACTIONS_PER_PAGE = 20;
|
||||
|
||||
@@ -33,7 +33,7 @@ export const MessageReactionCountsDialogList = ({
|
||||
count: REACTIONS_PER_PAGE,
|
||||
}));
|
||||
|
||||
const result = useQueries(inputs);
|
||||
const result = useLiveQueries(inputs);
|
||||
const reactions = result.flatMap((data) => data.data ?? []);
|
||||
const isPending = result.some((data) => data.isPending);
|
||||
const hasMore =
|
||||
@@ -41,7 +41,7 @@ export const MessageReactionCountsDialogList = ({
|
||||
|
||||
const userIds = reactions?.map((reaction) => reaction.collaboratorId) ?? [];
|
||||
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
userIds.map((userId) => ({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -6,8 +6,8 @@ import { EmojiElement } from '@colanode/ui/components/emojis/emoji-element';
|
||||
import { MessageReactionCountTooltip } from '@colanode/ui/components/messages/message-reaction-count-tooltip';
|
||||
import { MessageReactionCountsDialog } from '@colanode/ui/components/messages/message-reaction-counts-dialog';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface MessageReactionCountsProps {
|
||||
@@ -22,7 +22,7 @@ export const MessageReactionCounts = ({
|
||||
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const nodeReactionsAggregateQuery = useQuery({
|
||||
const nodeReactionsAggregateQuery = useLiveQuery({
|
||||
type: 'node.reactions.aggregate',
|
||||
nodeId: message.id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MessageAuthorAvatar } from '@colanode/ui/components/messages/message-au
|
||||
import { MessageAuthorName } from '@colanode/ui/components/messages/message-author-name';
|
||||
import { MessageContent } from '@colanode/ui/components/messages/message-content';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface MessageReferenceProps {
|
||||
messageId: string;
|
||||
@@ -11,7 +11,7 @@ interface MessageReferenceProps {
|
||||
|
||||
export const MessageReference = ({ messageId }: MessageReferenceProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: messageId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CircleX } from 'lucide-react';
|
||||
|
||||
import { LocalMessageNode } from '@colanode/client/types';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface MessageReplyBannerProps {
|
||||
message: LocalMessageNode;
|
||||
@@ -14,7 +14,7 @@ export const MessageReplyBanner = ({
|
||||
onCancel,
|
||||
}: MessageReplyBannerProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalPageNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface PageContainerTabProps {
|
||||
pageId: string;
|
||||
@@ -10,7 +10,7 @@ interface PageContainerTabProps {
|
||||
export const PageContainerTab = ({ pageId }: PageContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: pageId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getIdType, IdType } from '@colanode/core';
|
||||
import { RadarContext } from '@colanode/ui/contexts/radar';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface RadarProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const RadarProvider = ({ children }: RadarProviderProps) => {
|
||||
const radarDataQuery = useQuery({
|
||||
const radarDataQuery = useLiveQuery({
|
||||
type: 'radar.data.get',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalRecordNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface RecordContainerTabProps {
|
||||
recordId: string;
|
||||
@@ -10,7 +10,7 @@ interface RecordContainerTabProps {
|
||||
export const RecordContainerTab = ({ recordId }: RecordContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: recordId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LocalDatabaseNode } from '@colanode/client/types';
|
||||
import { NodeRole } from '@colanode/core';
|
||||
import { Database } from '@colanode/ui/components/databases/database';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface RecordDatabaseProps {
|
||||
id: string;
|
||||
@@ -13,7 +13,7 @@ interface RecordDatabaseProps {
|
||||
export const RecordDatabase = ({ id, role, children }: RecordDatabaseProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@colanode/ui/components/ui/command';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface RecordSearchProps {
|
||||
exclude?: string[];
|
||||
@@ -27,7 +27,7 @@ export const RecordSearch = ({
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const recordSearchQuery = useQuery({
|
||||
const recordSearchQuery = useLiveQuery({
|
||||
type: 'record.search',
|
||||
searchQuery: query,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { UserSearch } from '@colanode/ui/components/users/user-search';
|
||||
import { useRecord } from '@colanode/ui/contexts/record';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
|
||||
interface RecordCollaboratorValueProps {
|
||||
field: CollaboratorFieldAttributes;
|
||||
@@ -45,7 +45,7 @@ export const RecordCollaboratorValue = ({
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const collaboratorIds = record.getCollaboratorValue(field) ?? [];
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
collaboratorIds.map((id) => ({
|
||||
type: 'user.get',
|
||||
userId: id,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CreatedByFieldAttributes } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useRecord } from '@colanode/ui/contexts/record';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface RecordCreatedByValueProps {
|
||||
field: CreatedByFieldAttributes;
|
||||
@@ -11,7 +11,7 @@ interface RecordCreatedByValueProps {
|
||||
export const RecordCreatedByValue = ({ field }: RecordCreatedByValueProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const record = useRecord();
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { useRecord } from '@colanode/ui/contexts/record';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQueries } from '@colanode/ui/hooks/use-queries';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
|
||||
interface RecordRelationValueProps {
|
||||
field: RelationFieldAttributes;
|
||||
@@ -45,7 +45,7 @@ export const RecordRelationValue = ({
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const relationIds = record.getRelationValue(field) ?? [];
|
||||
const results = useQueries(
|
||||
const results = useLiveQueries(
|
||||
relationIds.map((id) => ({
|
||||
type: 'node.get',
|
||||
nodeId: id,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { UpdatedByFieldAttributes } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useRecord } from '@colanode/ui/contexts/record';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface RecordUpdatedByValueProps {
|
||||
field: UpdatedByFieldAttributes;
|
||||
@@ -14,7 +14,7 @@ export const RecordUpdatedByValue = ({ field }: RecordUpdatedByValueProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const record = useRecord();
|
||||
|
||||
const { data } = useQuery(
|
||||
const { data } = useLiveQuery(
|
||||
{
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ServerNotFound } from '@colanode/ui/components/servers/server-not-found';
|
||||
import { ServerContext } from '@colanode/ui/contexts/server';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { isFeatureSupported } from '@colanode/ui/lib/features';
|
||||
|
||||
interface ServerProviderProps {
|
||||
@@ -9,7 +9,7 @@ interface ServerProviderProps {
|
||||
}
|
||||
|
||||
export const ServerProvider = ({ domain, children }: ServerProviderProps) => {
|
||||
const serverListQuery = useQuery({
|
||||
const serverListQuery = useLiveQuery({
|
||||
type: 'server.list',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalSpaceNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface SpaceContainerTabProps {
|
||||
spaceId: string;
|
||||
@@ -10,7 +10,7 @@ interface SpaceContainerTabProps {
|
||||
export const SpaceContainerTab = ({ spaceId }: SpaceContainerTabProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: spaceId,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
} from '@colanode/ui/components/ui/collapsible';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { sortSpaceChildren } from '@colanode/ui/lib/spaces';
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
@@ -32,7 +32,7 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
|
||||
const role = extractNodeRole(space, workspace.userId);
|
||||
const canEdit = role === 'admin';
|
||||
|
||||
const nodeChildrenGetQuery = useQuery({
|
||||
const nodeChildrenGetQuery = useLiveQuery({
|
||||
type: 'node.children.get',
|
||||
nodeId: space.id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
114
packages/ui/src/components/ui/table.tsx
Normal file
114
packages/ui/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn('[&_tr]:border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
CommandList,
|
||||
} from '@colanode/ui/components/ui/command';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface UserSearchProps {
|
||||
exclude?: string[];
|
||||
@@ -22,7 +22,7 @@ export const UserSearch = ({ exclude, onSelect }: UserSearchProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const userSearchQuery = useQuery({
|
||||
const userSearchQuery = useLiveQuery({
|
||||
type: 'user.search',
|
||||
searchQuery: query,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
143
packages/ui/src/components/workspaces/storage-stats.tsx
Normal file
143
packages/ui/src/components/workspaces/storage-stats.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { FileSubtype, formatBytes } from '@colanode/core';
|
||||
import { bigintToPercent } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface SubtypeMetadata {
|
||||
subtype: FileSubtype;
|
||||
color: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const subtypeMetadata: SubtypeMetadata[] = [
|
||||
{
|
||||
subtype: 'image',
|
||||
color: 'bg-blue-500',
|
||||
name: 'Images',
|
||||
},
|
||||
{
|
||||
subtype: 'video',
|
||||
color: 'bg-green-500',
|
||||
name: 'Videos',
|
||||
},
|
||||
{
|
||||
subtype: 'audio',
|
||||
color: 'bg-purple-500',
|
||||
name: 'Audio',
|
||||
},
|
||||
{
|
||||
subtype: 'pdf',
|
||||
color: 'bg-red-500',
|
||||
name: 'PDFs',
|
||||
},
|
||||
{
|
||||
subtype: 'other',
|
||||
color: 'bg-gray-500',
|
||||
name: 'Other Files',
|
||||
},
|
||||
];
|
||||
|
||||
interface StorageSubtype {
|
||||
subtype: string;
|
||||
size: string;
|
||||
}
|
||||
|
||||
interface StorageStatsProps {
|
||||
usedBytes: bigint;
|
||||
limitBytes: bigint | null;
|
||||
subtypes: StorageSubtype[];
|
||||
}
|
||||
|
||||
export const StorageStats = ({
|
||||
usedBytes,
|
||||
limitBytes,
|
||||
subtypes,
|
||||
}: StorageStatsProps) => {
|
||||
const usedPercentage = limitBytes
|
||||
? bigintToPercent(limitBytes, usedBytes)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<span className="text-2xl font-medium">{formatBytes(usedBytes)}</span>
|
||||
<span className="text-xl text-muted-foreground">
|
||||
{' '}
|
||||
of {limitBytes ? formatBytes(limitBytes) : 'Unlimited'}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
({usedPercentage}%) used
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-3 bg-gray-200 rounded-full overflow-hidden flex">
|
||||
{subtypes.map((subtype) => {
|
||||
const size = BigInt(subtype.size);
|
||||
if (size === BigInt(0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentage = limitBytes ? bigintToPercent(limitBytes, size) : 0;
|
||||
const metadata = subtypeMetadata.find(
|
||||
(m) => m.subtype === subtype.subtype
|
||||
);
|
||||
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = metadata.name;
|
||||
const color = metadata.color;
|
||||
const title = `${name}: ${formatBytes(BigInt(subtype.size))}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={subtype.subtype}
|
||||
className={color}
|
||||
style={{ width: `${percentage}%` }}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{usedPercentage < 100 && (
|
||||
<div
|
||||
className="bg-gray-200"
|
||||
style={{ width: `${100 - usedPercentage}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6 space-y-2">
|
||||
{subtypes.map((subtype) => {
|
||||
const size = BigInt(subtype.size);
|
||||
if (size === BigInt(0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = subtypeMetadata.find(
|
||||
(m) => m.subtype === subtype.subtype
|
||||
);
|
||||
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = metadata.name;
|
||||
const color = metadata.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={subtype.subtype}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${color}`} />
|
||||
<span className="text-sm">{name}</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatBytes(BigInt(subtype.size))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
42
packages/ui/src/components/workspaces/user-storage-stats.tsx
Normal file
42
packages/ui/src/components/workspaces/user-storage-stats.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { StorageStats } from '@colanode/ui/components/workspaces/storage-stats';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const UserStorageStats = () => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const userStorageGetQuery = useLiveQuery({
|
||||
type: 'user.storage.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const data = userStorageGetQuery.data ?? {
|
||||
limit: '0',
|
||||
used: '0',
|
||||
subtypes: [],
|
||||
};
|
||||
const usedBytes = BigInt(data.used);
|
||||
const limitBytes = BigInt(data.limit);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">My storage</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
{userStorageGetQuery.isPending ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading storage data...
|
||||
</div>
|
||||
) : (
|
||||
<StorageStats
|
||||
usedBytes={usedBytes}
|
||||
limitBytes={limitBytes}
|
||||
subtypes={data.subtypes}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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,18 +1,10 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { Settings } from 'lucide-react';
|
||||
|
||||
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>
|
||||
<Settings className="size-4" />
|
||||
<span>Workspace Settings</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
Container,
|
||||
ContainerBody,
|
||||
ContainerHeader,
|
||||
} from '@colanode/ui/components/ui/container';
|
||||
import { Container, ContainerBody } 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';
|
||||
|
||||
@@ -19,55 +14,50 @@ export const WorkspaceSettings = () => {
|
||||
|
||||
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"
|
||||
/>
|
||||
<ContainerBody className="max-w-4xl 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 className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Danger Zone
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<WorkspaceDelete />
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { BadgeAlert, ServerCog } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { Separator } from '@colanode/ui/components/ui/separator';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { StorageStats } from '@colanode/ui/components/workspaces/storage-stats';
|
||||
import { WorkspaceStorageUserTable } from '@colanode/ui/components/workspaces/workspace-storage-user-table';
|
||||
import { useServer } from '@colanode/ui/contexts/server';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
|
||||
export const WorkspaceStorageStats = () => {
|
||||
const workspace = useWorkspace();
|
||||
const server = useServer();
|
||||
const isFeatureSupported = server.supports('workspace.storage.management');
|
||||
|
||||
const workspaceStorageGetQuery = useQuery(
|
||||
{
|
||||
type: 'workspace.storage.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
{
|
||||
enabled: isFeatureSupported,
|
||||
}
|
||||
);
|
||||
|
||||
const data = workspaceStorageGetQuery.data ?? {
|
||||
limit: '0',
|
||||
used: '0',
|
||||
subtypes: [],
|
||||
users: [],
|
||||
};
|
||||
const usedBytes = BigInt(data.used);
|
||||
const limitBytes = data.limit ? BigInt(data.limit) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Workspace storage
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
{!isFeatureSupported ? (
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<ServerCog className="size-8 text-muted-foreground" />
|
||||
<span>
|
||||
Workspace storage management is not supported by this server
|
||||
version. Please contact your administrator to upgrade the server.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
match(workspaceStorageGetQuery)
|
||||
.with({ isPending: true }, () => (
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<Spinner className="size-5" />
|
||||
<span>Loading storage data from the server...</span>
|
||||
</div>
|
||||
))
|
||||
.with({ isPending: false, isError: false }, () => (
|
||||
<>
|
||||
<StorageStats
|
||||
usedBytes={usedBytes}
|
||||
limitBytes={limitBytes}
|
||||
subtypes={data.subtypes}
|
||||
/>
|
||||
<WorkspaceStorageUserTable
|
||||
users={data.users}
|
||||
onUpdate={() => {
|
||||
workspaceStorageGetQuery.refetch();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<BadgeAlert className="size-8 text-red-400" />
|
||||
<span>
|
||||
Couldn't load storage information from the server. Please make
|
||||
sure that the server is accessible and you have permission to
|
||||
access this workspace storage data.
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Cylinder } from 'lucide-react';
|
||||
|
||||
export const WorkspaceStorageTab = () => {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Cylinder className="size-4" />
|
||||
<span>Workspace Storage</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Settings } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { formatBytes } from '@colanode/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@colanode/ui/components/ui/table';
|
||||
import { WorkspaceStorageUserUpdateDialog } from '@colanode/ui/components/workspaces/workspace-storage-user-update-dialog';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { bigintToPercent, cn } from '@colanode/ui/lib/utils';
|
||||
|
||||
interface WorkspaceStorageUser {
|
||||
id: string;
|
||||
used: string;
|
||||
limit: string;
|
||||
}
|
||||
|
||||
interface UserStorageProgressBarProps {
|
||||
used: bigint;
|
||||
limit: bigint;
|
||||
}
|
||||
|
||||
const UserStorageProgressBar = ({
|
||||
used,
|
||||
limit,
|
||||
}: UserStorageProgressBarProps) => {
|
||||
const percentage = limit > 0n ? bigintToPercent(limit, used) : 0;
|
||||
|
||||
const getBarColor = () => {
|
||||
if (percentage >= 90) return 'bg-red-500';
|
||||
if (percentage >= 70) return 'bg-orange-500';
|
||||
return 'bg-green-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">{formatBytes(used)}</span>
|
||||
<span className="text-muted-foreground">
|
||||
({percentage.toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn('h-full transition-all duration-300', getBarColor())}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceStorageUserRowProps {
|
||||
user: WorkspaceStorageUser;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
const WorkspaceStorageUserRow = ({
|
||||
user,
|
||||
onUpdate,
|
||||
}: WorkspaceStorageUserRowProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const [openUpdateDialog, setOpenUpdateDialog] = useState(false);
|
||||
|
||||
const userQuery = useQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const name = userQuery.data?.name ?? 'Unknown';
|
||||
const email = userQuery.data?.email ?? '';
|
||||
const avatar = userQuery.data?.avatar ?? null;
|
||||
|
||||
const usedBytes = BigInt(user.used);
|
||||
const limitBytes = BigInt(user.limit);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Avatar id={user.id} name={name} avatar={avatar} />
|
||||
<div className="flex-grow min-w-0">
|
||||
<p className="text-sm font-medium leading-none truncate">
|
||||
{name}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className="text-sm font-medium">{formatBytes(limitBytes)}</span>
|
||||
</TableCell>
|
||||
<TableCell className="min-w-[200px] text-center">
|
||||
<UserStorageProgressBar used={usedBytes} limit={limitBytes} />
|
||||
</TableCell>
|
||||
<TableCell className="w-10 text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setOpenUpdateDialog(true)}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{openUpdateDialog && (
|
||||
<WorkspaceStorageUserUpdateDialog
|
||||
userId={user.id}
|
||||
limit={user.limit}
|
||||
open={openUpdateDialog}
|
||||
onOpenChange={setOpenUpdateDialog}
|
||||
onUpdate={() => {
|
||||
onUpdate();
|
||||
setOpenUpdateDialog(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceStorageUserTableProps {
|
||||
users: WorkspaceStorageUser[];
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceStorageUserTable = ({
|
||||
users,
|
||||
onUpdate,
|
||||
}: WorkspaceStorageUserTableProps) => {
|
||||
if (users.length === 0) {
|
||||
return <div className="text-sm text-muted-foreground">No users found.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead className="text-center">Total Storage</TableHead>
|
||||
<TableHead className="text-center">Used Storage</TableHead>
|
||||
<TableHead className="w-10 text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.map((user) => (
|
||||
<WorkspaceStorageUserRow
|
||||
key={user.id}
|
||||
user={user}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Check, ChevronDown } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { Button } from '@colanode/ui/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@colanode/ui/components/ui/dialog';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@colanode/ui/components/ui/dropdown-menu';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@colanode/ui/components/ui/form';
|
||||
import { Input } from '@colanode/ui/components/ui/input';
|
||||
import { Spinner } from '@colanode/ui/components/ui/spinner';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useMutation } from '@colanode/ui/hooks/use-mutation';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
|
||||
const UNITS = [
|
||||
{ label: 'TB', value: 'TB', bytes: 1024 ** 4 },
|
||||
{ label: 'GB', value: 'GB', bytes: 1024 ** 3 },
|
||||
{ label: 'MB', value: 'MB', bytes: 1024 ** 2 },
|
||||
{ label: 'KB', value: 'KB', bytes: 1024 },
|
||||
{ label: 'Bytes', value: 'bytes', bytes: 1 },
|
||||
];
|
||||
|
||||
const convertBytesToUnit = (bytes: string) => {
|
||||
const bytesNum = parseInt(bytes);
|
||||
if (isNaN(bytesNum) || bytesNum === 0) {
|
||||
return { value: '0', unit: 'bytes' };
|
||||
}
|
||||
|
||||
for (const unit of UNITS) {
|
||||
if (bytesNum >= unit.bytes || unit.value === 'bytes') {
|
||||
const value = bytesNum / unit.bytes;
|
||||
return {
|
||||
value: value % 1 === 0 ? value.toString() : value.toFixed(2),
|
||||
unit: unit.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { value: '0', unit: 'bytes' };
|
||||
};
|
||||
|
||||
const convertUnitToBytes = (value: string, unit: string): string => {
|
||||
const unitData = UNITS.find((u) => u.value === unit);
|
||||
const selectedUnit = unitData || UNITS[UNITS.length - 1]!;
|
||||
const numValue = parseFloat(value || '0');
|
||||
return Math.round(numValue * selectedUnit.bytes).toString();
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: string): string => {
|
||||
const num = parseInt(bytes);
|
||||
if (isNaN(num)) return '0';
|
||||
return new Intl.NumberFormat().format(num);
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
limit: z.string().min(1, 'Storage limit is required'),
|
||||
});
|
||||
|
||||
interface WorkspaceStorageUserUpdateDialogProps {
|
||||
userId: string;
|
||||
limit: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onUpdate: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceStorageUserUpdateDialog = ({
|
||||
userId,
|
||||
limit,
|
||||
open,
|
||||
onOpenChange,
|
||||
onUpdate,
|
||||
}: WorkspaceStorageUserUpdateDialogProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const initialLimit = convertBytesToUnit(limit);
|
||||
|
||||
const [limitUnit, setLimitUnit] = useState(initialLimit.unit);
|
||||
|
||||
const userQuery = useQuery({
|
||||
type: 'user.get',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId,
|
||||
});
|
||||
|
||||
const name = userQuery.data?.name ?? 'Unknown';
|
||||
const email = userQuery.data?.email ?? '';
|
||||
const avatar = userQuery.data?.avatar ?? null;
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
limit: initialLimit.value,
|
||||
},
|
||||
});
|
||||
|
||||
const handleCancel = () => {
|
||||
form.reset();
|
||||
setLimitUnit(initialLimit.unit);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
if (isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiValues = {
|
||||
limit: convertUnitToBytes(values.limit, limitUnit),
|
||||
};
|
||||
|
||||
mutate({
|
||||
input: {
|
||||
type: 'user.storage.update',
|
||||
accountId: workspace.accountId,
|
||||
workspaceId: workspace.id,
|
||||
userId,
|
||||
limit: apiValues.limit,
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('User storage settings updated');
|
||||
form.reset();
|
||||
onUpdate();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unit = UNITS.find((u) => u.value === limitUnit);
|
||||
const unitLabel = unit?.label ?? 'bytes';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update storage settings</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the storage limit for this user
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex items-center space-x-3 py-4 border-b">
|
||||
<Avatar id={userId} name={name} avatar={avatar} />
|
||||
<div className="flex-grow min-w-0">
|
||||
<p className="text-sm font-medium leading-none truncate">{name}</p>
|
||||
<p className="text-sm text-muted-foreground truncate">{email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
>
|
||||
<div className="flex-grow space-y-6 py-2 pb-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="limit"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Storage Limit</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="5"
|
||||
{...field}
|
||||
className="flex-1"
|
||||
min="0"
|
||||
step="0.01"
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-20 justify-between"
|
||||
>
|
||||
{unitLabel}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{UNITS.map((unit) => (
|
||||
<DropdownMenuItem
|
||||
key={unit.value}
|
||||
onClick={() => setLimitUnit(unit.value)}
|
||||
className="flex items-center justify-between"
|
||||
>
|
||||
<span>{unit.label}</span>
|
||||
{limitUnit === unit.value && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FormControl>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
={' '}
|
||||
{formatBytes(
|
||||
convertUnitToBytes(field.value || '0', limitUnit)
|
||||
)}{' '}
|
||||
bytes
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
19
packages/ui/src/components/workspaces/workspace-storage.tsx
Normal file
19
packages/ui/src/components/workspaces/workspace-storage.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Container, ContainerBody } from '@colanode/ui/components/ui/container';
|
||||
import { UserStorageStats } from '@colanode/ui/components/workspaces/user-storage-stats';
|
||||
import { WorkspaceStorageStats } from '@colanode/ui/components/workspaces/workspace-storage-stats';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
|
||||
export const WorkspaceStorage = () => {
|
||||
const workspace = useWorkspace();
|
||||
const canManageStorage =
|
||||
workspace.role === 'owner' || workspace.role === 'admin';
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContainerBody className="max-w-4xl space-y-10">
|
||||
<UserStorageStats />
|
||||
{canManageStorage && <WorkspaceStorageStats />}
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,10 @@
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { Users } from 'lucide-react';
|
||||
|
||||
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>
|
||||
<Users className="size-4" />
|
||||
<span>Workspace Users</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,18 +4,13 @@ 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 { Container, ContainerBody } 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';
|
||||
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
|
||||
|
||||
const USERS_PER_PAGE = 50;
|
||||
|
||||
@@ -34,76 +29,69 @@ export const WorkspaceUsers = () => {
|
||||
workspaceId: workspace.id,
|
||||
}));
|
||||
|
||||
const result = useQueries(inputs);
|
||||
const result = useLiveQueries(inputs);
|
||||
const users = result.flatMap((data) => data.data ?? []);
|
||||
const isPending = result.some((data) => data.isPending);
|
||||
const hasMore = !isPending && users.length === lastPage * USERS_PER_PAGE;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<WorkspaceUserInvite workspace={workspace} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ContainerBody className="max-w-4xl space-y-8">
|
||||
{canEditUsers && (
|
||||
<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>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Invite</h2>
|
||||
<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;
|
||||
<WorkspaceUserInvite workspace={workspace} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
<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;
|
||||
|
||||
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}
|
||||
/>
|
||||
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>
|
||||
);
|
||||
})}
|
||||
<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>
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { Layout } from '@colanode/ui/components/layouts/layout';
|
||||
import { useAccount } from '@colanode/ui/contexts/account';
|
||||
import { WorkspaceContext } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface WorkspaceProps {
|
||||
workspace: WorkspaceType;
|
||||
@@ -15,7 +15,7 @@ interface WorkspaceProps {
|
||||
export const Workspace = ({ workspace }: WorkspaceProps) => {
|
||||
const account = useAccount();
|
||||
|
||||
const workspaceMetadataListQuery = useQuery({
|
||||
const workspaceMetadataListQuery = useLiveQuery({
|
||||
type: 'workspace.metadata.list',
|
||||
accountId: account.id,
|
||||
workspaceId: workspace.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { JSONContent } from '@tiptap/core';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { defaultClasses } from '@colanode/ui/editor/classes';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
interface MentionRendererProps {
|
||||
node: JSONContent;
|
||||
@@ -14,7 +14,7 @@ export const MentionRenderer = ({ node }: MentionRendererProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const target = node.attrs?.target;
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
userId: target,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -5,14 +5,14 @@ import { LocalDatabaseNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const DatabaseNodeView = ({ node }: NodeViewProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const layout = useLayout();
|
||||
|
||||
const id = node.attrs.id;
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -5,14 +5,14 @@ import { LocalFolderNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const FolderNodeView = ({ node }: NodeViewProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const layout = useLayout();
|
||||
|
||||
const id = node.attrs.id;
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -4,13 +4,13 @@ import { NodeViewWrapper } from '@tiptap/react';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { defaultClasses } from '@colanode/ui/editor/classes';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const MentionNodeView = ({ node }: NodeViewProps) => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const target = node.attrs.target;
|
||||
const userGetQuery = useQuery({
|
||||
const userGetQuery = useLiveQuery({
|
||||
type: 'user.get',
|
||||
userId: target,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -5,14 +5,14 @@ import { LocalPageNode } from '@colanode/client/types';
|
||||
import { Avatar } from '@colanode/ui/components/avatars/avatar';
|
||||
import { useLayout } from '@colanode/ui/contexts/layout';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
export const PageNodeView = ({ node }: NodeViewProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const layout = useLayout();
|
||||
|
||||
const id = node.attrs.id;
|
||||
const nodeGetQuery = useQuery({
|
||||
const nodeGetQuery = useLiveQuery({
|
||||
type: 'node.get',
|
||||
nodeId: id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { sha256 } from 'js-sha256';
|
||||
|
||||
import { QueryInput } from '@colanode/client/queries';
|
||||
|
||||
export const useQueries = <T extends QueryInput>(inputs: T[]) => {
|
||||
export const useLiveQueries = <T extends QueryInput>(inputs: T[]) => {
|
||||
const result = useTanstackQueries({
|
||||
queries: inputs.map((input) => {
|
||||
const hash = sha256(JSON.stringify(input));
|
||||
28
packages/ui/src/hooks/use-live-query.tsx
Normal file
28
packages/ui/src/hooks/use-live-query.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
useQuery as useTanstackQuery,
|
||||
UseQueryOptions as TanstackUseQueryOptions,
|
||||
} from '@tanstack/react-query';
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
import { QueryInput, QueryMap } from '@colanode/client/queries';
|
||||
|
||||
type UseLiveQueryOptions<T extends QueryInput> = Omit<
|
||||
TanstackUseQueryOptions<QueryMap[T['type']]['output']>,
|
||||
'queryFn' | 'queryKey'
|
||||
>;
|
||||
|
||||
export const useLiveQuery = <T extends QueryInput>(
|
||||
input: T,
|
||||
options?: UseLiveQueryOptions<T>
|
||||
) => {
|
||||
const inputJson = JSON.stringify(input);
|
||||
const hash = sha256(inputJson);
|
||||
|
||||
const result = useTanstackQuery({
|
||||
queryKey: [hash],
|
||||
queryFn: () => window.colanode.executeQueryAndSubscribe(hash, input),
|
||||
...options,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LocalNode } from '@colanode/client/types';
|
||||
import { NodeRole, extractNodeRole } from '@colanode/core';
|
||||
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
||||
import { useQuery } from '@colanode/ui/hooks/use-query';
|
||||
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
|
||||
|
||||
type UseNodeContainerResult<T extends LocalNode> =
|
||||
| {
|
||||
@@ -25,7 +25,7 @@ export const useNodeContainer = <T extends LocalNode>(
|
||||
): UseNodeContainerResult<T> => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const nodeTreeGetQuery = useQuery({
|
||||
const nodeTreeGetQuery = useLiveQuery({
|
||||
type: 'node.tree.get',
|
||||
nodeId: id,
|
||||
accountId: workspace.accountId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user