mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 19:57:46 +01:00
Remove react router
This commit is contained in:
@@ -104,7 +104,6 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"semver": "^7.7.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Migration } from 'kysely';
|
||||
|
||||
export const createMetadataTable: Migration = {
|
||||
up: async (db) => {
|
||||
await db.schema
|
||||
.createTable('metadata')
|
||||
.addColumn('key', 'text', (col) => col.notNull().primaryKey())
|
||||
.addColumn('value', 'text', (col) => col.notNull())
|
||||
.addColumn('created_at', 'text', (col) => col.notNull())
|
||||
.addColumn('updated_at', 'text')
|
||||
.execute();
|
||||
},
|
||||
down: async (db) => {
|
||||
await db.schema.dropTable('metadata').execute();
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Migration } from 'kysely';
|
||||
|
||||
import { createWorkspacesTable } from './00001-create-workspaces-table';
|
||||
import { createMetadataTable } from './00002-create-metadata-table';
|
||||
|
||||
export const accountDatabaseMigrations: Record<string, Migration> = {
|
||||
'00001-create-workspaces-table': createWorkspacesTable,
|
||||
'00002-create-metadata-table': createMetadataTable,
|
||||
};
|
||||
|
||||
@@ -18,6 +18,18 @@ export type SelectWorkspace = Selectable<WorkspaceTable>;
|
||||
export type CreateWorkspace = Insertable<WorkspaceTable>;
|
||||
export type UpdateWorkspace = Updateable<WorkspaceTable>;
|
||||
|
||||
interface AccountMetadataTable {
|
||||
key: ColumnType<string, string, never>;
|
||||
value: ColumnType<string, string, string>;
|
||||
created_at: ColumnType<string, string, never>;
|
||||
updated_at: ColumnType<string | null, string | null, string | null>;
|
||||
}
|
||||
|
||||
export type SelectAccountMetadata = Selectable<AccountMetadataTable>;
|
||||
export type CreateAccountMetadata = Insertable<AccountMetadataTable>;
|
||||
export type UpdateAccountMetadata = Updateable<AccountMetadataTable>;
|
||||
|
||||
export interface AccountDatabaseSchema {
|
||||
workspaces: WorkspaceTable;
|
||||
metadata: AccountMetadataTable;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
} from '@/main/databases/app';
|
||||
import { SelectEmoji } from '@/main/databases/emojis';
|
||||
import { SelectIcon } from '@/main/databases/icons';
|
||||
import { SelectWorkspace } from '@/main/databases/account';
|
||||
import {
|
||||
SelectAccountMetadata,
|
||||
SelectWorkspace,
|
||||
} from '@/main/databases/account';
|
||||
import {
|
||||
SelectFileState,
|
||||
SelectMutation,
|
||||
@@ -20,7 +23,11 @@ import {
|
||||
SelectDocumentState,
|
||||
SelectDocumentUpdate,
|
||||
} from '@/main/databases/workspace';
|
||||
import { Account } from '@/shared/types/accounts';
|
||||
import {
|
||||
Account,
|
||||
AccountMetadata,
|
||||
AccountMetadataKey,
|
||||
} from '@/shared/types/accounts';
|
||||
import { Server } from '@/shared/types/servers';
|
||||
import { User } from '@/shared/types/users';
|
||||
import { FileState } from '@/shared/types/files';
|
||||
@@ -223,6 +230,17 @@ export const mapAppMetadata = (row: SelectAppMetadata): AppMetadata => {
|
||||
};
|
||||
};
|
||||
|
||||
export const mapAccountMetadata = (
|
||||
row: SelectAccountMetadata
|
||||
): AccountMetadata => {
|
||||
return {
|
||||
key: row.key as AccountMetadataKey,
|
||||
value: JSON.parse(row.value),
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
export const mapWorkspaceMetadata = (
|
||||
row: SelectWorkspaceMetadata
|
||||
): WorkspaceMetadata => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { mapAccountMetadata } from '@/main/lib/mappers';
|
||||
import { appService } from '@/main/services/app-service';
|
||||
import {
|
||||
AccountMetadataDeleteMutationInput,
|
||||
AccountMetadataDeleteMutationOutput,
|
||||
} from '@/shared/mutations/accounts/account-metadata-delete';
|
||||
|
||||
export class AccountMetadataDeleteMutationHandler
|
||||
implements MutationHandler<AccountMetadataDeleteMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: AccountMetadataDeleteMutationInput
|
||||
): Promise<AccountMetadataDeleteMutationOutput> {
|
||||
const account = appService.getAccount(input.accountId);
|
||||
|
||||
if (!account) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const deletedMetadata = await account.database
|
||||
.deleteFrom('metadata')
|
||||
.where('key', '=', input.key)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!deletedMetadata) {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'account_metadata_deleted',
|
||||
accountId: input.accountId,
|
||||
metadata: mapAccountMetadata(deletedMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { mapAccountMetadata } from '@/main/lib/mappers';
|
||||
import { appService } from '@/main/services/app-service';
|
||||
import {
|
||||
AccountMetadataSaveMutationInput,
|
||||
AccountMetadataSaveMutationOutput,
|
||||
} from '@/shared/mutations/accounts/account-metadata-save';
|
||||
|
||||
export class AccountMetadataSaveMutationHandler
|
||||
implements MutationHandler<AccountMetadataSaveMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: AccountMetadataSaveMutationInput
|
||||
): Promise<AccountMetadataSaveMutationOutput> {
|
||||
const account = appService.getAccount(input.accountId);
|
||||
|
||||
if (!account) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const createdMetadata = await account.database
|
||||
.insertInto('metadata')
|
||||
.returningAll()
|
||||
.values({
|
||||
key: input.key,
|
||||
value: JSON.stringify(input.value),
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
.onConflict((cb) =>
|
||||
cb.columns(['key']).doUpdateSet({
|
||||
value: JSON.stringify(input.value),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!createdMetadata) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'account_metadata_saved',
|
||||
accountId: input.accountId,
|
||||
metadata: mapAccountMetadata(createdMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
37
apps/desktop/src/main/mutations/apps/app-metadata-delete.ts
Normal file
37
apps/desktop/src/main/mutations/apps/app-metadata-delete.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { mapAppMetadata } from '@/main/lib/mappers';
|
||||
import { appService } from '@/main/services/app-service';
|
||||
import {
|
||||
AppMetadataDeleteMutationInput,
|
||||
AppMetadataDeleteMutationOutput,
|
||||
} from '@/shared/mutations/apps/app-metadata-delete';
|
||||
|
||||
export class AppMetadataDeleteMutationHandler
|
||||
implements MutationHandler<AppMetadataDeleteMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: AppMetadataDeleteMutationInput
|
||||
): Promise<AppMetadataDeleteMutationOutput> {
|
||||
const deletedMetadata = await appService.database
|
||||
.deleteFrom('metadata')
|
||||
.where('key', '=', input.key)
|
||||
.returningAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!deletedMetadata) {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'app_metadata_deleted',
|
||||
metadata: mapAppMetadata(deletedMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
47
apps/desktop/src/main/mutations/apps/app-metadata-save.ts
Normal file
47
apps/desktop/src/main/mutations/apps/app-metadata-save.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import {
|
||||
AppMetadataSaveMutationInput,
|
||||
AppMetadataSaveMutationOutput,
|
||||
} from '@/shared/mutations/apps/app-metadata-save';
|
||||
import { eventBus } from '@/shared/lib/event-bus';
|
||||
import { mapAppMetadata } from '@/main/lib/mappers';
|
||||
import { appService } from '@/main/services/app-service';
|
||||
|
||||
export class AppMetadataSaveMutationHandler
|
||||
implements MutationHandler<AppMetadataSaveMutationInput>
|
||||
{
|
||||
async handleMutation(
|
||||
input: AppMetadataSaveMutationInput
|
||||
): Promise<AppMetadataSaveMutationOutput> {
|
||||
const createdMetadata = await appService.database
|
||||
.insertInto('metadata')
|
||||
.returningAll()
|
||||
.values({
|
||||
key: input.key,
|
||||
value: JSON.stringify(input.value),
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
.onConflict((cb) =>
|
||||
cb.columns(['key']).doUpdateSet({
|
||||
value: JSON.stringify(input.value),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!createdMetadata) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'app_metadata_saved',
|
||||
metadata: mapAppMetadata(createdMetadata),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,10 @@ import { UsersInviteMutationHandler } from '@/main/mutations/users/users-invite'
|
||||
import { WorkspaceMetadataSaveMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-save';
|
||||
import { WorkspaceMetadataDeleteMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-delete';
|
||||
import { DocumentUpdateMutationHandler } from '@/main/mutations/documents/document-update';
|
||||
import { AppMetadataSaveMutationHandler } from '@/main/mutations/apps/app-metadata-save';
|
||||
import { AppMetadataDeleteMutationHandler } from '@/main/mutations/apps/app-metadata-delete';
|
||||
import { AccountMetadataSaveMutationHandler } from '@/main/mutations/accounts/account-metadata-save';
|
||||
import { AccountMetadataDeleteMutationHandler } from '@/main/mutations/accounts/account-metadata-delete';
|
||||
import { MutationHandler } from '@/main/lib/types';
|
||||
import { MutationMap } from '@/shared/mutations';
|
||||
|
||||
@@ -128,4 +132,8 @@ export const mutationHandlerMap: MutationHandlerMap = {
|
||||
workspace_metadata_save: new WorkspaceMetadataSaveMutationHandler(),
|
||||
workspace_metadata_delete: new WorkspaceMetadataDeleteMutationHandler(),
|
||||
document_update: new DocumentUpdateMutationHandler(),
|
||||
app_metadata_save: new AppMetadataSaveMutationHandler(),
|
||||
app_metadata_delete: new AppMetadataDeleteMutationHandler(),
|
||||
account_metadata_save: new AccountMetadataSaveMutationHandler(),
|
||||
account_metadata_delete: new AccountMetadataDeleteMutationHandler(),
|
||||
};
|
||||
|
||||
@@ -20,12 +20,12 @@ export class WorkspaceMetadataSaveMutationHandler
|
||||
.returningAll()
|
||||
.values({
|
||||
key: input.key,
|
||||
value: input.value,
|
||||
value: JSON.stringify(input.value),
|
||||
created_at: new Date().toISOString(),
|
||||
})
|
||||
.onConflict((cb) =>
|
||||
cb.columns(['key']).doUpdateSet({
|
||||
value: input.value,
|
||||
value: JSON.stringify(input.value),
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
)
|
||||
@@ -38,7 +38,7 @@ export class WorkspaceMetadataSaveMutationHandler
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'workspace_metadata_updated',
|
||||
type: 'workspace_metadata_saved',
|
||||
accountId: input.accountId,
|
||||
workspaceId: input.workspaceId,
|
||||
metadata: mapWorkspaceMetadata(createdMetadata),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ChangeCheckResult, QueryHandler } from '@/main/lib/types';
|
||||
import { mapAccountMetadata } from '@/main/lib/mappers';
|
||||
import { AccountMetadataListQueryInput } from '@/shared/queries/accounts/account-metadata-list';
|
||||
import { Event } from '@/shared/types/events';
|
||||
import { AccountMetadata } from '@/shared/types/accounts';
|
||||
import { SelectAccountMetadata } from '@/main/databases/account/schema';
|
||||
import { appService } from '@/main/services/app-service';
|
||||
|
||||
export class AccountMetadataListQueryHandler
|
||||
implements QueryHandler<AccountMetadataListQueryInput>
|
||||
{
|
||||
public async handleQuery(
|
||||
input: AccountMetadataListQueryInput
|
||||
): Promise<AccountMetadata[]> {
|
||||
const rows = await this.getAccountMetadata(input.accountId);
|
||||
if (!rows) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rows.map(mapAccountMetadata);
|
||||
}
|
||||
|
||||
public async checkForChanges(
|
||||
event: Event,
|
||||
input: AccountMetadataListQueryInput,
|
||||
output: AccountMetadata[]
|
||||
): Promise<ChangeCheckResult<AccountMetadataListQueryInput>> {
|
||||
if (
|
||||
event.type === 'account_created' &&
|
||||
event.account.id === input.accountId
|
||||
) {
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'account_metadata_saved' &&
|
||||
event.accountId === input.accountId
|
||||
) {
|
||||
const newOutput = [
|
||||
...output.filter((metadata) => metadata.key !== event.metadata.key),
|
||||
event.metadata,
|
||||
];
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newOutput,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'account_metadata_deleted' &&
|
||||
event.accountId === input.accountId
|
||||
) {
|
||||
const newOutput = output.filter(
|
||||
(metadata) => metadata.key !== event.metadata.key
|
||||
);
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
result: newOutput,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasChanges: false,
|
||||
};
|
||||
}
|
||||
|
||||
private async getAccountMetadata(
|
||||
accountId: string
|
||||
): Promise<SelectAccountMetadata[] | undefined> {
|
||||
const account = appService.getAccount(accountId);
|
||||
|
||||
if (!account) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rows = await account.database
|
||||
.selectFrom('metadata')
|
||||
.selectAll()
|
||||
.execute();
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,11 @@ export class AppMetadataListQueryHandler
|
||||
_: AppMetadataListQueryInput,
|
||||
output: AppMetadata[]
|
||||
): Promise<ChangeCheckResult<AppMetadataListQueryInput>> {
|
||||
if (event.type === 'app_metadata_updated') {
|
||||
const newOutput = output.map((metadata) => {
|
||||
if (metadata.key === event.metadata.key) {
|
||||
return event.metadata;
|
||||
}
|
||||
return metadata;
|
||||
});
|
||||
if (event.type === 'app_metadata_saved') {
|
||||
const newOutput = [
|
||||
...output.filter((metadata) => metadata.key !== event.metadata.key),
|
||||
event.metadata,
|
||||
];
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
|
||||
@@ -34,6 +34,7 @@ import { ChatListQueryHandler } from '@/main/queries/chats/chat-list';
|
||||
import { DocumentGetQueryHandler } from '@/main/queries/documents/document-get';
|
||||
import { DocumentStateGetQueryHandler } from '@/main/queries/documents/document-state-get';
|
||||
import { DocumentUpdatesListQueryHandler } from '@/main/queries/documents/document-update-list';
|
||||
import { AccountMetadataListQueryHandler } from '@/main/queries/accounts/account-metadata-list';
|
||||
import { WorkspaceMetadataListQueryHandler } from '@/main/queries/workspaces/workspace-metadata-list';
|
||||
import { QueryHandler } from '@/main/lib/types';
|
||||
import { QueryMap } from '@/shared/queries';
|
||||
@@ -80,4 +81,5 @@ export const queryHandlerMap: QueryHandlerMap = {
|
||||
document_get: new DocumentGetQueryHandler(),
|
||||
document_state_get: new DocumentStateGetQueryHandler(),
|
||||
document_updates_list: new DocumentUpdatesListQueryHandler(),
|
||||
account_metadata_list: new AccountMetadataListQueryHandler(),
|
||||
};
|
||||
|
||||
@@ -41,16 +41,14 @@ export class WorkspaceMetadataListQueryHandler
|
||||
}
|
||||
|
||||
if (
|
||||
event.type === 'workspace_metadata_updated' &&
|
||||
event.type === 'workspace_metadata_saved' &&
|
||||
event.accountId === input.accountId &&
|
||||
event.workspaceId === input.workspaceId
|
||||
) {
|
||||
const newOutput = output.map((metadata) => {
|
||||
if (metadata.key === event.metadata.key) {
|
||||
return event.metadata;
|
||||
}
|
||||
return metadata;
|
||||
});
|
||||
const newOutput = [
|
||||
...output.filter((metadata) => metadata.key !== event.metadata.key),
|
||||
event.metadata,
|
||||
];
|
||||
|
||||
return {
|
||||
hasChanges: true,
|
||||
|
||||
@@ -69,7 +69,7 @@ export class MetadataService {
|
||||
}
|
||||
|
||||
eventBus.publish({
|
||||
type: 'app_metadata_updated',
|
||||
type: 'app_metadata_saved',
|
||||
metadata: mapAppMetadata(createdMetadata),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
|
||||
import { AppContext } from './contexts/app';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { AppContext } from '@/renderer/contexts/app';
|
||||
import { DelayedComponent } from '@/renderer/components/ui/delayed-component';
|
||||
import { AppLoader } from '@/renderer/app-loader';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { RadarProvider } from '@/renderer/radar-provider';
|
||||
import { Account } from '@/renderer/components/accounts/account';
|
||||
import { Login } from '@/renderer/components/accounts/login';
|
||||
|
||||
export const App = () => {
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [openLogin, setOpenLogin] = useState(false);
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
const { data: metadata, isPending: isPendingMetadata } = useQuery({
|
||||
type: 'app_metadata_list',
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const { data: accounts, isPending: isPendingAccounts } = useQuery({
|
||||
type: 'account_list',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.colanode.init().then(() => {
|
||||
setInitialized(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!initialized || isPending) {
|
||||
if (!initialized || isPendingMetadata || isPendingAccounts) {
|
||||
return (
|
||||
<DelayedComponent>
|
||||
<AppLoader />
|
||||
@@ -29,16 +34,51 @@ export const App = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const accountMetadata = metadata?.find(
|
||||
(metadata) => metadata.key === 'account'
|
||||
);
|
||||
|
||||
const account =
|
||||
accounts?.find((account) => account.id === accountMetadata?.value) ||
|
||||
accounts?.[0];
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
getMetadata: (key: string) => {
|
||||
return data?.find((metadata) => metadata.key === key)?.value;
|
||||
getMetadata: (key) => {
|
||||
return metadata?.find((metadata) => metadata.key === key)?.value;
|
||||
},
|
||||
setMetadata: (key, value) => {
|
||||
window.colanode.executeMutation({
|
||||
type: 'app_metadata_save',
|
||||
key,
|
||||
value,
|
||||
});
|
||||
},
|
||||
deleteMetadata: (key: string) => {
|
||||
window.colanode.executeMutation({
|
||||
type: 'app_metadata_delete',
|
||||
key,
|
||||
});
|
||||
},
|
||||
openLogin: () => setOpenLogin(true),
|
||||
closeLogin: () => setOpenLogin(false),
|
||||
openAccount: (id: string) => {
|
||||
setOpenLogin(false);
|
||||
window.colanode.executeMutation({
|
||||
type: 'app_metadata_save',
|
||||
key: 'account',
|
||||
value: id,
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RadarProvider>
|
||||
<Outlet />
|
||||
{!openLogin && account ? (
|
||||
<Account key={account.id} account={account} />
|
||||
) : (
|
||||
<Login />
|
||||
)}
|
||||
</RadarProvider>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { Button } from '@/renderer/components/ui/button';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { AccountContext } from '@/renderer/contexts/account';
|
||||
|
||||
export const AccountNotFound = () => {
|
||||
const navigate = useNavigate();
|
||||
const { data } = useQuery({
|
||||
type: 'account_list',
|
||||
});
|
||||
|
||||
const accounts = data ?? [];
|
||||
return (
|
||||
<div className="grid h-screen min-h-screen w-full grid-cols-5">
|
||||
<div className="col-span-2 flex items-center justify-center bg-zinc-950">
|
||||
<h1 className="font-neotrax text-8xl text-white">404</h1>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center justify-center py-12">
|
||||
<div className="mx-auto grid w-96 gap-6">
|
||||
<div className="grid gap-4 text-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Account not found
|
||||
</h1>
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
You have been logged out or your login has expired.
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
{accounts.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Continue with one of your existing accounts
|
||||
</p>
|
||||
<div className="flex flex-row items-center justify-center gap-4">
|
||||
{accounts.map((account) => (
|
||||
<AccountContext.Provider
|
||||
key={account.id}
|
||||
value={{
|
||||
...account,
|
||||
openSettings: () => {},
|
||||
openLogout: () => {},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-full flex items-center gap-2 text-left text-sm border border-gray-100 rounded-lg p-2 hover:bg-gray-100 hover:cursor-pointer"
|
||||
onClick={() => navigate(`/${account.id}`)}
|
||||
>
|
||||
<Avatar
|
||||
className="size-8 rounded-lg"
|
||||
id={account.id}
|
||||
name={account.name}
|
||||
avatar={account.avatar}
|
||||
/>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{account.name}
|
||||
</span>
|
||||
<span className="truncate text-xs">
|
||||
{account.email}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccountContext.Provider>
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Or login with your email
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
<Mail className="mr-2 size-4" />
|
||||
Login
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You need to login to continue
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => navigate('/login')}
|
||||
>
|
||||
<Mail className="mr-2 size-4" />
|
||||
Login
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const AccountRedirect = (): React.ReactNode => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
window.colanode
|
||||
.executeQuery({
|
||||
type: 'account_list',
|
||||
})
|
||||
.then((data) => {
|
||||
const firstAccount = data?.[0];
|
||||
|
||||
if (firstAccount) {
|
||||
navigate(`/${firstAccount.id}`);
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,42 +1,86 @@
|
||||
import React from 'react';
|
||||
import { Outlet, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { AccountLogout } from '@/renderer/components/accounts/account-logout';
|
||||
import { AccountSettingsDialog } from '@/renderer/components/accounts/account-settings-dialog';
|
||||
import { AccountContext } from '@/renderer/contexts/account';
|
||||
import { AccountNotFound } from '@/renderer/components/accounts/account-not-found';
|
||||
import { Account as AccountType } from '@/shared/types/accounts';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { WorkspaceCreate } from '@/renderer/components/workspaces/workspace-create';
|
||||
import { Workspace } from '@/renderer/components/workspaces/workspace';
|
||||
|
||||
export const Account = () => {
|
||||
const { accountId } = useParams<{ accountId: string }>();
|
||||
const navigate = useNavigate();
|
||||
interface AccountProps {
|
||||
account: AccountType;
|
||||
}
|
||||
|
||||
export const Account = ({ account }: AccountProps) => {
|
||||
const [openSettings, setOpenSettings] = React.useState(false);
|
||||
const [openLogout, setOpenLogout] = React.useState(false);
|
||||
const [openCreateWorkspace, setOpenCreateWorkspace] = React.useState(false);
|
||||
|
||||
const { data, isPending } = useQuery({
|
||||
type: 'account_get',
|
||||
accountId: accountId!,
|
||||
const { data: metadata, isPending: isPendingMetadata } = useQuery({
|
||||
type: 'account_metadata_list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
const { data: workspaces, isPending: isPendingWorkspaces } = useQuery({
|
||||
type: 'workspace_list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
if (isPendingMetadata || isPendingWorkspaces) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <AccountNotFound />;
|
||||
}
|
||||
const workspaceMetadata = metadata?.find(
|
||||
(metadata) => metadata.key === 'workspace'
|
||||
);
|
||||
|
||||
const workspace =
|
||||
workspaces?.find(
|
||||
(workspace) => workspace.id === workspaceMetadata?.value
|
||||
) || workspaces?.[0];
|
||||
|
||||
const handleWorkspaceCreateSuccess = (id: string) => {
|
||||
setOpenCreateWorkspace(false);
|
||||
window.colanode.executeMutation({
|
||||
type: 'account_metadata_save',
|
||||
accountId: account.id,
|
||||
key: 'workspace',
|
||||
value: id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleWorkspaceCreateCancel =
|
||||
(workspaces?.length || 0) > 0
|
||||
? () => setOpenCreateWorkspace(false)
|
||||
: undefined;
|
||||
|
||||
const account = data;
|
||||
return (
|
||||
<AccountContext.Provider
|
||||
value={{
|
||||
...account,
|
||||
openSettings: () => setOpenSettings(true),
|
||||
openLogout: () => setOpenLogout(true),
|
||||
openWorkspaceCreate: () => setOpenCreateWorkspace(true),
|
||||
openWorkspace: (id) => {
|
||||
setOpenCreateWorkspace(false);
|
||||
window.colanode.executeMutation({
|
||||
type: 'account_metadata_save',
|
||||
accountId: account.id,
|
||||
key: 'workspace',
|
||||
value: id,
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
{!openCreateWorkspace && workspace ? (
|
||||
<Workspace workspace={workspace} />
|
||||
) : (
|
||||
<WorkspaceCreate
|
||||
onSuccess={handleWorkspaceCreateSuccess}
|
||||
onCancel={handleWorkspaceCreateCancel}
|
||||
/>
|
||||
)}
|
||||
{openSettings && (
|
||||
<AccountSettingsDialog
|
||||
open={true}
|
||||
@@ -48,7 +92,6 @@ export const Account = () => {
|
||||
onCancel={() => setOpenLogout(false)}
|
||||
onLogout={() => {
|
||||
setOpenLogout(false);
|
||||
navigate('/');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useApp } from '@/renderer/contexts/app';
|
||||
import { EmailLogin } from '@/renderer/components/accounts/email-login';
|
||||
import { EmailRegister } from '@/renderer/components/accounts/email-register';
|
||||
import { EmailVerify } from '@/renderer/components/accounts/email-verify';
|
||||
@@ -31,8 +31,7 @@ type VerifyPanelState = {
|
||||
type PanelState = LoginPanelState | RegisterPanelState | VerifyPanelState;
|
||||
|
||||
export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const app = useApp();
|
||||
const [server, setServer] = React.useState<Server>(servers[0]!);
|
||||
const [panel, setPanel] = React.useState<PanelState>({
|
||||
type: 'login',
|
||||
@@ -52,8 +51,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
server={server}
|
||||
onSuccess={(output) => {
|
||||
if (output.type === 'success') {
|
||||
const userId = output.workspaces[0]?.id ?? '';
|
||||
navigate(`/${output.account.id}/${userId}`);
|
||||
app.openAccount(output.account.id);
|
||||
} else if (output.type === 'verify') {
|
||||
setPanel({
|
||||
type: 'verify',
|
||||
@@ -81,8 +79,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
server={server}
|
||||
onSuccess={(output) => {
|
||||
if (output.type === 'success') {
|
||||
const userId = output.workspaces[0]?.id ?? '';
|
||||
navigate(`/${output.account.id}/${userId}`);
|
||||
app.openAccount(output.account.id);
|
||||
} else if (output.type === 'verify') {
|
||||
setPanel({
|
||||
type: 'verify',
|
||||
@@ -113,8 +110,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
expiresAt={panel.expiresAt}
|
||||
onSuccess={(output) => {
|
||||
if (output.type === 'success') {
|
||||
const userId = output.workspaces[0]?.id ?? '';
|
||||
navigate(`/${output.account.id}/${userId}`);
|
||||
app.openAccount(output.account.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -137,7 +133,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
|
||||
<p
|
||||
className="text-center text-sm text-muted-foreground hover:cursor-pointer hover:underline"
|
||||
onClick={() => {
|
||||
navigate(-1);
|
||||
app.closeLogin();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Check, LogOut, Plus, Settings } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useApp } from '@/renderer/contexts/app';
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { NotificationBadge } from '@/renderer/components/ui/notification-badge';
|
||||
import {
|
||||
@@ -18,8 +18,8 @@ import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { AccountReadState } from '@/shared/types/radars';
|
||||
|
||||
export function SidebarMenuFooter() {
|
||||
const app = useApp();
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
const radar = useRadar();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -112,7 +112,7 @@ export function SidebarMenuFooter() {
|
||||
key={accountItem.id}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
navigate(`/${accountItem.id}`);
|
||||
app.openAccount(accountItem.id);
|
||||
}}
|
||||
>
|
||||
<AccountContext.Provider
|
||||
@@ -120,6 +120,8 @@ export function SidebarMenuFooter() {
|
||||
...accountItem,
|
||||
openSettings: () => {},
|
||||
openLogout: () => {},
|
||||
openWorkspace: () => {},
|
||||
openWorkspaceCreate: () => {},
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
@@ -157,7 +159,7 @@ export function SidebarMenuFooter() {
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
navigate(`/login`);
|
||||
app.openLogin();
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Bell, Check, Plus, Settings } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { NotificationBadge } from '@/renderer/components/ui/notification-badge';
|
||||
@@ -20,7 +19,6 @@ import { useQuery } from '@/renderer/hooks/use-query';
|
||||
export const SidebarMenuHeader = () => {
|
||||
const workspace = useWorkspace();
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
const radar = useRadar();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -104,7 +102,7 @@ export const SidebarMenuHeader = () => {
|
||||
key={workspaceItem.id}
|
||||
className="p-0"
|
||||
onClick={() => {
|
||||
navigate(`/${account.id}/${workspaceItem.id}`);
|
||||
account.openWorkspace(workspaceItem.id);
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
@@ -135,7 +133,7 @@ export const SidebarMenuHeader = () => {
|
||||
<DropdownMenuItem
|
||||
className="gap-2 p-2 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
navigate(`/${account.id}/create`);
|
||||
account.openWorkspaceCreate();
|
||||
}}
|
||||
>
|
||||
<Plus className="size-4" />
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { WorkspaceForm } from '@/renderer/components/workspaces/workspace-form';
|
||||
import { useAccount } from '@/renderer/contexts/account';
|
||||
import { useMutation } from '@/renderer/hooks/use-mutation';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { toast } from '@/renderer/hooks/use-toast';
|
||||
|
||||
export const WorkspaceCreate = () => {
|
||||
interface WorkspaceCreateProps {
|
||||
onSuccess: (id: string) => void;
|
||||
onCancel: (() => void) | undefined;
|
||||
}
|
||||
|
||||
export const WorkspaceCreate = ({
|
||||
onSuccess,
|
||||
onCancel,
|
||||
}: WorkspaceCreateProps) => {
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { mutate, isPending } = useMutation();
|
||||
|
||||
const { data } = useQuery({
|
||||
type: 'workspace_list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
const workspaces = data ?? [];
|
||||
const handleCancel = workspaces.length > 0 ? () => navigate('/') : undefined;
|
||||
|
||||
return (
|
||||
<div className="container flex flex-row justify-center">
|
||||
<div className="w-full max-w-[700px]">
|
||||
@@ -39,7 +34,7 @@ export const WorkspaceCreate = () => {
|
||||
avatar: values.avatar ?? null,
|
||||
},
|
||||
onSuccess(output) {
|
||||
navigate(`/${account.id}/${output.id}`);
|
||||
onSuccess(output.id);
|
||||
},
|
||||
onError(error) {
|
||||
toast({
|
||||
@@ -51,7 +46,7 @@ export const WorkspaceCreate = () => {
|
||||
});
|
||||
}}
|
||||
isSaving={isPending}
|
||||
onCancel={handleCancel}
|
||||
onCancel={onCancel}
|
||||
saveText="Create"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from '@/renderer/components/avatars/avatar';
|
||||
import { Button } from '@/renderer/components/ui/button';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import { useAccount } from '@/renderer/contexts/account';
|
||||
|
||||
export const WorkspaceNotFound = () => {
|
||||
const navigate = useNavigate();
|
||||
const account = useAccount();
|
||||
|
||||
const { data } = useQuery({
|
||||
type: 'workspace_list',
|
||||
accountId: account.id,
|
||||
});
|
||||
|
||||
const workspaces = data ?? [];
|
||||
return (
|
||||
<div className="grid h-screen min-h-screen w-full grid-cols-5">
|
||||
<div className="col-span-2 flex items-center justify-center bg-zinc-950">
|
||||
<h1 className="font-neotrax text-8xl text-white">404</h1>
|
||||
</div>
|
||||
<div className="col-span-3 flex items-center justify-center py-12">
|
||||
<div className="mx-auto grid w-96 gap-6">
|
||||
<div className="grid gap-4 text-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Workspace not found
|
||||
</h1>
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
It may have been deleted or your acces has been removed.
|
||||
</p>
|
||||
</div>
|
||||
<hr />
|
||||
{workspaces.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Continue with one of your existing workspaces
|
||||
</p>
|
||||
<div className="flex flex-row items-center justify-center gap-4">
|
||||
{workspaces.map((workspace) => (
|
||||
<div
|
||||
key={workspace.id}
|
||||
className="w-full flex items-center gap-2 text-left text-sm border border-gray-100 rounded-lg p-2 hover:bg-gray-100 hover:cursor-pointer"
|
||||
onClick={() => navigate(`/${account.id}/${workspace.id}`)}
|
||||
>
|
||||
<Avatar
|
||||
className="size-8 rounded-lg"
|
||||
id={workspace.id}
|
||||
name={workspace.name}
|
||||
avatar={workspace.avatar}
|
||||
/>
|
||||
<p className="grid flex-1 text-left text-sm leading-tight truncate font-semibold">
|
||||
{workspace.name}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Or create a new workspace
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => navigate(`/${account.id}/create`)}
|
||||
>
|
||||
<Plus className="mr-2 size-4" />
|
||||
Create new workspace
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create a new workspace
|
||||
</p>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => navigate(`/${account.id}/create`)}
|
||||
>
|
||||
<Plus className="mr-2 size-4" />
|
||||
Create new workspace
|
||||
</Button>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAccount } from '@/renderer/contexts/account';
|
||||
|
||||
export const WorkspaceRedirect = (): React.ReactNode => {
|
||||
const account = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
React.useEffect(() => {
|
||||
window.colanode
|
||||
.executeQuery({
|
||||
type: 'workspace_list',
|
||||
accountId: account.id,
|
||||
})
|
||||
.then((data) => {
|
||||
const workspaces = data ?? [];
|
||||
const firstWorkspace = workspaces[0];
|
||||
if (firstWorkspace) {
|
||||
navigate(`/${account.id}/${firstWorkspace.id}`);
|
||||
} else {
|
||||
navigate(`/${account.id}/create`);
|
||||
}
|
||||
});
|
||||
}, [navigate]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,47 +1,34 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Layout } from '@/renderer/components/layouts/layout';
|
||||
import { WorkspaceSettingsDialog } from '@/renderer/components/workspaces/workspace-settings-dialog';
|
||||
import { WorkspaceNotFound } from '@/renderer/components/workspaces/workspace-not-found';
|
||||
import { useAccount } from '@/renderer/contexts/account';
|
||||
import { WorkspaceContext } from '@/renderer/contexts/workspace';
|
||||
import { useQuery } from '@/renderer/hooks/use-query';
|
||||
import {
|
||||
WorkspaceMetadataKey,
|
||||
WorkspaceMetadataMap,
|
||||
Workspace as WorkspaceType,
|
||||
} from '@/shared/types/workspaces';
|
||||
|
||||
export const Workspace = () => {
|
||||
const { workspaceId } = useParams<{ workspaceId: string }>();
|
||||
interface WorkspaceProps {
|
||||
workspace: WorkspaceType;
|
||||
}
|
||||
|
||||
export const Workspace = ({ workspace }: WorkspaceProps) => {
|
||||
const account = useAccount();
|
||||
const [openSettings, setOpenSettings] = React.useState(false);
|
||||
|
||||
const { data: workspace, isPending: isPendingWorkspace } = useQuery({
|
||||
type: 'workspace_get',
|
||||
accountId: account.id,
|
||||
workspaceId: workspaceId!,
|
||||
});
|
||||
|
||||
const { data: metadata, isPending: isPendingMetadata } = useQuery({
|
||||
type: 'workspace_metadata_list',
|
||||
accountId: account.id,
|
||||
workspaceId: workspaceId!,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (isPendingWorkspace || isPendingMetadata) {
|
||||
if (isPendingMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
return <WorkspaceNotFound />;
|
||||
}
|
||||
|
||||
if (!workspace) {
|
||||
return <WorkspaceNotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkspaceContext.Provider
|
||||
value={{
|
||||
@@ -68,22 +55,22 @@ export const Workspace = () => {
|
||||
window.colanode.executeMutation({
|
||||
type: 'workspace_metadata_save',
|
||||
accountId: account.id,
|
||||
workspaceId: workspaceId!,
|
||||
workspaceId: workspace.id,
|
||||
key,
|
||||
value: JSON.stringify(value),
|
||||
value,
|
||||
});
|
||||
},
|
||||
deleteMetadata(key: string) {
|
||||
window.colanode.executeMutation({
|
||||
type: 'workspace_metadata_delete',
|
||||
accountId: account.id,
|
||||
workspaceId: workspaceId!,
|
||||
workspaceId: workspace.id,
|
||||
key,
|
||||
});
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Layout key={workspaceId} />
|
||||
<Layout key={workspace.id} />
|
||||
{openSettings && (
|
||||
<WorkspaceSettingsDialog
|
||||
open={openSettings}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Account } from '@/shared/types/accounts';
|
||||
interface AccountContext extends Account {
|
||||
openSettings: () => void;
|
||||
openLogout: () => void;
|
||||
openWorkspace: (id: string) => void;
|
||||
openWorkspaceCreate: () => void;
|
||||
}
|
||||
|
||||
export const AccountContext = createContext<AccountContext>(
|
||||
|
||||
@@ -6,6 +6,14 @@ interface AppContext {
|
||||
getMetadata: <K extends AppMetadataKey>(
|
||||
key: K
|
||||
) => AppMetadataMap[K]['value'] | undefined;
|
||||
setMetadata: <K extends AppMetadataKey>(
|
||||
key: K,
|
||||
value: AppMetadataMap[K]['value']
|
||||
) => void;
|
||||
deleteMetadata: <K extends AppMetadataKey>(key: K) => void;
|
||||
openLogin: () => void;
|
||||
closeLogin: () => void;
|
||||
openAccount: (id: string) => void;
|
||||
}
|
||||
|
||||
export const AppContext = createContext<AppContext>({} as AppContext);
|
||||
|
||||
@@ -4,56 +4,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { createHashRouter, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { HTML5Backend } from '@/shared/lib/dnd-backend';
|
||||
import { App } from '@/renderer/app';
|
||||
import { Account } from '@/renderer/components/accounts/account';
|
||||
import { AccountRedirect } from '@/renderer/components/accounts/account-redirect';
|
||||
import { Login } from '@/renderer/components/accounts/login';
|
||||
import { Toaster } from '@/renderer/components/ui/toaster';
|
||||
import { TooltipProvider } from '@/renderer/components/ui/tooltip';
|
||||
import { Workspace } from '@/renderer/components/workspaces/workspace';
|
||||
import { WorkspaceCreate } from '@/renderer/components/workspaces/workspace-create';
|
||||
import { WorkspaceRedirect } from '@/renderer/components/workspaces/workspace-redirect';
|
||||
import { useEventBus } from '@/renderer/hooks/use-event-bus';
|
||||
import { Event } from '@/shared/types/events';
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
path: '',
|
||||
element: <App />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <AccountRedirect />,
|
||||
},
|
||||
{
|
||||
path: ':accountId',
|
||||
element: <Account />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
element: <WorkspaceRedirect />,
|
||||
},
|
||||
{
|
||||
path: 'create',
|
||||
element: <WorkspaceCreate />,
|
||||
},
|
||||
{
|
||||
path: ':workspaceId',
|
||||
element: <Workspace />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
@@ -102,7 +60,7 @@ const Root = () => {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<TooltipProvider>
|
||||
<RouterProvider router={router} />
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
<Toaster />
|
||||
</DndProvider>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export type AccountMetadataDeleteMutationInput = {
|
||||
type: 'account_metadata_delete';
|
||||
accountId: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type AccountMetadataDeleteMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
account_metadata_delete: {
|
||||
input: AccountMetadataDeleteMutationInput;
|
||||
output: AccountMetadataDeleteMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
AccountMetadataKey,
|
||||
AccountMetadataMap,
|
||||
} from '@/shared/types/accounts';
|
||||
|
||||
export type AccountMetadataSaveMutationInput = {
|
||||
type: 'account_metadata_save';
|
||||
accountId: string;
|
||||
key: AccountMetadataKey;
|
||||
value: AccountMetadataMap[AccountMetadataKey]['value'];
|
||||
};
|
||||
|
||||
export type AccountMetadataSaveMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
account_metadata_save: {
|
||||
input: AccountMetadataSaveMutationInput;
|
||||
output: AccountMetadataSaveMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export type AppMetadataDeleteMutationInput = {
|
||||
type: 'app_metadata_delete';
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type AppMetadataDeleteMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
app_metadata_delete: {
|
||||
input: AppMetadataDeleteMutationInput;
|
||||
output: AppMetadataDeleteMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
20
apps/desktop/src/shared/mutations/apps/app-metadata-save.ts
Normal file
20
apps/desktop/src/shared/mutations/apps/app-metadata-save.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { AppMetadataKey, AppMetadataMap } from '@/shared/types/apps';
|
||||
|
||||
export type AppMetadataSaveMutationInput = {
|
||||
type: 'app_metadata_save';
|
||||
key: AppMetadataKey;
|
||||
value: AppMetadataMap[AppMetadataKey]['value'];
|
||||
};
|
||||
|
||||
export type AppMetadataSaveMutationOutput = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
declare module '@/shared/mutations' {
|
||||
interface MutationMap {
|
||||
app_metadata_save: {
|
||||
input: AppMetadataSaveMutationInput;
|
||||
output: AppMetadataSaveMutationOutput;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import {
|
||||
WorkspaceMetadataMap,
|
||||
WorkspaceMetadataKey,
|
||||
} from '@/shared/types/workspaces';
|
||||
|
||||
export type WorkspaceMetadataSaveMutationInput = {
|
||||
type: 'workspace_metadata_save';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
key: string;
|
||||
value: string;
|
||||
key: WorkspaceMetadataKey;
|
||||
value: WorkspaceMetadataMap[WorkspaceMetadataKey]['value'];
|
||||
};
|
||||
|
||||
export type WorkspaceMetadataSaveMutationOutput = {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AccountMetadata } from '@/shared/types/accounts';
|
||||
|
||||
export type AccountMetadataListQueryInput = {
|
||||
type: 'account_metadata_list';
|
||||
accountId: string;
|
||||
};
|
||||
|
||||
declare module '@/shared/queries' {
|
||||
interface QueryMap {
|
||||
account_metadata_list: {
|
||||
input: AccountMetadataListQueryInput;
|
||||
output: AccountMetadata[];
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,18 @@ export type Account = {
|
||||
updatedAt: string | null;
|
||||
syncedAt: string | null;
|
||||
};
|
||||
|
||||
export type AccountWorkspaceMetadata = {
|
||||
key: 'workspace';
|
||||
value: string;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AccountMetadata = AccountWorkspaceMetadata;
|
||||
|
||||
export type AccountMetadataKey = AccountMetadata['key'];
|
||||
|
||||
export type AccountMetadataMap = {
|
||||
workspace: AccountWorkspaceMetadata;
|
||||
};
|
||||
|
||||
@@ -34,10 +34,18 @@ export type AppWindowSizeMetadata = {
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AppAccountMetadata = {
|
||||
key: 'account';
|
||||
value: string;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export type AppMetadata =
|
||||
| AppPlatformMetadata
|
||||
| AppVersionMetadata
|
||||
| AppWindowSizeMetadata;
|
||||
| AppWindowSizeMetadata
|
||||
| AppAccountMetadata;
|
||||
|
||||
export type AppMetadataKey = AppMetadata['key'];
|
||||
|
||||
@@ -45,4 +53,5 @@ export type AppMetadataMap = {
|
||||
platform: AppPlatformMetadata;
|
||||
version: AppVersionMetadata;
|
||||
window_size: AppWindowSizeMetadata;
|
||||
account: AppAccountMetadata;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Message } from '@colanode/core';
|
||||
|
||||
import { AppMetadata } from '@/shared/types/apps';
|
||||
import { Account } from '@/shared/types/accounts';
|
||||
import { Account, AccountMetadata } from '@/shared/types/accounts';
|
||||
import { Server } from '@/shared/types/servers';
|
||||
import { Workspace, WorkspaceMetadata } from '@/shared/types/workspaces';
|
||||
import { User } from '@/shared/types/users';
|
||||
@@ -169,8 +169,8 @@ export type AccountConnectionMessageEvent = {
|
||||
message: Message;
|
||||
};
|
||||
|
||||
export type AppMetadataUpdatedEvent = {
|
||||
type: 'app_metadata_updated';
|
||||
export type AppMetadataSavedEvent = {
|
||||
type: 'app_metadata_saved';
|
||||
metadata: AppMetadata;
|
||||
};
|
||||
|
||||
@@ -179,8 +179,20 @@ export type AppMetadataDeletedEvent = {
|
||||
metadata: AppMetadata;
|
||||
};
|
||||
|
||||
export type WorkspaceMetadataUpdatedEvent = {
|
||||
type: 'workspace_metadata_updated';
|
||||
export type AccountMetadataSavedEvent = {
|
||||
type: 'account_metadata_saved';
|
||||
accountId: string;
|
||||
metadata: AccountMetadata;
|
||||
};
|
||||
|
||||
export type AccountMetadataDeletedEvent = {
|
||||
type: 'account_metadata_deleted';
|
||||
accountId: string;
|
||||
metadata: AccountMetadata;
|
||||
};
|
||||
|
||||
export type WorkspaceMetadataSavedEvent = {
|
||||
type: 'workspace_metadata_saved';
|
||||
accountId: string;
|
||||
workspaceId: string;
|
||||
metadata: WorkspaceMetadata;
|
||||
@@ -256,9 +268,11 @@ export type Event =
|
||||
| AccountConnectionOpenedEvent
|
||||
| AccountConnectionClosedEvent
|
||||
| AccountConnectionMessageEvent
|
||||
| AppMetadataUpdatedEvent
|
||||
| AppMetadataSavedEvent
|
||||
| AppMetadataDeletedEvent
|
||||
| WorkspaceMetadataUpdatedEvent
|
||||
| AccountMetadataSavedEvent
|
||||
| AccountMetadataDeletedEvent
|
||||
| WorkspaceMetadataSavedEvent
|
||||
| WorkspaceMetadataDeletedEvent
|
||||
| DocumentUpdatedEvent
|
||||
| DocumentDeletedEvent
|
||||
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -102,7 +102,6 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-router-dom": "^7.5.0",
|
||||
"semver": "^7.7.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -6922,12 +6921,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
|
||||
@@ -16669,55 +16662,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.0.tgz",
|
||||
"integrity": "sha512-estOHrRlDMKdlQa6Mj32gIks4J+AxNsYoE0DbTTxiMy2mPzZuWSDU+N85/r1IlNR7kGfznF3VCUlvc5IUO+B9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0",
|
||||
"turbo-stream": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.5.0.tgz",
|
||||
"integrity": "sha512-fFhGFCULy4vIseTtH5PNcY/VvDJK5gvOWcwJVHQp8JQcWVr85ENhJ3UpuF/zP1tQOIFYNRJHzXtyhU1Bdgw0RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router/node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-style-singleton": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||
@@ -17462,12 +17406,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -19097,12 +19035,6 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/turbo-windows-64": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.0.tgz",
|
||||
|
||||
Reference in New Issue
Block a user