Open records in a modal (#261)

This commit is contained in:
Hakan Shehu
2025-11-18 11:07:58 -08:00
committed by GitHub
parent a3df5e5f26
commit f46f65c7a3
59 changed files with 775 additions and 806 deletions

View File

@@ -2,11 +2,9 @@ import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/wor
import { parseApiError } from '@colanode/client/lib/ky';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { QueryError, QueryErrorCode } from '@colanode/client/queries';
import {
WorkspaceStorageGetQueryInput,
WorkspaceStorageGetQueryOutput,
} from '@colanode/client/queries/workspaces/workspace-storage-get';
import { WorkspaceStorageGetQueryInput } from '@colanode/client/queries/workspaces/workspace-storage-get';
import { Event } from '@colanode/client/types/events';
import { WorkspaceStorageGetOutput } from '@colanode/core';
export class WorkspaceStorageGetQueryHandler
extends WorkspaceQueryHandlerBase
@@ -14,13 +12,13 @@ export class WorkspaceStorageGetQueryHandler
{
async handleQuery(
input: WorkspaceStorageGetQueryInput
): Promise<WorkspaceStorageGetQueryOutput> {
): Promise<WorkspaceStorageGetOutput> {
const workspace = this.getWorkspace(input.userId);
try {
const output = await workspace.account.client
.get(`v1/workspaces/${workspace.workspaceId}/storage`)
.json<WorkspaceStorageGetQueryOutput>();
.json<WorkspaceStorageGetOutput>();
return output;
} catch (error) {
@@ -32,7 +30,7 @@ export class WorkspaceStorageGetQueryHandler
async checkForChanges(
_event: Event,
_input: WorkspaceStorageGetQueryInput,
_output: WorkspaceStorageGetQueryOutput
_output: WorkspaceStorageGetOutput
): Promise<ChangeCheckResult<WorkspaceStorageGetQueryInput>> {
return {
hasChanges: false,

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AccountSettingsHeader = () => {
export const AccountSettingsBreadcrumb = () => {
return (
<BreadcrumbItem
id="settings"

View File

@@ -1,9 +1,12 @@
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 } from '@colanode/ui/components/layouts/containers/container';
import { Separator } from '@colanode/ui/components/ui/separator';
export const AccountSettingsContainer = () => {
return (
<Container type="full" breadcrumb={<AccountSettingsBreadcrumb />}>
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
@@ -15,11 +18,14 @@ export const AccountSettingsContainer = () => {
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Danger Zone</h2>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<AccountDelete />
</div>
</div>
</Container>
);
};

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AppAppearanceSettingsHeader = () => {
export const AppAppearanceBreadcrumb = () => {
return (
<BreadcrumbItem
id="appearance"

View File

@@ -0,0 +1,138 @@
import { Check, Laptop, Moon, Sun } from 'lucide-react';
import { ThemeColor, ThemeMode } from '@colanode/client/types';
import { AppAppearanceBreadcrumb } from '@colanode/ui/components/app/app-appearance-breadcrumb';
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useMetadata } from '@colanode/ui/hooks/use-metadata';
import { cn } from '@colanode/ui/lib/utils';
interface ThemeModeOption {
key: string;
value: ThemeMode | null;
label: string;
icon: typeof Laptop;
title: string;
}
const themeModeOptions: ThemeModeOption[] = [
{
key: 'system',
value: null,
label: 'System',
icon: Laptop,
title: 'Follow system',
},
{
key: 'light',
value: 'light',
label: 'Light',
icon: Sun,
title: 'Light theme',
},
{
key: 'dark',
value: 'dark',
label: 'Dark',
icon: Moon,
title: 'Dark theme',
},
];
const themeColorOptions = [
{ value: 'default', label: 'Default', color: 'oklch(0.205 0 0)' },
{ value: 'blue', label: 'Blue', color: 'oklch(0.623 0.214 259.815)' },
{ value: 'red', label: 'Red', color: 'oklch(0.637 0.237 25.331)' },
{ value: 'rose', label: 'Rose', color: 'oklch(0.645 0.246 16.439)' },
{ value: 'orange', label: 'Orange', color: 'oklch(0.705 0.213 47.604)' },
{ value: 'green', label: 'Green', color: 'oklch(0.723 0.219 149.579)' },
{ value: 'yellow', label: 'Yellow', color: 'oklch(0.795 0.184 86.047)' },
{ value: 'violet', label: 'Violet', color: 'oklch(0.606 0.25 292.717)' },
];
export const AppAppearanceContainer = () => {
const [themeMode, setThemeMode] = useMetadata('app', 'theme.mode');
const [themeColor, setThemeColor] = useMetadata('app', 'theme.color');
return (
<Container type="full" breadcrumb={<AppAppearanceBreadcrumb />}>
<div className="max-w-4xl space-y-8">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Appearance</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{themeModeOptions.map((option) => {
const isActive =
option.value === null ? !themeMode : themeMode === option.value;
const Icon = option.icon;
return (
<Button
key={option.key}
variant="outline"
onClick={() => {
setThemeMode(option.value ?? undefined);
}}
className={cn(
'h-10 w-full justify-start gap-2 relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.title}
>
<Icon className="size-5" />
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
<div>
<h2 className="text-2xl font-semibold tracking-tight">Color</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 max-w-2xl">
{themeColorOptions.map((option) => {
const isDefault = option.value === 'default';
const isActive = isDefault
? !themeColor
: themeColor === option.value;
return (
<Button
key={option.value}
variant="outline"
onClick={() => {
if (isDefault) {
setThemeColor(undefined);
} else {
setThemeColor(option.value as ThemeColor);
}
}}
className={cn(
'h-10 justify-start gap-3 text-left relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.label}
>
<div
className="size-5 rounded-full border border-border/50 shrink-0"
style={{ backgroundColor: option.color }}
/>
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
</div>
</Container>
);
};

View File

@@ -1,134 +0,0 @@
import { Check, Laptop, Moon, Sun } from 'lucide-react';
import { ThemeColor, ThemeMode } from '@colanode/client/types';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useMetadata } from '@colanode/ui/hooks/use-metadata';
import { cn } from '@colanode/ui/lib/utils';
interface ThemeModeOption {
key: string;
value: ThemeMode | null;
label: string;
icon: typeof Laptop;
title: string;
}
const themeModeOptions: ThemeModeOption[] = [
{
key: 'system',
value: null,
label: 'System',
icon: Laptop,
title: 'Follow system',
},
{
key: 'light',
value: 'light',
label: 'Light',
icon: Sun,
title: 'Light theme',
},
{
key: 'dark',
value: 'dark',
label: 'Dark',
icon: Moon,
title: 'Dark theme',
},
];
const themeColorOptions = [
{ value: 'default', label: 'Default', color: 'oklch(0.205 0 0)' },
{ value: 'blue', label: 'Blue', color: 'oklch(0.623 0.214 259.815)' },
{ value: 'red', label: 'Red', color: 'oklch(0.637 0.237 25.331)' },
{ value: 'rose', label: 'Rose', color: 'oklch(0.645 0.246 16.439)' },
{ value: 'orange', label: 'Orange', color: 'oklch(0.705 0.213 47.604)' },
{ value: 'green', label: 'Green', color: 'oklch(0.723 0.219 149.579)' },
{ value: 'yellow', label: 'Yellow', color: 'oklch(0.795 0.184 86.047)' },
{ value: 'violet', label: 'Violet', color: 'oklch(0.606 0.25 292.717)' },
];
export const AppAppearanceSettingsContainer = () => {
const [themeMode, setThemeMode] = useMetadata('app', 'theme.mode');
const [themeColor, setThemeColor] = useMetadata('app', 'theme.color');
return (
<div className="max-w-4xl space-y-8">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Appearance</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{themeModeOptions.map((option) => {
const isActive =
option.value === null ? !themeMode : themeMode === option.value;
const Icon = option.icon;
return (
<Button
key={option.key}
variant="outline"
onClick={() => {
setThemeMode(option.value ?? undefined);
}}
className={cn(
'h-10 w-full justify-start gap-2 relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.title}
>
<Icon className="size-5" />
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
<div>
<h2 className="text-2xl font-semibold tracking-tight">Color</h2>
<Separator className="mt-3" />
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 max-w-2xl">
{themeColorOptions.map((option) => {
const isDefault = option.value === 'default';
const isActive = isDefault
? !themeColor
: themeColor === option.value;
return (
<Button
key={option.value}
variant="outline"
onClick={() => {
if (isDefault) {
setThemeColor(undefined);
} else {
setThemeColor(option.value as ThemeColor);
}
}}
className={cn(
'h-10 justify-start gap-3 text-left relative',
isActive && 'ring-1 ring-ring border-primary'
)}
title={option.label}
>
<div
className="size-5 rounded-full border border-border/50 shrink-0"
style={{ backgroundColor: option.color }}
/>
{option.label}
{isActive && (
<Check className="size-5 absolute -top-2 -right-2 text-background bg-primary rounded-full p-0.5" />
)}
</Button>
);
})}
</div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { TabItem } from '@colanode/ui/components/layouts/tabs/tab-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const AppAppearanceSettingsTab = () => {
export const AppAppearanceTab = () => {
return (
<TabItem
id="appearance"

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const LogoutHeader = () => {
export const LogoutBreadcrumb = () => {
return (
<BreadcrumbItem id="logout" avatar={defaultIcons.logout} name="Logout" />
);

View File

@@ -1,6 +1,8 @@
import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner';
import { LogoutBreadcrumb } from '@colanode/ui/components/auth/logout-breadcrumb';
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { Button } from '@colanode/ui/components/ui/button';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Spinner } from '@colanode/ui/components/ui/spinner';
@@ -13,6 +15,7 @@ export const LogoutContainer = () => {
const { mutate, isPending } = useMutation();
return (
<Container type="full" breadcrumb={<LogoutBreadcrumb />}>
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
@@ -58,5 +61,6 @@ export const LogoutContainer = () => {
</div>
</div>
</div>
</Container>
);
};

View File

@@ -1,28 +1,13 @@
import { LocalChannelNode } from '@colanode/client/types';
import { ChannelNotFound } from '@colanode/ui/components/channels/channel-not-found';
import { NodeRole } from '@colanode/core';
import { Conversation } from '@colanode/ui/components/messages/conversation';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
interface ChannelContainerProps {
channelId: string;
channel: LocalChannelNode;
role: NodeRole;
}
export const ChannelContainer = ({ channelId }: ChannelContainerProps) => {
const data = useNodeContainer<LocalChannelNode>(channelId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <ChannelNotFound />;
}
const { node: channel, role } = data;
export const ChannelContainer = ({ channel, role }: ChannelContainerProps) => {
return (
<Conversation
conversationId={channel.id}

View File

@@ -1,28 +1,13 @@
import { LocalChatNode } from '@colanode/client/types';
import { ChatNotFound } from '@colanode/ui/components/chats/chat-not-found';
import { NodeRole } from '@colanode/core';
import { Conversation } from '@colanode/ui/components/messages/conversation';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
interface ChatContainerProps {
chatId: string;
node: LocalChatNode;
role: NodeRole;
}
export const ChatContainer = ({ chatId }: ChatContainerProps) => {
const data = useNodeContainer<LocalChatNode>(chatId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <ChatNotFound />;
}
const { node, role } = data;
export const ChatContainer = ({ node, role }: ChatContainerProps) => {
return (
<Conversation conversationId={node.id} rootId={node.rootId} role={role} />
);

View File

@@ -36,9 +36,9 @@ export const BoardViewRecordCard = () => {
className="animate-fade-in flex cursor-pointer flex-col gap-1 rounded-md border p-2 text-left hover:bg-accent"
>
<Link
from="/workspace/$userId"
to="$nodeId"
params={{ nodeId: record.id }}
from="/workspace/$userId/$nodeId"
to="modal/$modalNodeId"
params={{ modalNodeId: record.id }}
>
<p className={hasName ? '' : 'text-muted-foreground'}>
{hasName ? name : 'Unnamed'}

View File

@@ -12,9 +12,9 @@ export const CalendarViewRecordCard = () => {
return (
<Link
from="/workspace/$userId"
to="$nodeId"
params={{ nodeId: record.id }}
from="/workspace/$userId/$nodeId"
to="modal/$modalNodeId"
params={{ modalNodeId: record.id }}
key={record.id}
className="animate-fade-in flex justify-start items-start cursor-pointer flex-col gap-1 rounded-md border p-1 pl-2 hover:bg-accent"
>

View File

@@ -1,29 +1,17 @@
import { LocalDatabaseNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { Database } from '@colanode/ui/components/databases/database';
import { DatabaseNotFound } from '@colanode/ui/components/databases/database-not-found';
import { DatabaseViews } from '@colanode/ui/components/databases/database-views';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
interface DatabaseContainerProps {
databaseId: string;
database: LocalDatabaseNode;
role: NodeRole;
}
export const DatabaseContainer = ({ databaseId }: DatabaseContainerProps) => {
const data = useNodeContainer<LocalDatabaseNode>(databaseId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <DatabaseNotFound />;
}
const { node: database, role } = data;
export const DatabaseContainer = ({
database,
role,
}: DatabaseContainerProps) => {
return (
<Database database={database} role={role}>
<DatabaseViews />

View File

@@ -105,9 +105,9 @@ export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
)}
</div>
<Link
from="/workspace/$userId"
to="$nodeId"
params={{ nodeId: record.id }}
from="/workspace/$userId/$nodeId"
to="modal/$modalNodeId"
params={{ modalNodeId: record.id }}
className="absolute right-2 flex h-6 cursor-pointer flex-row items-center gap-1 rounded-md border p-1 text-sm text-muted-foreground opacity-0 hover:bg-accent group-hover:opacity-100"
>
<SquareArrowOutUpRight className="mr-1 size-4" /> <p>Open</p>

View File

@@ -34,7 +34,7 @@ interface ViewProps {
export const View = ({ view }: ViewProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const navigate = useNavigate({ from: '/workspace/$userId' });
const navigate = useNavigate();
const fields: ViewField[] = database.fields
.map((field) => {
@@ -498,8 +498,9 @@ export const View = ({ view }: ViewProps) => {
toast.error(result.error.message);
} else {
navigate({
to: '$nodeId',
params: { nodeId: result.output.id },
from: '/workspace/$userId/$nodeId',
to: 'modal/$modalNodeId',
params: { modalNodeId: result.output.id },
});
}
},

View File

@@ -1,35 +0,0 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileNoPreview } from '@colanode/ui/components/files/file-no-preview';
import { FilePreview } from '@colanode/ui/components/files/file-preview';
import { FileSaveButton } from '@colanode/ui/components/files/file-save-button';
import { FileSidebar } from '@colanode/ui/components/files/file-sidebar';
import { canPreviewFile } from '@colanode/ui/lib/files';
interface FileBodyProps {
file: LocalFileNode;
}
export const FileBody = ({ file }: FileBodyProps) => {
const canPreview = canPreviewFile(file.attributes.subtype);
return (
<div className="flex h-full max-h-full w-full flex-row items-center gap-2">
<div className="flex flex-col w-full max-w-full h-full grow overflow-hidden">
<div className="flex flex-row w-full items-center justify-end p-4 gap-4">
<FileSaveButton file={file} />
</div>
<div className="flex flex-col grow items-center justify-center overflow-hidden p-10">
{canPreview ? (
<FilePreview file={file} />
) : (
<FileNoPreview mimeType={file.attributes.mimeType} />
)}
</div>
</div>
<div className="h-full w-72 min-w-72 overflow-hidden border-l border-border p-2 pl-3">
<FileSidebar file={file} />
</div>
</div>
);
};

View File

@@ -1,24 +1,35 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileBody } from '@colanode/ui/components/files/file-body';
import { FileNotFound } from '@colanode/ui/components/files/file-not-found';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
import { FileNoPreview } from '@colanode/ui/components/files/file-no-preview';
import { FilePreview } from '@colanode/ui/components/files/file-preview';
import { FileSaveButton } from '@colanode/ui/components/files/file-save-button';
import { FileSidebar } from '@colanode/ui/components/files/file-sidebar';
import { canPreviewFile } from '@colanode/ui/lib/files';
interface FileContainerProps {
fileId: string;
file: LocalFileNode;
}
export const FileContainer = ({ fileId }: FileContainerProps) => {
const data = useNodeContainer<LocalFileNode>(fileId);
useNodeRadar(data.node);
export const FileContainer = ({ file }: FileContainerProps) => {
const canPreview = canPreviewFile(file.attributes.subtype);
if (data.isPending) {
return null;
}
return (
<div className="flex h-full max-h-full w-full flex-row items-center gap-2">
<div className="flex flex-col w-full max-w-full h-full grow overflow-hidden">
<div className="flex flex-row w-full items-center justify-end p-4 gap-4">
<FileSaveButton file={file} />
</div>
if (!data.node) {
return <FileNotFound />;
}
return <FileBody file={data.node} />;
<div className="flex flex-col grow items-center justify-center overflow-hidden p-10">
{canPreview ? (
<FilePreview file={file} />
) : (
<FileNoPreview mimeType={file.attributes.mimeType} />
)}
</div>
</div>
<div className="h-full w-72 min-w-72 overflow-hidden border-l border-border p-2 pl-3">
<FileSidebar file={file} />
</div>
</div>
);
};

View File

@@ -1,27 +1,12 @@
import { LocalFolderNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { FolderBody } from '@colanode/ui/components/folders/folder-body';
import { FolderNotFound } from '@colanode/ui/components/folders/folder-not-found';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
interface FolderContainerProps {
folderId: string;
folder: LocalFolderNode;
role: NodeRole;
}
export const FolderContainer = ({ folderId }: FolderContainerProps) => {
const data = useNodeContainer<LocalFolderNode>(folderId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <FolderNotFound />;
}
const { node: folder, role } = data;
export const FolderContainer = ({ folder, role }: FolderContainerProps) => {
return <FolderBody folder={folder} role={role} />;
};

View File

@@ -1,21 +0,0 @@
import { useRouter, useLocation } from '@tanstack/react-router';
import { useMemo } from 'react';
export const ContainerHeader = () => {
const router = useRouter();
const location = useLocation();
const headerComponent = useMemo(() => {
const matches = router.matchRoutes(location.href);
for (let i = matches.length - 1; i >= 0; i--) {
const match = matches[i];
if (match?.context && 'header' in match.context) {
return match.context.header;
}
}
return null;
}, [router, location.href]);
return headerComponent;
};

View File

@@ -1,7 +1,6 @@
import { Outlet } from '@tanstack/react-router';
import { Fullscreen } from 'lucide-react';
import { useRef } from 'react';
import { ContainerHeader } from '@colanode/ui/components/layouts/containers/container-header';
import { SidebarMobile } from '@colanode/ui/components/layouts/sidebars/sidebar-mobile';
import {
ScrollArea,
@@ -9,11 +8,28 @@ import {
ScrollViewport,
} from '@colanode/ui/components/ui/scroll-area';
import { useApp } from '@colanode/ui/contexts/app';
import { ContainerContext } from '@colanode/ui/contexts/container';
import {
ContainerContext,
ContainerType,
} from '@colanode/ui/contexts/container';
import { useIsMobile } from '@colanode/ui/hooks/use-is-mobile';
import { cn } from '@colanode/ui/lib/utils';
export const Container = () => {
interface ContainerProps {
type: ContainerType;
children: React.ReactNode;
breadcrumb?: React.ReactNode;
actions?: React.ReactNode;
onFullscreen?: () => void;
}
export const Container = ({
type,
children,
breadcrumb,
actions,
onFullscreen,
}: ContainerProps) => {
const app = useApp();
const isMobile = useIsMobile();
@@ -23,6 +39,7 @@ export const Container = () => {
return (
<ContainerContext.Provider
value={{
type,
scrollAreaRef,
scrollViewportRef,
}}
@@ -34,15 +51,22 @@ export const Container = () => {
app.type === 'mobile' && 'p-0 pr-2'
)}
>
{isMobile && <SidebarMobile />}
<div className="flex-1">
<ContainerHeader />
{isMobile && type === 'full' && <SidebarMobile />}
{type === 'modal' && onFullscreen && (
<Fullscreen
className="size-4 cursor-pointer text-muted-foreground hover:text-foreground"
onClick={onFullscreen}
/>
)}
<div className="flex-1 flex justify-between items-center">
<div>{type === 'full' ? breadcrumb : null}</div>
<div>{actions}</div>
</div>
</div>
<ScrollArea ref={scrollAreaRef} className="overflow-hidden h-full">
<ScrollViewport ref={scrollViewportRef} className="h-full">
<div className="lg:px-10 px-4 min-h-0 flex-1 h-full">
<Outlet />
{children}
</div>
</ScrollViewport>
<ScrollBar orientation="horizontal" />

View File

@@ -97,7 +97,7 @@ export const SidebarSettings = () => {
</div>
<div className="flex w-full min-w-0 flex-col gap-1">
<SidebarHeader title="Account settings" />
<Link from="/workspace/$userId" to="account/settings">
<Link from="/workspace/$userId" to="account">
{({ isActive }) => (
<SidebarSettingsItem
title="General"
@@ -109,7 +109,7 @@ export const SidebarSettings = () => {
</div>
<div className="flex w-full min-w-0 flex-col gap-1">
<SidebarHeader title="App settings" />
<Link from="/workspace/$userId" to="app/appearance">
<Link from="/workspace/$userId" to="appearance">
{({ isActive }) => (
<SidebarSettingsItem
title="Appearance"

View File

@@ -1,40 +1,27 @@
import { LocalMessageNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { Message } from '@colanode/ui/components/messages/message';
import { MessageNotFound } from '@colanode/ui/components/messages/message-not-found';
import { ConversationContext } from '@colanode/ui/contexts/conversation';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
interface MessageContainerProps {
messageId: string;
message: LocalMessageNode;
role: NodeRole;
}
export const MessageContainer = ({ messageId }: MessageContainerProps) => {
const data = useNodeContainer<LocalMessageNode>(messageId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <MessageNotFound />;
}
export const MessageContainer = ({ message, role }: MessageContainerProps) => {
return (
<ConversationContext.Provider
value={{
id: data.node.id,
role: data.role,
rootId: data.node.rootId,
id: message.id,
role: role,
rootId: message.rootId,
canCreateMessage: true,
onReply: () => {},
onLastMessageIdChange: () => {},
canDeleteMessage: () => false,
}}
>
<Message message={data.node} />
<Message message={message} />
</ConversationContext.Provider>
);
};

View File

@@ -1,30 +1,85 @@
import { match } from 'ts-pattern';
import { Outlet } from '@tanstack/react-router';
import { getIdType, IdType } from '@colanode/core';
import { LocalNode } from '@colanode/client/types';
import { ChannelContainer } from '@colanode/ui/components/channels/channel-container';
import { ChatContainer } from '@colanode/ui/components/chats/chat-container';
import { DatabaseContainer } from '@colanode/ui/components/databases/database-container';
import { FileContainer } from '@colanode/ui/components/files/file-container';
import { FolderContainer } from '@colanode/ui/components/folders/folder-container';
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { MessageContainer } from '@colanode/ui/components/messages/message-container';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { NodeSettings } from '@colanode/ui/components/nodes/node-settings';
import { PageContainer } from '@colanode/ui/components/pages/page-container';
import { RecordContainer } from '@colanode/ui/components/records/record-container';
import { SpaceContainer } from '@colanode/ui/components/spaces/space-container';
import { ContainerType } from '@colanode/ui/contexts/container';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
interface NodeContainerProps {
type: ContainerType;
nodeId: string;
onFullscreen?: () => void;
}
export const NodeContainer = ({ nodeId }: NodeContainerProps) => {
return match(getIdType(nodeId))
.with(IdType.Space, () => <SpaceContainer spaceId={nodeId} />)
.with(IdType.Channel, () => <ChannelContainer channelId={nodeId} />)
.with(IdType.Page, () => <PageContainer pageId={nodeId} />)
.with(IdType.Database, () => <DatabaseContainer databaseId={nodeId} />)
.with(IdType.Record, () => <RecordContainer recordId={nodeId} />)
.with(IdType.Chat, () => <ChatContainer chatId={nodeId} />)
.with(IdType.Folder, () => <FolderContainer folderId={nodeId} />)
.with(IdType.File, () => <FileContainer fileId={nodeId} />)
.with(IdType.Message, () => <MessageContainer messageId={nodeId} />)
.otherwise(() => null);
const NodeContent = ({ type, nodeId, onFullscreen }: NodeContainerProps) => {
const data = useNodeContainer<LocalNode>(nodeId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return null;
}
return (
<Container
type={type}
breadcrumb={<NodeBreadcrumb nodes={data.breadcrumb} />}
actions={<NodeSettings node={data.node} role={data.role} />}
onFullscreen={onFullscreen}
>
{data.node.type === 'space' && (
<SpaceContainer space={data.node} role={data.role} />
)}
{data.node.type === 'channel' && (
<ChannelContainer channel={data.node} role={data.role} />
)}
{data.node.type === 'page' && (
<PageContainer page={data.node} role={data.role} />
)}
{data.node.type === 'database' && (
<DatabaseContainer database={data.node} role={data.role} />
)}
{data.node.type === 'record' && (
<RecordContainer record={data.node} role={data.role} />
)}
{data.node.type === 'chat' && (
<ChatContainer node={data.node} role={data.role} />
)}
{data.node.type === 'folder' && (
<FolderContainer folder={data.node} role={data.role} />
)}
{data.node.type === 'message' && (
<MessageContainer message={data.node} role={data.role} />
)}
{data.node.type === 'file' && <FileContainer file={data.node} />}
</Container>
);
};
export const NodeContainer = ({
type,
nodeId,
onFullscreen,
}: NodeContainerProps) => {
return (
<>
<NodeContent type={type} nodeId={nodeId} onFullscreen={onFullscreen} />
<Outlet />
</>
);
};

View File

@@ -1,27 +0,0 @@
import { LocalNode } from '@colanode/client/types';
import { NodeBreadcrumb } from '@colanode/ui/components/nodes/node-breadcrumb';
import { NodeSettings } from '@colanode/ui/components/nodes/node-settings';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
interface NodeHeaderProps {
nodeId: string;
}
export const NodeHeader = ({ nodeId }: NodeHeaderProps) => {
const data = useNodeContainer<LocalNode>(nodeId);
if (data.isPending) {
return null;
}
if (!data.node) {
return null;
}
return (
<div className="flex items-center gap-2">
<NodeBreadcrumb nodes={data.breadcrumb} />
<NodeSettings node={data.node} role={data.role} />
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { useNavigate } from '@tanstack/react-router';
import { NodeContainer } from '@colanode/ui/components/nodes/node-container';
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
interface NodeModalProps {
nodeId: string;
}
export const NodeModal = ({ nodeId }: NodeModalProps) => {
const navigate = useNavigate();
return (
<Dialog
open={true}
onOpenChange={(open) => {
if (!open) {
navigate({
from: '/workspace/$userId/$nodeId/modal/$modalNodeId',
to: '/workspace/$userId/$nodeId',
});
}
}}
>
<DialogContent
className="w-[90vw] h-[90vh] max-w-[90vw] max-h-[90vh] min-w-[90vw] min-h-[90vh] p-2"
showCloseButton={false}
>
<VisuallyHidden>
<DialogTitle>Modal</DialogTitle>
<DialogDescription>
This is a modal window. It is used to display a node in a modal
window.
</DialogDescription>
</VisuallyHidden>
<NodeContainer
type="modal"
nodeId={nodeId}
onFullscreen={() => {
navigate({
from: '/workspace/$userId/$nodeId/modal/$modalNodeId',
to: '/workspace/$userId/$nodeId',
params: {
nodeId: nodeId,
},
});
}}
/>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,14 +0,0 @@
import { LocalPageNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { Document } from '@colanode/ui/components/documents/document';
interface PageBodyProps {
page: LocalPageNode;
role: NodeRole;
}
export const PageBody = ({ page, role }: PageBodyProps) => {
const canEdit = hasNodeRole(role, 'editor');
return <Document node={page} canEdit={canEdit} autoFocus="start" />;
};

View File

@@ -1,26 +1,13 @@
import { LocalPageNode } from '@colanode/client/types';
import { PageBody } from '@colanode/ui/components/pages/page-body';
import { PageNotFound } from '@colanode/ui/components/pages/page-not-found';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { Document } from '@colanode/ui/components/documents/document';
interface PageContainerProps {
pageId: string;
page: LocalPageNode;
role: NodeRole;
}
export const PageContainer = ({ pageId }: PageContainerProps) => {
const data = useNodeContainer<LocalPageNode>(pageId);
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <PageNotFound />;
}
const { node: page, role } = data;
return <PageBody page={page} role={role} />;
export const PageContainer = ({ page, role }: PageContainerProps) => {
const canEdit = hasNodeRole(role, 'editor');
return <Document node={page} canEdit={canEdit} autoFocus="start" />;
};

View File

@@ -1,29 +0,0 @@
import { LocalRecordNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { Document } from '@colanode/ui/components/documents/document';
import { RecordAttributes } from '@colanode/ui/components/records/record-attributes';
import { RecordDatabase } from '@colanode/ui/components/records/record-database';
import { RecordProvider } from '@colanode/ui/components/records/record-provider';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
interface RecordBodyProps {
record: LocalRecordNode;
role: NodeRole;
}
export const RecordBody = ({ record, role }: RecordBodyProps) => {
const workspace = useWorkspace();
const canEdit =
record.createdBy === workspace.userId || hasNodeRole(role, 'editor');
return (
<RecordDatabase id={record.attributes.databaseId} role={role}>
<RecordProvider record={record} role={role}>
<RecordAttributes />
</RecordProvider>
<Separator className="my-4 w-full" />
<Document node={record} canEdit={canEdit} autoFocus={false} />
</RecordDatabase>
);
};

View File

@@ -1,27 +1,29 @@
import { LocalRecordNode } from '@colanode/client/types';
import { RecordBody } from '@colanode/ui/components/records/record-body';
import { RecordNotFound } from '@colanode/ui/components/records/record-not-found';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { Document } from '@colanode/ui/components/documents/document';
import { RecordAttributes } from '@colanode/ui/components/records/record-attributes';
import { RecordDatabase } from '@colanode/ui/components/records/record-database';
import { RecordProvider } from '@colanode/ui/components/records/record-provider';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
interface RecordContainerProps {
recordId: string;
record: LocalRecordNode;
role: NodeRole;
}
export const RecordContainer = ({ recordId }: RecordContainerProps) => {
const data = useNodeContainer<LocalRecordNode>(recordId);
export const RecordContainer = ({ record, role }: RecordContainerProps) => {
const workspace = useWorkspace();
useNodeRadar(data.node);
if (data.isPending) {
return null;
}
if (!data.node) {
return <RecordNotFound />;
}
const { node: record, role } = data;
return <RecordBody record={record} role={role} />;
const canEdit =
record.createdBy === workspace.userId || hasNodeRole(role, 'editor');
return (
<RecordDatabase id={record.attributes.databaseId} role={role}>
<RecordProvider record={record} role={role}>
<RecordAttributes />
</RecordProvider>
<Separator className="my-4 w-full" />
<Document node={record} canEdit={canEdit} autoFocus={false} />
</RecordDatabase>
);
};

View File

@@ -1,93 +0,0 @@
import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { NodeCollaborators } from '@colanode/ui/components/collaborators/node-collaborators';
import { SpaceDelete } from '@colanode/ui/components/spaces/space-delete';
import { SpaceForm } from '@colanode/ui/components/spaces/space-form';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
interface SpaceBodyProps {
space: LocalSpaceNode;
role: NodeRole;
}
export const SpaceBody = ({ space, role }: SpaceBodyProps) => {
const workspace = useWorkspace();
const navigate = useNavigate({ from: '/workspace/$userId' });
const { mutate, isPending } = useMutation();
const canEdit = hasNodeRole(role, 'admin');
const canDelete = hasNodeRole(role, 'admin');
return (
<div className="max-w-4xl space-y-8 w-full pb-10">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<SpaceForm
values={{
name: space.attributes.name,
description: space.attributes.description ?? '',
avatar: space.attributes.avatar ?? null,
}}
readOnly={!canEdit}
onSubmit={(values) => {
mutate({
input: {
type: 'space.update',
userId: workspace.userId,
spaceId: space.id,
name: values.name,
description: values.description,
avatar: values.avatar,
},
onSuccess() {
toast.success('Space 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">
Collaborators
</h2>
<Separator className="mt-3" />
</div>
<NodeCollaborators node={space} nodes={[space]} role={role} />
</div>
{canDelete && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<SpaceDelete
id={space.id}
onDeleted={() => {
navigate({
to: '/',
});
}}
/>
</div>
)}
</div>
);
};

View File

@@ -1,27 +1,93 @@
import { useNavigate } from '@tanstack/react-router';
import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { SpaceBody } from '@colanode/ui/components/spaces/space-body';
import { SpaceNotFound } from '@colanode/ui/components/spaces/space-not-found';
import { useNodeContainer } from '@colanode/ui/hooks/use-node-container';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
import { NodeRole, hasNodeRole } from '@colanode/core';
import { NodeCollaborators } from '@colanode/ui/components/collaborators/node-collaborators';
import { SpaceDelete } from '@colanode/ui/components/spaces/space-delete';
import { SpaceForm } from '@colanode/ui/components/spaces/space-form';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
interface SpaceContainerProps {
spaceId: string;
space: LocalSpaceNode;
role: NodeRole;
}
export const SpaceContainer = ({ spaceId }: SpaceContainerProps) => {
const data = useNodeContainer<LocalSpaceNode>(spaceId);
export const SpaceContainer = ({ space, role }: SpaceContainerProps) => {
const workspace = useWorkspace();
const navigate = useNavigate({ from: '/workspace/$userId' });
const { mutate, isPending } = useMutation();
useNodeRadar(data.node);
const canEdit = hasNodeRole(role, 'admin');
const canDelete = hasNodeRole(role, 'admin');
if (data.isPending) {
return null;
}
return (
<div className="max-w-4xl space-y-8 w-full pb-10">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<SpaceForm
values={{
name: space.attributes.name,
description: space.attributes.description ?? '',
avatar: space.attributes.avatar ?? null,
}}
readOnly={!canEdit}
onSubmit={(values) => {
mutate({
input: {
type: 'space.update',
userId: workspace.userId,
spaceId: space.id,
name: values.name,
description: values.description,
avatar: values.avatar,
},
onSuccess() {
toast.success('Space updated');
},
onError(error) {
toast.error(error.message);
},
});
}}
isSaving={isPending}
saveText="Update"
/>
</div>
if (!data.node) {
return <SpaceNotFound />;
}
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Collaborators
</h2>
<Separator className="mt-3" />
</div>
<NodeCollaborators node={space} nodes={[space]} role={role} />
</div>
const { node, role } = data;
return <SpaceBody space={node} role={role} />;
{canDelete && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<SpaceDelete
id={space.id}
onDeleted={() => {
navigate({
to: '/',
});
}}
/>
</div>
)}
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceDownloadsHeader = () => {
export const WorkspaceDownloadsBreadcrumb = () => {
return (
<BreadcrumbItem
id="downloads"

View File

@@ -1,8 +1,10 @@
import { useLiveQuery } from '@tanstack/react-db';
import { collections } from '@colanode/ui/collections';
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { Separator } from '@colanode/ui/components/ui/separator';
import { WorkspaceDownloadFile } from '@colanode/ui/components/workspaces/downloads/workspace-download-file';
import { WorkspaceDownloadsBreadcrumb } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-breadcrumb';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
export const WorkspaceDownloadsContainer = () => {
@@ -18,6 +20,7 @@ export const WorkspaceDownloadsContainer = () => {
const downloads = downloadsQuery.data ?? [];
return (
<Container type="full" breadcrumb={<WorkspaceDownloadsBreadcrumb />}>
<div className="overflow-y-auto">
<div className="max-w-4xl space-y-10">
<div>
@@ -31,5 +34,6 @@ export const WorkspaceDownloadsContainer = () => {
</div>
</div>
</div>
</Container>
);
};

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceStorageHeader = () => {
export const WorkspaceStorageBreadcrumb = () => {
return (
<BreadcrumbItem id="storage" avatar={defaultIcons.storage} name="Storage" />
);

View File

@@ -1,11 +1,15 @@
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { WorkspaceStorageBreadcrumb } from '@colanode/ui/components/workspaces/storage/workspace-storage-breadcrumb';
import { WorkspaceStorageStats } from '@colanode/ui/components/workspaces/storage/workspace-storage-stats';
import { WorkspaceStorageUsers } from '@colanode/ui/components/workspaces/storage/workspace-storage-users';
export const WorkspaceStorageContainer = () => {
return (
<Container type="full" breadcrumb={<WorkspaceStorageBreadcrumb />}>
<div className="max-w-4xl space-y-10">
<WorkspaceStorageStats />
<WorkspaceStorageUsers />
</div>
</Container>
);
};

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceUploadsHeader = () => {
export const WorkspaceUploadsBreadcrumb = () => {
return (
<BreadcrumbItem id="uploads" avatar={defaultIcons.uploads} name="Uploads" />
);

View File

@@ -2,8 +2,10 @@ import { useState } from 'react';
import { InView } from 'react-intersection-observer';
import { UploadListQueryInput } from '@colanode/client/queries';
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { Separator } from '@colanode/ui/components/ui/separator';
import { WorkspaceUploadFile } from '@colanode/ui/components/workspaces/uploads/workspace-upload-file';
import { WorkspaceUploadsBreadcrumb } from '@colanode/ui/components/workspaces/uploads/workspace-uploads-breadcrumb';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
@@ -29,6 +31,7 @@ export const WorkspaceUploadsContainer = () => {
const hasMore = !isPending && uploads.length === lastPage * UPLOADS_PER_PAGE;
return (
<Container type="full" breadcrumb={<WorkspaceUploadsBreadcrumb />}>
<div className="overflow-y-auto">
<div className="max-w-4xl space-y-10">
<div>
@@ -50,5 +53,6 @@ export const WorkspaceUploadsContainer = () => {
/>
</div>
</div>
</Container>
);
};

View File

@@ -1,6 +1,6 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceHomeHeader = () => {
export const WorkspaceHomeBreadcrumb = () => {
return <BreadcrumbItem id="home" avatar={defaultIcons.home} name="Home" />;
};

View File

@@ -1,5 +1,9 @@
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { WorkspaceHomeBreadcrumb } from '@colanode/ui/components/workspaces/workspace-home-breadcrumb';
export const WorkspaceHomeContainer = () => {
return (
<Container type="full" breadcrumb={<WorkspaceHomeBreadcrumb />}>
<div className="h-full w-full flex flex-col gap-1">
<div className="h-10 app-drag-region"></div>
<div className="grow flex items-center justify-center">
@@ -8,5 +12,6 @@ export const WorkspaceHomeContainer = () => {
</p>
</div>
</div>
</Container>
);
};

View File

@@ -1,4 +1,5 @@
import { Container } from '@colanode/ui/components/layouts/containers/container';
import { Outlet } from '@tanstack/react-router';
import { SidebarDesktop } from '@colanode/ui/components/layouts/sidebars/sidebar-desktop';
import { useIsMobile } from '@colanode/ui/hooks/use-is-mobile';
@@ -9,7 +10,7 @@ export const WorkspaceLayout = () => {
<div className="w-full h-full flex">
{!isMobile && <SidebarDesktop />}
<section className="min-w-0 flex-1">
<Container />
<Outlet />
</section>
</div>
);

View File

@@ -1,7 +1,7 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceSettingsHeader = () => {
export const WorkspaceSettingsBreadcrumb = () => {
return (
<BreadcrumbItem
id="settings"

View File

@@ -2,10 +2,12 @@ import { eq, useLiveQuery } from '@tanstack/react-db';
import { toast } from 'sonner';
import { collections } from '@colanode/ui/collections';
import { Container } from '@colanode/ui/components/layouts/containers/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 { WorkspaceNotFound } from '@colanode/ui/components/workspaces/workspace-not-found';
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';
@@ -32,6 +34,7 @@ export const WorkspaceSettingsContainer = () => {
}
return (
<Container type="full" breadcrumb={<WorkspaceSettingsBreadcrumb />}>
<div className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
@@ -70,11 +73,14 @@ export const WorkspaceSettingsContainer = () => {
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Danger Zone</h2>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<WorkspaceDelete />
</div>
</div>
</Container>
);
};

View File

@@ -1,6 +1,6 @@
import { BreadcrumbItem } from '@colanode/ui/components/layouts/containers/breadcrumb-item';
import { defaultIcons } from '@colanode/ui/lib/assets';
export const WorkspaceUsersHeader = () => {
export const WorkspaceUsersBreadcrumb = () => {
return <BreadcrumbItem id="users" avatar={defaultIcons.users} name="Users" />;
};

View File

@@ -4,10 +4,12 @@ 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 } from '@colanode/ui/components/layouts/containers/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 { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
@@ -33,6 +35,7 @@ export const WorkspaceUsersContainer = () => {
const hasMore = !isPending && users.length === lastPage * USERS_PER_PAGE;
return (
<Container type="full" breadcrumb={<WorkspaceUsersBreadcrumb />}>
<div className="max-w-4xl space-y-8">
{canEditUsers && (
<div className="space-y-6">
@@ -92,5 +95,6 @@ export const WorkspaceUsersContainer = () => {
</div>
</div>
</div>
</Container>
);
};

View File

@@ -1,6 +1,9 @@
import { createContext, useContext } from 'react';
export type ContainerType = 'full' | 'modal';
interface ContainerContext {
type: ContainerType;
scrollAreaRef: React.RefObject<HTMLDivElement>;
scrollViewportRef: React.RefObject<HTMLDivElement>;
}

View File

@@ -14,11 +14,11 @@ import {
import {
accountSettingsMaskRoute,
accountSettingsRoute,
} from '@colanode/ui/routes/workspace/account-settings';
} from '@colanode/ui/routes/workspace/account';
import {
appAppearanceMaskRoute,
appAppearanceRoute,
} from '@colanode/ui/routes/workspace/app-appearance';
} from '@colanode/ui/routes/workspace/appearance';
import {
workspaceDownloadsMaskRoute,
workspaceDownloadsRoute,
@@ -31,6 +31,7 @@ import {
logoutMaskRoute,
logoutRoute,
} from '@colanode/ui/routes/workspace/logout';
import { modalNodeRoute } from '@colanode/ui/routes/workspace/modal';
import { nodeMaskRoute, nodeRoute } from '@colanode/ui/routes/workspace/node';
import {
workspaceRedirectMaskRoute,
@@ -60,7 +61,7 @@ export const routeTree = rootRoute.addChildren([
workspaceRoute.addChildren([
workspaceRedirectRoute,
workspaceHomeRoute,
nodeRoute,
nodeRoute.addChildren([modalNodeRoute]),
workspaceDownloadsRoute,
workspaceUploadsRoute,
workspaceStorageRoute,

View File

@@ -101,8 +101,8 @@ export const workspaceDownloadsRouteMask = createRouteMask({
export const accountSettingsRouteMask = createRouteMask({
routeTree: routeTree,
from: '/workspace/$userId/account/settings',
to: '/$workspaceId/account/settings',
from: '/workspace/$userId/account',
to: '/$workspaceId/account',
params: (ctx) => {
const workspace = collections.workspaces.get(ctx.userId);
return {
@@ -125,8 +125,8 @@ export const accountLogoutRouteMask = createRouteMask({
export const appAppearanceRouteMask = createRouteMask({
routeTree: routeTree,
from: '/workspace/$userId/app/appearance',
to: '/$workspaceId/app/appearance',
from: '/workspace/$userId/appearance',
to: '/$workspaceId/appearance',
params: (ctx) => {
const workspace = collections.workspaces.get(ctx.userId);
return {
@@ -135,10 +135,24 @@ export const appAppearanceRouteMask = createRouteMask({
},
});
export const modalNodeRouteMask = createRouteMask({
routeTree: routeTree,
from: '/workspace/$userId/$nodeId/modal/$modalNodeId',
to: '/$workspaceId/$nodeId',
params: (ctx) => {
const workspace = collections.workspaces.get(ctx.userId);
return {
workspaceId: workspace?.workspaceId ?? 'unknown',
nodeId: ctx.modalNodeId,
};
},
});
export const routeMasks = [
workspaceRouteMask,
workspaceHomeRouteMask,
nodeRouteMask,
modalNodeRouteMask,
workspaceSettingsRouteMask,
workspaceUsersRouteMask,
workspaceStorageRouteMask,

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { AccountSettingsContainer } from '@colanode/ui/components/accounts/account-settings-container';
import { AccountSettingsHeader } from '@colanode/ui/components/accounts/account-settings-header';
import { AccountSettingsTab } from '@colanode/ui/components/accounts/account-settings-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -11,25 +10,24 @@ import {
export const accountSettingsRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/account/settings',
path: '/account',
component: AccountSettingsContainer,
context: () => {
return {
tab: <AccountSettingsTab />,
header: <AccountSettingsHeader />,
};
},
});
export const accountSettingsMaskRoute = createRoute({
getParentRoute: () => workspaceMaskRoute,
path: '/account/settings',
path: '/account',
component: () => null,
beforeLoad: (ctx) => {
const userId = getWorkspaceUserId(ctx.params.workspaceId);
if (userId) {
throw redirect({
to: '/workspace/$userId/account/settings',
to: '/workspace/$userId/account',
params: { userId },
replace: true,
});

View File

@@ -1,8 +1,7 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { AppAppearanceSettingsContainer } from '@colanode/ui/components/app/app-appearance-settings-container';
import { AppAppearanceSettingsHeader } from '@colanode/ui/components/app/app-appearance-settings-header';
import { AppAppearanceSettingsTab } from '@colanode/ui/components/app/app-appearance-settings-tab';
import { AppAppearanceContainer } from '@colanode/ui/components/app/app-appearance-container';
import { AppAppearanceTab } from '@colanode/ui/components/app/app-appearance-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
workspaceRoute,
@@ -11,25 +10,24 @@ import {
export const appAppearanceRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/app/appearance',
component: AppAppearanceSettingsContainer,
path: '/appearance',
component: AppAppearanceContainer,
context: () => {
return {
tab: <AppAppearanceSettingsTab />,
header: <AppAppearanceSettingsHeader />,
tab: <AppAppearanceTab />,
};
},
});
export const appAppearanceMaskRoute = createRoute({
getParentRoute: () => workspaceMaskRoute,
path: '/app/appearance',
path: '/appearance',
component: () => null,
beforeLoad: (ctx) => {
const userId = getWorkspaceUserId(ctx.params.workspaceId);
if (userId) {
throw redirect({
to: '/workspace/$userId/app/appearance',
to: '/workspace/$userId/appearance',
params: { userId },
replace: true,
});

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceDownloadsContainer } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-container';
import { WorkspaceDownloadsHeader } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-header';
import { WorkspaceDownloadsTab } from '@colanode/ui/components/workspaces/downloads/workspace-downloads-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -16,7 +15,6 @@ export const workspaceDownloadsRoute = createRoute({
context: () => {
return {
tab: <WorkspaceDownloadsTab />,
header: <WorkspaceDownloadsHeader />,
};
},
});

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceHomeContainer } from '@colanode/ui/components/workspaces/workspace-home-container';
import { WorkspaceHomeHeader } from '@colanode/ui/components/workspaces/workspace-home-header';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
workspaceMaskRoute,
@@ -12,11 +11,6 @@ export const workspaceHomeRoute = createRoute({
getParentRoute: () => workspaceRoute,
path: '/home',
component: WorkspaceHomeContainer,
context: () => {
return {
header: <WorkspaceHomeHeader />,
};
},
});
export const workspaceHomeMaskRoute = createRoute({

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { LogoutContainer } from '@colanode/ui/components/auth/logout-container';
import { LogoutHeader } from '@colanode/ui/components/auth/logout-header';
import { LogoutTab } from '@colanode/ui/components/auth/logout-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -16,7 +15,6 @@ export const logoutRoute = createRoute({
context: () => {
return {
tab: <LogoutTab />,
header: <LogoutHeader />,
};
},
});

View File

@@ -0,0 +1,21 @@
import { createRoute } from '@tanstack/react-router';
import { NodeErrorContainer } from '@colanode/ui/components/nodes/node-error-container';
import { NodeModal } from '@colanode/ui/components/nodes/node-modal';
import { NodeTab } from '@colanode/ui/components/nodes/node-tab';
import { nodeRoute } from '@colanode/ui/routes/workspace/node';
export const modalNodeRoute = createRoute({
getParentRoute: () => nodeRoute,
path: '/modal/$modalNodeId',
component: () => {
const { modalNodeId } = modalNodeRoute.useParams();
return <NodeModal nodeId={modalNodeId} />;
},
errorComponent: NodeErrorContainer,
context: (ctx) => {
return {
tab: <NodeTab userId={ctx.params.userId} nodeId={ctx.params.nodeId} />,
};
},
});

View File

@@ -2,7 +2,6 @@ import { createRoute, redirect } from '@tanstack/react-router';
import { NodeContainer } from '@colanode/ui/components/nodes/node-container';
import { NodeErrorContainer } from '@colanode/ui/components/nodes/node-error-container';
import { NodeHeader } from '@colanode/ui/components/nodes/node-header';
import { NodeTab } from '@colanode/ui/components/nodes/node-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -15,13 +14,12 @@ export const nodeRoute = createRoute({
path: '/$nodeId',
component: () => {
const { nodeId } = nodeRoute.useParams();
return <NodeContainer nodeId={nodeId} />;
return <NodeContainer type="full" nodeId={nodeId} />;
},
errorComponent: NodeErrorContainer,
context: (ctx) => {
return {
tab: <NodeTab userId={ctx.params.userId} nodeId={ctx.params.nodeId} />,
header: <NodeHeader nodeId={ctx.params.nodeId} />,
};
},
});

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceSettingsContainer } from '@colanode/ui/components/workspaces/workspace-settings-container';
import { WorkspaceSettingsHeader } from '@colanode/ui/components/workspaces/workspace-settings-header';
import { WorkspaceSettingsTab } from '@colanode/ui/components/workspaces/workspace-settings-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -16,7 +15,6 @@ export const workspaceSettingsRoute = createRoute({
context: () => {
return {
tab: <WorkspaceSettingsTab />,
header: <WorkspaceSettingsHeader />,
};
},
});

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceStorageContainer } from '@colanode/ui/components/workspaces/storage/workspace-storage-container';
import { WorkspaceStorageHeader } from '@colanode/ui/components/workspaces/storage/workspace-storage-header';
import { WorkspaceStorageTab } from '@colanode/ui/components/workspaces/storage/workspace-storage-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -16,7 +15,6 @@ export const workspaceStorageRoute = createRoute({
context: () => {
return {
tab: <WorkspaceStorageTab />,
header: <WorkspaceStorageHeader />,
};
},
});

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceUploadsContainer } from '@colanode/ui/components/workspaces/uploads/workspace-uploads-container';
import { WorkspaceUploadsHeader } from '@colanode/ui/components/workspaces/uploads/workspace-uploads-header';
import { WorkspaceUploadsTab } from '@colanode/ui/components/workspaces/uploads/workspace-uploads-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -16,7 +15,6 @@ export const workspaceUploadsRoute = createRoute({
context: () => {
return {
tab: <WorkspaceUploadsTab />,
header: <WorkspaceUploadsHeader />,
};
},
});

View File

@@ -1,7 +1,6 @@
import { createRoute, redirect } from '@tanstack/react-router';
import { WorkspaceUsersContainer } from '@colanode/ui/components/workspaces/workspace-users-container';
import { WorkspaceUsersHeader } from '@colanode/ui/components/workspaces/workspace-users-header';
import { WorkspaceUsersTab } from '@colanode/ui/components/workspaces/workspace-users-tab';
import { getWorkspaceUserId } from '@colanode/ui/routes/utils';
import {
@@ -16,7 +15,6 @@ export const workspaceUsersRoute = createRoute({
context: () => {
return {
tab: <WorkspaceUsersTab />,
header: <WorkspaceUsersHeader />,
};
},
});