Use tabs for containers

This commit is contained in:
Hakan Shehu
2025-01-27 12:32:15 +01:00
parent 514974a891
commit d02e2061ec
130 changed files with 2784 additions and 1213 deletions

View File

@@ -95,7 +95,7 @@
"lowlight": "^3.2.0",
"lucide-react": "^0.473.0",
"mime-types": "^2.1.35",
"re-resizable": "^6.10.1",
"re-resizable": "^6.10.3",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dnd": "^16.0.1",

View File

@@ -0,0 +1,16 @@
import { Migration } from 'kysely';
export const createMetadataTable: Migration = {
up: async (db) => {
await db.schema
.createTable('metadata')
.addColumn('key', 'text', (col) => col.notNull().primaryKey())
.addColumn('value', 'text', (col) => col.notNull())
.addColumn('created_at', 'text', (col) => col.notNull())
.addColumn('updated_at', 'text')
.execute();
},
down: async (db) => {
await db.schema.dropTable('metadata').execute();
},
};

View File

@@ -15,6 +15,7 @@ import { createMutationsTable } from './00012-create-mutations-table';
import { createEntryPathsTable } from './00013-create-entry-paths-table';
import { createTextsTable } from './00014-create-texts-table';
import { createCursorsTable } from './00015-create-cursors-table';
import { createMetadataTable } from './00016-create-metadata-table';
export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00001-create-users-table': createUsersTable,
@@ -32,4 +33,5 @@ export const workspaceDatabaseMigrations: Record<string, Migration> = {
'00013-create-entry-paths-table': createEntryPathsTable,
'00014-create-texts-table': createTextsTable,
'00015-create-cursors-table': createCursorsTable,
'00016-create-metadata-table': createMetadataTable,
};

View File

@@ -231,6 +231,21 @@ interface CursorTable {
updated_at: ColumnType<string | null, string | null, string | null>;
}
export type SelectCursor = Selectable<CursorTable>;
export type CreateCursor = Insertable<CursorTable>;
export type UpdateCursor = Updateable<CursorTable>;
interface MetadataTable {
key: ColumnType<string, string, never>;
value: ColumnType<string, string, string>;
created_at: ColumnType<string, string, never>;
updated_at: ColumnType<string | null, string | null, string | null>;
}
export type SelectWorkspaceMetadata = Selectable<MetadataTable>;
export type CreateWorkspaceMetadata = Insertable<MetadataTable>;
export type UpdateWorkspaceMetadata = Updateable<MetadataTable>;
export interface WorkspaceDatabaseSchema {
users: UserTable;
entries: EntryTable;
@@ -247,4 +262,5 @@ export interface WorkspaceDatabaseSchema {
mutations: MutationTable;
texts: TextTable;
cursors: CursorTable;
metadata: MetadataTable;
}

View File

@@ -22,12 +22,17 @@ import {
SelectMessageInteraction,
SelectFileInteraction,
SelectEntryInteraction,
SelectWorkspaceMetadata,
} from '@/main/databases/workspace';
import { Account } from '@/shared/types/accounts';
import { Server } from '@/shared/types/servers';
import { User } from '@/shared/types/users';
import { File, FileInteraction, FileState } from '@/shared/types/files';
import { Workspace } from '@/shared/types/workspaces';
import {
Workspace,
WorkspaceMetadata,
WorkspaceMetadataKey,
} from '@/shared/types/workspaces';
import {
MessageInteraction,
MessageNode,
@@ -285,3 +290,14 @@ export const mapIcon = (row: SelectIcon): Icon => {
tags: row.tags ? JSON.parse(row.tags) : [],
};
};
export const mapWorkspaceMetadata = (
row: SelectWorkspaceMetadata
): WorkspaceMetadata => {
return {
key: row.key as WorkspaceMetadataKey,
value: JSON.parse(row.value),
createdAt: row.created_at,
updatedAt: row.updated_at,
};
};

View File

@@ -1,4 +1,4 @@
import { extractFileType } from '@colanode/core';
import { extractFileType, getIdType, IdType } from '@colanode/core';
import {
DeleteResult,
InsertResult,
@@ -159,3 +159,76 @@ export const fetchUserStorageUsed = async (
return BigInt(storageUsedRow?.storage_used ?? 0);
};
export const fetchEntryBreadcrumb = async (
database: Kysely<WorkspaceDatabaseSchema>,
entryId: string
): Promise<string[]> => {
const rows = await database
.selectFrom('entry_paths')
.select('ancestor_id')
.where('descendant_id', '=', entryId)
.orderBy('level', 'asc')
.execute();
return rows.map((row) => row.ancestor_id);
};
export const fetchMessageBreadcrumb = async (
database: Kysely<WorkspaceDatabaseSchema>,
messageId: string
): Promise<string[]> => {
const message = await database
.selectFrom('messages')
.select('parent_id')
.where('id', '=', messageId)
.executeTakeFirst();
if (!message) {
return [];
}
const parentIdType = getIdType(message.parent_id);
if (parentIdType === IdType.Message) {
const messageBreadcrumb = await fetchMessageBreadcrumb(
database,
message.parent_id
);
return [...messageBreadcrumb, messageId];
}
const entryBreadcrumb = await fetchEntryBreadcrumb(
database,
message.parent_id
);
return [...entryBreadcrumb, messageId];
};
export const fetchFileBreadcrumb = async (
database: Kysely<WorkspaceDatabaseSchema>,
fileId: string
): Promise<string[]> => {
const file = await database
.selectFrom('files')
.select('parent_id')
.where('id', '=', fileId)
.executeTakeFirst();
if (!file) {
return [];
}
const parentIdType = getIdType(file.parent_id);
if (parentIdType === IdType.Message) {
const messageBreadcrumb = await fetchMessageBreadcrumb(
database,
file.parent_id
);
return [...messageBreadcrumb, fileId];
}
const entryBreadcrumb = await fetchEntryBreadcrumb(database, file.parent_id);
return [...entryBreadcrumb, fileId];
};

View File

@@ -59,6 +59,8 @@ import { WorkspaceCreateMutationHandler } from '@/main/mutations/workspaces/work
import { WorkspaceUpdateMutationHandler } from '@/main/mutations/workspaces/workspace-update';
import { UserRoleUpdateMutationHandler } from '@/main/mutations/users/user-role-update';
import { UsersInviteMutationHandler } from '@/main/mutations/users/users-invite';
import { WorkspaceMetadataSaveMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-save';
import { WorkspaceMetadataDeleteMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-delete';
import { MutationHandler } from '@/main/lib/types';
import { MutationMap } from '@/shared/mutations';
@@ -128,4 +130,6 @@ export const mutationHandlerMap: MutationHandlerMap = {
page_update: new PageUpdateMutationHandler(),
folder_update: new FolderUpdateMutationHandler(),
database_update: new DatabaseUpdateMutationHandler(),
workspace_metadata_save: new WorkspaceMetadataSaveMutationHandler(),
workspace_metadata_delete: new WorkspaceMetadataDeleteMutationHandler(),
};

View File

@@ -0,0 +1,41 @@
import { MutationHandler } from '@/main/lib/types';
import {
WorkspaceMetadataDeleteMutationInput,
WorkspaceMetadataDeleteMutationOutput,
} from '@/shared/mutations/workspaces/workspace-metadata-delete';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
import { eventBus } from '@/shared/lib/event-bus';
import { mapWorkspaceMetadata } from '@/main/lib/mappers';
export class WorkspaceMetadataDeleteMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<WorkspaceMetadataDeleteMutationInput>
{
async handleMutation(
input: WorkspaceMetadataDeleteMutationInput
): Promise<WorkspaceMetadataDeleteMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const deletedMetadata = await workspace.database
.deleteFrom('metadata')
.where('key', '=', input.key)
.returningAll()
.executeTakeFirst();
if (!deletedMetadata) {
return {
success: true,
};
}
eventBus.publish({
type: 'workspace_metadata_deleted',
accountId: input.accountId,
workspaceId: input.workspaceId,
metadata: mapWorkspaceMetadata(deletedMetadata),
});
return {
success: true,
};
}
}

View File

@@ -0,0 +1,51 @@
import { MutationHandler } from '@/main/lib/types';
import {
WorkspaceMetadataSaveMutationInput,
WorkspaceMetadataSaveMutationOutput,
} from '@/shared/mutations/workspaces/workspace-metadata-save';
import { WorkspaceMutationHandlerBase } from '@/main/mutations/workspace-mutation-handler-base';
import { eventBus } from '@/shared/lib/event-bus';
import { mapWorkspaceMetadata } from '@/main/lib/mappers';
export class WorkspaceMetadataSaveMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<WorkspaceMetadataSaveMutationInput>
{
async handleMutation(
input: WorkspaceMetadataSaveMutationInput
): Promise<WorkspaceMetadataSaveMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const createdMetadata = await workspace.database
.insertInto('metadata')
.returningAll()
.values({
key: input.key,
value: input.value,
created_at: new Date().toISOString(),
})
.onConflict((cb) =>
cb.columns(['key']).doUpdateSet({
value: input.value,
updated_at: new Date().toISOString(),
})
)
.executeTakeFirst();
if (!createdMetadata) {
return {
success: false,
};
}
eventBus.publish({
type: 'workspace_metadata_updated',
accountId: input.accountId,
workspaceId: input.workspaceId,
metadata: mapWorkspaceMetadata(createdMetadata),
});
return {
success: true,
};
}
}

View File

@@ -0,0 +1,67 @@
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
import { fetchEntryBreadcrumb } from '@/main/lib/utils';
import { EntryBreadcrumbGetQueryInput } from '@/shared/queries/entries/entry-breadcrumb-get';
import { Event } from '@/shared/types/events';
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
export class EntryBreadcrumbGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<EntryBreadcrumbGetQueryInput>
{
public async handleQuery(
input: EntryBreadcrumbGetQueryInput
): Promise<string[]> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
return fetchEntryBreadcrumb(workspace.database, input.entryId);
}
public async checkForChanges(
event: Event,
input: EntryBreadcrumbGetQueryInput,
output: string[]
): Promise<ChangeCheckResult<EntryBreadcrumbGetQueryInput>> {
if (
event.type === 'workspace_deleted' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'entry_created' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.entry.id === input.entryId
) {
const newResult = await this.handleQuery(input);
return {
hasChanges: true,
result: newResult,
};
}
if (
event.type === 'entry_deleted' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId
) {
const entryId = output.find((id) => id === event.entry.id);
if (entryId) {
const newResult = await this.handleQuery(input);
return {
hasChanges: true,
result: newResult,
};
}
}
return {
hasChanges: false,
result: output,
};
}
}

View File

@@ -0,0 +1,67 @@
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
import { FileBreadcrumbGetQueryInput } from '@/shared/queries/files/file-breadcrumb-get';
import { Event } from '@/shared/types/events';
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
import { fetchFileBreadcrumb } from '@/main/lib/utils';
export class FileBreadcrumbGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<FileBreadcrumbGetQueryInput>
{
public async handleQuery(
input: FileBreadcrumbGetQueryInput
): Promise<string[]> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
return fetchFileBreadcrumb(workspace.database, input.fileId);
}
public async checkForChanges(
event: Event,
input: FileBreadcrumbGetQueryInput,
output: string[]
): Promise<ChangeCheckResult<FileBreadcrumbGetQueryInput>> {
if (
event.type === 'workspace_deleted' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'file_created' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.file.id === input.fileId
) {
const newOutput = await this.handleQuery(input);
return {
hasChanges: true,
result: newOutput,
};
}
if (
event.type === 'file_deleted' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.file.id === input.fileId
) {
const fileId = output.find((id) => id === event.file.id);
if (fileId) {
const newOutput = await this.handleQuery(input);
return {
hasChanges: true,
result: newOutput,
};
}
}
return {
hasChanges: false,
};
}
}

View File

