Use tanstackdb for some node queries

This commit is contained in:
Hakan Shehu
2025-11-20 12:11:34 -08:00
parent 175cabfb29
commit b075742651
26 changed files with 174 additions and 721 deletions

View File

@@ -25,8 +25,6 @@ import { IconListQueryHandler } from './icons/icon-list';
import { IconSearchQueryHandler } from './icons/icon-search';
import { IconSvgGetQueryHandler } from './icons/icon-svg-get';
import { RadarDataGetQueryHandler } from './interactions/radar-data-get';
import { NodeChildrenGetQueryHandler } from './nodes/node-children-get';
import { NodeGetQueryHandler } from './nodes/node-get';
import { NodeListQueryHandler } from './nodes/node-list';
import { NodeReactionsListQueryHandler } from './nodes/node-reaction-list';
import { NodeReactionsAggregateQueryHandler } from './nodes/node-reactions-aggregate';
@@ -52,7 +50,6 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'account.list': new AccountListQueryHandler(app),
'node.reaction.list': new NodeReactionsListQueryHandler(app),
'node.reactions.aggregate': new NodeReactionsAggregateQueryHandler(app),
'node.get': new NodeGetQueryHandler(app),
'node.list': new NodeListQueryHandler(app),
'node.tree.get': new NodeTreeGetQueryHandler(app),
'record.list': new RecordListQueryHandler(app),
@@ -72,7 +69,6 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'icon.list': new IconListQueryHandler(app),
'icon.search': new IconSearchQueryHandler(app),
'icon.category.list': new IconCategoryListQueryHandler(app),
'node.children.get': new NodeChildrenGetQueryHandler(app),
'radar.data.get': new RadarDataGetQueryHandler(app),
'record.search': new RecordSearchQueryHandler(app),
'local.file.get': new LocalFileGetQueryHandler(app),

View File

@@ -1,102 +0,0 @@
import { SelectNode } from '@colanode/client/databases/workspace';
import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/workspace-query-handler-base';
import { mapNode } from '@colanode/client/lib';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { NodeChildrenGetQueryInput } from '@colanode/client/queries/nodes/node-children-get';
import { Event } from '@colanode/client/types/events';
import { LocalNode } from '@colanode/client/types/nodes';
export class NodeChildrenGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<NodeChildrenGetQueryInput>
{
public async handleQuery(
input: NodeChildrenGetQueryInput
): Promise<LocalNode[]> {
const rows = await this.fetchChildren(input);
return rows.map(mapNode) as LocalNode[];
}
public async checkForChanges(
event: Event,
input: NodeChildrenGetQueryInput,
output: LocalNode[]
): Promise<ChangeCheckResult<NodeChildrenGetQueryInput>> {
if (
event.type === 'workspace.deleted' &&
event.workspace.userId === input.userId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'node.created' &&
event.workspace.userId === input.userId &&
event.node.parentId === input.nodeId &&
(input.types === undefined || input.types.includes(event.node.type))
) {
const newChildren = [...output, event.node];
return {
hasChanges: true,
result: newChildren,
};
}
if (
event.type === 'node.updated' &&
event.workspace.userId === input.userId &&
event.node.parentId === input.nodeId &&
(input.types === undefined || input.types.includes(event.node.type))
) {
const node = output.find((n) => n.id === event.node.id);
if (node) {
const newChildren = output.map((node) =>
node.id === event.node.id ? event.node : node
);
return {
hasChanges: true,
result: newChildren,
};
}
}
if (
event.type === 'node.deleted' &&
event.workspace.userId === input.userId &&
event.node.parentId === input.nodeId &&
(input.types === undefined || input.types.includes(event.node.type))
) {
const node = output.find((n) => n.id === event.node.id);
if (node) {
const newChildren = output.filter((n) => n.id !== event.node.id);
return {
hasChanges: true,
result: newChildren,
};
}
}
return {
hasChanges: false,
};
}
private async fetchChildren(
input: NodeChildrenGetQueryInput
): Promise<SelectNode[]> {
const workspace = this.getWorkspace(input.userId);
const rows = await workspace.database
.selectFrom('nodes')
.selectAll()
.where('parent_id', '=', input.nodeId)
.where('type', 'in', input.types ?? [])
.execute();
return rows;
}
}

View File

