mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 03:37:51 +01:00
Open records in a modal (#261)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
@@ -1,25 +1,31 @@
|
||||
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 (
|
||||
<div 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" />
|
||||
<Container type="full" breadcrumb={<AccountSettingsBreadcrumb />}>
|
||||
<div 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 />
|
||||
</div>
|
||||
<AccountUpdate />
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Danger Zone</h2>
|
||||
<Separator className="mt-3" />
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Danger Zone
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<AccountDelete />
|
||||
</div>
|
||||
<AccountDelete />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
138
packages/ui/src/components/app/app-appearance-container.tsx
Normal file
138
packages/ui/src/components/app/app-appearance-container.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
@@ -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" />
|
||||
);
|
||||
@@ -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,50 +15,52 @@ export const LogoutContainer = () => {
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
return (
|
||||
<div 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 flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
||||
<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>
|
||||
<Container type="full" breadcrumb={<LogoutBreadcrumb />}>
|
||||
<div 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="w-full md:w-auto md:shrink-0">
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
className="w-full cursor-pointer md:w-20"
|
||||
onClick={async () => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'account.logout',
|
||||
accountId: workspace.accountId,
|
||||
},
|
||||
onSuccess() {
|
||||
navigate({
|
||||
to: '/',
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
Logout
|
||||
</Button>
|
||||
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
|
||||
<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="w-full md:w-auto md:shrink-0">
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={isPending}
|
||||
className="w-full cursor-pointer md:w-20"
|
||||
onClick={async () => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'account.logout',
|
||||
accountId: workspace.accountId,
|
||||
},
|
||||
onSuccess() {
|
||||
navigate({
|
||||
to: '/',
|
||||
replace: true,
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending && <Spinner className="mr-1" />}
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
57
packages/ui/src/components/nodes/node-modal.tsx
Normal file
57
packages/ui/src/components/nodes/node-modal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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" />;
|
||||
};
|
||||
@@ -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" />;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
@@ -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,18 +20,20 @@ export const WorkspaceDownloadsContainer = () => {
|
||||
const downloads = downloadsQuery.data ?? [];
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto">
|
||||
<div className="max-w-4xl space-y-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Downloads</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<div className="space-y-4 w-full">
|
||||
{downloads.map((download) => (
|
||||
<WorkspaceDownloadFile key={download.id} download={download} />
|
||||
))}
|
||||
<Container type="full" breadcrumb={<WorkspaceDownloadsBreadcrumb />}>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="max-w-4xl space-y-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Downloads</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<div className="space-y-4 w-full">
|
||||
{downloads.map((download) => (
|
||||
<WorkspaceDownloadFile key={download.id} download={download} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
@@ -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 (
|
||||
<div className="max-w-4xl space-y-10">
|
||||
<WorkspaceStorageStats />
|
||||
<WorkspaceStorageUsers />
|
||||
</div>
|
||||
<Container type="full" breadcrumb={<WorkspaceStorageBreadcrumb />}>
|
||||
<div className="max-w-4xl space-y-10">
|
||||
<WorkspaceStorageStats />
|
||||
<WorkspaceStorageUsers />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
@@ -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,26 +31,28 @@ export const WorkspaceUploadsContainer = () => {
|
||||
const hasMore = !isPending && uploads.length === lastPage * UPLOADS_PER_PAGE;
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto">
|
||||
<div className="max-w-4xl space-y-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Uploads</h2>
|
||||
<Separator className="mt-3" />
|
||||
<Container type="full" breadcrumb={<WorkspaceUploadsBreadcrumb />}>
|
||||
<div className="overflow-y-auto">
|
||||
<div className="max-w-4xl space-y-10">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Uploads</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<div className="space-y-4 w-full">
|
||||
{uploads.map((upload) => (
|
||||
<WorkspaceUploadFile key={upload.fileId} upload={upload} />
|
||||
))}
|
||||
</div>
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 w-full">
|
||||
{uploads.map((upload) => (
|
||||
<WorkspaceUploadFile key={upload.fileId} upload={upload} />
|
||||
))}
|
||||
</div>
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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" />;
|
||||
};
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Container } from '@colanode/ui/components/layouts/containers/container';
|
||||
import { WorkspaceHomeBreadcrumb } from '@colanode/ui/components/workspaces/workspace-home-breadcrumb';
|
||||
|
||||
export const WorkspaceHomeContainer = () => {
|
||||
return (
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
What did you get done this week?
|
||||
</p>
|
||||
<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">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
What did you get done this week?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
@@ -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,49 +34,53 @@ export const WorkspaceSettingsContainer = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div 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" />
|
||||
<Container type="full" breadcrumb={<WorkspaceSettingsBreadcrumb />}>
|
||||
<div 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: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? '',
|
||||
avatar: currentWorkspace.avatar ?? null,
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'workspace.update',
|
||||
id: workspace.workspaceId,
|
||||
userId: workspace.userId,
|
||||
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>
|
||||
<WorkspaceForm
|
||||
readOnly={!canEdit}
|
||||
values={{
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? '',
|
||||
avatar: currentWorkspace.avatar ?? null,
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
mutate({
|
||||
input: {
|
||||
type: 'workspace.update',
|
||||
id: workspace.workspaceId,
|
||||
userId: workspace.userId,
|
||||
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 className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Danger Zone
|
||||
</h2>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<WorkspaceDelete />
|
||||
</div>
|
||||
<WorkspaceDelete />
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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" />;
|
||||
};
|
||||
@@ -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,64 +35,66 @@ export const WorkspaceUsersContainer = () => {
|
||||
const hasMore = !isPending && users.length === lastPage * USERS_PER_PAGE;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-8">
|
||||
{canEditUsers && (
|
||||
<Container type="full" breadcrumb={<WorkspaceUsersBreadcrumb />}>
|
||||
<div className="max-w-4xl 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 />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Invite</h2>
|
||||
<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>
|
||||
<WorkspaceUserInvite />
|
||||
</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;
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Users</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
The list of all users on the workspace
|
||||
</p>
|
||||
<Separator className="mt-3" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{users.map((user) => {
|
||||
const name: string = user.name ?? 'Unknown';
|
||||
const email: string = user.email ?? ' ';
|
||||
const avatar: string | null | undefined = user.avatar;
|
||||
const role: WorkspaceRole = user.role;
|
||||
|
||||
if (!role) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={user.id} className="flex items-center space-x-3">
|
||||
<Avatar id={user.id} name={name} avatar={avatar} />
|
||||
<div className="grow">
|
||||
<p className="text-sm font-medium leading-none">{name}</p>
|
||||
<p className="text-sm text-muted-foreground">{email}</p>
|
||||
</div>
|
||||
<WorkspaceUserRoleDropdown
|
||||
userId={user.id}
|
||||
value={role}
|
||||
canEdit={canEditUsers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
{isPending && <Spinner />}
|
||||
</div>
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
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="grow">
|
||||
<p className="text-sm font-medium leading-none">{name}</p>
|
||||
<p className="text-sm text-muted-foreground">{email}</p>
|
||||
</div>
|
||||
<WorkspaceUserRoleDropdown
|
||||
userId={user.id}
|
||||
value={role}
|
||||
canEdit={canEditUsers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center justify-center space-x-3">
|
||||
{isPending && <Spinner />}
|
||||
</div>
|
||||
<InView
|
||||
rootMargin="200px"
|
||||
onChange={(inView) => {
|
||||
if (inView && hasMore && !isPending) {
|
||||
setLastPage(lastPage + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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 />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
21
packages/ui/src/routes/workspace/modal.tsx
Normal file
21
packages/ui/src/routes/workspace/modal.tsx
Normal 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} />,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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} />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 />,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user