@@ -8,6 +8,7 @@ import { EmojiGetBySkinIdQueryHandler } from '@/main/queries/emojis/emoji-get-by
import { FileListQueryHandler } from '@/main/queries/files/file-list';
import { FileGetQueryHandler } from '@/main/queries/files/file-get';
import { FileMetadataGetQueryHandler } from '@/main/queries/files/file-metadata-get';
import { FileBreadcrumbGetQueryHandler } from '@/main/queries/files/file-breadcrumb-get';
import { IconListQueryHandler } from '@/main/queries/icons/icon-list';
import { IconSearchQueryHandler } from '@/main/queries/icons/icon-search';
import { IconCategoryListQueryHandler } from '@/main/queries/icons/icon-category-list';
@@ -15,9 +16,11 @@ import { MessageGetQueryHandler } from '@/main/queries/messages/message-get';
import { MessageListQueryHandler } from '@/main/queries/messages/message-list';
import { MessageReactionsListQueryHandler } from '@/main/queries/messages/message-reaction-list';
import { MessageReactionsAggregateQueryHandler } from '@/main/queries/messages/message-reactions-aggregate';
import { MessageBreadcrumbGetQueryHandler } from '@/main/queries/messages/message-breadcrumb-get';
import { EntryChildrenGetQueryHandler } from '@/main/queries/entries/entry-children-get';
import { EntryGetQueryHandler } from '@/main/queries/entries/entry-get';
import { EntryTreeGetQueryHandler } from '@/main/queries/entries/entry-tree-get';
import { EntryBreadcrumbGetQueryHandler } from '@/main/queries/entries/entry-breadcrumb-get';
import { RadarDataGetQueryHandler } from '@/main/queries/interactions/radar-data-get';
import { RecordListQueryHandler } from '@/main/queries/records/record-list';
import { ServerListQueryHandler } from '@/main/queries/servers/server-list';
@@ -30,6 +33,7 @@ import { RecordSearchQueryHandler } from '@/main/queries/records/record-search';
import { UserGetQueryHandler } from '@/main/queries/users/user-get';
import { SpaceListQueryHandler } from '@/main/queries/spaces/space-list';
import { ChatListQueryHandler } from '@/main/queries/chats/chat-list';
import { WorkspaceMetadataListQueryHandler } from '@/main/queries/workspaces/workspace-metadata-list';
import { QueryHandler } from '@/main/lib/types';
import { QueryMap } from '@/shared/queries';
@@ -43,7 +47,9 @@ export const queryHandlerMap: QueryHandlerMap = {
message_reaction_list: new MessageReactionsListQueryHandler(),
message_reactions_aggregate: new MessageReactionsAggregateQueryHandler(),
message_get: new MessageGetQueryHandler(),
message_breadcrumb_get: new MessageBreadcrumbGetQueryHandler(),
entry_get: new EntryGetQueryHandler(),
entry_breadcrumb_get: new EntryBreadcrumbGetQueryHandler(),
record_list: new RecordListQueryHandler(),
server_list: new ServerListQueryHandler(),
user_search: new UserSearchQueryHandler(),
@@ -62,6 +68,7 @@ export const queryHandlerMap: QueryHandlerMap = {
entry_children_get: new EntryChildrenGetQueryHandler(),
radar_data_get: new RadarDataGetQueryHandler(),
file_metadata_get: new FileMetadataGetQueryHandler(),
file_breadcrumb_get: new FileBreadcrumbGetQueryHandler(),
account_get: new AccountGetQueryHandler(),
workspace_get: new WorkspaceGetQueryHandler(),
database_list: new DatabaseListQueryHandler(),
@@ -70,4 +77,5 @@ export const queryHandlerMap: QueryHandlerMap = {
file_get: new FileGetQueryHandler(),
chat_list: new ChatListQueryHandler(),
space_list: new SpaceListQueryHandler(),
workspace_metadata_list: new WorkspaceMetadataListQueryHandler(),
};

View File

@@ -0,0 +1,67 @@
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
import { MessageBreadcrumbGetQueryInput } from '@/shared/queries/messages/message-breadcrumb-get';
import { Event } from '@/shared/types/events';
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
import { fetchMessageBreadcrumb } from '@/main/lib/utils';
export class MessageBreadcrumbGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<MessageBreadcrumbGetQueryInput>
{
public async handleQuery(
input: MessageBreadcrumbGetQueryInput
): Promise<string[]> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
return fetchMessageBreadcrumb(workspace.database, input.messageId);
}
public async checkForChanges(
event: Event,
input: MessageBreadcrumbGetQueryInput,
output: string[]
): Promise<ChangeCheckResult<MessageBreadcrumbGetQueryInput>> {
if (
event.type === 'workspace_deleted' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'message_created' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.message.id === input.messageId
) {
const newOutput = await this.handleQuery(input);
return {
hasChanges: true,
result: newOutput,
};
}
if (
event.type === 'message_deleted' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.message.id === input.messageId
) {
const messageId = output.find((id) => id === event.message.id);
if (messageId) {
const newOutput = await this.handleQuery(input);
return {
hasChanges: true,
result: newOutput,
};
}
}
return {
hasChanges: false,
};
}
}

View File

@@ -0,0 +1,93 @@
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
import { mapWorkspaceMetadata } from '@/main/lib/mappers';
import { WorkspaceMetadataListQueryInput } from '@/shared/queries/workspaces/workspace-metadata-list';
import { Event } from '@/shared/types/events';
import { WorkspaceMetadata } from '@/shared/types/workspaces';
import { WorkspaceQueryHandlerBase } from '@/main/queries/workspace-query-handler-base';
import { SelectWorkspaceMetadata } from '@/main/databases/workspace/schema';
export class WorkspaceMetadataListQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<WorkspaceMetadataListQueryInput>
{
public async handleQuery(
input: WorkspaceMetadataListQueryInput
): Promise<WorkspaceMetadata[]> {
const rows = await this.getWorkspaceMetadata(
input.accountId,
input.workspaceId
);
if (!rows) {
return [];
}
return rows.map(mapWorkspaceMetadata);
}
public async checkForChanges(
event: Event,
input: WorkspaceMetadataListQueryInput,
output: WorkspaceMetadata[]
): Promise<ChangeCheckResult<WorkspaceMetadataListQueryInput>> {
if (
event.type === 'workspace_created' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
return {
hasChanges: true,
result: [],
};
}
if (
event.type === 'workspace_metadata_updated' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId
) {
const newOutput = output.map((metadata) => {
if (metadata.key === event.metadata.key) {
return event.metadata;
}
return metadata;
});
return {
hasChanges: true,
result: newOutput,
};
}
if (
event.type === 'workspace_metadata_deleted' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId
) {
const newOutput = output.filter(
(metadata) => metadata.key !== event.metadata.key
);
return {
hasChanges: true,
result: newOutput,
};
}
return {
hasChanges: false,
};
}
private async getWorkspaceMetadata(
accountId: string,
workspaceId: string
): Promise<SelectWorkspaceMetadata[] | undefined> {
const workspace = this.getWorkspace(accountId, workspaceId);
const rows = await workspace.database
.selectFrom('metadata')
.selectAll()
.execute();
return rows;
}
}

View File

@@ -1,34 +0,0 @@
import { ChannelEntry, EntryRole } from '@colanode/core';
import { useEffect } from 'react';
import { Conversation } from '@/renderer/components/messages/conversation';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useRadar } from '@/renderer/contexts/radar';
interface ChannelBodyProps {
channel: ChannelEntry;
role: EntryRole;
}
export const ChannelBody = ({ channel, role }: ChannelBodyProps) => {
const workspace = useWorkspace();
const radar = useRadar();
useEffect(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, channel.id);
const interval = setInterval(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, channel.id);
}, 60000);
return () => clearInterval(interval);
}, [channel.id, channel.type, channel.transactionId]);
return (
<Conversation
conversationId={channel.id}
rootId={channel.rootId}
role={role}
/>
);
};

View File

