mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
Workspace storage limits (#140)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
import { workspaceStorageGetRoute } from './workspace-storage-get';
|
||||
|
||||
export const storageRoutes: FastifyPluginCallback = (instance, _, done) => {
|
||||
instance.register(workspaceStorageGetRoute);
|
||||
|
||||
done();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
134
packages/client/src/handlers/queries/users/user-storage-get.ts
Normal file
134
packages/client/src/handlers/queries/users/user-storage-get.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
20
packages/client/src/mutations/users/user-storage-update.ts
Normal file
20
packages/client/src/mutations/users/user-storage-update.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
25
packages/client/src/queries/users/user-storage-get.ts
Normal file
25
packages/client/src/queries/users/user-storage-get.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -38,3 +38,4 @@ export * from './types/mentions';
|
||||
export * from './types/avatars';
|
||||
export * from './types/build';
|
||||
export * from './lib/servers';
|
||||
export * from './types/storage';
|
||||
|
||||
@@ -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 Number’s 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> = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
31
packages/core/src/types/storage.ts
Normal file
31
packages/core/src/types/storage.ts
Normal 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
|
||||
>;
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,11 +13,7 @@ export const AccountLogout = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<AccountLogoutBreadcrumb />
|
||||
</ContainerHeader>
|
||||
<ContainerBody className="max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
<ContainerBody className="max-w-4xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Logout</h2>
|
||||
@@ -33,8 +24,8 @@ export const AccountLogout = () => {
|
||||
<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.
|
||||
pending changes, they will be lost. If you login again, all the
|
||||
data will be re-synced.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
@@ -60,7 +51,6 @@ export const AccountLogout = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,11 +9,7 @@ export const AccountSettings = () => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContainerHeader>
|
||||
<AccountSettingsBreadcrumb />
|
||||
</ContainerHeader>
|
||||
<ContainerBody className="max-w-4xl">
|
||||
<div className="space-y-8">
|
||||
<ContainerBody className="max-w-4xl space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold tracking-tight">General</h2>
|
||||
@@ -36,7 +27,6 @@ export const AccountSettings = () => {
|
||||
</div>
|
||||
<AccountDelete />
|
||||
</div>
|
||||
</div>
|
||||
</ContainerBody>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ const GoogleLoginButton = ({ context, onSuccess }: GoogleLoginProps) => {
|
||||
|
||||
const login = useGoogleLogin({
|
||||
onSuccess: async (response) => {
|
||||
console.log('response', response);
|
||||
mutate({
|
||||
input: {
|
||||
type: 'google.login',
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} />)
|
||||
|
||||
@@ -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, () => (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user