@@ -1,86 +0,0 @@
import { SelectNode } from '@colanode/client/databases';
import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/workspace-query-handler-base';
import { mapNode } from '@colanode/client/lib';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib/types';
import { NodeGetQueryInput } from '@colanode/client/queries/nodes/node-get';
import { Event } from '@colanode/client/types/events';
import { LocalNode } from '@colanode/client/types/nodes';
export class NodeGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<NodeGetQueryInput>
{
public async handleQuery(
input: NodeGetQueryInput
): Promise<LocalNode | null> {
const row = await this.fetchNode(input);
return row ? mapNode(row) : null;
}
public async checkForChanges(
event: Event,
input: NodeGetQueryInput,
_: LocalNode | null
): Promise<ChangeCheckResult<NodeGetQueryInput>> {
if (
event.type === 'workspace.deleted' &&
event.workspace.userId === input.userId
) {
return {
hasChanges: true,
result: null,
};
}
if (
event.type === 'node.created' &&
event.workspace.userId === input.userId &&
event.node.id === input.nodeId
) {
return {
hasChanges: true,
result: event.node,
};
}
if (
event.type === 'node.updated' &&
event.workspace.userId === input.userId &&
event.node.id === input.nodeId
) {
return {
hasChanges: true,
result: event.node,
};
}
if (
event.type === 'node.deleted' &&
event.workspace.userId === input.userId &&
event.node.id === input.nodeId
) {
return {
hasChanges: true,
result: null,
};
}
return {
hasChanges: false,
};
}
private async fetchNode(
input: NodeGetQueryInput
): Promise<SelectNode | undefined> {
const workspace = this.getWorkspace(input.userId);
const row = await workspace.database
.selectFrom('nodes')
.selectAll()
.where('id', '=', input.nodeId)
.executeTakeFirst();
return row;
}
}

View File

@@ -16,8 +16,6 @@ export * from './icons/icon-category-list';
export * from './icons/icon-list';
export * from './icons/icon-search';
export * from './interactions/radar-data-get';
export * from './nodes/node-children-get';
export * from './nodes/node-get';
export * from './nodes/node-reaction-list';
export * from './nodes/node-reactions-aggregate';
export * from './nodes/node-tree-get';

View File

@@ -1,18 +0,0 @@
import { LocalNode } from '@colanode/client/types/nodes';
import { NodeType } from '@colanode/core';
export type NodeChildrenGetQueryInput = {
type: 'node.children.get';
nodeId: string;
userId: string;
types?: NodeType[];
};
declare module '@colanode/client/queries' {
interface QueryMap {
'node.children.get': {
input: NodeChildrenGetQueryInput;
output: LocalNode[];
};
}
}

View File

@@ -1,16 +0,0 @@
import { LocalNode } from '@colanode/client/types/nodes';
export type NodeGetQueryInput = {
type: 'node.get';
nodeId: string;
userId: string;
};
declare module '@colanode/client/queries' {
interface QueryMap {
'node.get': {
input: NodeGetQueryInput;
output: LocalNode | null;
};
}
}

View File

