Workspace storage limits (#140)

This commit is contained in:
Hakan Shehu
2025-07-21 09:22:15 +02:00
committed by GitHub
parent 4813d715ae
commit e1e503e7b2
131 changed files with 1943 additions and 625 deletions

View File

@@ -47,6 +47,19 @@ export const fileUploadRoute: FastifyPluginCallbackZod = (
const { workspaceId, fileId } = request.params;
const user = request.user;
const workspace = await database
.selectFrom('workspaces')
.selectAll()
.where('id', '=', workspaceId)
.executeTakeFirst();
if (!workspace) {
return reply.code(404).send({
code: ApiErrorCode.WorkspaceNotFound,
message: 'Workspace not found.',
});
}
const node = await database
.selectFrom('nodes')
.selectAll()
@@ -88,18 +101,49 @@ export const fileUploadRoute: FastifyPluginCallbackZod = (
});
}
const storageUsed = await fetchCounter(
if (file.attributes.size > BigInt(user.max_file_size)) {
return reply.code(400).send({
code: ApiErrorCode.FileUploadFailed,
message:
'The file size exceeds the maximum allowed size for your account.',
});
}
if (workspace.max_file_size) {
if (file.attributes.size > BigInt(workspace.max_file_size)) {
return reply.code(400).send({
code: ApiErrorCode.FileUploadFailed,
message: 'The file size exceeds the maximum allowed size.',
});
}
}
const userStorageUsed = await fetchCounter(
database,
`${user.id}.storage.used`
);
if (storageUsed >= BigInt(user.storage_limit)) {
if (userStorageUsed >= BigInt(user.storage_limit)) {
return reply.code(400).send({
code: ApiErrorCode.FileUploadInitFailed,
code: ApiErrorCode.FileUploadFailed,
message: 'You have reached the maximum storage limit.',
});
}
if (workspace.storage_limit) {
const workspaceStorageUsed = await fetchCounter(
database,
`${workspaceId}.storage.used`
);
if (workspaceStorageUsed >= BigInt(workspace.storage_limit)) {
return reply.code(400).send({
code: ApiErrorCode.FileUploadFailed,
message: 'The workspace has reached the maximum storage limit.',
});
}
}
const path = buildFilePath(workspaceId, fileId, file.attributes);
const stream = request.raw;

View File

@@ -5,6 +5,7 @@ import { workspaceAuthenticator } from '@colanode/server/api/client/plugins/work
import { fileRoutes } from './files';
import { mutationsRoutes } from './mutations';
import { storageRoutes } from './storage';
import { userRoutes } from './users';
import { workspaceCreateRoute } from './workspace-create';
import { workspaceDeleteRoute } from './workspace-delete';
@@ -27,6 +28,7 @@ export const workspaceRoutes: FastifyPluginCallback = (instance, _, done) => {
subInstance.register(fileRoutes, { prefix: '/files' });
subInstance.register(userRoutes, { prefix: '/users' });
subInstance.register(mutationsRoutes, { prefix: '/mutations' });
subInstance.register(storageRoutes, { prefix: '/storage' });
},
{
prefix: '/:workspaceId',

View File

@@ -0,0 +1,9 @@
import { FastifyPluginCallback } from 'fastify';
import { workspaceStorageGetRoute } from './workspace-storage-get';
export const storageRoutes: FastifyPluginCallback = (instance, _, done) => {
instance.register(workspaceStorageGetRoute);
done();
};

View File

@@ -0,0 +1,136 @@
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import { sql } from 'kysely';
import { z } from 'zod/v4';
import {
ApiErrorCode,
apiErrorOutputSchema,
compareString,
extractFileSubtype,
FileSubtype,
workspaceStorageGetOutputSchema,
} from '@colanode/core';
import { database } from '@colanode/server/data/database';
interface WorkspaceStorageAggregateRow {
mime_type: string;
total_size: string;
}
interface UserStorageRow {
id: string;
storage_limit: string;
storage_used: string | null;
}
export const workspaceStorageGetRoute: FastifyPluginCallbackZod = (
instance,
_,
done
) => {
instance.route({
method: 'GET',
url: '/',
schema: {
params: z.object({
workspaceId: z.string(),
}),
response: {
200: workspaceStorageGetOutputSchema,
400: apiErrorOutputSchema,
403: apiErrorOutputSchema,
404: apiErrorOutputSchema,
},
},
handler: async (request, reply) => {
const workspaceId = request.params.workspaceId;
const user = request.user;
if (user.role !== 'owner' && user.role !== 'admin') {
return reply.code(403).send({
code: ApiErrorCode.UserInviteNoAccess,
message: 'You do not have access to get workspace storage.',
});
}
const workspace = await database
.selectFrom('workspaces')
.selectAll()
.where('id', '=', workspaceId)
.executeTakeFirstOrThrow();
if (!workspace) {
return reply.code(404).send({
code: ApiErrorCode.WorkspaceNotFound,
message: 'Workspace not found.',
});
}
const [subtypeAggregates, usersWithStorage] = await Promise.all([
sql<WorkspaceStorageAggregateRow>`
SELECT
mime_type,
SUM(size) as total_size
FROM uploads
WHERE workspace_id = ${workspaceId}
GROUP BY mime_type
`.execute(database),
sql<UserStorageRow>`
SELECT
u.id,
u.storage_limit,
COALESCE(c.value, '0') as storage_used
FROM users u
LEFT JOIN counters c ON c.key = CONCAT(u.id, '.storage.used')
WHERE u.workspace_id = ${workspaceId}
`.execute(database),
]);
const subtypeGroups: Record<string, bigint> = {};
let totalUsed = 0n;
for (const row of subtypeAggregates.rows) {
const subtype = extractFileSubtype(row.mime_type);
const currentSize = subtypeGroups[subtype] || 0n;
subtypeGroups[subtype] = currentSize + BigInt(row.total_size);
totalUsed += BigInt(row.total_size);
}
const subtypes = Object.entries(subtypeGroups)
.sort((a, b) => {
const aSize = BigInt(a[1]);
const bSize = BigInt(b[1]);
return Number(bSize - aSize);
})
.map(([subtype, size]) => ({
subtype: subtype as FileSubtype,
size: size.toString(),
}));
const users = usersWithStorage.rows
.sort((a, b) => {
const aUsed = a.storage_used ? BigInt(a.storage_used) : 0n;
const bUsed = b.storage_used ? BigInt(b.storage_used) : 0n;
const diff = Number(aUsed - bUsed);
if (diff !== 0) {
return -diff;
}
return compareString(a.id, b.id);
})
.map((user) => ({
id: user.id,
used: user.storage_used ?? '0',
limit: user.storage_limit,
}));
return {
limit: workspace.storage_limit,
used: totalUsed.toString(),
subtypes: subtypes,
users: users,
};
},
});
done();
};

View File

@@ -1,11 +1,13 @@
import { FastifyPluginCallback } from 'fastify';
import { userRoleUpdateRoute } from './user-role-update';
import { userStorageUpdateRoute } from './user-storage-update';
import { usersCreateRoute } from './users-create';
export const userRoutes: FastifyPluginCallback = (instance, _, done) => {
instance.register(usersCreateRoute);
instance.register(userRoleUpdateRoute);
instance.register(userStorageUpdateRoute);
done();
};

View File

@@ -66,7 +66,7 @@ export const userRoleUpdateRoute: FastifyPluginCallbackZod = (
role: input.role,
status,
updated_at: new Date(),
updated_by: user.id,
updated_by: user.account_id,
})
.where('id', '=', userToUpdate.id)
.executeTakeFirst();

View File

@@ -0,0 +1,102 @@
import { FastifyPluginCallbackZod } from 'fastify-type-provider-zod';
import { z } from 'zod/v4';
import {
ApiErrorCode,
apiErrorOutputSchema,
userOutputSchema,
userStorageUpdateInputSchema,
} from '@colanode/core';
import { database } from '@colanode/server/data/database';
import { eventBus } from '@colanode/server/lib/event-bus';
export const userStorageUpdateRoute: FastifyPluginCallbackZod = (
instance,
_,
done
) => {
instance.route({
method: 'PATCH',
url: '/:userId/storage',
schema: {
params: z.object({
userId: z.string(),
}),
body: userStorageUpdateInputSchema,
response: {
200: userOutputSchema,
400: apiErrorOutputSchema,
403: apiErrorOutputSchema,
404: apiErrorOutputSchema,
},
},
handler: async (request, reply) => {
const userId = request.params.userId;
const input = request.body;
const user = request.user;
if (user.role !== 'owner' && user.role !== 'admin') {
return reply.code(403).send({
code: ApiErrorCode.UserUpdateNoAccess,
message: 'You do not have access to update users to this workspace.',
});
}
const userToUpdate = await database
.selectFrom('users')
.selectAll()
.where('id', '=', userId)
.executeTakeFirst();
if (!userToUpdate) {
return reply.code(404).send({
code: ApiErrorCode.UserNotFound,
message: 'User not found.',
});
}
const limit = BigInt(input.limit);
const updatedUser = await database
.updateTable('users')
.returningAll()
.set({
storage_limit: limit.toString(),
updated_at: new Date(),
updated_by: user.account_id,
})
.where('id', '=', userToUpdate.id)
.executeTakeFirst();
if (!updatedUser) {
return reply.code(400).send({
code: ApiErrorCode.UserNotFound,
message: 'User not found.',
});
}
eventBus.publish({
type: 'user.updated',
userId: userToUpdate.id,
accountId: userToUpdate.account_id,
workspaceId: userToUpdate.workspace_id,
});
return {
id: updatedUser.id,
email: updatedUser.email,
name: updatedUser.name,
avatar: updatedUser.avatar,
role: updatedUser.role,
customName: updatedUser.custom_name,
customAvatar: updatedUser.custom_avatar,
createdAt: updatedUser.created_at.toISOString(),
updatedAt: updatedUser.updated_at?.toISOString() ?? null,
revision: updatedUser.revision,
status: updatedUser.status,
};
},
});
done();
};

View File

@@ -0,0 +1,18 @@
import { Migration } from 'kysely';
export const addWorkspaceStorageLimitColumns: Migration = {
up: async (db) => {
await db.schema
.alterTable('workspaces')
.addColumn('storage_limit', 'bigint')
.addColumn('max_file_size', 'bigint')
.execute();
},
down: async (db) => {
await db.schema
.alterTable('workspaces')
.dropColumn('storage_limit')
.dropColumn('max_file_size')
.execute();
},
};

View File

@@ -0,0 +1,17 @@
import { Migration } from 'kysely';
export const addWorkspaceIndexToUploads: Migration = {
up: async (db) => {
await db.schema
.createIndex('uploads_workspace_id_idx')
.on('uploads')
.columns(['workspace_id'])
.execute();
},
down: async (db) => {
await db.schema
.dropIndex('uploads_workspace_id_idx')
.on('uploads')
.execute();
},
};

View File

@@ -28,6 +28,8 @@ import { createWorkspaceUploadCounterTriggers } from './00025-create-workspace-u
import { createUserUploadCounterTriggers } from './00026-create-user-upload-counter-triggers';
import { removeNodeUpdateRevisionTrigger } from './00027-remove-node-update-revision-trigger';
import { removeDocumentUpdateRevisionTrigger } from './00028-remove-document-update-revision-trigger';
import { addWorkspaceStorageLimitColumns } from './00029-add-workspace-storage-limit-columns';
import { addWorkspaceIndexToUploads } from './00030-add-workspace-index-to-uploads';
export const databaseMigrations: Record<string, Migration> = {
'00001_create_accounts_table': createAccountsTable,
@@ -62,4 +64,6 @@ export const databaseMigrations: Record<string, Migration> = {
'00027_remove_node_update_revision_trigger': removeNodeUpdateRevisionTrigger,
'00028_remove_document_update_revision_trigger':
removeDocumentUpdateRevisionTrigger,
'00029_add_workspace_storage_limit_columns': addWorkspaceStorageLimitColumns,
'00030_add_workspace_index_to_uploads': addWorkspaceIndexToUploads,
};

View File

@@ -69,6 +69,8 @@ interface WorkspaceTable {
created_by: ColumnType<string, string, never>;
updated_by: ColumnType<string | null, string | null, string>;
status: ColumnType<number, number, number>;
storage_limit: ColumnType<string | null, string | null, string | null>;
max_file_size: ColumnType<string | null, string | null, string | null>;
}
export type SelectWorkspace = Selectable<WorkspaceTable>;

View File

@@ -68,6 +68,7 @@ import { SpaceDeleteMutationHandler } from './spaces/space-delete';
import { SpaceDescriptionUpdateMutationHandler } from './spaces/space-description-update';
import { SpaceNameUpdateMutationHandler } from './spaces/space-name-update';
import { UserRoleUpdateMutationHandler } from './users/user-role-update';
import { UserStorageUpdateMutationHandler } from './users/user-storage-update';
import { UsersCreateMutationHandler } from './users/users-create';
import { WorkspaceCreateMutationHandler } from './workspaces/workspace-create';
import { WorkspaceDeleteMutationHandler } from './workspaces/workspace-delete';
@@ -162,5 +163,6 @@ export const buildMutationHandlerMap = (
'email.password.reset.complete':
new EmailPasswordResetCompleteMutationHandler(app),
'workspace.delete': new WorkspaceDeleteMutationHandler(app),
'user.storage.update': new UserStorageUpdateMutationHandler(app),
};
};

View File

@@ -0,0 +1,41 @@
import { WorkspaceMutationHandlerBase } from '@colanode/client/handlers/mutations/workspace-mutation-handler-base';
import { parseApiError } from '@colanode/client/lib/ky';
import { MutationHandler } from '@colanode/client/lib/types';
import { MutationError, MutationErrorCode } from '@colanode/client/mutations';
import {
UserStorageUpdateMutationInput,
UserStorageUpdateMutationOutput,
} from '@colanode/client/mutations/users/user-storage-update';
import { UserOutput, UserStorageUpdateInput } from '@colanode/core';
export class UserStorageUpdateMutationHandler
extends WorkspaceMutationHandlerBase
implements MutationHandler<UserStorageUpdateMutationInput>
{
async handleMutation(
input: UserStorageUpdateMutationInput
): Promise<UserStorageUpdateMutationOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
try {
const body: UserStorageUpdateInput = {
limit: input.limit,
};
const output = await workspace.account.client
.patch(`v1/workspaces/${workspace.id}/users/${input.userId}/storage`, {
json: body,
})
.json<UserOutput>();
await workspace.users.upsert(output);
return {
success: true,
};
} catch (error) {
const apiError = await parseApiError(error);
throw new MutationError(MutationErrorCode.ApiError, apiError.message);
}
}
}

View File

@@ -40,9 +40,11 @@ import { SpaceListQueryHandler } from './spaces/space-list';
import { UserGetQueryHandler } from './users/user-get';
import { UserListQueryHandler } from './users/user-list';
import { UserSearchQueryHandler } from './users/user-search';
import { UserStorageGetQueryHandler } from './users/user-storage-get';
import { WorkspaceGetQueryHandler } from './workspaces/workspace-get';
import { WorkspaceListQueryHandler } from './workspaces/workspace-list';
import { WorkspaceMetadataListQueryHandler } from './workspaces/workspace-metadata-list';
import { WorkspaceStorageGetQueryHandler } from './workspaces/workspace-storage-get';
export type QueryHandlerMap = {
[K in keyof QueryMap]: QueryHandler<QueryMap[K]['input']>;
@@ -81,6 +83,7 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'database.view.list': new DatabaseViewListQueryHandler(app),
'record.search': new RecordSearchQueryHandler(app),
'user.get': new UserGetQueryHandler(app),
'user.storage.get': new UserStorageGetQueryHandler(app),
'file.state.get': new FileStateGetQueryHandler(app),
'file.download.request.get': new FileDownloadRequestGetQueryHandler(app),
'file.save.list': new FileSaveListQueryHandler(app),
@@ -91,5 +94,6 @@ export const buildQueryHandlerMap = (app: AppService): QueryHandlerMap => {
'document.state.get': new DocumentStateGetQueryHandler(app),
'document.updates.list': new DocumentUpdatesListQueryHandler(app),
'account.metadata.list': new AccountMetadataListQueryHandler(app),
'workspace.storage.get': new WorkspaceStorageGetQueryHandler(app),
};
};

View File

@@ -0,0 +1,134 @@
import { sql } from 'kysely';
import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/workspace-query-handler-base';
import { ChangeCheckResult, QueryHandler } from '@colanode/client/lib';
import {
UserStorageGetQueryInput,
UserStorageGetQueryOutput,
} from '@colanode/client/queries/users/user-storage-get';
import { Event } from '@colanode/client/types/events';
import { FileStatus, FileSubtype } from '@colanode/core';
interface UserStorageAggregateRow {
subtype: string;
total_size: string;
}
export class UserStorageGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<UserStorageGetQueryInput>
{
public async handleQuery(
input: UserStorageGetQueryInput
): Promise<UserStorageGetQueryOutput> {
const result = await this.fetchStorage(input);
return result;
}
public async checkForChanges(
event: Event,
input: UserStorageGetQueryInput,
_: UserStorageGetQueryOutput
): Promise<ChangeCheckResult<UserStorageGetQueryInput>> {
if (
event.type === 'workspace.deleted' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
return {
hasChanges: true,
result: {
limit: '0',
used: '0',
subtypes: [],
},
};
}
if (
event.type === 'node.created' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.node.type === 'file' &&
event.node.attributes.status === FileStatus.Ready
) {
const output = await this.handleQuery(input);
return {
hasChanges: true,
result: output,
};
}
if (
event.type === 'node.updated' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.node.type === 'file' &&
event.node.attributes.status === FileStatus.Ready
) {
const output = await this.handleQuery(input);
return {
hasChanges: true,
result: output,
};
}
if (
event.type === 'node.deleted' &&
event.accountId === input.accountId &&
event.workspaceId === input.workspaceId &&
event.node.type === 'file' &&
event.node.attributes.status === FileStatus.Ready
) {
const output = await this.handleQuery(input);
return {
hasChanges: true,
result: output,
};
}
return {
hasChanges: false,
};
}
private async fetchStorage(
input: UserStorageGetQueryInput
): Promise<UserStorageGetQueryOutput> {
const workspace = this.getWorkspace(input.accountId, input.workspaceId);
const result = await sql<UserStorageAggregateRow>`
SELECT
json_extract(attributes, '$.subtype') as subtype,
SUM(COALESCE(CAST(json_extract(attributes, '$.size') as INTEGER), 0)) as total_size
FROM nodes
WHERE type = 'file'
AND created_by = ${workspace.userId}
AND json_extract(attributes, '$.status') = ${FileStatus.Ready}
GROUP BY json_extract(attributes, '$.subtype')
ORDER BY total_size DESC
`.execute(workspace.database);
const subtypes: {
subtype: FileSubtype;
size: string;
}[] = [];
let totalUsed = 0n;
for (const row of result.rows) {
const subtype = (row.subtype as FileSubtype) ?? 'other';
const sizeString = row.total_size || '0';
subtypes.push({
subtype,
size: sizeString,
});
totalUsed += BigInt(sizeString);
}
return {
limit: workspace.storageLimit,
used: totalUsed.toString(),
subtypes,
};
}
}

View File

@@ -0,0 +1,87 @@
import { WorkspaceQueryHandlerBase } from '@colanode/client/handlers/queries/workspace-query-handler-base';
import {
ChangeCheckResult,
parseApiError,
QueryHandler,
} from '@colanode/client/lib';
import { QueryError, QueryErrorCode } from '@colanode/client/queries';
import { WorkspaceStorageGetQueryInput } from '@colanode/client/queries/workspaces/workspace-storage-get';
import { Event } from '@colanode/client/types/events';
import { WorkspaceStorageGetOutput } from '@colanode/core';
const EMPTY_STORAGE_OUTPUT: WorkspaceStorageGetOutput = {
limit: '0',
used: '0',
subtypes: [],
users: [],
};
export class WorkspaceStorageGetQueryHandler
extends WorkspaceQueryHandlerBase
implements QueryHandler<WorkspaceStorageGetQueryInput>
{
public async handleQuery(
input: WorkspaceStorageGetQueryInput
): Promise<WorkspaceStorageGetOutput> {
return this.fetchWorkspaceStorage(input.accountId, input.workspaceId);
}
public async checkForChanges(
event: Event,
input: WorkspaceStorageGetQueryInput,
_: WorkspaceStorageGetOutput
): Promise<ChangeCheckResult<WorkspaceStorageGetQueryInput>> {
if (
event.type === 'workspace.created' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
const result = await this.fetchWorkspaceStorage(
input.accountId,
input.workspaceId
);
return {
hasChanges: true,
result,
};
}
if (
event.type === 'workspace.deleted' &&
event.workspace.accountId === input.accountId &&
event.workspace.id === input.workspaceId
) {
return {
hasChanges: true,
result: EMPTY_STORAGE_OUTPUT,
};
}
return {
hasChanges: false,
};
}
private async fetchWorkspaceStorage(
accountId: string,
workspaceId: string
): Promise<WorkspaceStorageGetOutput> {
const workspace = this.getWorkspace(accountId, workspaceId);
if (!workspace) {
return EMPTY_STORAGE_OUTPUT;
}
try {
const response = await workspace.account.client
.get(`v1/workspaces/${workspace.id}/storage`)
.json<WorkspaceStorageGetOutput>();
return response;
} catch (error) {
console.error(error);
const apiError = await parseApiError(error);
throw new QueryError(QueryErrorCode.ApiError, apiError.message);
}
}
}

View File

@@ -69,6 +69,7 @@ export * from './workspaces/workspace-metadata-delete';
export * from './workspaces/workspace-metadata-update';
export * from './workspaces/workspace-update';
export * from './users/user-role-update';
export * from './users/user-storage-update';
export * from './users/users-create';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@@ -0,0 +1,20 @@
export type UserStorageUpdateMutationInput = {
type: 'user.storage.update';
accountId: string;
workspaceId: string;
userId: string;
limit: string;
};
export type UserStorageUpdateMutationOutput = {
success: boolean;
};
declare module '@colanode/client/mutations' {
interface MutationMap {
'user.storage.update': {
input: UserStorageUpdateMutationInput;
output: UserStorageUpdateMutationOutput;
};
}
}

View File

@@ -34,11 +34,13 @@ export * from './spaces/space-list';
export * from './users/user-get';
export * from './users/user-list';
export * from './users/user-search';
export * from './users/user-storage-get';
export * from './workspaces/workspace-get';
export * from './workspaces/workspace-list';
export * from './workspaces/workspace-metadata-list';
export * from './avatars/avatar-url-get';
export * from './records/record-field-value-count';
export * from './workspaces/workspace-storage-get';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface QueryMap {}
@@ -58,4 +60,5 @@ export enum QueryErrorCode {
Unknown = 'unknown',
AccountNotFound = 'account_not_found',
WorkspaceNotFound = 'workspace_not_found',
ApiError = 'api_error',
}

View File

@@ -0,0 +1,25 @@
import { FileSubtype } from '@colanode/core';
export type UserStorageGetQueryInput = {
type: 'user.storage.get';
accountId: string;
workspaceId: string;
};
export type UserStorageGetQueryOutput = {
limit: string;
used: string;
subtypes: {
subtype: FileSubtype;
size: string;
}[];
};
declare module '@colanode/client/queries' {
interface QueryMap {
'user.storage.get': {
input: UserStorageGetQueryInput;
output: UserStorageGetQueryOutput;
};
}
}

View File

@@ -0,0 +1,16 @@
import { WorkspaceStorageGetOutput } from '@colanode/core';
export type WorkspaceStorageGetQueryInput = {
type: 'workspace.storage.get';
accountId: string;
workspaceId: string;
};
declare module '@colanode/client/queries' {
interface QueryMap {
'workspace.storage.get': {
input: WorkspaceStorageGetQueryInput;
output: WorkspaceStorageGetOutput;
};
}
}

View File

@@ -67,6 +67,7 @@ export type WorkspaceMetadataMap = {
export enum SpecialContainerTabPath {
Downloads = 'downloads',
WorkspaceSettings = 'workspace/settings',
WorkspaceStorage = 'workspace/storage',
WorkspaceUsers = 'workspace/users',
WorkspaceDelete = 'workspace/delete',
AccountSettings = 'account/settings',

View File

@@ -38,3 +38,4 @@ export * from './types/mentions';
export * from './types/avatars';
export * from './types/build';
export * from './lib/servers';
export * from './types/storage';

View File

@@ -22,30 +22,32 @@ export const extractFileSubtype = (mimeType: string): FileSubtype => {
export const formatBytes = (
bytes: number | bigint,
decimals?: number
maxDecimals: number = 2
): string => {
if (bytes === 0) {
return '0 Bytes';
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] as const;
const BASE = 1024;
// Work with a bigint internally to stay safe for very large values.
let valueBig = typeof bytes === 'bigint' ? bytes : BigInt(bytes);
let unitIdx = 0;
// Find the most suitable unit (stop at YB to avoid overflow).
while (valueBig >= BigInt(BASE) && unitIdx < UNITS.length - 1) {
valueBig /= BigInt(BASE);
unitIdx++;
}
const bytesBigInt = BigInt(bytes);
const k = BigInt(1024);
const dm = decimals || 2;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// Convert the original byte value to a JS number **after** determining the unit,
// so it is safely within Numbers range for formatting.
const divisor = Math.pow(BASE, unitIdx);
const valueNum =
(typeof bytes === 'bigint' ? Number(bytes) : bytes) / divisor;
// Find the largest power of k that's smaller than bytes
let i = 0;
let reducedBytes = bytesBigInt;
while (reducedBytes >= k && i < sizes.length - 1) {
reducedBytes = reducedBytes / k;
i++;
}
// Round to the requested precision, then trim superfluous zeros.
const rounded = valueNum.toFixed(maxDecimals);
const trimmed = rounded.replace(/\.?0+$/, '');
// Convert to decimal representation with proper precision
const factor = Math.pow(10, dm);
const value = Number((reducedBytes * BigInt(factor)) / BigInt(factor));
return `${value.toFixed(dm)} ${sizes[i]}`;
return `${trimmed} ${UNITS[unitIdx]}`;
};
const mimeTypeNames: Record<string, string> = {

View File

@@ -33,6 +33,7 @@ export enum ApiErrorCode {
FileOwnerMismatch = 'file_owner_mismatch',
FileAlreadyUploaded = 'file_already_uploaded',
FileUploadInitFailed = 'file_upload_init_failed',
FileUploadFailed = 'file_upload_failed',
WorkspaceMismatch = 'workspace_mismatch',
FileError = 'file_error',
FileSizeMismatch = 'file_size_mismatch',

View File

@@ -0,0 +1,31 @@
import { z } from 'zod/v4';
import { fileSubtypeSchema } from '@colanode/core/types/files';
export const workspaceStorageFileSubtypeSchema = z.object({
subtype: fileSubtypeSchema,
size: z.string(),
});
export type WorkspaceStorageFileSubtype = z.infer<
typeof workspaceStorageFileSubtypeSchema
>;
export const workspaceStorageUserSchema = z.object({
id: z.string(),
used: z.string(),
limit: z.string(),
});
export type WorkspaceStorageUser = z.infer<typeof workspaceStorageUserSchema>;
export const workspaceStorageGetOutputSchema = z.object({
limit: z.string().nullable().optional(),
used: z.string(),
subtypes: z.array(workspaceStorageFileSubtypeSchema),
users: z.array(workspaceStorageUserSchema),
});
export type WorkspaceStorageGetOutput = z.infer<
typeof workspaceStorageGetOutputSchema
>;

View File

@@ -86,7 +86,7 @@ export const userOutputSchema = z.object({
export type UserOutput = z.infer<typeof userOutputSchema>;
export const userCreateErrorOutputSchema = z.object({
email: z.string().email(),
email: z.email(),
error: z.string(),
});
@@ -104,3 +104,11 @@ export const userRoleUpdateInputSchema = z.object({
});
export type UserRoleUpdateInput = z.infer<typeof userRoleUpdateInputSchema>;
export const userStorageUpdateInputSchema = z.object({
limit: z.string(),
});
export type UserStorageUpdateInput = z.infer<
typeof userStorageUpdateInputSchema
>;

View File

@@ -1,29 +0,0 @@
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
} from '@colanode/ui/components/ui/breadcrumb';
import { useAccount } from '@colanode/ui/contexts/account';
export const AccountLogoutBreadcrumb = () => {
const account = useAccount();
return (
<Breadcrumb className="flex-grow">
<BreadcrumbList>
<BreadcrumbItem className="cursor-pointer hover:text-foreground">
<div className="flex items-center space-x-2">
<Avatar
id={account.id}
name={account.name}
avatar={account.avatar}
className="size-4"
/>
<span>{account.name} Logout</span>
</div>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@@ -1,18 +1,10 @@
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useAccount } from '@colanode/ui/contexts/account';
import { LogOut } from 'lucide-react';
export const AccountLogoutTab = () => {
const account = useAccount();
return (
<div className="flex items-center space-x-2">
<Avatar
id={account.id}
name={account.name}
avatar={account.avatar}
size="small"
/>
<span>{account.name} Logout</span>
<LogOut className="size-4" />
<span>Logout</span>
</div>
);
};

View File

@@ -1,12 +1,7 @@
import { toast } from 'sonner';
import { AccountLogoutBreadcrumb } from '@colanode/ui/components/accounts/account-logout-breadcrumb';
import { Button } from '@colanode/ui/components/ui/button';
import {
Container,
ContainerBody,
ContainerHeader,
} from '@colanode/ui/components/ui/container';
import { Container, ContainerBody } from '@colanode/ui/components/ui/container';
import { Separator } from '@colanode/ui/components/ui/separator';
import { Spinner } from '@colanode/ui/components/ui/spinner';
import { useAccount } from '@colanode/ui/contexts/account';
@@ -18,46 +13,41 @@ export const AccountLogout = () => {
return (
<Container>
<ContainerHeader>
<AccountLogoutBreadcrumb />
</ContainerHeader>
<ContainerBody className="max-w-4xl">
<div className="space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Logout</h2>
<Separator className="mt-3" />
<ContainerBody className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">Logout</h2>
<Separator className="mt-3" />
</div>
<div className="flex items-center justify-between gap-6">
<div className="flex-1 space-y-2">
<h3 className="font-semibold">Sign out of your account</h3>
<p className="text-sm text-muted-foreground">
All your data will be removed from this device. If there are
pending changes, they will be lost. If you login again, all the
data will be re-synced.
</p>
</div>
<div className="flex items-center justify-between gap-6">
<div className="flex-1 space-y-2">
<h3 className="font-semibold">Sign out of your account</h3>
<p className="text-sm text-muted-foreground">
All your data will be removed from this device. If there are
pending changes, they will be lost. If you login again, all
the data will be re-synced.
</p>
</div>
<div className="flex-shrink-0">
<Button
variant="destructive"
disabled={isPending}
className="w-20 cursor-pointer"
onClick={async () => {
mutate({
input: {
type: 'account.logout',
accountId: account.id,
},
onError(error) {
toast.error(error.message);
},
});
}}
>
{isPending && <Spinner className="mr-1" />}
Logout
</Button>
</div>
<div className="flex-shrink-0">
<Button
variant="destructive"
disabled={isPending}
className="w-20 cursor-pointer"
onClick={async () => {
mutate({
input: {
type: 'account.logout',
accountId: account.id,
},
onError(error) {
toast.error(error.message);
},
});
}}
>
{isPending && <Spinner className="mr-1" />}
Logout
</Button>
</div>
</div>
</div>

View File

@@ -1,29 +0,0 @@
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
} from '@colanode/ui/components/ui/breadcrumb';
import { useAccount } from '@colanode/ui/contexts/account';
export const AccountSettingsBreadcrumb = () => {
const account = useAccount();
return (
<Breadcrumb className="flex-grow">
<BreadcrumbList>
<BreadcrumbItem className="cursor-pointer hover:text-foreground">
<div className="flex items-center space-x-2">
<Avatar
id={account.id}
name={account.name}
avatar={account.avatar}
className="size-4"
/>
<span>{account.name} Settings</span>
</div>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@@ -1,92 +0,0 @@
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { Info, Trash2 } from 'lucide-react';
import { AccountUpdate } from '@colanode/ui/components/accounts/account-update';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import {
Dialog,
DialogContent,
DialogTitle,
} from '@colanode/ui/components/ui/dialog';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@colanode/ui/components/ui/tabs';
import { useAccount } from '@colanode/ui/contexts/account';
interface AccountSettingsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const AccountSettingsDialog = ({
open,
onOpenChange,
}: AccountSettingsDialogProps) => {
const account = useAccount();
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className="md:min-h-3/4 md:max-h-3/4 p-3 md:h-3/4 md:w-3/4 md:max-w-full"
aria-describedby={undefined}
>
<VisuallyHidden>
<DialogTitle>Workspace Settings</DialogTitle>
</VisuallyHidden>
<Tabs
defaultValue="info"
className="grid h-full max-h-full grid-cols-[240px_minmax(0,1fr)] overflow-hidden"
>
<TabsList className="flex w-full max-h-full flex-col items-start justify-start gap-1 rounded-none border-r border-r-gray-100 bg-white pr-3">
<div className="mb-1 flex h-10 w-full items-center justify-between bg-gray-50 p-1 text-foreground/80">
<div className="flex items-center gap-2">
<Avatar
id={account.id}
name={account.name}
avatar={account.avatar}
size="small"
/>
<span className="truncate font-semibold">{account.name}</span>
</div>
</div>
<TabsTrigger
key={`tab-trigger-info`}
className="w-full justify-start p-2 hover:bg-gray-50 cursor-pointer"
value="info"
>
<Info className="mr-2 size-4" />
Info
</TabsTrigger>
<TabsTrigger
key={`tab-trigger-delete`}
className="w-full justify-start p-2 hover:bg-gray-50 cursor-pointer"
value="delete"
>
<Trash2 className="mr-2 size-4" />
Delete
</TabsTrigger>
</TabsList>
<div className="overflow-auto p-4">
<TabsContent
key="tab-content-info"
className="focus-visible:ring-0 focus-visible:ring-offset-0"
value="info"
>
<AccountUpdate account={account} />
</TabsContent>
<TabsContent
key="tab-content-delete"
className="focus-visible:ring-0 focus-visible:ring-offset-0"
value="delete"
>
<p>Coming soon.</p>
</TabsContent>
</div>
</Tabs>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,18 +1,10 @@
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useAccount } from '@colanode/ui/contexts/account';
import { Settings } from 'lucide-react';
export const AccountSettingsTab = () => {
const account = useAccount();
return (
<div className="flex items-center space-x-2">
<Avatar
id={account.id}
name={account.name}
avatar={account.avatar}
size="small"
/>
<span>{account.name} Settings</span>
<Settings className="size-4" />
<span>Account Settings</span>
</div>
);
};

View File

@@ -1,11 +1,6 @@
import { AccountDelete } from '@colanode/ui/components/accounts/account-delete';
import { AccountSettingsBreadcrumb } from '@colanode/ui/components/accounts/account-settings-breadcrumb';
import { AccountUpdate } from '@colanode/ui/components/accounts/account-update';
import {
Container,
ContainerBody,
ContainerHeader,
} from '@colanode/ui/components/ui/container';
import { Container, ContainerBody } from '@colanode/ui/components/ui/container';
import { Separator } from '@colanode/ui/components/ui/separator';
import { useAccount } from '@colanode/ui/contexts/account';
@@ -14,28 +9,23 @@ export const AccountSettings = () => {
return (
<Container>
<ContainerHeader>
<AccountSettingsBreadcrumb />
</ContainerHeader>
<ContainerBody className="max-w-4xl">
<div className="space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<AccountUpdate account={account} />
<ContainerBody className="max-w-4xl space-y-8">
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
<Separator className="mt-3" />
</div>
<AccountUpdate account={account} />
</div>
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<AccountDelete />
<div className="space-y-6">
<div>
<h2 className="text-2xl font-semibold tracking-tight">
Danger Zone
</h2>
<Separator className="mt-3" />
</div>
<AccountDelete />
</div>
</ContainerBody>
</Container>

View File

@@ -4,7 +4,7 @@ import { Account as AccountType } from '@colanode/client/types';
import { Workspace } from '@colanode/ui/components/workspaces/workspace';
import { WorkspaceCreate } from '@colanode/ui/components/workspaces/workspace-create';
import { AccountContext } from '@colanode/ui/contexts/account';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface AccountProps {
account: AccountType;
@@ -13,12 +13,12 @@ interface AccountProps {
export const Account = ({ account }: AccountProps) => {
const [openCreateWorkspace, setOpenCreateWorkspace] = useState(false);
const accountMetadataListQuery = useQuery({
const accountMetadataListQuery = useLiveQuery({
type: 'account.metadata.list',
accountId: account.id,
});
const workspaceListQuery = useQuery({
const workspaceListQuery = useLiveQuery({
type: 'workspace.list',
accountId: account.id,
});

View File

@@ -20,7 +20,6 @@ const GoogleLoginButton = ({ context, onSuccess }: GoogleLoginProps) => {
const login = useGoogleLogin({
onSuccess: async (response) => {
console.log('response', response);
mutate({
input: {
type: 'google.login',

View File

@@ -1,12 +1,12 @@
import { LoginForm } from '@colanode/ui/components/accounts/login-form';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const Login = () => {
const accountListQuery = useQuery({
const accountListQuery = useLiveQuery({
type: 'account.list',
});
const serverListQuery = useQuery({
const serverListQuery = useLiveQuery({
type: 'server.list',
});

View File

@@ -8,7 +8,7 @@ import { RadarProvider } from '@colanode/ui/components/radar-provider';
import { ServerProvider } from '@colanode/ui/components/servers/server-provider';
import { DelayedComponent } from '@colanode/ui/components/ui/delayed-component';
import { AppContext } from '@colanode/ui/contexts/app';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface AppProps {
type: AppType;
@@ -18,11 +18,11 @@ export const App = ({ type }: AppProps) => {
const [initialized, setInitialized] = useState(false);
const [openLogin, setOpenLogin] = useState(false);
const appMetadataListQuery = useQuery({
const appMetadataListQuery = useLiveQuery({
type: 'app.metadata.list',
});
const accountListQuery = useQuery({
const accountListQuery = useLiveQuery({
type: 'account.list',
});

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { AvatarFallback } from '@colanode/ui/components/avatars/avatar-fallback';
import { useAccount } from '@colanode/ui/contexts/account';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { AvatarProps, getAvatarSizeClasses } from '@colanode/ui/lib/avatars';
import { cn } from '@colanode/ui/lib/utils';
@@ -10,7 +10,7 @@ export const AvatarImage = (props: AvatarProps) => {
const account = useAccount();
const [failed, setFailed] = useState(false);
const { data, isPending } = useQuery(
const { data, isPending } = useLiveQuery(
{
type: 'avatar.url.get',
accountId: account.id,

View File

@@ -3,7 +3,7 @@ import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface ChannelContainerTabProps {
channelId: string;
@@ -17,7 +17,7 @@ export const ChannelContainerTab = ({
const workspace = useWorkspace();
const radar = useRadar();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: channelId,
accountId: workspace.accountId,

View File

@@ -1,7 +1,7 @@
import { LocalChatNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface ChatBreadcrumbItemProps {
chat: LocalChatNode;
@@ -17,7 +17,7 @@ export const ChatBreadcrumbItem = ({ chat }: ChatBreadcrumbItemProps) => {
) ?? '')
: '';
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -3,7 +3,7 @@ import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface ChatContainerTabProps {
chatId: string;
@@ -17,7 +17,7 @@ export const ChatContainerTab = ({
const workspace = useWorkspace();
const radar = useRadar();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: chatId,
accountId: workspace.accountId,
@@ -31,7 +31,7 @@ export const ChatContainerTab = ({
) ?? '')
: '';
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -6,7 +6,7 @@ import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { useLayout } from '@colanode/ui/contexts/layout';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { cn } from '@colanode/ui/lib/utils';
interface ChatSidebarItemProps {
@@ -23,7 +23,7 @@ export const ChatSidebarItem = ({ chat }: ChatSidebarItemProps) => {
(id) => id !== workspace.userId
) ?? '';
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -1,7 +1,7 @@
import { timeAgo } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface NodeCollaboratorAuditProps {
collaboratorId: string;
@@ -14,7 +14,7 @@ export const NodeCollaboratorAudit = ({
}: NodeCollaboratorAuditProps) => {
const workspace = useWorkspace();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -19,7 +19,7 @@ import {
PopoverTrigger,
} from '@colanode/ui/components/ui/popover';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface NodeCollaboratorSearchProps {
excluded: string[];
@@ -37,7 +37,7 @@ export const NodeCollaboratorSearch = ({
const [query, setQuery] = useState('');
const [open, setOpen] = useState(false);
const userSearchQuery = useQuery({
const userSearchQuery = useLiveQuery({
type: 'user.search',
searchQuery: query,
exclude: excluded,

View File

@@ -5,8 +5,8 @@ import { NodeRole } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { NodeCollaboratorRoleDropdown } from '@colanode/ui/components/collaborators/node-collaborator-role-dropdown';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { useQuery } from '@colanode/ui/hooks/use-query';
interface NodeCollaboratorProps {
nodeId: string;
@@ -26,7 +26,7 @@ export const NodeCollaborator = ({
const workspace = useWorkspace();
const { mutate } = useMutation();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -13,7 +13,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface BoardViewColumnsCollaboratorProps {
field: CollaboratorFieldAttributes;
@@ -26,7 +26,7 @@ export const BoardViewColumnsCollaborator = ({
const database = useDatabase();
const view = useDatabaseView();
const collaboratorCountQuery = useQuery({
const collaboratorCountQuery = useLiveQuery({
type: 'record.field.value.count',
databaseId: database.id,
filters: view.filters,
@@ -200,7 +200,7 @@ const BoardViewColumnCollaboratorHeader = ({
}: BoardViewColumnCollaboratorHeaderProps) => {
const workspace = useWorkspace();
const userQuery = useQuery(
const userQuery = useLiveQuery(
{
type: 'user.get',
userId: collaborator ?? '',

View File

@@ -11,7 +11,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface BoardViewColumnsCreatedByProps {
field: CreatedByFieldAttributes;
@@ -24,7 +24,7 @@ export const BoardViewColumnsCreatedBy = ({
const database = useDatabase();
const view = useDatabaseView();
const createdByCountQuery = useQuery({
const createdByCountQuery = useLiveQuery({
type: 'record.field.value.count',
databaseId: database.id,
filters: view.filters,
@@ -89,7 +89,7 @@ const BoardViewColumnCreatedByHeader = ({
}: BoardViewColumnCreatedByHeaderProps) => {
const workspace = useWorkspace();
const userQuery = useQuery({
const userQuery = useLiveQuery({
type: 'user.get',
userId: createdBy,
accountId: workspace.accountId,

View File

@@ -13,7 +13,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases';
interface BoardViewColumnsMultiSelectProps {
@@ -27,7 +27,7 @@ export const BoardViewColumnsMultiSelect = ({
const database = useDatabase();
const view = useDatabaseView();
const selectOptionCountQuery = useQuery({
const selectOptionCountQuery = useLiveQuery({
type: 'record.field.value.count',
databaseId: database.id,
filters: view.filters,

View File

@@ -12,7 +12,7 @@ import { BoardViewContext } from '@colanode/ui/contexts/board-view';
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { getSelectOptionLightColorClass } from '@colanode/ui/lib/databases';
interface BoardViewColumnsSelectProps {
@@ -26,7 +26,7 @@ export const BoardViewColumnsSelect = ({
const database = useDatabase();
const view = useDatabaseView();
const selectOptionCountQuery = useQuery({
const selectOptionCountQuery = useLiveQuery({
type: 'record.field.value.count',
databaseId: database.id,
filters: view.filters,

View File

@@ -13,7 +13,7 @@ import {
import { useDatabase } from '@colanode/ui/contexts/database';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface CalendarViewNoValueCountProps {
field: FieldAttributes;
@@ -36,7 +36,7 @@ export const CalendarViewNoValueCount = ({
},
];
const noValueCountQuery = useQuery({
const noValueCountQuery = useLiveQuery({
type: 'record.field.value.count',
databaseId: database.id,
filters: filters,

View File

@@ -1,7 +1,7 @@
import { LocalDatabaseNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface DatabaseContainerTabProps {
databaseId: string;
@@ -12,7 +12,7 @@ export const DatabaseContainerTab = ({
}: DatabaseContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: databaseId,
accountId: workspace.accountId,

View File

@@ -17,7 +17,7 @@ import {
PopoverTrigger,
} from '@colanode/ui/components/ui/popover';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { cn } from '@colanode/ui/lib/utils';
interface DatabaseSelectProps {
@@ -29,7 +29,7 @@ export const DatabaseSelect = ({ id, onChange }: DatabaseSelectProps) => {
const workspace = useWorkspace();
const [open, setOpen] = useState(false);
const databaseListQuery = useQuery({
const databaseListQuery = useLiveQuery({
type: 'database.list',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -6,14 +6,14 @@ import { ScrollBar } from '@colanode/ui/components/ui/scroll-area';
import { useDatabase } from '@colanode/ui/contexts/database';
import { DatabaseViewsContext } from '@colanode/ui/contexts/database-views';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const DatabaseViews = () => {
const workspace = useWorkspace();
const database = useDatabase();
const [activeViewId, setActiveViewId] = useState<string | null>(null);
const databaseViewListQuery = useQuery({
const databaseViewListQuery = useLiveQuery({
type: 'database.view.list',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -24,7 +24,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
import { UserSearch } from '@colanode/ui/components/users/user-search';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
import {
collaboratorFieldFilterOperators,
createdByFieldFilterOperators,
@@ -71,7 +71,7 @@ export const ViewCollaboratorFieldFilter = ({
) ?? collaboratorFieldFilterOperators[0]!;
const collaboratorIds = (filter.value as string[]) ?? [];
const results = useQueries(
const results = useLiveQueries(
collaboratorIds.map((id) => ({
type: 'user.get',
userId: id,

View File

@@ -24,7 +24,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
import { UserSearch } from '@colanode/ui/components/users/user-search';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
import { createdByFieldFilterOperators } from '@colanode/ui/lib/databases';
interface ViewCreatedByFieldFilterProps {
@@ -63,7 +63,7 @@ export const ViewCreatedByFieldFilter = ({
) ?? createdByFieldFilterOperators[0]!;
const collaboratorIds = (filter.value as string[]) ?? [];
const results = useQueries(
const results = useLiveQueries(
collaboratorIds.map((id) => ({
type: 'user.get',
userId: id,

View File

@@ -24,7 +24,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
import { UserSearch } from '@colanode/ui/components/users/user-search';
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
import { updatedByFieldFilterOperators } from '@colanode/ui/lib/databases';
interface ViewUpdatedByFieldFilterProps {
@@ -68,7 +68,7 @@ export const ViewUpdatedByFieldFilter = ({
) ?? updatedByFieldFilterOperators[0]!;
const collaboratorIds = (filter.value as string[]) ?? [];
const results = useQueries(
const results = useLiveQueries(
collaboratorIds.map((id) => ({
type: 'user.get',
userId: id,

View File

@@ -3,7 +3,7 @@ import { FocusPosition } from '@tiptap/core';
import { LocalNode } from '@colanode/client/types';
import { DocumentEditor } from '@colanode/ui/components/documents/document-editor';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface DocumentProps {
node: LocalNode;
@@ -14,14 +14,14 @@ interface DocumentProps {
export const Document = ({ node, canEdit, autoFocus }: DocumentProps) => {
const workspace = useWorkspace();
const documentStateQuery = useQuery({
const documentStateQuery = useLiveQuery({
type: 'document.state.get',
documentId: node.id,
accountId: workspace.accountId,
workspaceId: workspace.id,
});
const documentUpdatesQuery = useQuery({
const documentUpdatesQuery = useLiveQuery({
type: 'document.updates.list',
documentId: node.id,
accountId: workspace.accountId,

View File

@@ -2,12 +2,12 @@ import { Download } from 'lucide-react';
import { SaveStatus } from '@colanode/client/types';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const DownloadsContainerTab = () => {
const workspace = useWorkspace();
const fileSaveListQuery = useQuery({
const fileSaveListQuery = useLiveQuery({
type: 'file.save.list',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -10,12 +10,12 @@ import {
TooltipTrigger,
} from '@colanode/ui/components/ui/tooltip';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const DownloadsList = () => {
const workspace = useWorkspace();
const fileSaveListQuery = useQuery({
const fileSaveListQuery = useLiveQuery({
type: 'file.save.list',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -1,6 +1,6 @@
import { EmojiPickerItemsRow } from '@colanode/client/types';
import { EmojiPickerItem } from '@colanode/ui/components/emojis/emoji-picker-item';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface EmojiBrowserItemsProps {
row: EmojiPickerItemsRow;
@@ -8,7 +8,7 @@ interface EmojiBrowserItemsProps {
}
export const EmojiBrowserItems = ({ row, style }: EmojiBrowserItemsProps) => {
const emojiListQuery = useQuery({
const emojiListQuery = useLiveQuery({
type: 'emoji.list',
category: row.category,
page: row.page,

View File

@@ -4,12 +4,12 @@ import { useMemo, useRef } from 'react';
import { EmojiPickerRowData } from '@colanode/client/types';
import { EmojiBrowserCategory } from '@colanode/ui/components/emojis/emoji-browser-category';
import { EmojiBrowserItems } from '@colanode/ui/components/emojis/emoji-browser-items';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
const EMOJIS_PER_ROW = 10;
export const EmojiBrowser = () => {
const emojiCategoryListQuery = useQuery({
const emojiCategoryListQuery = useLiveQuery({
type: 'emoji.category.list',
});

View File

@@ -1,12 +1,12 @@
import { EmojiPickerItem } from '@colanode/ui/components/emojis/emoji-picker-item';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface EmojiSearchProps {
query: string;
}
export const EmojiSearch = ({ query }: EmojiSearchProps) => {
const emojiSearchQuery = useQuery({
const emojiSearchQuery = useLiveQuery({
type: 'emoji.search',
query,
count: 100,

View File

@@ -6,7 +6,7 @@ import {
PopoverContent,
PopoverTrigger,
} from '@colanode/ui/components/ui/popover';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { defaultEmojis } from '@colanode/ui/lib/assets';
interface EmojiSkinToneSelectorProps {
@@ -20,7 +20,7 @@ export const EmojiSkinToneSelector = ({
}: EmojiSkinToneSelectorProps) => {
const [open, setOpen] = useState<boolean>(false);
const emojiGetQuery = useQuery({
const emojiGetQuery = useLiveQuery({
type: 'emoji.get',
id: defaultEmojis.hand,
});

View File

@@ -2,8 +2,8 @@ import { LocalFileNode } from '@colanode/client/types';
import { FilePreview } from '@colanode/ui/components/files/file-preview';
import { useLayout } from '@colanode/ui/contexts/layout';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useNodeRadar } from '@colanode/ui/hooks/use-node-radar';
import { useQuery } from '@colanode/ui/hooks/use-query';
interface FileBlockProps {
id: string;
@@ -13,7 +13,7 @@ export const FileBlock = ({ id }: FileBlockProps) => {
const workspace = useWorkspace();
const layout = useLayout();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: id,
accountId: workspace.accountId,

View File

@@ -1,7 +1,7 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface FileContainerTabProps {
fileId: string;
@@ -10,7 +10,7 @@ interface FileContainerTabProps {
export const FileContainerTab = ({ fileId }: FileContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: fileId,
accountId: workspace.accountId,

View File

@@ -7,8 +7,8 @@ import { FilePreviewAudio } from '@colanode/ui/components/files/previews/file-pr
import { FilePreviewImage } from '@colanode/ui/components/files/previews/file-preview-image';
import { FilePreviewVideo } from '@colanode/ui/components/files/previews/file-preview-video';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { useQuery } from '@colanode/ui/hooks/use-query';
interface FilePreviewProps {
file: LocalFileNode;
@@ -18,7 +18,7 @@ export const FilePreview = ({ file }: FilePreviewProps) => {
const workspace = useWorkspace();
const mutation = useMutation();
const fileStateQuery = useQuery({
const fileStateQuery = useLiveQuery({
type: 'file.state.get',
id: file.id,
accountId: workspace.accountId,

View File

@@ -8,8 +8,8 @@ import { Spinner } from '@colanode/ui/components/ui/spinner';
import { useApp } from '@colanode/ui/contexts/app';
import { useLayout } from '@colanode/ui/contexts/layout';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { useQuery } from '@colanode/ui/hooks/use-query';
interface FileSaveButtonProps {
file: LocalFileNode;
@@ -22,7 +22,7 @@ export const FileSaveButton = ({ file }: FileSaveButtonProps) => {
const layout = useLayout();
const [isSaving, setIsSaving] = useState(false);
const fileStateQuery = useQuery({
const fileStateQuery = useLiveQuery({
type: 'file.state.get',
id: file.id,
accountId: workspace.accountId,

View File

@@ -5,7 +5,7 @@ import { formatBytes, formatDate } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { FileThumbnail } from '@colanode/ui/components/files/file-thumbnail';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface FileSidebarProps {
file: LocalFileNode;
@@ -23,7 +23,7 @@ const FileMeta = ({ title, value }: { title: string; value: string }) => {
export const FileSidebar = ({ file }: FileSidebarProps) => {
const workspace = useWorkspace();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -1,7 +1,7 @@
import { LocalFileNode } from '@colanode/client/types';
import { FileIcon } from '@colanode/ui/components/files/file-icon';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { cn } from '@colanode/ui/lib/utils';
interface FileThumbnailProps {
@@ -12,7 +12,7 @@ interface FileThumbnailProps {
export const FileThumbnail = ({ file, className }: FileThumbnailProps) => {
const workspace = useWorkspace();
const fileStateGetQuery = useQuery({
const fileStateGetQuery = useLiveQuery({
type: 'file.state.get',
id: file.id,
accountId: workspace.accountId,

View File

@@ -1,7 +1,7 @@
import { LocalFolderNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface FolderContainerTabProps {
folderId: string;
@@ -10,7 +10,7 @@ interface FolderContainerTabProps {
export const FolderContainerTab = ({ folderId }: FolderContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: folderId,
accountId: workspace.accountId,

View File

@@ -9,7 +9,7 @@ import { ListLayout } from '@colanode/ui/components/folders/lists/list-layout';
import { FolderContext } from '@colanode/ui/contexts/folder';
import { useLayout } from '@colanode/ui/contexts/layout';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const FILES_PER_PAGE = 100;
@@ -39,7 +39,7 @@ export const FolderFiles = ({
page: i + 1,
}));
const result = useQueries(inputs);
const result = useLiveQueries(inputs);
const files = result.flatMap((data) => data.data ?? []);
return (

View File

@@ -1,6 +1,6 @@
import { IconPickerItemsRow } from '@colanode/client/types';
import { IconPickerItem } from '@colanode/ui/components/icons/icon-picker-item';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface IconBrowserItemsProps {
row: IconPickerItemsRow;
@@ -8,7 +8,7 @@ interface IconBrowserItemsProps {
}
export const IconBrowserItems = ({ row, style }: IconBrowserItemsProps) => {
const iconListQuery = useQuery({
const iconListQuery = useLiveQuery({
type: 'icon.list',
category: row.category,
page: row.page,

View File

@@ -4,12 +4,12 @@ import { useMemo, useRef } from 'react';
import { IconPickerRowData } from '@colanode/client/types';
import { IconBrowserCategory } from '@colanode/ui/components/icons/icon-browser-category';
import { IconBrowserItems } from '@colanode/ui/components/icons/icon-browser-items';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
const ICONS_PER_ROW = 10;
export const IconBrowser = () => {
const iconCategoryListQuery = useQuery({
const iconCategoryListQuery = useLiveQuery({
type: 'icon.category.list',
});

View File

@@ -1,12 +1,12 @@
import { IconPickerItem } from '@colanode/ui/components/icons/icon-picker-item';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface IconSearchProps {
query: string;
}
export const IconSearch = ({ query }: IconSearchProps) => {
const iconSearchQuery = useQuery({
const iconSearchQuery = useLiveQuery({
type: 'icon.search',
query,
count: 100,

View File

@@ -16,6 +16,7 @@ import { RecordContainer } from '@colanode/ui/components/records/record-containe
import { SpaceContainer } from '@colanode/ui/components/spaces/space-container';
import { TabsContent } from '@colanode/ui/components/ui/tabs';
import { WorkspaceSettings } from '@colanode/ui/components/workspaces/workspace-settings';
import { WorkspaceStorage } from '@colanode/ui/components/workspaces/workspace-storage';
import { WorkspaceUsers } from '@colanode/ui/components/workspaces/workspace-users';
interface ContainerTabContentProps {
@@ -43,6 +44,10 @@ const ContainerTabContentBody = ({ tab }: ContainerTabContentProps) => {
return <AccountLogout />;
}
if (tab.path === SpecialContainerTabPath.WorkspaceStorage) {
return <WorkspaceStorage />;
}
return match(getIdType(tab.path))
.with(IdType.Space, () => <SpaceContainer spaceId={tab.path} />)
.with(IdType.Channel, () => <ChannelContainer channelId={tab.path} />)

View File

@@ -19,6 +19,7 @@ import { RecordContainerTab } from '@colanode/ui/components/records/record-conta
import { SpaceContainerTab } from '@colanode/ui/components/spaces/space-container-tab';
import { TabsTrigger } from '@colanode/ui/components/ui/tabs';
import { WorkspaceSettingsTab } from '@colanode/ui/components/workspaces/workspace-settings-tab';
import { WorkspaceStorageTab } from '@colanode/ui/components/workspaces/workspace-storage-tab';
import { WorkspaceUsersTab } from '@colanode/ui/components/workspaces/workspace-users-tab';
import { cn } from '@colanode/ui/lib/utils';
@@ -50,6 +51,10 @@ const ContainerTabTriggerContent = ({ tab }: { tab: ContainerTab }) => {
return <AccountLogoutTab />;
}
if (tab.path === SpecialContainerTabPath.WorkspaceStorage) {
return <WorkspaceStorageTab />;
}
return match(getIdType(tab.path))
.with(IdType.Space, () => <SpaceContainerTab spaceId={tab.path} />)
.with(IdType.Channel, () => (

View File

@@ -3,14 +3,14 @@ import { ChatSidebarItem } from '@colanode/ui/components/chats/chat-sidebar-item
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
import { useLayout } from '@colanode/ui/contexts/layout';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { cn } from '@colanode/ui/lib/utils';
export const SidebarChats = () => {
const workspace = useWorkspace();
const layout = useLayout();
const chatListQuery = useQuery({
const chatListQuery = useLiveQuery({
type: 'chat.list',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -15,7 +15,7 @@ import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { AccountContext, useAccount } from '@colanode/ui/contexts/account';
import { useApp } from '@colanode/ui/contexts/app';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export function SidebarMenuFooter() {
const app = useApp();
@@ -23,7 +23,7 @@ export function SidebarMenuFooter() {
const radar = useRadar();
const [open, setOpen] = useState(false);
const accountListQuery = useQuery({
const accountListQuery = useLiveQuery({
type: 'account.list',
});

View File

@@ -14,7 +14,7 @@ import { UnreadBadge } from '@colanode/ui/components/ui/unread-badge';
import { useAccount } from '@colanode/ui/contexts/account';
import { useRadar } from '@colanode/ui/contexts/radar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const SidebarMenuHeader = () => {
const workspace = useWorkspace();
@@ -22,7 +22,7 @@ export const SidebarMenuHeader = () => {
const radar = useRadar();
const [open, setOpen] = useState(false);
const workspaceListQuery = useQuery({
const workspaceListQuery = useLiveQuery({
type: 'workspace.list',
accountId: account.id,
});

View File

@@ -1,4 +1,4 @@
import { LogOut, Settings, Users } from 'lucide-react';
import { Cylinder, LogOut, Settings, Users } from 'lucide-react';
import { SpecialContainerTabPath } from '@colanode/client/types';
import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-header';
@@ -20,6 +20,11 @@ export const SidebarSettings = () => {
icon={Users}
path={SpecialContainerTabPath.WorkspaceUsers}
/>
<SidebarSettingsItem
title="Storage"
icon={Cylinder}
path={SpecialContainerTabPath.WorkspaceStorage}
/>
</div>
<div className="flex w-full min-w-0 flex-col gap-1">
<SidebarHeader title="Account settings" />

View File

@@ -2,14 +2,14 @@ import { SidebarHeader } from '@colanode/ui/components/layouts/sidebars/sidebar-
import { SpaceCreateButton } from '@colanode/ui/components/spaces/space-create-button';
import { SpaceSidebarItem } from '@colanode/ui/components/spaces/space-sidebar-item';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
export const SidebarSpaces = () => {
const workspace = useWorkspace();
const canCreateSpace =
workspace.role !== 'guest' && workspace.role !== 'none';
const spaceListQuery = useQuery({
const spaceListQuery = useLiveQuery({
type: 'space.list',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -1,7 +1,7 @@
import { LocalMessageNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface MessageAuthorAvatarProps {
message: LocalMessageNode;
@@ -13,7 +13,7 @@ export const MessageAuthorAvatar = ({
className,
}: MessageAuthorAvatarProps) => {
const workspace = useWorkspace();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -1,6 +1,6 @@
import { LocalMessageNode } from '@colanode/client/types';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { cn } from '@colanode/ui/lib/utils';
interface MessageAuthorNameProps {
@@ -14,7 +14,7 @@ export const MessageAuthorName = ({
}: MessageAuthorNameProps) => {
const workspace = useWorkspace();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -6,7 +6,7 @@ import { compareString } from '@colanode/core';
import { Message } from '@colanode/ui/components/messages/message';
import { useConversation } from '@colanode/ui/contexts/conversation';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const MESSAGES_PER_PAGE = 50;
@@ -28,7 +28,7 @@ export const MessageList = () => {
count: MESSAGES_PER_PAGE,
}));
const result = useQueries(inputs);
const result = useLiveQueries(inputs);
const messages = result
.flatMap((data) => data.data ?? [])
.sort((a, b) => compareString(a.id, b.id));

View File

@@ -1,5 +1,5 @@
import { EmojiElement } from '@colanode/ui/components/emojis/emoji-element';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface MessageQuickReactionProps {
emoji: string;
@@ -10,7 +10,7 @@ export const MessageQuickReaction = ({
emoji,
onClick,
}: MessageQuickReactionProps) => {
const emojiGetQuery = useQuery({
const emojiGetQuery = useLiveQuery({
type: 'emoji.get',
id: emoji,
});

View File

@@ -1,8 +1,8 @@
import { NodeReactionCount, LocalMessageNode } from '@colanode/client/types';
import { EmojiElement } from '@colanode/ui/components/emojis/emoji-element';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface MessageReactionCountTooltipContentProps {
message: LocalMessageNode;
@@ -15,12 +15,12 @@ export const MessageReactionCountTooltipContent = ({
}: MessageReactionCountTooltipContentProps) => {
const workspace = useWorkspace();
const emojiGetQuery = useQuery({
const emojiGetQuery = useLiveQuery({
type: 'emoji.get.by.skin.id',
id: reactionCount.reaction,
});
const nodeReactionListQuery = useQuery({
const nodeReactionListQuery = useLiveQuery({
type: 'node.reaction.list',
nodeId: message.id,
reaction: reactionCount.reaction,
@@ -34,7 +34,7 @@ export const MessageReactionCountTooltipContent = ({
nodeReactionListQuery.data?.map((reaction) => reaction.collaboratorId) ??
[];
const results = useQueries(
const results = useLiveQueries(
userIds.map((userId) => ({
type: 'user.get',
accountId: workspace.accountId,

View File

@@ -5,7 +5,7 @@ import { NodeReactionListQueryInput } from '@colanode/client/queries';
import { NodeReactionCount, LocalMessageNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
const REACTIONS_PER_PAGE = 20;
@@ -33,7 +33,7 @@ export const MessageReactionCountsDialogList = ({
count: REACTIONS_PER_PAGE,
}));
const result = useQueries(inputs);
const result = useLiveQueries(inputs);
const reactions = result.flatMap((data) => data.data ?? []);
const isPending = result.some((data) => data.isPending);
const hasMore =
@@ -41,7 +41,7 @@ export const MessageReactionCountsDialogList = ({
const userIds = reactions?.map((reaction) => reaction.collaboratorId) ?? [];
const results = useQueries(
const results = useLiveQueries(
userIds.map((userId) => ({
type: 'user.get',
accountId: workspace.accountId,

View File

@@ -6,8 +6,8 @@ import { EmojiElement } from '@colanode/ui/components/emojis/emoji-element';
import { MessageReactionCountTooltip } from '@colanode/ui/components/messages/message-reaction-count-tooltip';
import { MessageReactionCountsDialog } from '@colanode/ui/components/messages/message-reaction-counts-dialog';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
import { useMutation } from '@colanode/ui/hooks/use-mutation';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { cn } from '@colanode/ui/lib/utils';
interface MessageReactionCountsProps {
@@ -22,7 +22,7 @@ export const MessageReactionCounts = ({
const { mutate, isPending } = useMutation();
const nodeReactionsAggregateQuery = useQuery({
const nodeReactionsAggregateQuery = useLiveQuery({
type: 'node.reactions.aggregate',
nodeId: message.id,
accountId: workspace.accountId,

View File

@@ -3,7 +3,7 @@ import { MessageAuthorAvatar } from '@colanode/ui/components/messages/message-au
import { MessageAuthorName } from '@colanode/ui/components/messages/message-author-name';
import { MessageContent } from '@colanode/ui/components/messages/message-content';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface MessageReferenceProps {
messageId: string;
@@ -11,7 +11,7 @@ interface MessageReferenceProps {
export const MessageReference = ({ messageId }: MessageReferenceProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: messageId,
accountId: workspace.accountId,

View File

@@ -2,7 +2,7 @@ import { CircleX } from 'lucide-react';
import { LocalMessageNode } from '@colanode/client/types';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface MessageReplyBannerProps {
message: LocalMessageNode;
@@ -14,7 +14,7 @@ export const MessageReplyBanner = ({
onCancel,
}: MessageReplyBannerProps) => {
const workspace = useWorkspace();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -1,7 +1,7 @@
import { LocalPageNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface PageContainerTabProps {
pageId: string;
@@ -10,7 +10,7 @@ interface PageContainerTabProps {
export const PageContainerTab = ({ pageId }: PageContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: pageId,
accountId: workspace.accountId,

View File

@@ -1,13 +1,13 @@
import { getIdType, IdType } from '@colanode/core';
import { RadarContext } from '@colanode/ui/contexts/radar';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RadarProviderProps {
children: React.ReactNode;
}
export const RadarProvider = ({ children }: RadarProviderProps) => {
const radarDataQuery = useQuery({
const radarDataQuery = useLiveQuery({
type: 'radar.data.get',
});

View File

@@ -1,7 +1,7 @@
import { LocalRecordNode } from '@colanode/client/types';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordContainerTabProps {
recordId: string;
@@ -10,7 +10,7 @@ interface RecordContainerTabProps {
export const RecordContainerTab = ({ recordId }: RecordContainerTabProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
nodeId: recordId,
accountId: workspace.accountId,

View File

@@ -2,7 +2,7 @@ import { LocalDatabaseNode } from '@colanode/client/types';
import { NodeRole } from '@colanode/core';
import { Database } from '@colanode/ui/components/databases/database';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordDatabaseProps {
id: string;
@@ -13,7 +13,7 @@ interface RecordDatabaseProps {
export const RecordDatabase = ({ id, role, children }: RecordDatabaseProps) => {
const workspace = useWorkspace();
const nodeGetQuery = useQuery({
const nodeGetQuery = useLiveQuery({
type: 'node.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -11,7 +11,7 @@ import {
CommandList,
} from '@colanode/ui/components/ui/command';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordSearchProps {
exclude?: string[];
@@ -27,7 +27,7 @@ export const RecordSearch = ({
const workspace = useWorkspace();
const [query, setQuery] = useState('');
const recordSearchQuery = useQuery({
const recordSearchQuery = useLiveQuery({
type: 'record.search',
searchQuery: query,
accountId: workspace.accountId,

View File

@@ -14,7 +14,7 @@ import { Separator } from '@colanode/ui/components/ui/separator';
import { UserSearch } from '@colanode/ui/components/users/user-search';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
interface RecordCollaboratorValueProps {
field: CollaboratorFieldAttributes;
@@ -45,7 +45,7 @@ export const RecordCollaboratorValue = ({
const [open, setOpen] = useState(false);
const collaboratorIds = record.getCollaboratorValue(field) ?? [];
const results = useQueries(
const results = useLiveQueries(
collaboratorIds.map((id) => ({
type: 'user.get',
userId: id,

View File

@@ -2,7 +2,7 @@ import { CreatedByFieldAttributes } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordCreatedByValueProps {
field: CreatedByFieldAttributes;
@@ -11,7 +11,7 @@ interface RecordCreatedByValueProps {
export const RecordCreatedByValue = ({ field }: RecordCreatedByValueProps) => {
const workspace = useWorkspace();
const record = useRecord();
const userGetQuery = useQuery({
const userGetQuery = useLiveQuery({
type: 'user.get',
accountId: workspace.accountId,
workspaceId: workspace.id,

View File

@@ -13,7 +13,7 @@ import {
import { Separator } from '@colanode/ui/components/ui/separator';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQueries } from '@colanode/ui/hooks/use-queries';
import { useLiveQueries } from '@colanode/ui/hooks/use-live-queries';
interface RecordRelationValueProps {
field: RelationFieldAttributes;
@@ -45,7 +45,7 @@ export const RecordRelationValue = ({
const [open, setOpen] = useState(false);
const relationIds = record.getRelationValue(field) ?? [];
const results = useQueries(
const results = useLiveQueries(
relationIds.map((id) => ({
type: 'node.get',
nodeId: id,

View File

@@ -4,7 +4,7 @@ import { UpdatedByFieldAttributes } from '@colanode/core';
import { Avatar } from '@colanode/ui/components/avatars/avatar';
import { useRecord } from '@colanode/ui/contexts/record';
import { useWorkspace } from '@colanode/ui/contexts/workspace';
import { useQuery } from '@colanode/ui/hooks/use-query';
import { useLiveQuery } from '@colanode/ui/hooks/use-live-query';
interface RecordUpdatedByValueProps {
field: UpdatedByFieldAttributes;
@@ -14,7 +14,7 @@ export const RecordUpdatedByValue = ({ field }: RecordUpdatedByValueProps) => {
const workspace = useWorkspace();
const record = useRecord();
const { data } = useQuery(
const { data } = useLiveQuery(
{
type: 'user.get',
accountId: workspace.accountId,

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