mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Use tabs for containers
This commit is contained in:
@@ -95,7 +95,7 @@
|
|||||||
"lowlight": "^3.2.0",
|
"lowlight": "^3.2.0",
|
||||||
"lucide-react": "^0.473.0",
|
"lucide-react": "^0.473.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"re-resizable": "^6.10.1",
|
"re-resizable": "^6.10.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dnd": "^16.0.1",
|
"react-dnd": "^16.0.1",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ import { createMutationsTable } from './00012-create-mutations-table';
|
|||||||
import { createEntryPathsTable } from './00013-create-entry-paths-table';
|
import { createEntryPathsTable } from './00013-create-entry-paths-table';
|
||||||
import { createTextsTable } from './00014-create-texts-table';
|
import { createTextsTable } from './00014-create-texts-table';
|
||||||
import { createCursorsTable } from './00015-create-cursors-table';
|
import { createCursorsTable } from './00015-create-cursors-table';
|
||||||
|
import { createMetadataTable } from './00016-create-metadata-table';
|
||||||
|
|
||||||
export const workspaceDatabaseMigrations: Record<string, Migration> = {
|
export const workspaceDatabaseMigrations: Record<string, Migration> = {
|
||||||
'00001-create-users-table': createUsersTable,
|
'00001-create-users-table': createUsersTable,
|
||||||
@@ -32,4 +33,5 @@ export const workspaceDatabaseMigrations: Record<string, Migration> = {
|
|||||||
'00013-create-entry-paths-table': createEntryPathsTable,
|
'00013-create-entry-paths-table': createEntryPathsTable,
|
||||||
'00014-create-texts-table': createTextsTable,
|
'00014-create-texts-table': createTextsTable,
|
||||||
'00015-create-cursors-table': createCursorsTable,
|
'00015-create-cursors-table': createCursorsTable,
|
||||||
|
'00016-create-metadata-table': createMetadataTable,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -231,6 +231,21 @@ interface CursorTable {
|
|||||||
updated_at: ColumnType<string | null, string | null, string | null>;
|
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 {
|
export interface WorkspaceDatabaseSchema {
|
||||||
users: UserTable;
|
users: UserTable;
|
||||||
entries: EntryTable;
|
entries: EntryTable;
|
||||||
@@ -247,4 +262,5 @@ export interface WorkspaceDatabaseSchema {
|
|||||||
mutations: MutationTable;
|
mutations: MutationTable;
|
||||||
texts: TextTable;
|
texts: TextTable;
|
||||||
cursors: CursorTable;
|
cursors: CursorTable;
|
||||||
|
metadata: MetadataTable;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,17 @@ import {
|
|||||||
SelectMessageInteraction,
|
SelectMessageInteraction,
|
||||||
SelectFileInteraction,
|
SelectFileInteraction,
|
||||||
SelectEntryInteraction,
|
SelectEntryInteraction,
|
||||||
|
SelectWorkspaceMetadata,
|
||||||
} from '@/main/databases/workspace';
|
} from '@/main/databases/workspace';
|
||||||
import { Account } from '@/shared/types/accounts';
|
import { Account } from '@/shared/types/accounts';
|
||||||
import { Server } from '@/shared/types/servers';
|
import { Server } from '@/shared/types/servers';
|
||||||
import { User } from '@/shared/types/users';
|
import { User } from '@/shared/types/users';
|
||||||
import { File, FileInteraction, FileState } from '@/shared/types/files';
|
import { File, FileInteraction, FileState } from '@/shared/types/files';
|
||||||
import { Workspace } from '@/shared/types/workspaces';
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceMetadata,
|
||||||
|
WorkspaceMetadataKey,
|
||||||
|
} from '@/shared/types/workspaces';
|
||||||
import {
|
import {
|
||||||
MessageInteraction,
|
MessageInteraction,
|
||||||
MessageNode,
|
MessageNode,
|
||||||
@@ -285,3 +290,14 @@ export const mapIcon = (row: SelectIcon): Icon => {
|
|||||||
tags: row.tags ? JSON.parse(row.tags) : [],
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { extractFileType } from '@colanode/core';
|
import { extractFileType, getIdType, IdType } from '@colanode/core';
|
||||||
import {
|
import {
|
||||||
DeleteResult,
|
DeleteResult,
|
||||||
InsertResult,
|
InsertResult,
|
||||||
@@ -159,3 +159,76 @@ export const fetchUserStorageUsed = async (
|
|||||||
|
|
||||||
return BigInt(storageUsedRow?.storage_used ?? 0);
|
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];
|
||||||
|
};
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ import { WorkspaceCreateMutationHandler } from '@/main/mutations/workspaces/work
|
|||||||
import { WorkspaceUpdateMutationHandler } from '@/main/mutations/workspaces/workspace-update';
|
import { WorkspaceUpdateMutationHandler } from '@/main/mutations/workspaces/workspace-update';
|
||||||
import { UserRoleUpdateMutationHandler } from '@/main/mutations/users/user-role-update';
|
import { UserRoleUpdateMutationHandler } from '@/main/mutations/users/user-role-update';
|
||||||
import { UsersInviteMutationHandler } from '@/main/mutations/users/users-invite';
|
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 { MutationHandler } from '@/main/lib/types';
|
||||||
import { MutationMap } from '@/shared/mutations';
|
import { MutationMap } from '@/shared/mutations';
|
||||||
|
|
||||||
@@ -128,4 +130,6 @@ export const mutationHandlerMap: MutationHandlerMap = {
|
|||||||
page_update: new PageUpdateMutationHandler(),
|
page_update: new PageUpdateMutationHandler(),
|
||||||
folder_update: new FolderUpdateMutationHandler(),
|
folder_update: new FolderUpdateMutationHandler(),
|
||||||
database_update: new DatabaseUpdateMutationHandler(),
|
database_update: new DatabaseUpdateMutationHandler(),
|
||||||
|
workspace_metadata_save: new WorkspaceMetadataSaveMutationHandler(),
|
||||||
|
workspace_metadata_delete: new WorkspaceMetadataDeleteMutationHandler(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
67
apps/desktop/src/main/queries/files/file-breadcrumb-get.ts
Normal file
67
apps/desktop/src/main/queries/files/file-breadcrumb-get.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { EmojiGetBySkinIdQueryHandler } from '@/main/queries/emojis/emoji-get-by
|
|||||||
import { FileListQueryHandler } from '@/main/queries/files/file-list';
|
import { FileListQueryHandler } from '@/main/queries/files/file-list';
|
||||||
import { FileGetQueryHandler } from '@/main/queries/files/file-get';
|
import { FileGetQueryHandler } from '@/main/queries/files/file-get';
|
||||||
import { FileMetadataGetQueryHandler } from '@/main/queries/files/file-metadata-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 { IconListQueryHandler } from '@/main/queries/icons/icon-list';
|
||||||
import { IconSearchQueryHandler } from '@/main/queries/icons/icon-search';
|
import { IconSearchQueryHandler } from '@/main/queries/icons/icon-search';
|
||||||
import { IconCategoryListQueryHandler } from '@/main/queries/icons/icon-category-list';
|
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 { MessageListQueryHandler } from '@/main/queries/messages/message-list';
|
||||||
import { MessageReactionsListQueryHandler } from '@/main/queries/messages/message-reaction-list';
|
import { MessageReactionsListQueryHandler } from '@/main/queries/messages/message-reaction-list';
|
||||||
import { MessageReactionsAggregateQueryHandler } from '@/main/queries/messages/message-reactions-aggregate';
|
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 { EntryChildrenGetQueryHandler } from '@/main/queries/entries/entry-children-get';
|
||||||
import { EntryGetQueryHandler } from '@/main/queries/entries/entry-get';
|
import { EntryGetQueryHandler } from '@/main/queries/entries/entry-get';
|
||||||
import { EntryTreeGetQueryHandler } from '@/main/queries/entries/entry-tree-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 { RadarDataGetQueryHandler } from '@/main/queries/interactions/radar-data-get';
|
||||||
import { RecordListQueryHandler } from '@/main/queries/records/record-list';
|
import { RecordListQueryHandler } from '@/main/queries/records/record-list';
|
||||||
import { ServerListQueryHandler } from '@/main/queries/servers/server-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 { UserGetQueryHandler } from '@/main/queries/users/user-get';
|
||||||
import { SpaceListQueryHandler } from '@/main/queries/spaces/space-list';
|
import { SpaceListQueryHandler } from '@/main/queries/spaces/space-list';
|
||||||
import { ChatListQueryHandler } from '@/main/queries/chats/chat-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 { QueryHandler } from '@/main/lib/types';
|
||||||
import { QueryMap } from '@/shared/queries';
|
import { QueryMap } from '@/shared/queries';
|
||||||
|
|
||||||
@@ -43,7 +47,9 @@ export const queryHandlerMap: QueryHandlerMap = {
|
|||||||
message_reaction_list: new MessageReactionsListQueryHandler(),
|
message_reaction_list: new MessageReactionsListQueryHandler(),
|
||||||
message_reactions_aggregate: new MessageReactionsAggregateQueryHandler(),
|
message_reactions_aggregate: new MessageReactionsAggregateQueryHandler(),
|
||||||
message_get: new MessageGetQueryHandler(),
|
message_get: new MessageGetQueryHandler(),
|
||||||
|
message_breadcrumb_get: new MessageBreadcrumbGetQueryHandler(),
|
||||||
entry_get: new EntryGetQueryHandler(),
|
entry_get: new EntryGetQueryHandler(),
|
||||||
|
entry_breadcrumb_get: new EntryBreadcrumbGetQueryHandler(),
|
||||||
record_list: new RecordListQueryHandler(),
|
record_list: new RecordListQueryHandler(),
|
||||||
server_list: new ServerListQueryHandler(),
|
server_list: new ServerListQueryHandler(),
|
||||||
user_search: new UserSearchQueryHandler(),
|
user_search: new UserSearchQueryHandler(),
|
||||||
@@ -62,6 +68,7 @@ export const queryHandlerMap: QueryHandlerMap = {
|
|||||||
entry_children_get: new EntryChildrenGetQueryHandler(),
|
entry_children_get: new EntryChildrenGetQueryHandler(),
|
||||||
radar_data_get: new RadarDataGetQueryHandler(),
|
radar_data_get: new RadarDataGetQueryHandler(),
|
||||||
file_metadata_get: new FileMetadataGetQueryHandler(),
|
file_metadata_get: new FileMetadataGetQueryHandler(),
|
||||||
|
file_breadcrumb_get: new FileBreadcrumbGetQueryHandler(),
|
||||||
account_get: new AccountGetQueryHandler(),
|
account_get: new AccountGetQueryHandler(),
|
||||||
workspace_get: new WorkspaceGetQueryHandler(),
|
workspace_get: new WorkspaceGetQueryHandler(),
|
||||||
database_list: new DatabaseListQueryHandler(),
|
database_list: new DatabaseListQueryHandler(),
|
||||||
@@ -70,4 +77,5 @@ export const queryHandlerMap: QueryHandlerMap = {
|
|||||||
file_get: new FileGetQueryHandler(),
|
file_get: new FileGetQueryHandler(),
|
||||||
chat_list: new ChatListQueryHandler(),
|
chat_list: new ChatListQueryHandler(),
|
||||||
space_list: new SpaceListQueryHandler(),
|
space_list: new SpaceListQueryHandler(),
|
||||||
|
workspace_metadata_list: new WorkspaceMetadataListQueryHandler(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,21 +1,35 @@
|
|||||||
import { ChannelEntry } from '@colanode/core';
|
import { ChannelEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||||
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
|
||||||
interface ChannelBreadcrumbItemProps {
|
interface ChannelBreadcrumbItemProps {
|
||||||
channel: ChannelEntry;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChannelBreadcrumbItem = ({
|
export const ChannelBreadcrumbItem = ({ id }: ChannelBreadcrumbItemProps) => {
|
||||||
channel,
|
const workspace = useWorkspace();
|
||||||
}: ChannelBreadcrumbItemProps) => {
|
const { data } = useQuery({
|
||||||
|
type: 'entry_get',
|
||||||
|
entryId: id,
|
||||||
|
accountId: workspace.accountId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = data as ChannelEntry;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="small"
|
|
||||||
id={channel.id}
|
id={channel.id}
|
||||||
name={channel.attributes.name}
|
name={channel.attributes.name}
|
||||||
avatar={channel.attributes.avatar}
|
avatar={channel.attributes.avatar}
|
||||||
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
<span>{channel.attributes.name}</span>
|
<span>{channel.attributes.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { ChannelNotFound } from '@/renderer/components/channels/channel-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import {
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
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 {
|
interface ChannelContainerProps {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChannelContainer = ({ channelId }: ChannelContainerProps) => {
|
export const ChannelContainer = ({ channelId }: ChannelContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useEntryContainer<ChannelEntry>(channelId);
|
||||||
|
|
||||||
const { data: entry, isPending: isPendingEntry } = useQuery({
|
useEntryRadar(data.entry);
|
||||||
type: 'entry_get',
|
|
||||||
entryId: channelId,
|
|
||||||
accountId: workspace.accountId,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const channel = entry as ChannelEntry;
|
if (data.isPending) {
|
||||||
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)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!channel || !root) {
|
if (!data.entry) {
|
||||||
return <ChannelNotFound />;
|
return <ChannelNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = extractEntryRole(root, workspace.userId);
|
const { entry: channel, role } = data;
|
||||||
if (!role) {
|
|
||||||
return <ChannelNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<ChannelHeader channel={channel} role={role} />
|
<ContainerHeader>
|
||||||
<ChannelBody channel={channel} role={role} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<ChannelSettings channel={channel} role={role} />
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<Conversation
|
||||||
|
conversationId={channel.id}
|
||||||
|
rootId={channel.rootId}
|
||||||
|
role={role}
|
||||||
|
/>
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
interface ChannelCreateDialogProps {
|
interface ChannelCreateDialogProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -24,6 +25,7 @@ export const ChannelCreateDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: ChannelCreateDialogProps) => {
|
}: ChannelCreateDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +63,7 @@ export const ChannelCreateDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess(output) {
|
onSuccess(output) {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.openInMain(output.id);
|
layout.openLeft(output.id);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/renderer/components/ui/alert-dialog';
|
} from '@/renderer/components/ui/alert-dialog';
|
||||||
import { Button } from '@/renderer/components/ui/button';
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
@@ -24,6 +25,7 @@ export const ChannelDeleteDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: ChannelDeleteDialogProps) => {
|
}: ChannelDeleteDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +55,7 @@ export const ChannelDeleteDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.closeEntry(channelId);
|
layout.close(channelId);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -30,7 +30,7 @@ export const ChannelSettings = ({ channel, role }: ChannelSettingsProps) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
||||||
<DropdownMenuLabel>{channel.attributes.name}</DropdownMenuLabel>
|
<DropdownMenuLabel>{channel.attributes.name}</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Avatar } from '@/renderer/components/avatars/avatar';
|
|||||||
import { ReadStateIndicator } from '@/renderer/components/layouts/read-state-indicator';
|
import { ReadStateIndicator } from '@/renderer/components/layouts/read-state-indicator';
|
||||||
import { useRadar } from '@/renderer/contexts/radar';
|
import { useRadar } from '@/renderer/contexts/radar';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
interface ChannelSidebarItemProps {
|
interface ChannelSidebarItemProps {
|
||||||
@@ -13,9 +14,10 @@ interface ChannelSidebarItemProps {
|
|||||||
|
|
||||||
export const ChannelSidebarItem = ({ channel }: ChannelSidebarItemProps) => {
|
export const ChannelSidebarItem = ({ channel }: ChannelSidebarItemProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const radar = useRadar();
|
const radar = useRadar();
|
||||||
|
|
||||||
const isActive = workspace.isEntryActive(channel.id);
|
const isActive = layout.activeTab === channel.id;
|
||||||
const channelState = radar.getChannelState(
|
const channelState = radar.getChannelState(
|
||||||
workspace.accountId,
|
workspace.accountId,
|
||||||
workspace.id,
|
workspace.id,
|
||||||
|
|||||||
@@ -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} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,35 +1,48 @@
|
|||||||
import { ChatEntry } from '@colanode/core';
|
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
|
||||||
interface ChatBreadcrumbItemProps {
|
interface ChatBreadcrumbItemProps {
|
||||||
chat: ChatEntry;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatBreadcrumbItem = ({ chat }: ChatBreadcrumbItemProps) => {
|
export const ChatBreadcrumbItem = ({ id }: ChatBreadcrumbItemProps) => {
|
||||||
const workspace = useWorkspace();
|
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',
|
type: 'user_get',
|
||||||
accountId: workspace.accountId,
|
accountId: workspace.accountId,
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPending || !data) {
|
if (!chat || !user) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Avatar size="small" id={data.id} name={data.name} avatar={data.avatar} />
|
<Avatar
|
||||||
<span>{data.name}</span>
|
id={user.id}
|
||||||
|
name={user.name}
|
||||||
|
avatar={user.avatar}
|
||||||
|
className="size-4"
|
||||||
|
/>
|
||||||
|
<span>{user.name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,43 +1,56 @@
|
|||||||
import { extractEntryRole } from '@colanode/core';
|
import { ChatEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { ChatBody } from '@/renderer/components/chats/chat-body';
|
import {
|
||||||
import { ChatHeader } from '@/renderer/components/chats/chat-header';
|
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 { ChatNotFound } from '@/renderer/components/chats/chat-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { EntryCollaboratorsPopover } from '@/renderer/components/collaborators/entry-collaborators-popover';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
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 {
|
interface ChatContainerProps {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatContainer = ({ chatId }: ChatContainerProps) => {
|
export const ChatContainer = ({ chatId }: ChatContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useEntryContainer<ChatEntry>(chatId);
|
||||||
|
|
||||||
const { data, isPending } = useQuery({
|
useEntryRadar(data.entry);
|
||||||
type: 'entry_get',
|
|
||||||
entryId: chatId,
|
|
||||||
accountId: workspace.accountId,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPending) {
|
if (data.isPending) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = data;
|
if (!data.entry) {
|
||||||
if (!node || node.type !== 'chat') {
|
|
||||||
return <ChatNotFound />;
|
return <ChatNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = extractEntryRole(node, workspace.userId);
|
const { entry, role } = data;
|
||||||
if (!role) {
|
|
||||||
return <ChatNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<ChatHeader chat={node} role={role} />
|
<ContainerHeader>
|
||||||
<ChatBody chat={node} role={role} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<EntryCollaboratorsPopover
|
||||||
|
entry={entry}
|
||||||
|
entries={[entry]}
|
||||||
|
role={role}
|
||||||
|
/>
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<Conversation
|
||||||
|
conversationId={entry.id}
|
||||||
|
rootId={entry.rootId}
|
||||||
|
role={role}
|
||||||
|
/>
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,17 +10,19 @@ import { UserSearch } from '@/renderer/components/users/user-search';
|
|||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
export const ChatCreatePopover = () => {
|
export const ChatCreatePopover = () => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
const layout = useLayout();
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<SquarePen className="mr-2 size-4 cursor-pointer" />
|
<SquarePen className="size-4 cursor-pointer" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-96 p-1">
|
<PopoverContent className="w-96 p-1">
|
||||||
<UserSearch
|
<UserSearch
|
||||||
@@ -35,7 +37,7 @@ export const ChatCreatePopover = () => {
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
onSuccess(output) {
|
onSuccess(output) {
|
||||||
workspace.openInMain(output.id);
|
layout.openLeft(output.id);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,6 +6,7 @@ import { ReadStateIndicator } from '@/renderer/components/layouts/read-state-ind
|
|||||||
import { useRadar } from '@/renderer/contexts/radar';
|
import { useRadar } from '@/renderer/contexts/radar';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
interface ChatSidebarItemProps {
|
interface ChatSidebarItemProps {
|
||||||
@@ -14,6 +15,7 @@ interface ChatSidebarItemProps {
|
|||||||
|
|
||||||
export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
|
export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const radar = useRadar();
|
const radar = useRadar();
|
||||||
|
|
||||||
const userId =
|
const userId =
|
||||||
@@ -37,7 +39,7 @@ export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
|
|||||||
workspace.id,
|
workspace.id,
|
||||||
chat.id
|
chat.id
|
||||||
);
|
);
|
||||||
const isActive = workspace.isEntryActive(chat.id);
|
const isActive = layout.activeTab === chat.id;
|
||||||
const unreadCount =
|
const unreadCount =
|
||||||
nodeReadState.unseenMessagesCount + nodeReadState.mentionsCount;
|
nodeReadState.unseenMessagesCount + nodeReadState.mentionsCount;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useDrag } from 'react-dnd';
|
|||||||
import { RecordFieldValue } from '@/renderer/components/records/record-field-value';
|
import { RecordFieldValue } from '@/renderer/components/records/record-field-value';
|
||||||
import { useRecord } from '@/renderer/contexts/record';
|
import { useRecord } from '@/renderer/contexts/record';
|
||||||
import { useView } from '@/renderer/contexts/view';
|
import { useView } from '@/renderer/contexts/view';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
interface DragResult {
|
interface DragResult {
|
||||||
option: SelectOptionAttributes;
|
option: SelectOptionAttributes;
|
||||||
@@ -13,7 +13,7 @@ interface DragResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BoardViewRecordCard = () => {
|
export const BoardViewRecordCard = () => {
|
||||||
const workspace = useWorkspace();
|
const layout = useLayout();
|
||||||
const view = useView();
|
const view = useView();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
|
||||||
@@ -52,7 +52,9 @@ export const BoardViewRecordCard = () => {
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
key={record.id}
|
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"
|
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'}>
|
<p className={hasName ? '' : 'text-muted-foreground'}>
|
||||||
{hasName ? name : 'Unnamed'}
|
{hasName ? name : 'Unnamed'}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const BoardView = () => {
|
|||||||
const selectOptions = Object.values(groupByField.options ?? {});
|
const selectOptions = Object.values(groupByField.options ?? {});
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="mt-2 flex flex-row justify-between border-b">
|
<div className="flex flex-row justify-between border-b">
|
||||||
<ViewTabs />
|
<ViewTabs />
|
||||||
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
||||||
<BoardViewSettings />
|
<BoardViewSettings />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { RecordFieldValue } from '@/renderer/components/records/record-field-value';
|
import { RecordFieldValue } from '@/renderer/components/records/record-field-value';
|
||||||
import { useRecord } from '@/renderer/contexts/record';
|
import { useRecord } from '@/renderer/contexts/record';
|
||||||
import { useView } from '@/renderer/contexts/view';
|
import { useView } from '@/renderer/contexts/view';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
export const CalendarViewRecordCard = () => {
|
export const CalendarViewRecordCard = () => {
|
||||||
const workspace = useWorkspace();
|
const layout = useLayout();
|
||||||
const view = useView();
|
const view = useView();
|
||||||
const record = useRecord();
|
const record = useRecord();
|
||||||
|
|
||||||
@@ -16,7 +16,9 @@ export const CalendarViewRecordCard = () => {
|
|||||||
role="presentation"
|
role="presentation"
|
||||||
key={record.id}
|
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"
|
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'}>
|
<p className={hasName ? '' : 'text-muted-foreground'}>
|
||||||
{name ?? 'Unnamed'}
|
{name ?? 'Unnamed'}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const CalendarView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="mt-2 flex flex-row justify-between border-b">
|
<div className="flex flex-row justify-between border-b">
|
||||||
<ViewTabs />
|
<ViewTabs />
|
||||||
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
||||||
<CalendarViewSettings />
|
<CalendarViewSettings />
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,21 +1,32 @@
|
|||||||
import { DatabaseEntry } from '@colanode/core';
|
import { DatabaseEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||||
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
|
||||||
interface DatabaseBreadcrumbItemProps {
|
interface DatabaseBreadcrumbItemProps {
|
||||||
database: DatabaseEntry;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DatabaseBreadcrumbItem = ({
|
export const DatabaseBreadcrumbItem = ({ id }: DatabaseBreadcrumbItemProps) => {
|
||||||
database,
|
const workspace = useWorkspace();
|
||||||
}: DatabaseBreadcrumbItemProps) => {
|
const { data } = useQuery({
|
||||||
|
type: 'entry_get',
|
||||||
|
entryId: id,
|
||||||
|
accountId: workspace.accountId,
|
||||||
|
workspaceId: workspace.id,
|
||||||
|
});
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const database = data as DatabaseEntry;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="small"
|
|
||||||
id={database.id}
|
id={database.id}
|
||||||
name={database.attributes.name}
|
name={database.attributes.name}
|
||||||
avatar={database.attributes.avatar}
|
avatar={database.attributes.avatar}
|
||||||
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
<span>{database.attributes.name}</span>
|
<span>{database.attributes.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { DatabaseNotFound } from '@/renderer/components/databases/database-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import {
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
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 {
|
interface DatabaseContainerProps {
|
||||||
databaseId: string;
|
databaseId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DatabaseContainer = ({ databaseId }: DatabaseContainerProps) => {
|
export const DatabaseContainer = ({ databaseId }: DatabaseContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useEntryContainer<DatabaseEntry>(databaseId);
|
||||||
|
|
||||||
const { data: entry, isPending: isPendingEntry } = useQuery({
|
useEntryRadar(data.entry);
|
||||||
type: 'entry_get',
|
|
||||||
entryId: databaseId,
|
|
||||||
accountId: workspace.accountId,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const database = entry as DatabaseEntry;
|
if (data.isPending) {
|
||||||
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)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!database || !root) {
|
if (!data.entry) {
|
||||||
return <DatabaseNotFound />;
|
return <DatabaseNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = extractEntryRole(root, workspace.userId);
|
const { entry: database, role } = data;
|
||||||
if (!role) {
|
|
||||||
return <DatabaseNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<DatabaseHeader database={database} role={role} />
|
<ContainerHeader>
|
||||||
<DatabaseBody database={database} role={role} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<DatabaseSettings database={database} role={role} />
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<Database database={database} role={role}>
|
||||||
|
<DatabaseViews />
|
||||||
|
</Database>
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
interface DatabaseCreateDialogProps {
|
interface DatabaseCreateDialogProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -24,6 +25,7 @@ export const DatabaseCreateDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DatabaseCreateDialogProps) => {
|
}: DatabaseCreateDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,7 +63,7 @@ export const DatabaseCreateDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess(output) {
|
onSuccess(output) {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.openInMain(output.id);
|
layout.openLeft(output.id);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@/renderer/components/ui/alert-dialog';
|
} from '@/renderer/components/ui/alert-dialog';
|
||||||
import { Button } from '@/renderer/components/ui/button';
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export const DatabaseDeleteDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DatabaseDeleteDialogProps) => {
|
}: DatabaseDeleteDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +55,7 @@ export const DatabaseDeleteDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.closeEntry(entryId);
|
layout.close(entryId);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -30,7 +30,7 @@ export const DatabaseSettings = ({ database, role }: DatabaseSettingsProps) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
||||||
<DropdownMenuLabel>{database.attributes.name}</DropdownMenuLabel>
|
<DropdownMenuLabel>{database.attributes.name}</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DatabaseEntry } from '@colanode/core';
|
import { DatabaseEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
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';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
interface DatabaseSidebarItemProps {
|
interface DatabaseSidebarItemProps {
|
||||||
@@ -9,8 +9,8 @@ interface DatabaseSidebarItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DatabaseSidebarItem = ({ database }: DatabaseSidebarItemProps) => {
|
export const DatabaseSidebarItem = ({ database }: DatabaseSidebarItemProps) => {
|
||||||
const workspace = useWorkspace();
|
const layout = useLayout();
|
||||||
const isActive = workspace.isEntryActive(database.id);
|
const isActive = layout.activeTab === database.id;
|
||||||
const isUnread = false;
|
const isUnread = false;
|
||||||
const mentionsCount = 0;
|
const mentionsCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const DatabaseViews = () => {
|
|||||||
>
|
>
|
||||||
<div className="h-full w-full overflow-y-auto">
|
<div className="h-full w-full overflow-y-auto">
|
||||||
<ScrollAreaPrimitive.Root className="relative overflow-hidden">
|
<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} />}
|
{activeView && <View view={activeView} />}
|
||||||
</ScrollAreaPrimitive.Viewport>
|
</ScrollAreaPrimitive.Viewport>
|
||||||
<ScrollBar orientation="horizontal" />
|
<ScrollBar orientation="horizontal" />
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { Fragment } from 'react';
|
|||||||
import { Spinner } from '@/renderer/components/ui/spinner';
|
import { Spinner } from '@/renderer/components/ui/spinner';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
|
||||||
interface NameEditorProps {
|
interface NameEditorProps {
|
||||||
@@ -58,6 +59,7 @@ interface TableViewNameCellProps {
|
|||||||
|
|
||||||
export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
|
export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const [isEditing, setIsEditing] = React.useState(false);
|
const [isEditing, setIsEditing] = React.useState(false);
|
||||||
|
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
@@ -111,7 +113,9 @@ export const TableViewNameCell = ({ record }: TableViewNameCellProps) => {
|
|||||||
<button
|
<button
|
||||||
type="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"
|
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>
|
<Maximize2 className="mr-1 size-4" /> <p>Open</p>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ViewTabs } from '@/renderer/components/databases/view-tabs';
|
|||||||
export const TableView = () => {
|
export const TableView = () => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className="mt-2 flex flex-row justify-between border-b">
|
<div className="flex flex-row justify-between border-b">
|
||||||
<ViewTabs />
|
<ViewTabs />
|
||||||
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
<div className="invisible flex flex-row items-center justify-end group-hover/database:visible">
|
||||||
<TableViewSettings />
|
<TableViewSettings />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { useDatabase } from '@/renderer/contexts/database';
|
|||||||
import { ViewContext } from '@/renderer/contexts/view';
|
import { ViewContext } from '@/renderer/contexts/view';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import {
|
import {
|
||||||
generateFieldValuesFromFilters,
|
generateFieldValuesFromFilters,
|
||||||
generateViewFieldIndex,
|
generateViewFieldIndex,
|
||||||
@@ -34,6 +35,7 @@ interface ViewProps {
|
|||||||
export const View = ({ view }: ViewProps) => {
|
export const View = ({ view }: ViewProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const database = useDatabase();
|
const database = useDatabase();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate } = useMutation();
|
const { mutate } = useMutation();
|
||||||
|
|
||||||
const fields: ViewField[] = React.useMemo(() => {
|
const fields: ViewField[] = React.useMemo(() => {
|
||||||
@@ -537,7 +539,7 @@ export const View = ({ view }: ViewProps) => {
|
|||||||
fields,
|
fields,
|
||||||
},
|
},
|
||||||
onSuccess: (output) => {
|
onSuccess: (output) => {
|
||||||
workspace.openInModal(output.id);
|
layout.previewLeft(output.id, true);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FilePreview } from '@/renderer/components/files/file-preview';
|
import { FilePreview } from '@/renderer/components/files/file-preview';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
|
||||||
interface FileBlockProps {
|
interface FileBlockProps {
|
||||||
@@ -8,6 +9,7 @@ interface FileBlockProps {
|
|||||||
|
|
||||||
export const FileBlock = ({ id }: FileBlockProps) => {
|
export const FileBlock = ({ id }: FileBlockProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
type: 'file_get',
|
type: 'file_get',
|
||||||
@@ -24,7 +26,7 @@ export const FileBlock = ({ id }: FileBlockProps) => {
|
|||||||
<div
|
<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"
|
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={() => {
|
onClick={() => {
|
||||||
workspace.openInModal(id);
|
layout.previewLeft(id, true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FilePreview file={data} />
|
<FilePreview file={data} />
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { SquareArrowOutUpRight } from 'lucide-react';
|
import { SquareArrowOutUpRight } from 'lucide-react';
|
||||||
// import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { FilePreview } from '@/renderer/components/files/file-preview';
|
import { FilePreview } from '@/renderer/components/files/file-preview';
|
||||||
import { FileSidebar } from '@/renderer/components/files/file-sidebar';
|
import { FileSidebar } from '@/renderer/components/files/file-sidebar';
|
||||||
import { Button } from '@/renderer/components/ui/button';
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
// import { useRadar } from '@/renderer/contexts/radar';
|
|
||||||
import { FileWithState } from '@/shared/types/files';
|
import { FileWithState } from '@/shared/types/files';
|
||||||
|
|
||||||
interface FileBodyProps {
|
interface FileBodyProps {
|
||||||
@@ -14,27 +12,6 @@ interface FileBodyProps {
|
|||||||
|
|
||||||
export const FileBody = ({ file }: FileBodyProps) => {
|
export const FileBody = ({ file }: FileBodyProps) => {
|
||||||
const workspace = useWorkspace();
|
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 (
|
return (
|
||||||
<div className="flex h-full max-h-full w-full flex-row items-center gap-2">
|
<div className="flex h-full max-h-full w-full flex-row items-center gap-2">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,72 +1,41 @@
|
|||||||
import { extractEntryRole } from '@colanode/core';
|
|
||||||
|
|
||||||
import { FileBody } from '@/renderer/components/files/file-body';
|
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 { FileNotFound } from '@/renderer/components/files/file-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useFileContainer } from '@/renderer/hooks/use-file-container';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { FileSettings } from '@/renderer/components/files/file-settings';
|
||||||
|
|
||||||
interface FileContainerProps {
|
interface FileContainerProps {
|
||||||
fileId: string;
|
fileId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileContainer = ({ fileId }: FileContainerProps) => {
|
export const FileContainer = ({ fileId }: FileContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useFileContainer(fileId);
|
||||||
|
|
||||||
const { data: file, isPending: isPendingFile } = useQuery({
|
if (data.isPending) {
|
||||||
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)
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file || !entry || !root) {
|
if (!data.file) {
|
||||||
return <FileNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const role = extractEntryRole(root, workspace.userId);
|
|
||||||
if (!role) {
|
|
||||||
return <FileNotFound />;
|
return <FileNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<FileHeader file={file} role={role} entry={entry} />
|
<ContainerHeader>
|
||||||
<FileBody file={file} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<FileSettings file={data.file} role={data.role} />
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<FileBody file={data.file} />
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ContextMenuShortcut,
|
ContextMenuShortcut,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from '@/renderer/components/ui/context-menu';
|
} from '@/renderer/components/ui/context-menu';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
interface FileContextMenuProps {
|
interface FileContextMenuProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,7 +17,7 @@ interface FileContextMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FileContextMenu = ({ id, children }: FileContextMenuProps) => {
|
export const FileContextMenu = ({ id, children }: FileContextMenuProps) => {
|
||||||
const workspace = useWorkspace();
|
const layout = useLayout();
|
||||||
const [openDelete, setOpenDelete] = React.useState(false);
|
const [openDelete, setOpenDelete] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,7 +27,7 @@ export const FileContextMenu = ({ id, children }: FileContextMenuProps) => {
|
|||||||
<ContextMenuContent className="w-64">
|
<ContextMenuContent className="w-64">
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
workspace.openInModal(id);
|
layout.previewLeft(id, true);
|
||||||
}}
|
}}
|
||||||
className="pl-2"
|
className="pl-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Copy, Settings, Trash2 } from 'lucide-react';
|
import { Copy, Settings, Trash2 } from 'lucide-react';
|
||||||
import React from '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 { FileDeleteDialog } from '@/renderer/components/files/file-delete-dialog';
|
||||||
import {
|
import {
|
||||||
@@ -15,14 +15,13 @@ import { useWorkspace } from '@/renderer/contexts/workspace';
|
|||||||
interface FileSettingsProps {
|
interface FileSettingsProps {
|
||||||
file: FileWithState;
|
file: FileWithState;
|
||||||
role: EntryRole;
|
role: EntryRole;
|
||||||
entry: Entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileSettings = ({ file, role, entry }: FileSettingsProps) => {
|
export const FileSettings = ({ file, role }: FileSettingsProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
|
||||||
const canDelete =
|
const canDelete =
|
||||||
file.parentId === entry.id &&
|
file.parentId === file.entryId &&
|
||||||
(file.createdBy === workspace.userId || hasEntryRole(role, 'editor'));
|
(file.createdBy === workspace.userId || hasEntryRole(role, 'editor'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
List,
|
List,
|
||||||
Upload,
|
Upload,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import React, { useEffect } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { FolderFiles } from '@/renderer/components/folders/folder-files';
|
import { FolderFiles } from '@/renderer/components/folders/folder-files';
|
||||||
import { Button } from '@/renderer/components/ui/button';
|
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 { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { FolderLayoutType } from '@/shared/types/folders';
|
import { FolderLayoutType } from '@/shared/types/folders';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
import { useRadar } from '@/renderer/contexts/radar';
|
|
||||||
|
|
||||||
export type FolderLayout = {
|
export type FolderLayoutOption = {
|
||||||
value: FolderLayoutType;
|
value: FolderLayoutType;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const folderLayouts: FolderLayout[] = [
|
export const folderLayouts: FolderLayoutOption[] = [
|
||||||
{
|
{
|
||||||
name: 'Grid',
|
name: 'Grid',
|
||||||
value: 'grid',
|
value: 'grid',
|
||||||
@@ -62,7 +61,6 @@ interface FolderBodyProps {
|
|||||||
|
|
||||||
export const FolderBody = ({ folder }: FolderBodyProps) => {
|
export const FolderBody = ({ folder }: FolderBodyProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const radar = useRadar();
|
|
||||||
const { mutate } = useMutation();
|
const { mutate } = useMutation();
|
||||||
|
|
||||||
const [layout, setLayout] = React.useState<FolderLayoutType>('grid');
|
const [layout, setLayout] = React.useState<FolderLayoutType>('grid');
|
||||||
@@ -120,16 +118,6 @@ export const FolderBody = ({ folder }: FolderBodyProps) => {
|
|||||||
isDialogOpenedRef.current = false;
|
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 (
|
return (
|
||||||
<Dropzone
|
<Dropzone
|
||||||
text="Drop files here to upload them in the folder"
|
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));
|
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 justify-between">
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<Button type="button" variant="outline" onClick={openFileDialog}>
|
<Button type="button" variant="outline" onClick={openFileDialog}>
|
||||||
@@ -181,8 +169,8 @@ export const FolderBody = ({ folder }: FolderBodyProps) => {
|
|||||||
{/* <FolderUploads
|
{/* <FolderUploads
|
||||||
uploads={Object.values(uploads)}
|
uploads={Object.values(uploads)}
|
||||||
open={openUploads}
|
open={openUploads}
|
||||||
setOpen={setOpenUploads}
|
setOpen={setOpenUploads}
|
||||||
/> */}
|
/> */}
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import { FolderEntry } from '@colanode/core';
|
import { FolderEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||||
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
|
||||||
interface FolderBreadcrumbItemProps {
|
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 (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="small"
|
|
||||||
id={folder.id}
|
id={folder.id}
|
||||||
name={folder.attributes.name}
|
name={folder.attributes.name}
|
||||||
avatar={folder.attributes.avatar}
|
avatar={folder.attributes.avatar}
|
||||||
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
<span>{folder.attributes.name}</span>
|
<span>{folder.attributes.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { FolderNotFound } from '@/renderer/components/folders/folder-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import {
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
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 {
|
interface FolderContainerProps {
|
||||||
folderId: string;
|
folderId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FolderContainer = ({ folderId }: FolderContainerProps) => {
|
export const FolderContainer = ({ folderId }: FolderContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useEntryContainer<FolderEntry>(folderId);
|
||||||
|
|
||||||
const { data: entry, isPending: isPendingEntry } = useQuery({
|
useEntryRadar(data.entry);
|
||||||
type: 'entry_get',
|
|
||||||
entryId: folderId,
|
|
||||||
accountId: workspace.accountId,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const folder = entry as FolderEntry;
|
if (data.isPending) {
|
||||||
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)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!folder || !root) {
|
if (!data.entry) {
|
||||||
return <FolderNotFound />;
|
return <FolderNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = extractEntryRole(root, workspace.userId);
|
const { entry: folder, role } = data;
|
||||||
if (!role) {
|
|
||||||
return <FolderNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<FolderHeader folder={folder} role={role} />
|
<ContainerHeader>
|
||||||
<FolderBody folder={folder} role={role} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<FolderSettings folder={folder} role={role} />
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<FolderBody folder={folder} role={role} />
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
interface FolderCreateDialogProps {
|
interface FolderCreateDialogProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -24,6 +25,7 @@ export const FolderCreateDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: FolderCreateDialogProps) => {
|
}: FolderCreateDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,7 +64,7 @@ export const FolderCreateDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess(output) {
|
onSuccess(output) {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.openInMain(output.id);
|
layout.previewLeft(output.id);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Button } from '@/renderer/components/ui/button';
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
|
||||||
interface FolderDeleteDialogProps {
|
interface FolderDeleteDialogProps {
|
||||||
@@ -24,6 +25,7 @@ export const FolderDeleteDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: FolderDeleteDialogProps) => {
|
}: FolderDeleteDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +55,7 @@ export const FolderDeleteDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.closeEntry(entryId);
|
layout.close(entryId);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getIdType, IdType } from '@colanode/core';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@@ -10,6 +9,7 @@ import { useWorkspace } from '@/renderer/contexts/workspace';
|
|||||||
import { useQueries } from '@/renderer/hooks/use-queries';
|
import { useQueries } from '@/renderer/hooks/use-queries';
|
||||||
import { FileListQueryInput } from '@/shared/queries/files/file-list';
|
import { FileListQueryInput } from '@/shared/queries/files/file-list';
|
||||||
import { FolderLayoutType } from '@/shared/types/folders';
|
import { FolderLayoutType } from '@/shared/types/folders';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
const FILES_PER_PAGE = 100;
|
const FILES_PER_PAGE = 100;
|
||||||
|
|
||||||
@@ -19,8 +19,14 @@ interface FolderFilesProps {
|
|||||||
layout: FolderLayoutType;
|
layout: FolderLayoutType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FolderFiles = ({ id, name, layout }: FolderFilesProps) => {
|
export const FolderFiles = ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
layout: folderLayout,
|
||||||
|
}: FolderFilesProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
|
|
||||||
const [lastPage] = React.useState<number>(1);
|
const [lastPage] = React.useState<number>(1);
|
||||||
const inputs: FileListQueryInput[] = Array.from({
|
const inputs: FileListQueryInput[] = Array.from({
|
||||||
length: lastPage,
|
length: lastPage,
|
||||||
@@ -46,18 +52,12 @@ export const FolderFiles = ({ id, name, layout }: FolderFilesProps) => {
|
|||||||
console.log('onClick');
|
console.log('onClick');
|
||||||
},
|
},
|
||||||
onDoubleClick: (_, id) => {
|
onDoubleClick: (_, id) => {
|
||||||
const idType = getIdType(id);
|
layout.previewLeft(id, true);
|
||||||
|
|
||||||
if (idType === IdType.Folder) {
|
|
||||||
workspace.openInMain(id);
|
|
||||||
} else if (idType === IdType.File) {
|
|
||||||
workspace.openInModal(id);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onMove: () => {},
|
onMove: () => {},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{match(layout)
|
{match(folderLayout)
|
||||||
.with('grid', () => <GridLayout />)
|
.with('grid', () => <GridLayout />)
|
||||||
.with('list', () => <ListLayout />)
|
.with('list', () => <ListLayout />)
|
||||||
.with('gallery', () => <GalleryLayout />)
|
.with('gallery', () => <GalleryLayout />)
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -30,7 +30,7 @@ export const FolderSettings = ({ folder, role }: FolderSettingsProps) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
||||||
<DropdownMenuLabel>{folder.attributes.name}</DropdownMenuLabel>
|
<DropdownMenuLabel>{folder.attributes.name}</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FolderEntry } from '@colanode/core';
|
import { FolderEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
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';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
interface FolderSidebarItemProps {
|
interface FolderSidebarItemProps {
|
||||||
@@ -9,8 +9,8 @@ interface FolderSidebarItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FolderSidebarItem = ({ folder }: FolderSidebarItemProps) => {
|
export const FolderSidebarItem = ({ folder }: FolderSidebarItemProps) => {
|
||||||
const workspace = useWorkspace();
|
const layout = useLayout();
|
||||||
const isActive = workspace.isEntryActive(folder.id);
|
const isActive = layout.activeTab === folder.id;
|
||||||
const isUnread = false;
|
const isUnread = false;
|
||||||
const mentionsCount = 0;
|
const mentionsCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
123
apps/desktop/src/renderer/components/layouts/layout-tabs.tsx
Normal file
123
apps/desktop/src/renderer/components/layouts/layout-tabs.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,20 +1,149 @@
|
|||||||
import { LayoutMain } from '@/renderer/components/layouts/layout-main';
|
import { Resizable } from 're-resizable';
|
||||||
import { LayoutModal } from '@/renderer/components/layouts/layout-modal';
|
|
||||||
|
import { LayoutTabs } from '@/renderer/components/layouts/layout-tabs';
|
||||||
import { Sidebar } from '@/renderer/components/layouts/sidebars/sidebar';
|
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 {
|
export const Layout = () => {
|
||||||
main?: string | null;
|
const windowSize = useWindowSize();
|
||||||
modal?: string | null;
|
|
||||||
}
|
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 (
|
return (
|
||||||
<div className="w-screen min-w-screen h-screen min-h-screen flex flex-row">
|
<LayoutContext.Provider
|
||||||
<Sidebar />
|
value={{
|
||||||
<main className="h-full max-h-screen w-full min-w-128 flex-grow overflow-hidden bg-white">
|
open: handleOpen,
|
||||||
{main && <LayoutMain entryId={main} />}
|
openLeft: handleOpenLeft,
|
||||||
</main>
|
openRight: handleOpenRight,
|
||||||
{modal && <LayoutModal entryId={modal} />}
|
close: handleClose,
|
||||||
</div>
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
import { Header } from '@/renderer/components/ui/header';
|
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { ChatSidebarItem } from '@/renderer/components/chats/chat-sidebar-item';
|
import { ChatSidebarItem } from '@/renderer/components/chats/chat-sidebar-item';
|
||||||
import { ChatCreatePopover } from '@/renderer/components/chats/chat-create-popover';
|
import { ChatCreatePopover } from '@/renderer/components/chats/chat-create-popover';
|
||||||
import { cn } from '@/shared/lib/utils';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
export const SidebarChats = () => {
|
export const SidebarChats = () => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
type: 'chat_list',
|
type: 'chat_list',
|
||||||
@@ -19,24 +20,27 @@ export const SidebarChats = () => {
|
|||||||
const chats = data ?? [];
|
const chats = data ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col group/sidebar-spaces h-full px-2">
|
<div className="flex flex-col group/sidebar-chats h-full px-2">
|
||||||
<Header>
|
<div className="flex items-center justify-between h-12 pl-2 pr-1">
|
||||||
<p className="font-medium text-muted-foreground flex-grow">Chats</p>
|
<p className="font-bold 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="text-muted-foreground opacity-0 transition-opacity group-hover/sidebar-chats:opacity-100 flex items-center justify-center">
|
||||||
<ChatCreatePopover />
|
<ChatCreatePopover />
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</div>
|
||||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||||
{chats.map((item) => (
|
{chats.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={cn(
|
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',
|
'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'
|
'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
workspace.openInMain(item.id);
|
layout.preview(item.id);
|
||||||
|
}}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
layout.open(item.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatSidebarItem chat={item} />
|
<ChatSidebarItem chat={item} />
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ import { FolderSidebarItem } from '@/renderer/components/folders/folder-sidebar-
|
|||||||
import { PageSidebarItem } from '@/renderer/components/pages/page-sidebar-item';
|
import { PageSidebarItem } from '@/renderer/components/pages/page-sidebar-item';
|
||||||
import { SpaceSidebarItem } from '@/renderer/components/spaces/space-sidebar-item';
|
import { SpaceSidebarItem } from '@/renderer/components/spaces/space-sidebar-item';
|
||||||
|
|
||||||
interface EntrySidebarItemProps {
|
interface SidebarItemProps {
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EntrySidebarItem = ({
|
export const SidebarItem = ({ entry }: SidebarItemProps): React.ReactNode => {
|
||||||
entry,
|
|
||||||
}: EntrySidebarItemProps): React.ReactNode => {
|
|
||||||
switch (entry.type) {
|
switch (entry.type) {
|
||||||
case 'space':
|
case 'space':
|
||||||
return <SpaceSidebarItem space={entry} />;
|
return <SpaceSidebarItem space={entry} />;
|
||||||
@@ -3,10 +3,11 @@ import { LayoutGrid, MessageCircle } from 'lucide-react';
|
|||||||
import { SidebarMenuIcon } from '@/renderer/components/layouts/sidebars/sidebar-menu-icon';
|
import { SidebarMenuIcon } from '@/renderer/components/layouts/sidebars/sidebar-menu-icon';
|
||||||
import { SidebarMenuHeader } from '@/renderer/components/layouts/sidebars/sidebar-menu-header';
|
import { SidebarMenuHeader } from '@/renderer/components/layouts/sidebars/sidebar-menu-header';
|
||||||
import { SidebarMenuFooter } from '@/renderer/components/layouts/sidebars/sidebar-menu-footer';
|
import { SidebarMenuFooter } from '@/renderer/components/layouts/sidebars/sidebar-menu-footer';
|
||||||
|
import { SidebarMenuType } from '@/shared/types/workspaces';
|
||||||
|
|
||||||
interface SidebarMenuProps {
|
interface SidebarMenuProps {
|
||||||
value: string;
|
value: SidebarMenuType;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: SidebarMenuType) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarMenu = ({ value, onChange }: SidebarMenuProps) => {
|
export const SidebarMenu = ({ value, onChange }: SidebarMenuProps) => {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
import { Header } from '@/renderer/components/ui/header';
|
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { SpaceCreateButton } from '@/renderer/components/spaces/space-create-button';
|
import { SpaceCreateButton } from '@/renderer/components/spaces/space-create-button';
|
||||||
import { SpaceSidebarItem } from '@/renderer/components/spaces/space-sidebar-item';
|
import { SpaceSidebarItem } from '@/renderer/components/spaces/space-sidebar-item';
|
||||||
@@ -22,14 +21,14 @@ export const SidebarSpaces = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col group/sidebar-spaces h-full px-2">
|
<div className="flex flex-col group/sidebar-spaces h-full px-2">
|
||||||
<Header>
|
<div className="flex items-center justify-between h-12 pl-2 pr-1">
|
||||||
<p className="font-medium text-muted-foreground flex-grow">Spaces</p>
|
<p className="font-bold text-muted-foreground flex-grow">Spaces</p>
|
||||||
{canCreateSpace && (
|
{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 />
|
<SpaceCreateButton />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Header>
|
</div>
|
||||||
<div className="flex w-full min-w-0 flex-col gap-1">
|
<div className="flex w-full min-w-0 flex-col gap-1">
|
||||||
{spaces.map((space) => (
|
{spaces.map((space) => (
|
||||||
<SpaceSidebarItem space={space} key={space.id} />
|
<SpaceSidebarItem space={space} key={space.id} />
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { SidebarMenu } from '@/renderer/components/layouts/sidebars/sidebar-menu';
|
import { SidebarMenu } from '@/renderer/components/layouts/sidebars/sidebar-menu';
|
||||||
import { SidebarChats } from '@/renderer/components/layouts/sidebars/sidebar-chats';
|
import { SidebarChats } from '@/renderer/components/layouts/sidebars/sidebar-chats';
|
||||||
import { SidebarSpaces } from '@/renderer/components/layouts/sidebars/sidebar-spaces';
|
import { SidebarSpaces } from '@/renderer/components/layouts/sidebars/sidebar-spaces';
|
||||||
|
import { SidebarMenuType } from '@/shared/types/workspaces';
|
||||||
|
|
||||||
export const Sidebar = () => {
|
interface SidebarProps {
|
||||||
const [menu, setMenu] = React.useState('spaces');
|
menu: SidebarMenuType;
|
||||||
|
onMenuChange: (menu: SidebarMenuType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar = ({ menu, onMenuChange }: SidebarProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen min-h-screen max-h-screen w-80 min-w-80 flex-row bg-slate-50">
|
<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={setMenu} />
|
<SidebarMenu value={menu} onChange={onMenuChange} />
|
||||||
<div className="min-h-0 flex-grow overflow-auto">
|
<div className="min-h-0 flex-grow overflow-auto">
|
||||||
{menu === 'spaces' && <SidebarSpaces />}
|
{menu === 'spaces' && <SidebarSpaces />}
|
||||||
{menu === 'chats' && <SidebarChats />}
|
{menu === 'chats' && <SidebarChats />}
|
||||||
|
|||||||
@@ -97,25 +97,27 @@ export const Conversation = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollArea
|
<div className="h-full min-h-full w-full min-w-full flex flex-col">
|
||||||
ref={viewportRef}
|
<ScrollArea
|
||||||
onScroll={handleScroll}
|
ref={viewportRef}
|
||||||
className="flex-grow overflow-y-auto px-10"
|
onScroll={handleScroll}
|
||||||
>
|
className="flex-grow overflow-y-auto"
|
||||||
<div className="container" ref={containerRef}>
|
|
||||||
<MessageList />
|
|
||||||
</div>
|
|
||||||
<InView
|
|
||||||
className="h-4"
|
|
||||||
rootMargin="20px"
|
|
||||||
onChange={(inView) => {
|
|
||||||
bottomVisibleRef.current = inView;
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div ref={bottomRef} className="h-4"></div>
|
<div ref={containerRef}>
|
||||||
</InView>
|
<MessageList />
|
||||||
</ScrollArea>
|
</div>
|
||||||
<MessageCreate ref={messageCreateRef} />
|
<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>
|
</ConversationContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -139,7 +139,7 @@ export const MessageCreate = React.forwardRef<MessageCreateRefProps>(
|
|||||||
}, [messageEditorRef]);
|
}, [messageEditorRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mt-1 px-10">
|
<div className="mt-1">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{conversation.canCreateMessage && replyTo && (
|
{conversation.canCreateMessage && replyTo && (
|
||||||
<MessageReplyBanner
|
<MessageReplyBanner
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { EntryRole, PageEntry, hasEntryRole } from '@colanode/core';
|
import { EntryRole, PageEntry, hasEntryRole } from '@colanode/core';
|
||||||
import { JSONContent } from '@tiptap/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 { ScrollArea } from '@/renderer/components/ui/scroll-area';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
import { useRadar } from '@/renderer/contexts/radar';
|
|
||||||
|
|
||||||
interface PageBodyProps {
|
interface PageBodyProps {
|
||||||
page: PageEntry;
|
page: PageEntry;
|
||||||
@@ -15,7 +14,6 @@ interface PageBodyProps {
|
|||||||
|
|
||||||
export const PageBody = ({ page, role }: PageBodyProps) => {
|
export const PageBody = ({ page, role }: PageBodyProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const radar = useRadar();
|
|
||||||
const canEdit = hasEntryRole(role, 'editor');
|
const canEdit = hasEntryRole(role, 'editor');
|
||||||
|
|
||||||
const handleUpdate = useCallback(
|
const handleUpdate = useCallback(
|
||||||
@@ -40,18 +38,8 @@ export const PageBody = ({ page, role }: PageBodyProps) => {
|
|||||||
[workspace.accountId, workspace.id, page.id]
|
[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 (
|
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
|
<Document
|
||||||
entryId={page.id}
|
entryId={page.id}
|
||||||
rootId={page.rootId}
|
rootId={page.rootId}
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
import { PageEntry } from '@colanode/core';
|
import { PageEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||||
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
|
||||||
interface PageBreadcrumbItemProps {
|
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 (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="small"
|
|
||||||
id={page.id}
|
id={page.id}
|
||||||
name={page.attributes.name}
|
name={page.attributes.name}
|
||||||
avatar={page.attributes.avatar}
|
avatar={page.attributes.avatar}
|
||||||
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
<span>{page.attributes.name}</span>
|
<span>{page.attributes.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { PageNotFound } from '@/renderer/components/pages/page-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
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 {
|
interface PageContainerProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageContainer = ({ pageId }: PageContainerProps) => {
|
export const PageContainer = ({ pageId }: PageContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useEntryContainer<PageEntry>(pageId);
|
||||||
|
|
||||||
const { data: entry, isPending: isPendingEntry } = useQuery({
|
useEntryRadar(data.entry);
|
||||||
type: 'entry_get',
|
|
||||||
entryId: pageId,
|
|
||||||
accountId: workspace.accountId,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const page = entry as PageEntry;
|
if (data.isPending) {
|
||||||
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)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!page || !root) {
|
if (!data.entry) {
|
||||||
return <PageNotFound />;
|
return <PageNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = extractEntryRole(root, workspace.userId);
|
const { entry: page, role } = data;
|
||||||
if (!role) {
|
|
||||||
return <PageNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<PageHeader page={page} role={role} />
|
<ContainerHeader>
|
||||||
<PageBody page={page} role={role} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<PageSettings page={page} role={role} />
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<PageBody page={page} role={role} />
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
|
|
||||||
interface PageCreateDialogProps {
|
interface PageCreateDialogProps {
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
@@ -24,6 +25,7 @@ export const PageCreateDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: PageCreateDialogProps) => {
|
}: PageCreateDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,7 +64,7 @@ export const PageCreateDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess(output) {
|
onSuccess(output) {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.openInMain(output.id);
|
layout.openLeft(output.id);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Button } from '@/renderer/components/ui/button';
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
|
||||||
interface PageDeleteDialogProps {
|
interface PageDeleteDialogProps {
|
||||||
@@ -24,6 +25,7 @@ export const PageDeleteDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: PageDeleteDialogProps) => {
|
}: PageDeleteDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +55,7 @@ export const PageDeleteDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.closeEntry(pageId);
|
layout.close(pageId);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -30,7 +30,7 @@ export const PageSettings = ({ page, role }: PageSettingsProps) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<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>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
<DropdownMenuContent side="bottom" className="mr-2 w-80">
|
||||||
<DropdownMenuLabel>{page.attributes.name}</DropdownMenuLabel>
|
<DropdownMenuLabel>{page.attributes.name}</DropdownMenuLabel>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PageEntry } from '@colanode/core';
|
import { PageEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
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';
|
import { cn } from '@/shared/lib/utils';
|
||||||
|
|
||||||
interface PageSidebarItemProps {
|
interface PageSidebarItemProps {
|
||||||
@@ -9,8 +9,8 @@ interface PageSidebarItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PageSidebarItem = ({ page }: PageSidebarItemProps) => {
|
export const PageSidebarItem = ({ page }: PageSidebarItemProps) => {
|
||||||
const workspace = useWorkspace();
|
const layout = useLayout();
|
||||||
const isActive = workspace.isEntryActive(page.id);
|
const isActive = layout.activeTab === page.id;
|
||||||
const isUnread = false;
|
const isUnread = false;
|
||||||
const mentionsCount = 0;
|
const mentionsCount = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import {
|
import { EntryRole, hasEntryRole, RecordEntry } from '@colanode/core';
|
||||||
DatabaseEntry,
|
|
||||||
EntryRole,
|
|
||||||
hasEntryRole,
|
|
||||||
RecordEntry,
|
|
||||||
} from '@colanode/core';
|
|
||||||
import { JSONContent } from '@tiptap/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 { Document } from '@/renderer/components/documents/document';
|
||||||
import { RecordAttributes } from '@/renderer/components/records/record-attributes';
|
import { RecordAttributes } from '@/renderer/components/records/record-attributes';
|
||||||
import { RecordProvider } from '@/renderer/components/records/record-provider';
|
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 { Separator } from '@/renderer/components/ui/separator';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
import { useRadar } from '@/renderer/contexts/radar';
|
import { RecordDatabase } from '@/renderer/components/records/record-database';
|
||||||
|
|
||||||
interface RecordBodyProps {
|
interface RecordBodyProps {
|
||||||
record: RecordEntry;
|
record: RecordEntry;
|
||||||
database: DatabaseEntry;
|
|
||||||
role: EntryRole;
|
role: EntryRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RecordBody = ({ record, database, role }: RecordBodyProps) => {
|
export const RecordBody = ({ record, role }: RecordBodyProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const radar = useRadar();
|
|
||||||
|
|
||||||
const canEdit =
|
const canEdit =
|
||||||
record.createdBy === workspace.userId || hasEntryRole(role, 'editor');
|
record.createdBy === workspace.userId || hasEntryRole(role, 'editor');
|
||||||
@@ -52,19 +44,9 @@ export const RecordBody = ({ record, database, role }: RecordBodyProps) => {
|
|||||||
[workspace.accountId, workspace.id, record.id]
|
[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 (
|
return (
|
||||||
<Database database={database} role={role}>
|
<RecordDatabase id={record.attributes.databaseId} role={role}>
|
||||||
<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">
|
||||||
<RecordProvider record={record} role={role}>
|
<RecordProvider record={record} role={role}>
|
||||||
<RecordAttributes />
|
<RecordAttributes />
|
||||||
</RecordProvider>
|
</RecordProvider>
|
||||||
@@ -79,6 +61,6 @@ export const RecordBody = ({ record, database, role }: RecordBodyProps) => {
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
/>
|
/>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Database>
|
</RecordDatabase>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import { RecordEntry } from '@colanode/core';
|
import { RecordEntry } from '@colanode/core';
|
||||||
|
|
||||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||||
|
import { useQuery } from '@/renderer/hooks/use-query';
|
||||||
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
|
|
||||||
interface RecordBreadcrumbItemProps {
|
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 (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="small"
|
|
||||||
id={record.id}
|
id={record.id}
|
||||||
name={record.attributes.name}
|
name={record.attributes.name}
|
||||||
avatar={record.attributes.avatar}
|
avatar={record.attributes.avatar}
|
||||||
|
className="size-4"
|
||||||
/>
|
/>
|
||||||
<span>{record.attributes.name}</span>
|
<span>{record.attributes.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { RecordNotFound } from '@/renderer/components/records/record-not-found';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useEntryContainer } from '@/renderer/hooks/use-entry-container';
|
||||||
import { useQuery } from '@/renderer/hooks/use-query';
|
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 {
|
interface RecordContainerProps {
|
||||||
recordId: string;
|
recordId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RecordContainer = ({ recordId }: RecordContainerProps) => {
|
export const RecordContainer = ({ recordId }: RecordContainerProps) => {
|
||||||
const workspace = useWorkspace();
|
const data = useEntryContainer<RecordEntry>(recordId);
|
||||||
|
|
||||||
const { data: entry, isPending: isPendingEntry } = useQuery({
|
useEntryRadar(data.entry);
|
||||||
type: 'entry_get',
|
|
||||||
entryId: recordId,
|
|
||||||
accountId: workspace.accountId,
|
|
||||||
workspaceId: workspace.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const record = entry as RecordEntry;
|
if (data.isPending) {
|
||||||
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) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!record || !root || !database) {
|
if (!data.entry) {
|
||||||
return <RecordNotFound />;
|
return <RecordNotFound />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = extractEntryRole(root, workspace.userId);
|
const { entry: record, role } = data;
|
||||||
if (!role) {
|
|
||||||
return <RecordNotFound />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col">
|
<Container>
|
||||||
<RecordHeader record={record} role={role} />
|
<ContainerHeader>
|
||||||
<RecordBody record={record} database={database} role={role} />
|
<ContainerBreadcrumb breadcrumb={data.breadcrumb} />
|
||||||
</div>
|
<ContainerSettings>
|
||||||
|
<RecordSettings record={record} role={role} />
|
||||||
|
</ContainerSettings>
|
||||||
|
</ContainerHeader>
|
||||||
|
<ContainerBody>
|
||||||
|
<RecordBody record={record} role={role} />
|
||||||
|
</ContainerBody>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Button } from '@/renderer/components/ui/button';
|
import { Button } from '@/renderer/components/ui/button';
|
||||||
import { useWorkspace } from '@/renderer/contexts/workspace';
|
import { useWorkspace } from '@/renderer/contexts/workspace';
|
||||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||||
|
import { useLayout } from '@/renderer/contexts/layout';
|
||||||
import { toast } from '@/renderer/hooks/use-toast';
|
import { toast } from '@/renderer/hooks/use-toast';
|
||||||
|
|
||||||
interface RecordDeleteDialogProps {
|
interface RecordDeleteDialogProps {
|
||||||
@@ -24,6 +25,7 @@ export const RecordDeleteDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: RecordDeleteDialogProps) => {
|
}: RecordDeleteDialogProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
|
const layout = useLayout();
|
||||||
const { mutate, isPending } = useMutation();
|
const { mutate, isPending } = useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +55,7 @@ export const RecordDeleteDialog = ({
|
|||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
workspace.closeEntry(entryId);
|
layout.close(entryId);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user