@@ -2,12 +2,15 @@ import { Collection, createLiveQueryCollection, eq } from '@tanstack/react-db';
import {
Download,
LocalChannelNode,
LocalChatNode,
LocalDatabaseNode,
LocalDatabaseViewNode,
LocalFileNode,
LocalFolderNode,
LocalMessageNode,
LocalNode,
LocalPageNode,
LocalRecordNode,
LocalSpaceNode,
Upload,
@@ -35,9 +38,12 @@ class WorkspaceCollections {
public readonly views: Collection<LocalDatabaseViewNode>;
public readonly records: Collection<LocalRecordNode>;
public readonly chats: Collection<LocalChatNode>;
public readonly channels: Collection<LocalChannelNode>;
public readonly spaces: Collection<LocalSpaceNode>;
public readonly files: Collection<LocalFileNode>;
public readonly messages: Collection<LocalMessageNode>;
public readonly folders: Collection<LocalFolderNode>;
public readonly pages: Collection<LocalPageNode>;
constructor(userId: string) {
this.userId = userId;
@@ -59,6 +65,11 @@ class WorkspaceCollections {
this.chats = createLiveQueryCollection((q) =>
q.from({ nodes: this.nodes }).where(({ nodes }) => eq(nodes.type, 'chat'))
);
this.channels = createLiveQueryCollection((q) =>
q
.from({ nodes: this.nodes })
.where(({ nodes }) => eq(nodes.type, 'channel'))
);
this.spaces = createLiveQueryCollection((q) =>
q
.from({ nodes: this.nodes })
@@ -77,6 +88,14 @@ class WorkspaceCollections {
.from({ nodes: this.nodes })
.where(({ nodes }) => eq(nodes.type, 'message'))
);
this.folders = createLiveQueryCollection((q) =>
q
.from({ nodes: this.nodes })
.where(({ nodes }) => eq(nodes.type, 'folder'))
);
this.pages = createLiveQueryCollection((q) =>
q.from({ nodes: this.nodes }).where(({ nodes }) => eq(nodes.type, 'page'))
);
}
}

View File

@@ -1,59 +0,0 @@
import { LocalChannelNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface ChannelContainerTabProps {
channelId: string;
isActive: boolean;
}
export const ChannelContainerTab = ({
channelId,
isActive,
}: ChannelContainerTabProps) => {
const workspace = useWorkspace();
const radar = useRadar();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: channelId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const channel = nodeGetQuery.data as LocalChannelNode;
if (!channel) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
const name =
channel.attributes.name && channel.attributes.name.length > 0
? channel.attributes.name
: 'Unnamed';
const unreadState = radar.getNodeState(workspace.userId, channel.id);
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={channel.id}
name={name}
avatar={channel.attributes.avatar}
/>
<span>{name}</span>
{!isActive && (
<UnreadBadge
count={unreadState.unreadCount}
unread={unreadState.hasUnread}
/>
)}
</div>
);
};

View File

@@ -1,71 +0,0 @@
import { eq, useLiveQuery as useLiveQueryTanstack } from '@tanstack/react-db';
import { LocalChatNode } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface ChatContainerTabProps {
chatId: string;
isActive: boolean;
}
export const ChatContainerTab = ({
chatId,
isActive,
}: ChatContainerTabProps) => {
const workspace = useWorkspace();
const radar = useRadar();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: chatId,
userId: workspace.userId,
});
const chat = nodeGetQuery.data as LocalChatNode;
const userId = chat
? (Object.keys(chat.attributes.collaborators).find(
(id) => id !== workspace.userId
) ?? '')
: '';
const userQuery = useLiveQueryTanstack((q) =>
q
.from({ users: collections.workspace(workspace.userId).users })
.where(({ users }) => eq(users.id, userId))
.select(({ users }) => ({
id: users.id,
name: users.name,
avatar: users.avatar,
}))
.findOne()
);
const user = userQuery.data;
if (nodeGetQuery.isPending || userQuery.isLoading) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
if (!chat || !user) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
const unreadState = radar.getNodeState(workspace.userId, chat.id);
return (
<div className="flex items-center space-x-2">
<Avatar size="small" id={user.id} name={user.name} avatar={user.avatar} />
<span>{user.name}</span>
{!isActive && (
<UnreadBadge
count={unreadState.unreadCount}
unread={unreadState.hasUnread}
/>
)}
</div>
);
};

View File

@@ -1,46 +0,0 @@
import { LocalDatabaseNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface DatabaseContainerTabProps {
databaseId: string;
}
export const DatabaseContainerTab = ({
databaseId,
}: DatabaseContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: databaseId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const database = nodeGetQuery.data as LocalDatabaseNode;
if (!database) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
const name =
database.attributes.name && database.attributes.name.length > 0
? database.attributes.name
: 'Unnamed';
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={database.id}
name={name}
avatar={database.attributes.avatar}
/>
<span>{name}</span>
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { eq, inArray, useLiveQuery } from '@tanstack/react-db';
import { ChevronDown, Trash2, X } from 'lucide-react';
import { LocalRecordNode } from '@colanode/client/types';
@@ -5,6 +6,7 @@ import {
DatabaseViewFieldFilterAttributes,
RelationFieldAttributes,
} from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { FieldIcon } from '@colanode/ui/components/databases/fields/field-icon';
import { RecordSearch } from '@colanode/ui/components/records/record-search';
@@ -24,7 +26,6 @@ import {
import { Separator } from '@colanode/ui/components/ui/separator';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
import { relationFieldFilterOperators } from '@colanode/ui/lib/databases';
interface ViewRelationFieldFilterProps {
@@ -64,21 +65,22 @@ export const ViewRelationFieldFilter = ({
) ?? relationFieldFilterOperators[0]!;
const relationIds = (filter.value as string[]) ?? [];
const results = useLiveQueries(
relationIds.map((id) => ({
type: 'node.get',
nodeId: id,
userId: workspace.userId,
}))
const relationsQuery = useLiveQuery(
(q) => {
if (relationIds.length === 0 || !field.databaseId) {
return q
.from({ records: collections.workspace(workspace.userId).records })
.where(({ records }) => eq(records.id, '')); // Return empty result
}
return q
.from({ records: collections.workspace(workspace.userId).records })
.where(({ records }) => inArray(records.id, relationIds));
},
[workspace.userId, field.databaseId, relationIds]
);
const relations: LocalRecordNode[] = [];
for (const result of results) {
if (result.data && result.data.type === 'record') {
relations.push(result.data);
}
}
const relations = relationsQuery.data;
const hideInput = isOperatorWithoutValue(operator.value);
if (!field.databaseId) {

View File

@@ -1,9 +1,11 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { LocalFileNode } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { FileIcon } from '@colanode/ui/components/files/file-icon';
import { FilePreview } from '@colanode/ui/components/files/file-preview';
import { Link } from '@colanode/ui/components/ui/link';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { canPreviewFile } from '@colanode/ui/lib/files';
interface FileBlockProps {
@@ -13,17 +15,20 @@ interface FileBlockProps {
export const FileBlock = ({ id }: FileBlockProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: id,
userId: workspace.userId,
});
const fileGetQuery = useLiveQuery(
(q) =>
q
.from({ files: collections.workspace(workspace.userId).files })
.where(({ files }) => eq(files.id, id))
.findOne(),
[workspace.userId, id]
);
if (nodeGetQuery.isPending || !nodeGetQuery.data) {
if (fileGetQuery.isLoading || !fileGetQuery.data) {
return null;
}
const file = nodeGetQuery.data as LocalFileNode;
const file = fileGetQuery.data as LocalFileNode;
const canPreview = canPreviewFile(file.attributes.subtype);
return (

View File

@@ -1,38 +0,0 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface FileContainerTabProps {
fileId: string;
}
export const FileContainerTab = ({ fileId }: FileContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: fileId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const file = nodeGetQuery.data as LocalFileNode;
if (!file) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<FileThumbnail
userId={workspace.userId}
file={file}
className="size-4 rounded object-contain"
/>
<span>{file.attributes.name}</span>
</div>
);
};

View File

@@ -1,44 +0,0 @@
import { LocalFolderNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface FolderContainerTabProps {
folderId: string;
}
export const FolderContainerTab = ({ folderId }: FolderContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: folderId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const folder = nodeGetQuery.data as LocalFolderNode;
if (!folder) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
const name =
folder.attributes.name && folder.attributes.name.length > 0
? folder.attributes.name
: 'Unnamed';
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={folder.id}
name={name}
avatar={folder.attributes.avatar}
/>
<span>{name}</span>
</div>
);
};

View File

@@ -1,9 +1,11 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { LocalMessageNode } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { MessageAuthorAvatar } from '@colanode/ui/components/messages/message-author-avatar';
import { MessageAuthorName } from '@colanode/ui/components/messages/message-author-name';
import { MessageContent } from '@colanode/ui/components/messages/message-content';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface MessageReferenceProps {
messageId: string;
@@ -11,17 +13,20 @@ interface MessageReferenceProps {
export const MessageReference = ({ messageId }: MessageReferenceProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: messageId,
userId: workspace.userId,
});
const messageGetQuery = useLiveQuery(
(q) =>
q
.from({ messages: collections.workspace(workspace.userId).messages })
.where(({ messages }) => eq(messages.id, messageId))
.findOne(),
[workspace.userId, messageId]
);
if (nodeGetQuery.isPending) {
if (messageGetQuery.isLoading) {
return null;
}
const message = nodeGetQuery.data as LocalMessageNode;
const message = messageGetQuery.data as LocalMessageNode;
if (!message) {
return (
@@ -36,7 +41,7 @@ export const MessageReference = ({ messageId }: MessageReferenceProps) => {
return (
<div className="flex flex-row gap-2 border-l-4 p-2">
<MessageAuthorAvatar message={message} className="size-5 mt-1" />
<div className='"grow flex-col gap-1'>
<div className="grow flex-col gap-1">
<MessageAuthorName message={message} />
<MessageContent message={message} />
</div>

View File

@@ -1,3 +1,6 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { collections } from '@colanode/ui/collections';
import { ChannelTab } from '@colanode/ui/components/channels/channel-tab';
import { ChatTab } from '@colanode/ui/components/chats/chat-tab';
import { DatabaseTab } from '@colanode/ui/components/databases/database-tab';
@@ -7,7 +10,6 @@ import { MessageTab } from '@colanode/ui/components/messages/message-tab';
import { PageTab } from '@colanode/ui/components/pages/page-tab';
import { RecordTab } from '@colanode/ui/components/records/record-tab';
import { SpaceTab } from '@colanode/ui/components/spaces/space-tab';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface NodeTabProps {
userId: string;
@@ -15,13 +17,16 @@ interface NodeTabProps {
}
export const NodeTab = ({ userId, nodeId }: NodeTabProps) => {
const query = useLiveQuery({
type: 'node.get',
userId: userId,
nodeId: nodeId,
});
const query = useLiveQuery(
(q) =>
q
.from({ nodes: collections.workspace(userId).nodes })
.where(({ nodes }) => eq(nodes.id, nodeId))
.findOne(),
[userId, nodeId]
);
if (query.isPending) {
if (query.isLoading) {
return null;
}

View File

@@ -1,44 +0,0 @@
import { LocalPageNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface PageContainerTabProps {
pageId: string;
}
export const PageContainerTab = ({ pageId }: PageContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: pageId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const page = nodeGetQuery.data as LocalPageNode;
if (!page) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
const name =
page.attributes.name && page.attributes.name.length > 0
? page.attributes.name
: 'Unnamed';
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={page.id}
name={name}
avatar={page.attributes.avatar}
/>
<span>{name}</span>
</div>
);
};

View File

@@ -1,44 +0,0 @@
import { LocalRecordNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordContainerTabProps {
recordId: string;
}
export const RecordContainerTab = ({ recordId }: RecordContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: recordId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const record = nodeGetQuery.data as LocalRecordNode;
if (!record) {
return <p>Not found</p>;
}
const name =
record.attributes.name && record.attributes.name.length > 0
? record.attributes.name
: 'Unnamed';
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={record.id}
name={name}
avatar={record.attributes.avatar}
/>
<span>{name}</span>
</div>
);
};

View File

@@ -1,8 +1,10 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { LocalDatabaseNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { Database } from '@colanode/ui/components/databases/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordDatabaseProps {
id: string;
@@ -13,21 +15,24 @@ interface RecordDatabaseProps {
export const RecordDatabase = ({ id, role, children }: RecordDatabaseProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
userId: workspace.userId,
nodeId: id,
});
const databaseGetQuery = useLiveQuery(
(q) =>
q
.from({ databases: collections.workspace(workspace.userId).databases })
.where(({ databases }) => eq(databases.id, id))
.findOne(),
[workspace.userId, id]
);
if (nodeGetQuery.isPending) {
if (databaseGetQuery.isLoading) {
return null;
}
if (!nodeGetQuery.data) {
if (!databaseGetQuery.data) {
return null;
}
const database = nodeGetQuery.data as LocalDatabaseNode;
const database = databaseGetQuery.data as LocalDatabaseNode;
return (
<Database database={database} role={role}>
{children}

View File

@@ -1,8 +1,10 @@
import { eq, inArray, useLiveQuery } from '@tanstack/react-db';
import { X } from 'lucide-react';
import { Fragment, useState } from 'react';
import { LocalRecordNode } from '@colanode/client/types';
import { RelationFieldAttributes } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { RecordSearch } from '@colanode/ui/components/records/record-search';
import { Badge } from '@colanode/ui/components/ui/badge';
@@ -14,7 +16,6 @@ import {
import { Separator } from '@colanode/ui/components/ui/separator';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
interface RecordRelationValueProps {
field: RelationFieldAttributes;
@@ -46,21 +47,22 @@ export const RecordRelationValue = ({
const [open, setOpen] = useState(false);
const relationIds = record.getRelationValue(field) ?? [];
const results = useLiveQueries(
relationIds.map((id) => ({
type: 'node.get',
nodeId: id,
userId: workspace.userId,
}))
const relationsQuery = useLiveQuery(
(q) => {
if (relationIds.length === 0 || !field.databaseId) {
return q
.from({ records: collections.workspace(workspace.userId).records })
.where(({ records }) => eq(records.id, '')); // Return empty result
}
return q
.from({ records: collections.workspace(workspace.userId).records })
.where(({ records }) => inArray(records.id, relationIds));
},
[workspace.userId, field.databaseId, relationIds]
);
const relations: LocalRecordNode[] = [];
for (const result of results) {
if (result.data && result.data.type === 'record') {
relations.push(result.data);
}
}
const relations = relationsQuery.data;
if (!field.databaseId) {
return null;
}

View File

@@ -1,44 +0,0 @@
import { LocalSpaceNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface SpaceContainerTabProps {
spaceId: string;
}
export const SpaceContainerTab = ({ spaceId }: SpaceContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: spaceId,
userId: workspace.userId,
});
if (nodeGetQuery.isPending) {
return <p className="text-sm text-muted-foreground">Loading...</p>;
}
const space = nodeGetQuery.data as LocalSpaceNode;
if (!space) {
return <p className="text-sm text-muted-foreground">Not found</p>;
}
const name =
space.attributes.name && space.attributes.name.length > 0
? space.attributes.name
: 'Unnamed';
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={space.id}
name={name}
avatar={space.attributes.avatar}
/>
<span>{name}</span>
</div>
);
};

View File

@@ -1,3 +1,4 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { ChevronRight } from 'lucide-react';
import { RefAttributes, useRef } from 'react';
import { useDrop } from 'react-dnd';
@@ -5,6 +6,7 @@ import { toast } from 'sonner';
import { LocalSpaceNode } from '@colanode/client/types';
import { extractNodeRole } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { WorkspaceSidebarItem } from '@colanode/ui/components/layouts/sidebars/sidebar-item';
import { SpaceSidebarDropdown } from '@colanode/ui/components/spaces/space-sidebar-dropdown';
@@ -15,7 +17,6 @@ import {
} from '@colanode/ui/components/ui/collapsible';
import { Link } from '@colanode/ui/components/ui/link';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { sortSpaceChildren } from '@colanode/ui/lib/spaces';
import { cn } from '@colanode/ui/lib/utils';
@@ -31,12 +32,13 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
const role = extractNodeRole(space, workspace.userId);
const canEdit = role === 'admin';
const nodeChildrenGetQuery = useLiveQuery({
type: 'node.children.get',
nodeId: space.id,
userId: workspace.userId,
types: ['page', 'channel', 'database', 'folder'],
});
const nodeChildrenGetQuery = useLiveQuery(
(q) =>
q
.from({ nodes: collections.workspace(workspace.userId).nodes })
.where(({ nodes }) => eq(nodes.parentId, space.id)),
[workspace.userId, space.id]
);
const [dropMonitor, dropRef] = useDrop({
accept: 'sidebar-item',
@@ -52,7 +54,7 @@ export const SpaceSidebarItem = ({ space }: SpaceSidebarItemProps) => {
const divRef = useRef<HTMLDivElement>(null);
const dropDivRef = dropRef(divRef);
const children = sortSpaceChildren(space, nodeChildrenGetQuery.data ?? []);
const children = sortSpaceChildren(space, nodeChildrenGetQuery.data);
const handleDragEnd = (childId: string, after: string | null) => {
mutation.mutate({

View File

@@ -1,8 +1,10 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { useNavigate } from '@tanstack/react-router';
import { Folder } from 'lucide-react';
import { LocalFileNode, Download } from '@colanode/client/types';
import { formatBytes, timeAgo } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { FileIcon } from '@colanode/ui/components/files/file-icon';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import {
@@ -12,7 +14,6 @@ import {
} from '@colanode/ui/components/ui/tooltip';
import { WorkspaceDownloadStatus } from '@colanode/ui/components/workspaces/downloads/workspace-download-status';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface WorkspaceDownloadFileProps {
download: Download;
@@ -24,11 +25,14 @@ export const WorkspaceDownloadFile = ({
const workspace = useWorkspace();
const navigate = useNavigate({ from: '/workspace/$userId' });
const fileQuery = useLiveQuery({
type: 'node.get',
userId: workspace.userId,
nodeId: download.fileId,
});
const fileQuery = useLiveQuery(
(q) =>
q
.from({ files: collections.workspace(workspace.userId).files })
.where(({ files }) => eq(files.id, download.fileId))
.findOne(),
[workspace.userId, download.fileId]
);
const file = fileQuery.data as LocalFileNode | undefined;

View File

@@ -1,12 +1,13 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { BadgeAlert } from 'lucide-react';
import { Upload, LocalFileNode } from '@colanode/client/types';
import { formatBytes, timeAgo } from '@colanode/core';
import { collections } from '@colanode/ui/collections';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import { Link } from '@colanode/ui/components/ui/link';
import { WorkspaceUploadStatus } from '@colanode/ui/components/workspaces/uploads/workspace-upload-status';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface WorkspaceUploadFileProps {
upload: Upload;
@@ -15,11 +16,14 @@ interface WorkspaceUploadFileProps {
export const WorkspaceUploadFile = ({ upload }: WorkspaceUploadFileProps) => {
const workspace = useWorkspace();
const fileQuery = useLiveQuery({
type: 'node.get',
userId: workspace.userId,
nodeId: upload.fileId,
});
const fileQuery = useLiveQuery(
(q) =>
q
.from({ files: collections.workspace(workspace.userId).files })
.where(({ files }) => eq(files.id, upload.fileId))
.findOne(),
[workspace.userId, upload.fileId]
);
const file = fileQuery.data as LocalFileNode;

View File

@@ -1,37 +1,46 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { type NodeViewProps } from '@tiptap/core';
import { NodeViewWrapper } from '@tiptap/react';
import { LocalFolderNode } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { Link } from '@colanode/ui/components/ui/link';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const FolderNodeView = ({ node }: NodeViewProps) => {
const workspace = useWorkspace();
const id = node.attrs.id;
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: id,
userId: workspace.userId,
});
if (!id) {
return null;
}
if (nodeGetQuery.isPending) {
const folderGetQuery = useLiveQuery(
(q) =>
q
.from({ folders: collections.workspace(workspace.userId).folders })
.where(({ folders }) => eq(folders.id, id))
.select(({ folders }) => ({
id: folders.id,
name: folders.attributes.name,
avatar: folders.attributes.avatar,
}))
.findOne(),
[workspace.userId, id]
);
if (folderGetQuery.isLoading) {
return null;
}
const folder = nodeGetQuery.data as LocalFolderNode;
const folder = folderGetQuery.data;
if (!folder) {
return null;
}
const name = folder.attributes.name ?? 'Unnamed';
const avatar = folder.attributes.avatar;
const name = folder.name ?? 'Unnamed';
const avatar = folder.avatar;
return (
<NodeViewWrapper data-id={node.attrs.id}>

View File

@@ -1,37 +1,46 @@
import { eq, useLiveQuery } from '@tanstack/react-db';
import { type NodeViewProps } from '@tiptap/core';
import { NodeViewWrapper } from '@tiptap/react';
import { LocalPageNode } from '@colanode/client/types';
import { collections } from '@colanode/ui/collections';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { Link } from '@colanode/ui/components/ui/link';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const PageNodeView = ({ node }: NodeViewProps) => {
const workspace = useWorkspace();
const id = node.attrs.id;
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: id,
userId: workspace.userId,
});
if (!id) {
return null;
}
if (nodeGetQuery.isPending) {
const pageGetQuery = useLiveQuery(
(q) =>
q
.from({ pages: collections.workspace(workspace.userId).pages })
.where(({ pages }) => eq(pages.id, id))
.select(({ pages }) => ({
id: pages.id,
name: pages.attributes.name,
avatar: pages.attributes.avatar,
}))
.findOne(),
[workspace.userId, id]
);
if (pageGetQuery.isLoading) {
return null;
}
const page = nodeGetQuery.data as LocalPageNode;
const page = pageGetQuery.data;
if (!page) {
return null;
}
const name = page.attributes.name ?? 'Unnamed';
const avatar = page.attributes.avatar;
const name = page.name ?? 'Unnamed';
const avatar = page.avatar;
return (
<NodeViewWrapper data-id={node.attrs.id}>