@@ -1,21 +1,35 @@
import { ChannelEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface ChannelBreadcrumbItemProps {
channel: ChannelEntry;
id: string;
}
export const ChannelBreadcrumbItem = ({
channel,
}: ChannelBreadcrumbItemProps) => {
export const ChannelBreadcrumbItem = ({ id }: ChannelBreadcrumbItemProps) => {
const workspace = useWorkspace();
const { data } = useQuery({
type: 'entry_get',
entryId: id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!data) {
return null;
}
const channel = data as ChannelEntry;
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={channel.id}
name={channel.attributes.name}
avatar={channel.attributes.avatar}
className="size-4"
/>
<span>{channel.attributes.name}</span>
</div>

View File

@@ -0,0 +1,39 @@
import { ChannelEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface ChannelContainerTabProps {
channelId: string;
}
export const ChannelContainerTab = ({
channelId,
}: ChannelContainerTabProps) => {
const workspace = useWorkspace();
const { data: entry } = useQuery({
type: 'entry_get',
entryId: channelId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const channel = entry as ChannelEntry;
if (!channel) {
return <p>Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={channel.id}
name={channel.attributes.name}
avatar={channel.attributes.avatar}
/>
<span>{channel.attributes.name}</span>
</div>
);
};

View File

@@ -1,57 +1,52 @@
import { ChannelEntry, extractEntryRole } from '@colanode/core';
import { ChannelEntry } from '@colanode/core';
import { ChannelBody } from '@/renderer/components/channels/channel-body';
import { ChannelHeader } from '@/renderer/components/channels/channel-header';
import { ChannelNotFound } from '@/renderer/components/channels/channel-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
import { ChannelSettings } from '@/renderer/components/channels/channel-settings';
import { Conversation } from '@/renderer/components/messages/conversation';
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
interface ChannelContainerProps {
channelId: string;
}
export const ChannelContainer = ({ channelId }: ChannelContainerProps) => {
const workspace = useWorkspace();
const data = useEntryContainer<ChannelEntry>(channelId);
const { data: entry, isPending: isPendingEntry } = useQuery({
type: 'entry_get',
entryId: channelId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
useEntryRadar(data.entry);
const channel = entry as ChannelEntry;
const channelExists = !!channel;
const { data: root, isPending: isPendingRoot } = useQuery(
{
type: 'entry_get',
entryId: channel?.rootId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: channelExists,
}
);
if (isPendingEntry || (isPendingRoot && channelExists)) {
if (data.isPending) {
return null;
}
if (!channel || !root) {
if (!data.entry) {
return <ChannelNotFound />;
}
const role = extractEntryRole(root, workspace.userId);
if (!role) {
return <ChannelNotFound />;
}
const { entry: channel, role } = data;
return (
<div className="flex h-full w-full flex-col">
<ChannelHeader channel={channel} role={role} />
<ChannelBody channel={channel} role={role} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<ChannelSettings channel={channel} role={role} />
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<Conversation
conversationId={channel.id}
rootId={channel.rootId}
role={role}
/>
</ContainerBody>
</Container>
);
};

View File

@@ -11,6 +11,7 @@ import {
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
import { useLayout } from '@/renderer/contexts/layout';
interface ChannelCreateDialogProps {
spaceId: string;
@@ -24,6 +25,7 @@ export const ChannelCreateDialog = ({
onOpenChange,
}: ChannelCreateDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -61,7 +63,7 @@ export const ChannelCreateDialog = ({
},
onSuccess(output) {
onOpenChange(false);
workspace.openInMain(output.id);
layout.openLeft(output.id);
},
onError(error) {
toast({

View File

@@ -8,6 +8,7 @@ import {
AlertDialogTitle,
} from '@/renderer/components/ui/alert-dialog';
import { Button } from '@/renderer/components/ui/button';
import { useLayout } from '@/renderer/contexts/layout';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
@@ -24,6 +25,7 @@ export const ChannelDeleteDialog = ({
onOpenChange,
}: ChannelDeleteDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -53,7 +55,7 @@ export const ChannelDeleteDialog = ({
},
onSuccess() {
onOpenChange(false);
workspace.closeEntry(channelId);
layout.close(channelId);
},
onError(error) {
toast({

View File

@@ -1,32 +0,0 @@
import { ChannelEntry, EntryRole } from '@colanode/core';
import { ChannelSettings } from '@/renderer/components/channels/channel-settings';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
interface ChannelHeaderProps {
channel: ChannelEntry;
role: EntryRole;
}
export const ChannelHeader = ({ channel, role }: ChannelHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
{container.mode === 'main' && <EntryBreadcrumb entry={channel} />}
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={channel.id} />
)}
</div>
<div className="flex items-center gap-2">
<ChannelSettings channel={channel} role={role} />
</div>
</div>
</Header>
);
};

View File

@@ -30,7 +30,7 @@ export const ChannelSettings = ({ channel, role }: ChannelSettingsProps) => {
<React.Fragment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
<Settings className="size-4 cursor-pointer text-muted-foreground hover:text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="mr-2 w-80">
<DropdownMenuLabel>{channel.attributes.name}</DropdownMenuLabel>

View File

@@ -5,6 +5,7 @@ import { Avatar } from '@/renderer/components/avatars/avatar';
import { ReadStateIndicator } from '@/renderer/components/layouts/read-state-indicator';
import { useRadar } from '@/renderer/contexts/radar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
import { cn } from '@/shared/lib/utils';
interface ChannelSidebarItemProps {
@@ -13,9 +14,10 @@ interface ChannelSidebarItemProps {
export const ChannelSidebarItem = ({ channel }: ChannelSidebarItemProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const radar = useRadar();
const isActive = workspace.isEntryActive(channel.id);
const isActive = layout.activeTab === channel.id;
const channelState = radar.getChannelState(
workspace.accountId,
workspace.id,

View File

@@ -1,30 +0,0 @@
import { ChatEntry, EntryRole } from '@colanode/core';
import { useEffect } from 'react';
import { Conversation } from '@/renderer/components/messages/conversation';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useRadar } from '@/renderer/contexts/radar';
interface ChatBodyProps {
chat: ChatEntry;
role: EntryRole;
}
export const ChatBody = ({ chat, role }: ChatBodyProps) => {
const workspace = useWorkspace();
const radar = useRadar();
useEffect(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, chat.id);
const interval = setInterval(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, chat.id);
}, 60000);
return () => clearInterval(interval);
}, [chat.id, chat.type, chat.transactionId]);
return (
<Conversation conversationId={chat.id} rootId={chat.rootId} role={role} />
);
};

View File

@@ -1,35 +1,48 @@
import { ChatEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface ChatBreadcrumbItemProps {
chat: ChatEntry;
id: string;
}
export const ChatBreadcrumbItem = ({ chat }: ChatBreadcrumbItemProps) => {
export const ChatBreadcrumbItem = ({ id }: ChatBreadcrumbItemProps) => {
const workspace = useWorkspace();
const userId =
Object.keys(chat.attributes.collaborators).find(
(id) => id !== workspace.userId
) ?? '';
const { data, isPending } = useQuery({
const { data: chat } = useQuery({
type: 'entry_get',
entryId: id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const userId =
chat && chat.type === 'chat'
? (Object.keys(chat.attributes.collaborators).find(
(id) => id !== workspace.userId
) ?? '')
: '';
const { data: user } = useQuery({
type: 'user_get',
accountId: workspace.accountId,
workspaceId: workspace.id,
userId,
});
if (isPending || !data) {
if (!chat || !user) {
return null;
}
return (
<div className="flex items-center space-x-2">
<Avatar size="small" id={data.id} name={data.name} avatar={data.avatar} />
<span>{data.name}</span>
<Avatar
id={user.id}
name={user.name}
avatar={user.avatar}
className="size-4"
/>
<span>{user.name}</span>
</div>
);
};

View File

@@ -0,0 +1,43 @@
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface ChatContainerTabProps {
chatId: string;
}
export const ChatContainerTab = ({ chatId }: ChatContainerTabProps) => {
const workspace = useWorkspace();
const { data: chat } = useQuery({
type: 'entry_get',
entryId: chatId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const userId =
chat && chat.type === 'chat'
? (Object.keys(chat.attributes.collaborators).find(
(id) => id !== workspace.userId
) ?? '')
: '';
const { data: user } = useQuery({
type: 'user_get',
accountId: workspace.accountId,
workspaceId: workspace.id,
userId,
});
if (!chat || !user) {
return <p>Not found</p>;
}
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>
</div>
);
};

View File

@@ -1,43 +1,56 @@
import { extractEntryRole } from '@colanode/core';
import { ChatEntry } from '@colanode/core';
import { ChatBody } from '@/renderer/components/chats/chat-body';
import { ChatHeader } from '@/renderer/components/chats/chat-header';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { ChatNotFound } from '@/renderer/components/chats/chat-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import { EntryCollaboratorsPopover } from '@/renderer/components/collaborators/entry-collaborators-popover';
import { Conversation } from '@/renderer/components/messages/conversation';
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
interface ChatContainerProps {
chatId: string;
}
export const ChatContainer = ({ chatId }: ChatContainerProps) => {
const workspace = useWorkspace();
const data = useEntryContainer<ChatEntry>(chatId);
const { data, isPending } = useQuery({
type: 'entry_get',
entryId: chatId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
useEntryRadar(data.entry);
if (isPending) {
if (data.isPending) {
return null;
}
const node = data;
if (!node || node.type !== 'chat') {
if (!data.entry) {
return <ChatNotFound />;
}
const role = extractEntryRole(node, workspace.userId);
if (!role) {
return <ChatNotFound />;
}
const { entry, role } = data;
return (
<div className="flex h-full w-full flex-col">
<ChatHeader chat={node} role={role} />
<ChatBody chat={node} role={role} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<EntryCollaboratorsPopover
entry={entry}
entries={[entry]}
role={role}
/>
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<Conversation
conversationId={entry.id}
rootId={entry.rootId}
role={role}
/>
</ContainerBody>
</Container>
);
};

View File

@@ -10,17 +10,19 @@ import { UserSearch } from '@/renderer/components/users/user-search';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
import { useLayout } from '@/renderer/contexts/layout';
export const ChatCreatePopover = () => {
const workspace = useWorkspace();
const { mutate, isPending } = useMutation();
const layout = useLayout();
const [open, setOpen] = React.useState(false);
return (
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverTrigger asChild>
<SquarePen className="mr-2 size-4 cursor-pointer" />
<SquarePen className="size-4 cursor-pointer" />
</PopoverTrigger>
<PopoverContent className="w-96 p-1">
<UserSearch
@@ -35,7 +37,7 @@ export const ChatCreatePopover = () => {
userId: user.id,
},
onSuccess(output) {
workspace.openInMain(output.id);
layout.openLeft(output.id);
setOpen(false);
},
onError(error) {

View File

@@ -1,36 +0,0 @@
import { ChatEntry, EntryRole } from '@colanode/core';
import { EntryCollaboratorsPopover } from '@/renderer/components/collaborators/entry-collaborators-popover';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
interface ChatHeaderProps {
chat: ChatEntry;
role: EntryRole;
}
export const ChatHeader = ({ chat, role }: ChatHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
<EntryBreadcrumb entry={chat} />
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={chat.id} />
)}
</div>
<div className="flex items-center gap-2">
<EntryCollaboratorsPopover
entry={chat}
entries={[chat]}
role={role}
/>
</div>
</div>
</Header>
);
};

View File

@@ -6,6 +6,7 @@ import { ReadStateIndicator } from '@/renderer/components/layouts/read-state-ind
import { useRadar } from '@/renderer/contexts/radar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import { useLayout } from '@/renderer/contexts/layout';
import { cn } from '@/shared/lib/utils';
interface ChatSidebarItemProps {
@@ -14,6 +15,7 @@ interface ChatSidebarItemProps {
export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const radar = useRadar();
const userId =
@@ -37,7 +39,7 @@ export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
workspace.id,
chat.id
);
const isActive = workspace.isEntryActive(chat.id);
const isActive = layout.activeTab === chat.id;
const unreadCount =
nodeReadState.unseenMessagesCount + nodeReadState.mentionsCount;

View File

@@ -5,7 +5,7 @@ import { useDrag } from 'react-dnd';
import { RecordFieldValue } from '@/renderer/components/records/record-field-value';
import { useRecord } from '@/renderer/contexts/record';
import { useView } from '@/renderer/contexts/view';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
interface DragResult {
option: SelectOptionAttributes;
@@ -13,7 +13,7 @@ interface DragResult {
}
export const BoardViewRecordCard = () => {
const workspace = useWorkspace();
const layout = useLayout();
const view = useView();
const record = useRecord();
@@ -52,7 +52,9 @@ export const BoardViewRecordCard = () => {
role="presentation"
key={record.id}
className="animate-fade-in flex cursor-pointer flex-col gap-1 rounded-md border p-2 text-left hover:bg-gray-50"
onClick={() => workspace.openInModal(record.id)}
onClick={() => {
layout.previewLeft(record.id, true);
}}
>
<p className={hasName ? '' : 'text-muted-foreground'}>
{hasName ? name : 'Unnamed'}

View File

@@ -24,7 +24,7 @@ export const BoardView = () => {
const selectOptions = Object.values(groupByField.options ?? {});
return (
<React.Fragment>
<div className="mt-2 flex flex-row justify-between border-b">
<div className="flex flex-row justify-between border-b">
<ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
<BoardViewSettings />

View File

@@ -1,10 +1,10 @@
import { RecordFieldValue } from '@/renderer/components/records/record-field-value';
import { useRecord } from '@/renderer/contexts/record';
import { useView } from '@/renderer/contexts/view';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
export const CalendarViewRecordCard = () => {
const workspace = useWorkspace();
const layout = useLayout();
const view = useView();
const record = useRecord();
@@ -16,7 +16,9 @@ export const CalendarViewRecordCard = () => {
role="presentation"
key={record.id}
className="animate-fade-in flex justify-start items-start cursor-pointer flex-col gap-1 rounded-md border p-2 hover:bg-gray-50"
onClick={() => workspace.openInModal(record.id)}
onClick={() => {
layout.previewLeft(record.id, true);
}}
>
<p className={hasName ? '' : 'text-muted-foreground'}>
{name ?? 'Unnamed'}

View File

@@ -23,7 +23,7 @@ export const CalendarView = () => {
return (
<React.Fragment>
<div className="mt-2 flex flex-row justify-between border-b">
<div className="flex flex-row justify-between border-b">
<ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
<CalendarViewSettings />

View File

@@ -1,33 +0,0 @@
import { DatabaseEntry, EntryRole } from '@colanode/core';
import { useEffect } from 'react';
import { Database } from '@/renderer/components/databases/database';
import { DatabaseViews } from '@/renderer/components/databases/database-views';
import { useRadar } from '@/renderer/contexts/radar';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface DatabaseBodyProps {
database: DatabaseEntry;
role: EntryRole;
}
export const DatabaseBody = ({ database, role }: DatabaseBodyProps) => {
const workspace = useWorkspace();
const radar = useRadar();
useEffect(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, database.id);
const interval = setInterval(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, database.id);
}, 60000);
return () => clearInterval(interval);
}, [database.id, database.type, database.transactionId]);
return (
<Database database={database} role={role}>
<DatabaseViews />
</Database>
);
};

View File

@@ -1,21 +1,32 @@
import { DatabaseEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface DatabaseBreadcrumbItemProps {
database: DatabaseEntry;
id: string;
}
export const DatabaseBreadcrumbItem = ({
database,
}: DatabaseBreadcrumbItemProps) => {
export const DatabaseBreadcrumbItem = ({ id }: DatabaseBreadcrumbItemProps) => {
const workspace = useWorkspace();
const { data } = useQuery({
type: 'entry_get',
entryId: id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!data) {
return null;
}
const database = data as DatabaseEntry;
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={database.id}
name={database.attributes.name}
avatar={database.attributes.avatar}
className="size-4"
/>
<span>{database.attributes.name}</span>
</div>

View File

@@ -0,0 +1,39 @@
import { DatabaseEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface DatabaseContainerTabProps {
databaseId: string;
}
export const DatabaseContainerTab = ({
databaseId,
}: DatabaseContainerTabProps) => {
const workspace = useWorkspace();
const { data: entry } = useQuery({
type: 'entry_get',
entryId: databaseId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const database = entry as DatabaseEntry;
if (!database) {
return <p>Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={database.id}
name={database.attributes.name}
avatar={database.attributes.avatar}
/>
<span>{database.attributes.name}</span>
</div>
);
};

View File

@@ -1,57 +1,51 @@
import { DatabaseEntry, extractEntryRole } from '@colanode/core';
import { DatabaseEntry } from '@colanode/core';
import { DatabaseBody } from '@/renderer/components/databases/database-body';
import { DatabaseHeader } from '@/renderer/components/databases/database-header';
import { DatabaseNotFound } from '@/renderer/components/databases/database-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
import { DatabaseSettings } from '@/renderer/components/databases/database-settings';
import { Database } from '@/renderer/components/databases/database';
import { DatabaseViews } from '@/renderer/components/databases/database-views';
interface DatabaseContainerProps {
databaseId: string;
}
export const DatabaseContainer = ({ databaseId }: DatabaseContainerProps) => {
const workspace = useWorkspace();
const data = useEntryContainer<DatabaseEntry>(databaseId);
const { data: entry, isPending: isPendingEntry } = useQuery({
type: 'entry_get',
entryId: databaseId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
useEntryRadar(data.entry);
const database = entry as DatabaseEntry;
const databaseExists = !!database;
const { data: root, isPending: isPendingRoot } = useQuery(
{
type: 'entry_get',
entryId: database?.rootId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: databaseExists,
}
);
if (isPendingEntry || (isPendingRoot && databaseExists)) {
if (data.isPending) {
return null;
}
if (!database || !root) {
if (!data.entry) {
return <DatabaseNotFound />;
}
const role = extractEntryRole(root, workspace.userId);
if (!role) {
return <DatabaseNotFound />;
}
const { entry: database, role } = data;
return (
<div className="flex h-full w-full flex-col">
<DatabaseHeader database={database} role={role} />
<DatabaseBody database={database} role={role} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<DatabaseSettings database={database} role={role} />
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<Database database={database} role={role}>
<DatabaseViews />
</Database>
</ContainerBody>
</Container>
);
};

View File

@@ -11,6 +11,7 @@ import {
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
import { useLayout } from '@/renderer/contexts/layout';
interface DatabaseCreateDialogProps {
spaceId: string;
@@ -24,6 +25,7 @@ export const DatabaseCreateDialog = ({
onOpenChange,
}: DatabaseCreateDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -61,7 +63,7 @@ export const DatabaseCreateDialog = ({
},
onSuccess(output) {
onOpenChange(false);
workspace.openInMain(output.id);
layout.openLeft(output.id);
},
onError(error) {
toast({

View File

@@ -9,6 +9,7 @@ import {
} from '@/renderer/components/ui/alert-dialog';
import { Button } from '@/renderer/components/ui/button';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
@@ -24,6 +25,7 @@ export const DatabaseDeleteDialog = ({
onOpenChange,
}: DatabaseDeleteDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -53,7 +55,7 @@ export const DatabaseDeleteDialog = ({
},
onSuccess() {
onOpenChange(false);
workspace.closeEntry(entryId);
layout.close(entryId);
},
onError(error) {
toast({

View File

@@ -1,32 +0,0 @@
import { DatabaseEntry, EntryRole } from '@colanode/core';
import { DatabaseSettings } from '@/renderer/components/databases/database-settings';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
interface DatabaseHeaderProps {
database: DatabaseEntry;
role: EntryRole;
}
export const DatabaseHeader = ({ database, role }: DatabaseHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
{container.mode === 'main' && <EntryBreadcrumb entry={database} />}
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={database.id} />
)}
</div>
<div className="flex items-center gap-2">
<DatabaseSettings database={database} role={role} />
</div>
</div>
</Header>
);
};

View File

@@ -30,7 +30,7 @@ export const DatabaseSettings = ({ database, role }: DatabaseSettingsProps) => {
<React.Fragment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
<Settings className="size-4 cursor-pointer text-muted-foreground hover:text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="mr-2 w-80">
<DropdownMenuLabel>{database.attributes.name}</DropdownMenuLabel>

View File

@@ -1,7 +1,7 @@
import { DatabaseEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
import { cn } from '@/shared/lib/utils';
interface DatabaseSidebarItemProps {
@@ -9,8 +9,8 @@ interface DatabaseSidebarItemProps {
}
export const DatabaseSidebarItem = ({ database }: DatabaseSidebarItemProps) => {
const workspace = useWorkspace();
const isActive = workspace.isEntryActive(database.id);
const layout = useLayout();
const isActive = layout.activeTab === database.id;
const isUnread = false;
const mentionsCount = 0;

View File

@@ -25,7 +25,7 @@ export const DatabaseViews = () => {
>
<div className="h-full w-full overflow-y-auto">
<ScrollAreaPrimitive.Root className="relative overflow-hidden">
<ScrollAreaPrimitive.Viewport className="group/database h-full max-h-[calc(100vh-130px)] w-full overflow-y-auto rounded-[inherit] px-10 pb-12">
<ScrollAreaPrimitive.Viewport className="group/database h-full max-h-[calc(100vh-130px)] w-full overflow-y-auto rounded-[inherit]">
{activeView && <View view={activeView} />}
</ScrollAreaPrimitive.Viewport>
<ScrollBar orientation="horizontal" />

View File

@@ -6,6 +6,7 @@ import React, { Fragment } from 'react';
import { Spinner } from '@/renderer/components/ui/spinner';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { useLayout } from '@/renderer/contexts/layout';
import { toast } from '@/renderer/hooks/use-toast';
interface NameEditorProps {
@@ -58,6 +59,7 @@ interface TableViewNameCellProps {
export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const [isEditing, setIsEditing] = React.useState(false);
const { mutate, isPending } = useMutation();
@@ -111,7 +113,9 @@ export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
<button
type="button"
className="absolute right-2 flex h-6 cursor-pointer flex-row items-center gap-1 rounded-md border bg-white p-1 text-sm text-muted-foreground opacity-0 hover:bg-gray-50 group-hover:opacity-100"
onClick={() => workspace.openInModal(record.id)}
onClick={() => {
layout.previewLeft(record.id, true);
}}
>
<Maximize2 className="mr-1 size-4" /> <p>Open</p>
</button>

View File

@@ -12,7 +12,7 @@ import { ViewTabs } from '@/renderer/components/databases/view-tabs';
export const TableView = () => {
return (
<React.Fragment>
<div className="mt-2 flex flex-row justify-between border-b">
<div className="flex flex-row justify-between border-b">
<ViewTabs />
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
<TableViewSettings />

View File

@@ -16,6 +16,7 @@ import { useDatabase } from '@/renderer/contexts/database';
import { ViewContext } from '@/renderer/contexts/view';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { useLayout } from '@/renderer/contexts/layout';
import {
generateFieldValuesFromFilters,
generateViewFieldIndex,
@@ -34,6 +35,7 @@ interface ViewProps {
export const View = ({ view }: ViewProps) => {
const workspace = useWorkspace();
const database = useDatabase();
const layout = useLayout();
const { mutate } = useMutation();
const fields: ViewField[] = React.useMemo(() => {
@@ -537,7 +539,7 @@ export const View = ({ view }: ViewProps) => {
fields,
},
onSuccess: (output) => {
workspace.openInModal(output.id);
layout.previewLeft(output.id, true);
},
onError(error) {
toast({

View File

@@ -1,5 +1,6 @@
import { FilePreview } from '@/renderer/components/files/file-preview';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
import { useQuery } from '@/renderer/hooks/use-query';
interface FileBlockProps {
@@ -8,6 +9,7 @@ interface FileBlockProps {
export const FileBlock = ({ id }: FileBlockProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { data } = useQuery({
type: 'file_get',
@@ -24,7 +26,7 @@ export const FileBlock = ({ id }: FileBlockProps) => {
<div
className="flex h-72 max-h-72 max-w-128 w-full cursor-pointer overflow-hidden rounded-md p-2 hover:bg-gray-100"
onClick={() => {
workspace.openInModal(id);
layout.previewLeft(id, true);
}}
>
<FilePreview file={data} />

View File

@@ -1,11 +1,9 @@
import { SquareArrowOutUpRight } from 'lucide-react';
// import { useEffect } from 'react';
import { FilePreview } from '@/renderer/components/files/file-preview';
import { FileSidebar } from '@/renderer/components/files/file-sidebar';
import { Button } from '@/renderer/components/ui/button';
import { useWorkspace } from '@/renderer/contexts/workspace';
// import { useRadar } from '@/renderer/contexts/radar';
import { FileWithState } from '@/shared/types/files';
interface FileBodyProps {
@@ -14,27 +12,6 @@ interface FileBodyProps {
export const FileBody = ({ file }: FileBodyProps) => {
const workspace = useWorkspace();
// const radar = useRadar();
// useEffect(() => {
// radar.markAsOpened(
// workspace.userId,
// file.id,
// 'file',
// file.transactionId
// );
// const interval = setInterval(() => {
// radar.markAsOpened(
// workspace.userId,
// file.id,
// file.type,
// file.transactionId
// );
// }, 60000);
// return () => clearInterval(interval);
// }, [file.id, file.type, file.transactionId]);
return (
<div className="flex h-full max-h-full w-full flex-row items-center gap-2">

View File

@@ -0,0 +1,32 @@
import { FileThumbnail } from '@/renderer/components/files/file-thumbnail';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface FileBreadcrumbItemProps {
id: string;
}
export const FileBreadcrumbItem = ({ id }: FileBreadcrumbItemProps) => {
const workspace = useWorkspace();
const { data: file } = useQuery({
type: 'file_get',
id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!file) {
return null;
}
return (
<div className="flex items-center space-x-2">
<FileThumbnail
file={file}
className="size-4 overflow-hidden rounded object-contain"
/>
<span>{file.name}</span>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { FileThumbnail } from '@/renderer/components/files/file-thumbnail';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface FileContainerTabProps {
fileId: string;
}
export const FileContainerTab = ({ fileId }: FileContainerTabProps) => {
const workspace = useWorkspace();
const { data: file } = useQuery({
type: 'file_get',
id: fileId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!file) {
return <p>Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<FileThumbnail
file={file}
className="size-4 overflow-hidden rounded object-contain"
/>
<span>{file.name}</span>
</div>
);
};

View File

@@ -1,72 +1,41 @@
import { extractEntryRole } from '@colanode/core';
import { FileBody } from '@/renderer/components/files/file-body';
import { FileHeader } from '@/renderer/components/files/file-header';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { FileNotFound } from '@/renderer/components/files/file-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import { useFileContainer } from '@/renderer/hooks/use-file-container';
import { FileSettings } from '@/renderer/components/files/file-settings';
interface FileContainerProps {
fileId: string;
}
export const FileContainer = ({ fileId }: FileContainerProps) => {
const workspace = useWorkspace();
const data = useFileContainer(fileId);
const { data: file, isPending: isPendingFile } = useQuery({
type: 'file_get',
id: fileId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const fileExists = !!file;
const { data: entry, isPending: isPendingEntry } = useQuery(
{
type: 'entry_get',
entryId: file?.entryId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: fileExists,
}
);
const { data: root, isPending: isPendingRoot } = useQuery(
{
type: 'entry_get',
entryId: file?.rootId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: fileExists,
}
);
if (
isPendingFile ||
(isPendingEntry && fileExists) ||
(isPendingRoot && fileExists)
) {
if (data.isPending) {
return null;
}
if (!file || !entry || !root) {
return <FileNotFound />;
}
const role = extractEntryRole(root, workspace.userId);
if (!role) {
if (!data.file) {
return <FileNotFound />;
}
return (
<div className="flex h-full w-full flex-col">
<FileHeader file={file} role={role} entry={entry} />
<FileBody file={file} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<FileSettings file={data.file} role={data.role} />
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<FileBody file={data.file} />
</ContainerBody>
</Container>
);
};

View File

@@ -9,7 +9,7 @@ import {
ContextMenuShortcut,
ContextMenuTrigger,
} from '@/renderer/components/ui/context-menu';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
interface FileContextMenuProps {
id: string;
@@ -17,7 +17,7 @@ interface FileContextMenuProps {
}
export const FileContextMenu = ({ id, children }: FileContextMenuProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const [openDelete, setOpenDelete] = React.useState(false);
return (
@@ -27,7 +27,7 @@ export const FileContextMenu = ({ id, children }: FileContextMenuProps) => {
<ContextMenuContent className="w-64">
<ContextMenuItem
onSelect={() => {
workspace.openInModal(id);
layout.previewLeft(id, true);
}}
className="pl-2"
>

View File

@@ -1,34 +0,0 @@
import { Entry, EntryRole } from '@colanode/core';
import { FileSettings } from '@/renderer/components/files/file-settings';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
import { FileWithState } from '@/shared/types/files';
interface FileHeaderProps {
file: FileWithState;
entry: Entry;
role: EntryRole;
}
export const FileHeader = ({ file, entry, role }: FileHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
{container.mode === 'main' && <EntryBreadcrumb entry={entry} />}
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={file.id} />
)}
</div>
<div className="flex items-center gap-2">
<FileSettings file={file} role={role} entry={entry} />
</div>
</div>
</Header>
);
};

View File

@@ -1,6 +1,6 @@
import { Copy, Settings, Trash2 } from 'lucide-react';
import React from 'react';
import { Entry, EntryRole, hasEntryRole } from '@colanode/core';
import { EntryRole, hasEntryRole } from '@colanode/core';
import { FileDeleteDialog } from '@/renderer/components/files/file-delete-dialog';
import {
@@ -15,14 +15,13 @@ import { useWorkspace } from '@/renderer/contexts/workspace';
interface FileSettingsProps {
file: FileWithState;
role: EntryRole;
entry: Entry;
}
export const FileSettings = ({ file, role, entry }: FileSettingsProps) => {
export const FileSettings = ({ file, role }: FileSettingsProps) => {
const workspace = useWorkspace();
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
const canDelete =
file.parentId === entry.id &&
file.parentId === file.entryId &&
(file.createdBy === workspace.userId || hasEntryRole(role, 'editor'));
return (

View File

@@ -7,7 +7,7 @@ import {
List,
Upload,
} from 'lucide-react';
import React, { useEffect } from 'react';
import React from 'react';
import { FolderFiles } from '@/renderer/components/folders/folder-files';
import { Button } from '@/renderer/components/ui/button';
@@ -25,16 +25,15 @@ import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { FolderLayoutType } from '@/shared/types/folders';
import { toast } from '@/renderer/hooks/use-toast';
import { useRadar } from '@/renderer/contexts/radar';
export type FolderLayout = {
export type FolderLayoutOption = {
value: FolderLayoutType;
name: string;
description: string;
icon: React.FC<React.SVGProps<SVGSVGElement>>;
};
export const folderLayouts: FolderLayout[] = [
export const folderLayouts: FolderLayoutOption[] = [
{
name: 'Grid',
value: 'grid',
@@ -62,7 +61,6 @@ interface FolderBodyProps {
export const FolderBody = ({ folder }: FolderBodyProps) => {
const workspace = useWorkspace();
const radar = useRadar();
const { mutate } = useMutation();
const [layout, setLayout] = React.useState<FolderLayoutType>('grid');
@@ -120,16 +118,6 @@ export const FolderBody = ({ folder }: FolderBodyProps) => {
isDialogOpenedRef.current = false;
};
useEffect(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, folder.id);
const interval = setInterval(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, folder.id);
}, 60000);
return () => clearInterval(interval);
}, [folder.id, folder.type, folder.transactionId]);
return (
<Dropzone
text="Drop files here to upload them in the folder"
@@ -137,7 +125,7 @@ export const FolderBody = ({ folder }: FolderBodyProps) => {
files.forEach((file) => console.log(file));
}}
>
<div className="flex h-full max-h-full flex-col gap-4 overflow-y-auto px-10 pt-4">
<div className="flex h-full max-h-full flex-col gap-4 overflow-y-auto">
<div className="flex flex-row justify-between">
<div className="flex flex-row gap-2">
<Button type="button" variant="outline" onClick={openFileDialog}>
@@ -181,8 +169,8 @@ export const FolderBody = ({ folder }: FolderBodyProps) => {
{/* <FolderUploads
uploads={Object.values(uploads)}
open={openUploads}
setOpen={setOpenUploads}
/> */}
setOpen={setOpenUploads}
/> */}
</Dropzone>
);
};

View File

@@ -1,19 +1,33 @@
import { FolderEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface FolderBreadcrumbItemProps {
folder: FolderEntry;
id: string;
}
export const FolderBreadcrumbItem = ({ folder }: FolderBreadcrumbItemProps) => {
export const FolderBreadcrumbItem = ({ id }: FolderBreadcrumbItemProps) => {
const workspace = useWorkspace();
const { data } = useQuery({
type: 'entry_get',
entryId: id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!data) {
return null;
}
const folder = data as FolderEntry;
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={folder.id}
name={folder.attributes.name}
avatar={folder.attributes.avatar}
className="size-4"
/>
<span>{folder.attributes.name}</span>
</div>

View File

@@ -0,0 +1,37 @@
import { FolderEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface FolderContainerTabProps {
folderId: string;
}
export const FolderContainerTab = ({ folderId }: FolderContainerTabProps) => {
const workspace = useWorkspace();
const { data: entry } = useQuery({
type: 'entry_get',
entryId: folderId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const folder = entry as FolderEntry;
if (!folder) {
return <p>Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={folder.id}
name={folder.attributes.name}
avatar={folder.attributes.avatar}
/>
<span>{folder.attributes.name}</span>
</div>
);
};

View File

@@ -1,57 +1,48 @@
import { extractEntryRole, FolderEntry } from '@colanode/core';
import { FolderEntry } from '@colanode/core';
import { FolderBody } from '@/renderer/components/folders/folder-body';
import { FolderHeader } from '@/renderer/components/folders/folder-header';
import { FolderNotFound } from '@/renderer/components/folders/folder-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
import { FolderSettings } from '@/renderer/components/folders/folder-settings';
import { FolderBody } from '@/renderer/components/folders/folder-body';
interface FolderContainerProps {
folderId: string;
}
export const FolderContainer = ({ folderId }: FolderContainerProps) => {
const workspace = useWorkspace();
const data = useEntryContainer<FolderEntry>(folderId);
const { data: entry, isPending: isPendingEntry } = useQuery({
type: 'entry_get',
entryId: folderId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
useEntryRadar(data.entry);
const folder = entry as FolderEntry;
const folderExists = !!folder;
const { data: root, isPending: isPendingRoot } = useQuery(
{
type: 'entry_get',
entryId: folder?.rootId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: folderExists,
}
);
if (isPendingEntry || (isPendingRoot && folderExists)) {
if (data.isPending) {
return null;
}
if (!folder || !root) {
if (!data.entry) {
return <FolderNotFound />;
}
const role = extractEntryRole(root, workspace.userId);
if (!role) {
return <FolderNotFound />;
}
const { entry: folder, role } = data;
return (
<div className="flex h-full w-full flex-col">
<FolderHeader folder={folder} role={role} />
<FolderBody folder={folder} role={role} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<FolderSettings folder={folder} role={role} />
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<FolderBody folder={folder} role={role} />
</ContainerBody>
</Container>
);
};

View File

@@ -11,6 +11,7 @@ import {
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
import { useLayout } from '@/renderer/contexts/layout';
interface FolderCreateDialogProps {
spaceId: string;
@@ -24,6 +25,7 @@ export const FolderCreateDialog = ({
onOpenChange,
}: FolderCreateDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -62,7 +64,7 @@ export const FolderCreateDialog = ({
},
onSuccess(output) {
onOpenChange(false);
workspace.openInMain(output.id);
layout.previewLeft(output.id);
},
onError(error) {
toast({

View File

@@ -10,6 +10,7 @@ import {
import { Button } from '@/renderer/components/ui/button';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { useLayout } from '@/renderer/contexts/layout';
import { toast } from '@/renderer/hooks/use-toast';
interface FolderDeleteDialogProps {
@@ -24,6 +25,7 @@ export const FolderDeleteDialog = ({
onOpenChange,
}: FolderDeleteDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -53,7 +55,7 @@ export const FolderDeleteDialog = ({
},
onSuccess() {
onOpenChange(false);
workspace.closeEntry(entryId);
layout.close(entryId);
},
onError(error) {
toast({

View File

@@ -1,4 +1,3 @@
import { getIdType, IdType } from '@colanode/core';
import React from 'react';
import { match } from 'ts-pattern';
@@ -10,6 +9,7 @@ import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQueries } from '@/renderer/hooks/use-queries';
import { FileListQueryInput } from '@/shared/queries/files/file-list';
import { FolderLayoutType } from '@/shared/types/folders';
import { useLayout } from '@/renderer/contexts/layout';
const FILES_PER_PAGE = 100;
@@ -19,8 +19,14 @@ interface FolderFilesProps {
layout: FolderLayoutType;
}
export const FolderFiles = ({ id, name, layout }: FolderFilesProps) => {
export const FolderFiles = ({
id,
name,
layout: folderLayout,
}: FolderFilesProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const [lastPage] = React.useState<number>(1);
const inputs: FileListQueryInput[] = Array.from({
length: lastPage,
@@ -46,18 +52,12 @@ export const FolderFiles = ({ id, name, layout }: FolderFilesProps) => {
console.log('onClick');
},
onDoubleClick: (_, id) => {
const idType = getIdType(id);
if (idType === IdType.Folder) {
workspace.openInMain(id);
} else if (idType === IdType.File) {
workspace.openInModal(id);
}
layout.previewLeft(id, true);
},
onMove: () => {},
}}
>
{match(layout)
{match(folderLayout)
.with('grid', () => <GridLayout />)
.with('list', () => <ListLayout />)
.with('gallery', () => <GalleryLayout />)

View File

@@ -1,32 +0,0 @@
import { FolderEntry, EntryRole } from '@colanode/core';
import { FolderSettings } from '@/renderer/components/folders/folder-settings';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
interface FolderHeaderProps {
folder: FolderEntry;
role: EntryRole;
}
export const FolderHeader = ({ folder, role }: FolderHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
<EntryBreadcrumb entry={folder} />
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={folder.id} />
)}
</div>
<div className="flex items-center gap-2">
<FolderSettings folder={folder} role={role} />
</div>
</div>
</Header>
);
};

View File

@@ -30,7 +30,7 @@ export const FolderSettings = ({ folder, role }: FolderSettingsProps) => {
<React.Fragment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
<Settings className="size-4 cursor-pointer text-muted-foreground hover:text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="mr-2 w-80">
<DropdownMenuLabel>{folder.attributes.name}</DropdownMenuLabel>

View File

@@ -1,7 +1,7 @@
import { FolderEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
import { cn } from '@/shared/lib/utils';
interface FolderSidebarItemProps {
@@ -9,8 +9,8 @@ interface FolderSidebarItemProps {
}
export const FolderSidebarItem = ({ folder }: FolderSidebarItemProps) => {
const workspace = useWorkspace();
const isActive = workspace.isEntryActive(folder.id);
const layout = useLayout();
const isActive = layout.activeTab === folder.id;
const isUnread = false;
const mentionsCount = 0;

View File

@@ -0,0 +1,44 @@
import { getIdType, IdType } from '@colanode/core';
import { ChannelBreadcrumbItem } from '@/renderer/components/channels/channel-breadcrumb-item';
import { ChatBreadcrumbItem } from '@/renderer/components/chats/chat-breadcrumb-item';
import { DatabaseBreadcrumbItem } from '@/renderer/components/databases/database-breadcrumb-item';
import { FolderBreadcrumbItem } from '@/renderer/components/folders/folder-breadcrumb-item';
import { PageBreadcrumbItem } from '@/renderer/components/pages/page-breadcrumb-item';
import { RecordBreadcrumbItem } from '@/renderer/components/records/record-breadcrumb-item';
import { SpaceBreadcrumbItem } from '@/renderer/components/spaces/space-breadcrumb-item';
import { FileBreadcrumbItem } from '@/renderer/components/files/file-breadcrumb-item';
import { MessageBreadcrumbItem } from '@/renderer/components/messages/message-breadcrumb-item';
interface ContainerBreadcrumbItemProps {
id: string;
}
export const ContainerBreadcrumbItem = ({
id,
}: ContainerBreadcrumbItemProps) => {
const idType = getIdType(id);
switch (idType) {
case IdType.Space:
return <SpaceBreadcrumbItem id={id} />;
case IdType.Channel:
return <ChannelBreadcrumbItem id={id} />;
case IdType.Chat:
return <ChatBreadcrumbItem id={id} />;
case IdType.Page:
return <PageBreadcrumbItem id={id} />;
case IdType.Database:
return <DatabaseBreadcrumbItem id={id} />;
case IdType.Record:
return <RecordBreadcrumbItem id={id} />;
case IdType.Folder:
return <FolderBreadcrumbItem id={id} />;
case IdType.File:
return <FileBreadcrumbItem id={id} />;
case IdType.Message:
return <MessageBreadcrumbItem id={id} />;
default:
return null;
}
};

View File

@@ -0,0 +1,94 @@
import React from 'react';
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbSeparator,
} from '@/renderer/components/ui/breadcrumb';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/renderer/components/ui/dropdown-menu';
import { useLayout } from '@/renderer/contexts/layout';
import { ContainerBreadcrumbItem } from '@/renderer/components/layouts/breadcrumbs/container-breadcrumb-item';
interface ContainerBreadcrumbProps {
breadcrumb: string[];
}
export const ContainerBreadcrumb = ({
breadcrumb,
}: ContainerBreadcrumbProps) => {
const layout = useLayout();
// Show ellipsis if we have more than 3 nodes (first + last two)
const showEllipsis = breadcrumb.length > 3;
// Get visible entries: first entry + last two entries
const visibleItems = showEllipsis
? [breadcrumb[0], ...breadcrumb.slice(-2)]
: breadcrumb;
// Get middle entries for ellipsis (everything except first and last two)
const ellipsisItems = showEllipsis ? breadcrumb.slice(1, -2) : [];
return (
<Breadcrumb className="flex-grow">
<BreadcrumbList>
{visibleItems.map((item, index) => {
if (!item) {
return null;
}
const isFirst = index === 0;
return (
<React.Fragment key={item}>
{!isFirst && <BreadcrumbSeparator />}
<BreadcrumbItem
className="hover:cursor-pointer hover:text-foreground"
onClick={() => {
layout.openLeft(item);
}}
>
<ContainerBreadcrumbItem id={item} />
</BreadcrumbItem>
{showEllipsis && isFirst && (
<React.Fragment>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1">
<BreadcrumbEllipsis className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{ellipsisItems.map((ellipsisItem) => {
return (
<DropdownMenuItem
key={ellipsisItem}
onClick={() => {
layout.openLeft(ellipsisItem);
}}
>
<BreadcrumbItem className="hover:cursor-pointer hover:text-foreground">
<ContainerBreadcrumbItem id={ellipsisItem} />
</BreadcrumbItem>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
</React.Fragment>
)}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@@ -1,34 +0,0 @@
import { Entry } from '@colanode/core';
import { ChannelBreadcrumbItem } from '@/renderer/components/channels/channel-breadcrumb-item';
import { ChatBreadcrumbItem } from '@/renderer/components/chats/chat-breadcrumb-item';
import { DatabaseBreadcrumbItem } from '@/renderer/components/databases/database-breadcrumb-item';
import { FolderBreadcrumbItem } from '@/renderer/components/folders/folder-breadcrumb-item';
import { PageBreadcrumbItem } from '@/renderer/components/pages/page-breadcrumb-item';
import { RecordBreadcrumbItem } from '@/renderer/components/records/record-breadcrumb-item';
import { SpaceBreadcrumbItem } from '@/renderer/components/spaces/space-breadcrumb-item';
interface EntryBreadcrumbItemProps {
entry: Entry;
}
export const EntryBreadcrumbItem = ({ entry }: EntryBreadcrumbItemProps) => {
switch (entry.type) {
case 'space':
return <SpaceBreadcrumbItem space={entry} />;
case 'channel':
return <ChannelBreadcrumbItem channel={entry} />;
case 'chat':
return <ChatBreadcrumbItem chat={entry} />;
case 'page':
return <PageBreadcrumbItem page={entry} />;
case 'database':
return <DatabaseBreadcrumbItem database={entry} />;
case 'record':
return <RecordBreadcrumbItem record={entry} />;
case 'folder':
return <FolderBreadcrumbItem folder={entry} />;
default:
return null;
}
};

View File

@@ -1,128 +0,0 @@
import { Entry, EntryType } from '@colanode/core';
import React from 'react';
import { EntryBreadcrumbItem } from '@/renderer/components/layouts/entry-breadcrumb-item';
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbSeparator,
} from '@/renderer/components/ui/breadcrumb';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/renderer/components/ui/dropdown-menu';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface EntryBreadcrumbProps {
entry: Entry;
}
const isClickable = (type: EntryType) => type !== 'space';
export const EntryBreadcrumb = ({ entry }: EntryBreadcrumbProps) => {
const workspace = useWorkspace();
const { data } = useQuery(
{
type: 'entry_tree_get',
entryId: entry.id,
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: entry.type !== 'chat',
}
);
const entries = data?.length ? data : [entry];
// Show ellipsis if we have more than 3 nodes (first + last two)
const showEllipsis = entries.length > 3;
// Get visible entries: first entry + last two entries
const visibleEntries = showEllipsis
? [entries[0], ...entries.slice(-2)]
: entries;
// Get middle entries for ellipsis (everything except first and last two)
const ellipsisEntries = showEllipsis ? entries.slice(1, -2) : [];
return (
<Breadcrumb>
<BreadcrumbList>
{visibleEntries.map((entry, index) => {
if (!entry) {
return null;
}
const isFirst = index === 0;
const isClickableEntry = isClickable(entry.type);
return (
<React.Fragment key={entry.id}>
{!isFirst && <BreadcrumbSeparator />}
<BreadcrumbItem
className={
isClickableEntry
? 'hover:cursor-pointer hover:text-foreground'
: ''
}
onClick={() => {
if (isClickableEntry) {
workspace.openInMain(entry.id);
}
}}
>
<EntryBreadcrumbItem entry={entry} />
</BreadcrumbItem>
{showEllipsis && isFirst && (
<React.Fragment>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center gap-1">
<BreadcrumbEllipsis className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{ellipsisEntries.map((ellipsisEntry) => {
const isClickableEllipsisEntry = isClickable(
ellipsisEntry.type
);
return (
<DropdownMenuItem
key={ellipsisEntry.id}
disabled={!isClickableEllipsisEntry}
onClick={() => {
if (isClickableEllipsisEntry) {
workspace.openInMain(ellipsisEntry.id);
}
}}
>
<BreadcrumbItem
className={
isClickableEllipsisEntry
? 'hover:cursor-pointer hover:text-foreground'
: ''
}
>
<EntryBreadcrumbItem entry={ellipsisEntry} />
</BreadcrumbItem>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
</React.Fragment>
)}
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@@ -1,36 +0,0 @@
import { getIdType, IdType } from '@colanode/core';
import { ChannelContainer } from '@/renderer/components/channels/channel-container';
import { ChatContainer } from '@/renderer/components/chats/chat-container';
import { DatabaseContainer } from '@/renderer/components/databases/database-container';
import { FileContainer } from '@/renderer/components/files/file-container';
import { FolderContainer } from '@/renderer/components/folders/folder-container';
import { PageContainer } from '@/renderer/components/pages/page-container';
import { RecordContainer } from '@/renderer/components/records/record-container';
interface EntryContainerProps {
entryId: string;
}
export const EntryContainer = ({ entryId }: EntryContainerProps) => {
const idType = getIdType(entryId);
switch (idType) {
case IdType.Channel:
return <ChannelContainer channelId={entryId} />;
case IdType.Page:
return <PageContainer pageId={entryId} />;
case IdType.Database:
return <DatabaseContainer databaseId={entryId} />;
case IdType.Record:
return <RecordContainer recordId={entryId} />;
case IdType.Chat:
return <ChatContainer chatId={entryId} />;
case IdType.Folder:
return <FolderContainer folderId={entryId} />;
case IdType.File:
return <FileContainer fileId={entryId} />;
default:
return null;
}
};

View File

@@ -1,22 +0,0 @@
import { Fullscreen } from 'lucide-react';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface EntryFullscreenButtonProps {
entryId: string;
}
export const EntryFullscreenButton = ({
entryId,
}: EntryFullscreenButtonProps) => {
const workspace = useWorkspace();
return (
<Fullscreen
className="size-5 cursor-pointer text-muted-foreground hover:text-foreground"
onClick={() => {
workspace.openInMain(entryId);
}}
/>
);
};

View File

@@ -1,14 +0,0 @@
import { EntryContainer } from '@/renderer/components/layouts/entry-container';
import { ContainerContext } from '@/renderer/contexts/container';
interface LayoutMainProps {
entryId: string;
}
export const LayoutMain = ({ entryId }: LayoutMainProps) => {
return (
<ContainerContext.Provider value={{ entryId, mode: 'main' }}>
<EntryContainer entryId={entryId} />
</ContainerContext.Provider>
);
};

View File

@@ -1,47 +0,0 @@
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import React from 'react';
import { EntryContainer } from '@/renderer/components/layouts/entry-container';
import {
Dialog,
DialogContent,
DialogTitle,
} from '@/renderer/components/ui/dialog';
import { ContainerContext } from '@/renderer/contexts/container';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface LayoutModalProps {
entryId: string;
}
export const LayoutModal = ({ entryId }: LayoutModalProps) => {
const workspace = useWorkspace();
const [open, setOpen] = React.useState(true);
React.useEffect(() => {
if (!open) {
workspace.closeModal();
}
}, [open]);
return (
<Dialog
open={open}
onOpenChange={(open) => {
setOpen(open);
}}
>
<DialogContent
className="flex h-[calc(100vh-100px)] max-h-full w-8/12 max-w-full flex-col gap-1 overflow-hidden px-0.5 pt-0 md:w-8/12"
aria-describedby={undefined}
>
<VisuallyHidden>
<DialogTitle>Modal</DialogTitle>
</VisuallyHidden>
<ContainerContext.Provider value={{ entryId, mode: 'modal' }}>
<EntryContainer entryId={entryId} />
</ContainerContext.Provider>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,14 +0,0 @@
import { EntryContainer } from '@/renderer/components/layouts/entry-container';
import { ContainerContext } from '@/renderer/contexts/container';
interface LayoutRightProps {
entryId: string;
}
export const LayoutRight = ({ entryId }: LayoutRightProps) => {
return (
<ContainerContext.Provider value={{ entryId, mode: 'panel' }}>
<EntryContainer entryId={entryId} />
</ContainerContext.Provider>
);
};

View File

@@ -0,0 +1,123 @@
import { X } from 'lucide-react';
import { match } from 'ts-pattern';
import { getIdType, IdType } from '@colanode/core';
import { ScrollArea, ScrollBar } from '@/renderer/components/ui/scroll-area';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@/renderer/components/ui/tabs';
import { cn } from '@/shared/lib/utils';
import { ChannelContainer } from '@/renderer/components/channels/channel-container';
import { ChatContainer } from '@/renderer/components/chats/chat-container';
import { DatabaseContainer } from '@/renderer/components/databases/database-container';
import { FileContainer } from '@/renderer/components/files/file-container';
import { FolderContainer } from '@/renderer/components/folders/folder-container';
import { PageContainer } from '@/renderer/components/pages/page-container';
import { RecordContainer } from '@/renderer/components/records/record-container';
import { ChannelContainerTab } from '@/renderer/components/channels/channel-container-tab';
import { DatabaseContainerTab } from '@/renderer/components/databases/database-container-tab';
import { FileContainerTab } from '@/renderer/components/files/file-container-tab';
import { FolderContainerTab } from '@/renderer/components/folders/folder-container-tab';
import { PageContainerTab } from '@/renderer/components/pages/page-container-tab';
import { RecordContainerTab } from '@/renderer/components/records/record-container-tab';
import { ChatContainerTab } from '@/renderer/components/chats/chat-container-tab';
import { ContainerTab } from '@/shared/types/workspaces';
interface LayoutTabsProps {
tabs: ContainerTab[];
onTabChange: (value: string) => void;
onFocus: () => void;
onClose: (value: string) => void;
}
export const LayoutTabs = ({
tabs,
onTabChange,
onFocus,
onClose,
}: LayoutTabsProps) => {
const activeTab = tabs.find((t) => t.active)?.id;
return (
<Tabs
defaultValue={tabs[0]?.id}
value={activeTab}
onValueChange={onTabChange}
onFocus={onFocus}
className="h-full min-h-full w-full min-w-full flex flex-col"
>
<ScrollArea>
<TabsList className="h-10 bg-slate-50 w-full justify-start p-0 app-drag-region">
{tabs.map((tab) => (
<TabsTrigger
value={tab.id}
key={tab.id}
className={cn(
'overflow-hidden rounded-b-none bg-muted py-2 data-[state=active]:z-10 data-[state=active]:shadow-none h-10 group/tab app-no-drag-region',
tab.preview && 'italic'
)}
onAuxClick={(e) => {
if (e.button === 1) {
e.preventDefault();
onClose(tab.id);
}
}}
>
{match(getIdType(tab.id))
.with(IdType.Channel, () => (
<ChannelContainerTab channelId={tab.id} />
))
.with(IdType.Page, () => <PageContainerTab pageId={tab.id} />)
.with(IdType.Database, () => (
<DatabaseContainerTab databaseId={tab.id} />
))
.with(IdType.Record, () => (
<RecordContainerTab recordId={tab.id} />
))
.with(IdType.Chat, () => <ChatContainerTab chatId={tab.id} />)
.with(IdType.Folder, () => (
<FolderContainerTab folderId={tab.id} />
))
.with(IdType.File, () => <FileContainerTab fileId={tab.id} />)
.otherwise(() => null)}
<div
className="opacity-0 group-hover/tab:opacity-100 group-data-[state=active]/tab:opacity-100 transition-opacity duration-200"
onClick={() => onClose(tab.id)}
>
<X className="size-4 text-muted-foreground ml-2 hover:text-primary" />
</div>
</TabsTrigger>
))}
</TabsList>
<ScrollBar orientation="horizontal" />
</ScrollArea>
<div className="flex-grow overflow-hidden">
{tabs.map((tab) => (
<TabsContent
value={tab.id}
key={tab.id}
className="h-full min-h-full w-full min-w-full m-0 pt-2"
>
{match(getIdType(tab.id))
.with(IdType.Channel, () => (
<ChannelContainer channelId={tab.id} />
))
.with(IdType.Page, () => <PageContainer pageId={tab.id} />)
.with(IdType.Database, () => (
<DatabaseContainer databaseId={tab.id} />
))
.with(IdType.Record, () => <RecordContainer recordId={tab.id} />)
.with(IdType.Chat, () => <ChatContainer chatId={tab.id} />)
.with(IdType.Folder, () => <FolderContainer folderId={tab.id} />)
.with(IdType.File, () => <FileContainer fileId={tab.id} />)
.otherwise(() => null)}
</TabsContent>
))}
</div>
</Tabs>
);
};

View File

@@ -1,20 +1,149 @@
import { LayoutMain } from '@/renderer/components/layouts/layout-main';
import { LayoutModal } from '@/renderer/components/layouts/layout-modal';
import { Resizable } from 're-resizable';
import { LayoutTabs } from '@/renderer/components/layouts/layout-tabs';
import { Sidebar } from '@/renderer/components/layouts/sidebars/sidebar';
import { LayoutContext } from '@/renderer/contexts/layout';
import { useLayoutState } from '@/renderer/hooks/user-layout-state';
import { useWindowSize } from '@/renderer/hooks/use-window-size';
import { percentToNumber } from '@/shared/lib/utils';
interface LayoutProps {
main?: string | null;
modal?: string | null;
}
export const Layout = () => {
const windowSize = useWindowSize();
const {
activeContainer,
sidebarMetadata,
leftContainerMetadata,
rightContainerMetadata,
handleSidebarResize,
handleMenuChange,
handleRightContainerResize,
handleFocus,
handleOpen,
handleOpenLeft,
handleOpenRight,
handleClose,
handleCloseLeft,
handleCloseRight,
handlePreview,
handlePreviewLeft,
handlePreviewRight,
handleActivateLeft,
handleActivateRight,
} = useLayoutState();
const shouldDisplayLeft = leftContainerMetadata.tabs.length > 0;
const shouldDisplayRight =
shouldDisplayLeft && rightContainerMetadata.tabs.length > 0;
export const Layout = ({ main, modal }: LayoutProps) => {
return (
<div className="w-screen min-w-screen h-screen min-h-screen flex flex-row">
<Sidebar />
<main className="h-full max-h-screen w-full min-w-128 flex-grow overflow-hidden bg-white">
{main && <LayoutMain entryId={main} />}
</main>
{modal && <LayoutModal entryId={modal} />}
</div>
<LayoutContext.Provider
value={{
open: handleOpen,
openLeft: handleOpenLeft,
openRight: handleOpenRight,
close: handleClose,
closeLeft: handleCloseLeft,
closeRight: handleCloseRight,
preview: handlePreview,
previewLeft: handlePreviewLeft,
previewRight: handlePreviewRight,
activeTab:
activeContainer === 'left'
? leftContainerMetadata.tabs.find((t) => t.active)?.id
: rightContainerMetadata.tabs.find((t) => t.active)?.id,
}}
>
<div className="w-screen min-w-screen h-screen min-h-screen flex flex-row">
<Resizable
as="aside"
size={{ width: sidebarMetadata.width, height: '100vh' }}
className="border-r border-gray-200"
minWidth={200}
maxWidth={500}
enable={{
bottom: false,
bottomLeft: false,
bottomRight: false,
left: false,
right: true,
top: false,
topLeft: false,
topRight: false,
}}
handleClasses={{
right: 'opacity-0 hover:opacity-100 bg-blue-300 z-30',
}}
handleStyles={{
right: {
width: '3px',
right: '-3px',
},
}}
onResize={(_, __, ref) => {
handleSidebarResize(ref.offsetWidth);
}}
>
<Sidebar
menu={sidebarMetadata.menu}
onMenuChange={handleMenuChange}
/>
</Resizable>
{shouldDisplayLeft && (
<div className="h-full max-h-screen w-full flex-grow overflow-hidden bg-white">
<LayoutTabs
tabs={leftContainerMetadata.tabs}
onFocus={() => {
handleFocus('left');
}}
onClose={handleCloseLeft}
onTabChange={handleActivateLeft}
/>
</div>
)}
{shouldDisplayRight && (
<Resizable
as="div"
className="h-full max-h-full min-h-full overflow-hidden border-l border-gray-200 bg-white"
size={{ width: rightContainerMetadata.width, height: '100%' }}
minWidth={percentToNumber(windowSize.width, 20)}
maxWidth={percentToNumber(windowSize.width, 50)}
enable={{
bottom: false,
bottomLeft: false,
bottomRight: false,
left: true,
right: false,
top: false,
topLeft: false,
topRight: false,
}}
handleClasses={{
left: 'opacity-0 hover:opacity-100 bg-blue-300 z-30',
}}
handleStyles={{
left: {
width: '3px',
left: '-3px',
},
}}
onResize={(_, __, ref) => {
handleRightContainerResize(ref.offsetWidth);
}}
>
<LayoutTabs
tabs={rightContainerMetadata.tabs}
onFocus={() => {
handleFocus('right');
}}
onTabChange={handleActivateRight}
onClose={handleCloseRight}
/>
</Resizable>
)}
</div>
</LayoutContext.Provider>
);
};

View File

@@ -1,12 +1,13 @@
import { useQuery } from '@/renderer/hooks/use-query';
import { Header } from '@/renderer/components/ui/header';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { ChatSidebarItem } from '@/renderer/components/chats/chat-sidebar-item';
import { ChatCreatePopover } from '@/renderer/components/chats/chat-create-popover';
import { cn } from '@/shared/lib/utils';
import { useLayout } from '@/renderer/contexts/layout';
export const SidebarChats = () => {
const workspace = useWorkspace();
const layout = useLayout();
const { data } = useQuery({
type: 'chat_list',
@@ -19,24 +20,27 @@ export const SidebarChats = () => {
const chats = data ?? [];
return (
<div className="flex flex-col group/sidebar-spaces h-full px-2">
<Header>
<p className="font-medium text-muted-foreground flex-grow">Chats</p>
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-spaces:opacity-100 flex items-center justify-center p-0">
<div className="flex flex-col group/sidebar-chats h-full px-2">
<div className="flex items-center justify-between h-12 pl-2 pr-1">
<p className="font-bold text-muted-foreground flex-grow">Chats</p>
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-chats:opacity-100 flex items-center justify-center">
<ChatCreatePopover />
</div>
</Header>
</div>
<div className="flex w-full min-w-0 flex-col gap-1">
{chats.map((item) => (
<button
key={item.id}
className={cn(
'px-2 flex w-full items-center gap-2 overflow-hidden rounded-md text-left text-sm hover:bg-sidebar-accent hover:text-sidebar-accent-foreground h-7',
workspace.isEntryActive(item.id) &&
layout.activeTab === item.id &&
'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
)}
onClick={() => {
workspace.openInMain(item.id);
layout.preview(item.id);
}}
onDoubleClick={() => {
layout.open(item.id);
}}
>
<ChatSidebarItem chat={item} />

View File

@@ -8,13 +8,11 @@ import { FolderSidebarItem } from '@/renderer/components/folders/folder-sidebar-
import { PageSidebarItem } from '@/renderer/components/pages/page-sidebar-item';
import { SpaceSidebarItem } from '@/renderer/components/spaces/space-sidebar-item';
interface EntrySidebarItemProps {
interface SidebarItemProps {
entry: Entry;
}
export const EntrySidebarItem = ({
entry,
}: EntrySidebarItemProps): React.ReactNode => {
export const SidebarItem = ({ entry }: SidebarItemProps): React.ReactNode => {
switch (entry.type) {
case 'space':
return <SpaceSidebarItem space={entry} />;

View File

@@ -3,10 +3,11 @@ import { LayoutGrid, MessageCircle } from 'lucide-react';
import { SidebarMenuIcon } from '@/renderer/components/layouts/sidebars/sidebar-menu-icon';
import { SidebarMenuHeader } from '@/renderer/components/layouts/sidebars/sidebar-menu-header';
import { SidebarMenuFooter } from '@/renderer/components/layouts/sidebars/sidebar-menu-footer';
import { SidebarMenuType } from '@/shared/types/workspaces';
interface SidebarMenuProps {
value: string;
onChange: (value: string) => void;
value: SidebarMenuType;
onChange: (value: SidebarMenuType) => void;
}
export const SidebarMenu = ({ value, onChange }: SidebarMenuProps) => {

View File

@@ -1,5 +1,4 @@
import { useQuery } from '@/renderer/hooks/use-query';
import { Header } from '@/renderer/components/ui/header';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { SpaceCreateButton } from '@/renderer/components/spaces/space-create-button';
import { SpaceSidebarItem } from '@/renderer/components/spaces/space-sidebar-item';
@@ -22,14 +21,14 @@ export const SidebarSpaces = () => {
return (
<div className="flex flex-col group/sidebar-spaces h-full px-2">
<Header>
<p className="font-medium text-muted-foreground flex-grow">Spaces</p>
<div className="flex items-center justify-between h-12 pl-2 pr-1">
<p className="font-bold text-muted-foreground flex-grow">Spaces</p>
{canCreateSpace && (
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-spaces:opacity-100 flex items-center justify-center p-0">
<div className="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-spaces:opacity-100 flex items-center justify-center">
<SpaceCreateButton />
</div>
)}
</Header>
</div>
<div className="flex w-full min-w-0 flex-col gap-1">
{spaces.map((space) => (
<SpaceSidebarItem space={space} key={space.id} />

View File

@@ -1,15 +1,17 @@
import React from 'react';
import { SidebarMenu } from '@/renderer/components/layouts/sidebars/sidebar-menu';
import { SidebarChats } from '@/renderer/components/layouts/sidebars/sidebar-chats';
import { SidebarSpaces } from '@/renderer/components/layouts/sidebars/sidebar-spaces';
import { SidebarMenuType } from '@/shared/types/workspaces';
export const Sidebar = () => {
const [menu, setMenu] = React.useState('spaces');
interface SidebarProps {
menu: SidebarMenuType;
onMenuChange: (menu: SidebarMenuType) => void;
}
export const Sidebar = ({ menu, onMenuChange }: SidebarProps) => {
return (
<div className="flex h-screen min-h-screen max-h-screen w-80 min-w-80 flex-row bg-slate-50">
<SidebarMenu value={menu} onChange={setMenu} />
<div className="flex h-screen min-h-screen max-h-screen w-full min-w-full flex-row bg-slate-50">
<SidebarMenu value={menu} onChange={onMenuChange} />
<div className="min-h-0 flex-grow overflow-auto">
{menu === 'spaces' && <SidebarSpaces />}
{menu === 'chats' && <SidebarChats />}

View File

@@ -97,25 +97,27 @@ export const Conversation = ({
},
}}
>
<ScrollArea
ref={viewportRef}
onScroll={handleScroll}
className="flex-grow overflow-y-auto px-10"
>
<div className="container" ref={containerRef}>
<MessageList />
</div>
<InView
className="h-4"
rootMargin="20px"
onChange={(inView) => {
bottomVisibleRef.current = inView;
}}
<div className="h-full min-h-full w-full min-w-full flex flex-col">
<ScrollArea
ref={viewportRef}
onScroll={handleScroll}
className="flex-grow overflow-y-auto"
>
<div ref={bottomRef} className="h-4"></div>
</InView>
</ScrollArea>
<MessageCreate ref={messageCreateRef} />
<div ref={containerRef}>
<MessageList />
</div>
<InView
className="h-4"
rootMargin="20px"
onChange={(inView) => {
bottomVisibleRef.current = inView;
}}
>
<div ref={bottomRef} className="h-4"></div>
</InView>
</ScrollArea>
<MessageCreate ref={messageCreateRef} />
</div>
</ConversationContext.Provider>
);
};

View File

@@ -0,0 +1,14 @@
import { MessageCircle } from 'lucide-react';
interface MessageBreadcrumbItemProps {
id: string;
}
export const MessageBreadcrumbItem = ({ id }: MessageBreadcrumbItemProps) => {
return (
<div className="flex items-center space-x-2" id={id}>
<MessageCircle className="size-4" />
<span>Message</span>
</div>
);
};

View File

@@ -139,7 +139,7 @@ export const MessageCreate = React.forwardRef<MessageCreateRefProps>(
}, [messageEditorRef]);
return (
<div className="container mt-1 px-10">
<div className="mt-1">
<div className="flex flex-col">
{conversation.canCreateMessage && replyTo && (
<MessageReplyBanner

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { EntryRole, PageEntry, hasEntryRole } from '@colanode/core';
import { JSONContent } from '@tiptap/core';
@@ -6,7 +6,6 @@ import { Document } from '@/renderer/components/documents/document';
import { ScrollArea } from '@/renderer/components/ui/scroll-area';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { toast } from '@/renderer/hooks/use-toast';
import { useRadar } from '@/renderer/contexts/radar';
interface PageBodyProps {
page: PageEntry;
@@ -15,7 +14,6 @@ interface PageBodyProps {
export const PageBody = ({ page, role }: PageBodyProps) => {
const workspace = useWorkspace();
const radar = useRadar();
const canEdit = hasEntryRole(role, 'editor');
const handleUpdate = useCallback(
@@ -40,18 +38,8 @@ export const PageBody = ({ page, role }: PageBodyProps) => {
[workspace.accountId, workspace.id, page.id]
);
useEffect(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, page.id);
const interval = setInterval(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, page.id);
}, 60000);
return () => clearInterval(interval);
}, [page.id, page.type, page.transactionId]);
return (
<ScrollArea className="h-full max-h-full w-full overflow-y-auto px-10 pb-12">
<ScrollArea className="h-full max-h-full w-full overflow-y-auto">
<Document
entryId={page.id}
rootId={page.rootId}

View File

@@ -1,19 +1,35 @@
import { PageEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface PageBreadcrumbItemProps {
page: PageEntry;
id: string;
}
export const PageBreadcrumbItem = ({ page }: PageBreadcrumbItemProps) => {
export const PageBreadcrumbItem = ({ id }: PageBreadcrumbItemProps) => {
const workspace = useWorkspace();
const { data } = useQuery({
type: 'entry_get',
entryId: id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!data) {
return null;
}
const page = data as PageEntry;
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={page.id}
name={page.attributes.name}
avatar={page.attributes.avatar}
className="size-4"
/>
<span>{page.attributes.name}</span>
</div>

View File

@@ -0,0 +1,37 @@
import { PageEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface PageContainerTabProps {
pageId: string;
}
export const PageContainerTab = ({ pageId }: PageContainerTabProps) => {
const workspace = useWorkspace();
const { data: entry } = useQuery({
type: 'entry_get',
entryId: pageId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const page = entry as PageEntry;
if (!page) {
return <p>Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={page.id}
name={page.attributes.name}
avatar={page.attributes.avatar}
/>
<span>{page.attributes.name}</span>
</div>
);
};

View File

@@ -1,57 +1,48 @@
import { extractEntryRole, PageEntry } from '@colanode/core';
import { PageEntry } from '@colanode/core';
import { PageBody } from '@/renderer/components/pages/page-body';
import { PageHeader } from '@/renderer/components/pages/page-header';
import { PageNotFound } from '@/renderer/components/pages/page-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
import { PageSettings } from '@/renderer/components/pages/page-settings';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { PageBody } from '@/renderer/components/pages/page-body';
interface PageContainerProps {
pageId: string;
}
export const PageContainer = ({ pageId }: PageContainerProps) => {
const workspace = useWorkspace();
const data = useEntryContainer<PageEntry>(pageId);
const { data: entry, isPending: isPendingEntry } = useQuery({
type: 'entry_get',
entryId: pageId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
useEntryRadar(data.entry);
const page = entry as PageEntry;
const pageExists = !!page;
const { data: root, isPending: isPendingRoot } = useQuery(
{
type: 'entry_get',
entryId: page?.rootId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: pageExists,
}
);
if (isPendingEntry || (isPendingRoot && pageExists)) {
if (data.isPending) {
return null;
}
if (!page || !root) {
if (!data.entry) {
return <PageNotFound />;
}
const role = extractEntryRole(root, workspace.userId);
if (!role) {
return <PageNotFound />;
}
const { entry: page, role } = data;
return (
<div className="flex h-full w-full flex-col">
<PageHeader page={page} role={role} />
<PageBody page={page} role={role} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<PageSettings page={page} role={role} />
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<PageBody page={page} role={role} />
</ContainerBody>
</Container>
);
};

View File

@@ -11,6 +11,7 @@ import {
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { toast } from '@/renderer/hooks/use-toast';
import { useLayout } from '@/renderer/contexts/layout';
interface PageCreateDialogProps {
spaceId: string;
@@ -24,6 +25,7 @@ export const PageCreateDialog = ({
onOpenChange,
}: PageCreateDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -62,7 +64,7 @@ export const PageCreateDialog = ({
},
onSuccess(output) {
onOpenChange(false);
workspace.openInMain(output.id);
layout.openLeft(output.id);
},
onError(error) {
toast({

View File

@@ -10,6 +10,7 @@ import {
import { Button } from '@/renderer/components/ui/button';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { useLayout } from '@/renderer/contexts/layout';
import { toast } from '@/renderer/hooks/use-toast';
interface PageDeleteDialogProps {
@@ -24,6 +25,7 @@ export const PageDeleteDialog = ({
onOpenChange,
}: PageDeleteDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -53,7 +55,7 @@ export const PageDeleteDialog = ({
},
onSuccess() {
onOpenChange(false);
workspace.closeEntry(pageId);
layout.close(pageId);
},
onError(error) {
toast({

View File

@@ -1,32 +0,0 @@
import { EntryRole, PageEntry } from '@colanode/core';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { PageSettings } from '@/renderer/components/pages/page-settings';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
interface PageHeaderProps {
page: PageEntry;
role: EntryRole;
}
export const PageHeader = ({ page, role }: PageHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
<EntryBreadcrumb entry={page} />
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={page.id} />
)}
</div>
<div className="flex items-center gap-2">
<PageSettings page={page} role={role} />
</div>
</div>
</Header>
);
};

View File

@@ -30,7 +30,7 @@ export const PageSettings = ({ page, role }: PageSettingsProps) => {
<React.Fragment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Settings className="size-5 cursor-pointer text-muted-foreground hover:text-foreground" />
<Settings className="size-4 cursor-pointer text-muted-foreground hover:text-foreground" />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="mr-2 w-80">
<DropdownMenuLabel>{page.attributes.name}</DropdownMenuLabel>

View File

@@ -1,7 +1,7 @@
import { PageEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useLayout } from '@/renderer/contexts/layout';
import { cn } from '@/shared/lib/utils';
interface PageSidebarItemProps {
@@ -9,8 +9,8 @@ interface PageSidebarItemProps {
}
export const PageSidebarItem = ({ page }: PageSidebarItemProps) => {
const workspace = useWorkspace();
const isActive = workspace.isEntryActive(page.id);
const layout = useLayout();
const isActive = layout.activeTab === page.id;
const isUnread = false;
const mentionsCount = 0;

View File

@@ -1,13 +1,7 @@
import {
DatabaseEntry,
EntryRole,
hasEntryRole,
RecordEntry,
} from '@colanode/core';
import { EntryRole, hasEntryRole, RecordEntry } from '@colanode/core';
import { JSONContent } from '@tiptap/core';
import { useCallback, useEffect } from 'react';
import { useCallback } from 'react';
import { Database } from '@/renderer/components/databases/database';
import { Document } from '@/renderer/components/documents/document';
import { RecordAttributes } from '@/renderer/components/records/record-attributes';
import { RecordProvider } from '@/renderer/components/records/record-provider';
@@ -15,17 +9,15 @@ import { ScrollArea } from '@/renderer/components/ui/scroll-area';
import { Separator } from '@/renderer/components/ui/separator';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { toast } from '@/renderer/hooks/use-toast';
import { useRadar } from '@/renderer/contexts/radar';
import { RecordDatabase } from '@/renderer/components/records/record-database';
interface RecordBodyProps {
record: RecordEntry;
database: DatabaseEntry;
role: EntryRole;
}
export const RecordBody = ({ record, database, role }: RecordBodyProps) => {
export const RecordBody = ({ record, role }: RecordBodyProps) => {
const workspace = useWorkspace();
const radar = useRadar();
const canEdit =
record.createdBy === workspace.userId || hasEntryRole(role, 'editor');
@@ -52,19 +44,9 @@ export const RecordBody = ({ record, database, role }: RecordBodyProps) => {
[workspace.accountId, workspace.id, record.id]
);
useEffect(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, record.id);
const interval = setInterval(() => {
radar.markEntryAsOpened(workspace.accountId, workspace.id, record.id);
}, 60000);
return () => clearInterval(interval);
}, [record.id, record.type, record.transactionId]);
return (
<Database database={database} role={role}>
<ScrollArea className="h-full max-h-full w-full overflow-y-auto px-10 pb-12">
<RecordDatabase id={record.attributes.databaseId} role={role}>
<ScrollArea className="h-full max-h-full w-full overflow-y-auto">
<RecordProvider record={record} role={role}>
<RecordAttributes />
</RecordProvider>
@@ -79,6 +61,6 @@ export const RecordBody = ({ record, database, role }: RecordBodyProps) => {
autoFocus={false}
/>
</ScrollArea>
</Database>
</RecordDatabase>
);
};

View File

@@ -1,19 +1,32 @@
import { RecordEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface RecordBreadcrumbItemProps {
record: RecordEntry;
id: string;
}
export const RecordBreadcrumbItem = ({ record }: RecordBreadcrumbItemProps) => {
export const RecordBreadcrumbItem = ({ id }: RecordBreadcrumbItemProps) => {
const workspace = useWorkspace();
const { data } = useQuery({
type: 'entry_get',
entryId: id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
if (!data) {
return null;
}
const record = data as RecordEntry;
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={record.id}
name={record.attributes.name}
avatar={record.attributes.avatar}
className="size-4"
/>
<span>{record.attributes.name}</span>
</div>

View File

@@ -0,0 +1,37 @@
import { RecordEntry } from '@colanode/core';
import { Avatar } from '@/renderer/components/avatars/avatar';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
interface RecordContainerTabProps {
recordId: string;
}
export const RecordContainerTab = ({ recordId }: RecordContainerTabProps) => {
const workspace = useWorkspace();
const { data: entry } = useQuery({
type: 'entry_get',
entryId: recordId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const record = entry as RecordEntry;
if (!record) {
return <p>Not found</p>;
}
return (
<div className="flex items-center space-x-2">
<Avatar
size="small"
id={record.id}
name={record.attributes.name}
avatar={record.attributes.avatar}
/>
<span>{record.attributes.name}</span>
</div>
);
};

View File

@@ -1,71 +1,48 @@
import { DatabaseEntry, extractEntryRole, RecordEntry } from '@colanode/core';
import { RecordEntry } from '@colanode/core';
import { RecordBody } from '@/renderer/components/records/record-body';
import { RecordHeader } from '@/renderer/components/records/record-header';
import { RecordNotFound } from '@/renderer/components/records/record-not-found';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query';
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
import { useEntryRadar } from '@/renderer/hooks/use-entry-radar';
import {
Container,
ContainerBody,
ContainerHeader,
ContainerSettings,
} from '@/renderer/components/ui/container';
import { ContainerBreadcrumb } from '@/renderer/components/layouts/breadcrumbs/container-breadrumb';
import { RecordBody } from '@/renderer/components/records/record-body';
import { RecordSettings } from '@/renderer/components/records/record-settings';
interface RecordContainerProps {
recordId: string;
}
export const RecordContainer = ({ recordId }: RecordContainerProps) => {
const workspace = useWorkspace();
const data = useEntryContainer<RecordEntry>(recordId);
const { data: entry, isPending: isPendingEntry } = useQuery({
type: 'entry_get',
entryId: recordId,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
useEntryRadar(data.entry);
const record = entry as RecordEntry;
const recordExists = !!record;
const { data: root, isPending: isPendingRoot } = useQuery(
{
type: 'entry_get',
entryId: record?.rootId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: recordExists,
}
);
const { data: databaseEntry, isPending: isPendingDatabase } = useQuery(
{
type: 'entry_get',
entryId: record?.attributes.databaseId ?? '',
accountId: workspace.accountId,
workspaceId: workspace.id,
},
{
enabled: recordExists,
}
);
const database = databaseEntry as DatabaseEntry;
if (isPendingEntry || isPendingRoot || isPendingDatabase) {
if (data.isPending) {
return null;
}
if (!record || !root || !database) {
if (!data.entry) {
return <RecordNotFound />;
}
const role = extractEntryRole(root, workspace.userId);
if (!role) {
return <RecordNotFound />;
}
const { entry: record, role } = data;
return (
<div className="flex h-full w-full flex-col">
<RecordHeader record={record} role={role} />
<RecordBody record={record} database={database} role={role} />
</div>
<Container>
<ContainerHeader>
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
<ContainerSettings>
<RecordSettings record={record} role={role} />
</ContainerSettings>
</ContainerHeader>
<ContainerBody>
<RecordBody record={record} role={role} />
</ContainerBody>
</Container>
);
};

View File

@@ -0,0 +1,36 @@
import { DatabaseEntry, EntryRole } from '@colanode/core';
import { Database } from '@/renderer/components/databases/database';
import { useQuery } from '@/renderer/hooks/use-query';
import { useWorkspace } from '@/renderer/contexts/workspace';
interface RecordDatabaseProps {
id: string;
role: EntryRole;
children: React.ReactNode;
}
export const RecordDatabase = ({ id, role, children }: RecordDatabaseProps) => {
const workspace = useWorkspace();
const { data, isPending } = useQuery({
type: 'entry_get',
accountId: workspace.accountId,
workspaceId: workspace.id,
entryId: id,
});
if (isPending) {
return null;
}
if (!data) {
return null;
}
return (
<Database database={data as DatabaseEntry} role={role}>
{children}
</Database>
);
};

View File

@@ -10,6 +10,7 @@ import {
import { Button } from '@/renderer/components/ui/button';
import { useWorkspace } from '@/renderer/contexts/workspace';
import { useMutation } from '@/renderer/hooks/use-mutation';
import { useLayout } from '@/renderer/contexts/layout';
import { toast } from '@/renderer/hooks/use-toast';
interface RecordDeleteDialogProps {
@@ -24,6 +25,7 @@ export const RecordDeleteDialog = ({
onOpenChange,
}: RecordDeleteDialogProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const { mutate, isPending } = useMutation();
return (
@@ -53,7 +55,7 @@ export const RecordDeleteDialog = ({
},
onSuccess() {
onOpenChange(false);
workspace.closeEntry(entryId);
layout.close(entryId);
},
onError(error) {
toast({

View File

@@ -1,32 +0,0 @@
import { EntryRole, RecordEntry } from '@colanode/core';
import { EntryBreadcrumb } from '@/renderer/components/layouts/entry-breadcrumb';
import { EntryFullscreenButton } from '@/renderer/components/layouts/entry-fullscreen-button';
import { RecordSettings } from '@/renderer/components/records/record-settings';
import { Header } from '@/renderer/components/ui/header';
import { useContainer } from '@/renderer/contexts/container';
interface RecordHeaderProps {
record: RecordEntry;
role: EntryRole;
}
export const RecordHeader = ({ record, role }: RecordHeaderProps) => {
const container = useContainer();
return (
<Header>
<div className="flex w-full items-center gap-2 px-4">
<div className="flex-grow">
{container.mode === 'main' && <EntryBreadcrumb entry={record} />}
{container.mode === 'modal' && (
<EntryFullscreenButton entryId={record.id} />
)}
</div>
<div className="flex items-center gap-2">
<RecordSettings record={record} role={role} />
</div>
</div>
</Header>
);
};

Some files were not shown because too many files have changed in this diff Show More