Workspace storage limits (#140)

This commit is contained in:
Hakan Shehu
2025-07-21 09:22:15 +02:00
committed by GitHub
parent 4813d715ae
commit e1e503e7b2
131 changed files with 1943 additions and 625 deletions

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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,
});

View File

@@ -20,7 +20,6 @@ const GoogleLoginButton = ({ context, onSuccess }: GoogleLoginProps) => {
const login = useGoogleLogin({
onSuccess: async (response) => {
console.log('response', response);
mutate({
input: {
type: 'google.login',

View File

@@ -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',
});

View File

@@ -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',
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 ?? '',

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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',
});

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 (

View File

@@ -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,

View File

@@ -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',
});

View File

@@ -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,

View File

@@ -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} />)

View File

@@ -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, () => (

View File

@@ -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,

View File

@@ -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',
});

View File

@@ -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,
});

View File

@@ -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" />

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));

View File

@@ -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,
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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',
});

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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',
});

View File

@@ -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,

View File

@@ -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,

View 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,
};

View File

@@ -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,

View 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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));

View 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;
};

View File

@@ -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