Remove react router

This commit is contained in:
Hakan Shehu
2025-04-10 09:57:33 +02:00
parent 1011eda582
commit 5316911a55
40 changed files with 651 additions and 480 deletions

View File

@@ -104,7 +104,6 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.2", "react-hook-form": "^7.53.2",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"react-router-dom": "^7.5.0",
"semver": "^7.7.1", "semver": "^7.7.1",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

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

View File

@@ -1,7 +1,9 @@
import { Migration } from 'kysely'; import { Migration } from 'kysely';
import { createWorkspacesTable } from './00001-create-workspaces-table'; import { createWorkspacesTable } from './00001-create-workspaces-table';
import { createMetadataTable } from './00002-create-metadata-table';
export const accountDatabaseMigrations: Record<string, Migration> = { export const accountDatabaseMigrations: Record<string, Migration> = {
'00001-create-workspaces-table': createWorkspacesTable, '00001-create-workspaces-table': createWorkspacesTable,
'00002-create-metadata-table': createMetadataTable,
}; };

View File

@@ -18,6 +18,18 @@ export type SelectWorkspace = Selectable<WorkspaceTable>;
export type CreateWorkspace = Insertable<WorkspaceTable>; export type CreateWorkspace = Insertable<WorkspaceTable>;
export type UpdateWorkspace = Updateable<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 { export interface AccountDatabaseSchema {
workspaces: WorkspaceTable; workspaces: WorkspaceTable;
metadata: AccountMetadataTable;
} }

View File

@@ -7,7 +7,10 @@ import {
} from '@/main/databases/app'; } from '@/main/databases/app';
import { SelectEmoji } from '@/main/databases/emojis'; import { SelectEmoji } from '@/main/databases/emojis';
import { SelectIcon } from '@/main/databases/icons'; import { SelectIcon } from '@/main/databases/icons';
import { SelectWorkspace } from '@/main/databases/account'; import {
SelectAccountMetadata,
SelectWorkspace,
} from '@/main/databases/account';
import { import {
SelectFileState, SelectFileState,
SelectMutation, SelectMutation,
@@ -20,7 +23,11 @@ import {
SelectDocumentState, SelectDocumentState,
SelectDocumentUpdate, SelectDocumentUpdate,
} from '@/main/databases/workspace'; } 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 { Server } from '@/shared/types/servers';
import { User } from '@/shared/types/users'; import { User } from '@/shared/types/users';
import { FileState } from '@/shared/types/files'; 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 = ( export const mapWorkspaceMetadata = (
row: SelectWorkspaceMetadata row: SelectWorkspaceMetadata
): WorkspaceMetadata => { ): WorkspaceMetadata => {

View File

@@ -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,
};
}
}

View File

@@ -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,
};
}
}

View 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,
};
}
}

View 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,
};
}
}

View File

@@ -59,6 +59,10 @@ import { UsersInviteMutationHandler } from '@/main/mutations/users/users-invite'
import { WorkspaceMetadataSaveMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-save'; import { WorkspaceMetadataSaveMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-save';
import { WorkspaceMetadataDeleteMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-delete'; import { WorkspaceMetadataDeleteMutationHandler } from '@/main/mutations/workspaces/workspace-metadata-delete';
import { DocumentUpdateMutationHandler } from '@/main/mutations/documents/document-update'; 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 { MutationHandler } from '@/main/lib/types';
import { MutationMap } from '@/shared/mutations'; import { MutationMap } from '@/shared/mutations';
@@ -128,4 +132,8 @@ export const mutationHandlerMap: MutationHandlerMap = {
workspace_metadata_save: new WorkspaceMetadataSaveMutationHandler(), workspace_metadata_save: new WorkspaceMetadataSaveMutationHandler(),
workspace_metadata_delete: new WorkspaceMetadataDeleteMutationHandler(), workspace_metadata_delete: new WorkspaceMetadataDeleteMutationHandler(),
document_update: new DocumentUpdateMutationHandler(), document_update: new DocumentUpdateMutationHandler(),
app_metadata_save: new AppMetadataSaveMutationHandler(),
app_metadata_delete: new AppMetadataDeleteMutationHandler(),
account_metadata_save: new AccountMetadataSaveMutationHandler(),
account_metadata_delete: new AccountMetadataDeleteMutationHandler(),
}; };

View File

@@ -20,12 +20,12 @@ export class WorkspaceMetadataSaveMutationHandler
.returningAll() .returningAll()
.values({ .values({
key: input.key, key: input.key,
value: input.value, value: JSON.stringify(input.value),
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}) })
.onConflict((cb) => .onConflict((cb) =>
cb.columns(['key']).doUpdateSet({ cb.columns(['key']).doUpdateSet({
value: input.value, value: JSON.stringify(input.value),
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}) })
) )
@@ -38,7 +38,7 @@ export class WorkspaceMetadataSaveMutationHandler
} }
eventBus.publish({ eventBus.publish({
type: 'workspace_metadata_updated', type: 'workspace_metadata_saved',
accountId: input.accountId, accountId: input.accountId,
workspaceId: input.workspaceId, workspaceId: input.workspaceId,
metadata: mapWorkspaceMetadata(createdMetadata), metadata: mapWorkspaceMetadata(createdMetadata),

View File

@@ -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;
}
}

View File

@@ -25,13 +25,11 @@ export class AppMetadataListQueryHandler
_: AppMetadataListQueryInput, _: AppMetadataListQueryInput,
output: AppMetadata[] output: AppMetadata[]
): Promise<ChangeCheckResult<AppMetadataListQueryInput>> { ): Promise<ChangeCheckResult<AppMetadataListQueryInput>> {
if (event.type === 'app_metadata_updated') { if (event.type === 'app_metadata_saved') {
const newOutput = output.map((metadata) => { const newOutput = [
if (metadata.key === event.metadata.key) { ...output.filter((metadata) => metadata.key !== event.metadata.key),
return event.metadata; event.metadata,
} ];
return metadata;
});
return { return {
hasChanges: true, hasChanges: true,

View File

@@ -34,6 +34,7 @@ import { ChatListQueryHandler } from '@/main/queries/chats/chat-list';
import { DocumentGetQueryHandler } from '@/main/queries/documents/document-get'; import { DocumentGetQueryHandler } from '@/main/queries/documents/document-get';
import { DocumentStateGetQueryHandler } from '@/main/queries/documents/document-state-get'; import { DocumentStateGetQueryHandler } from '@/main/queries/documents/document-state-get';
import { DocumentUpdatesListQueryHandler } from '@/main/queries/documents/document-update-list'; 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 { WorkspaceMetadataListQueryHandler } from '@/main/queries/workspaces/workspace-metadata-list';
import { QueryHandler } from '@/main/lib/types'; import { QueryHandler } from '@/main/lib/types';
import { QueryMap } from '@/shared/queries'; import { QueryMap } from '@/shared/queries';
@@ -80,4 +81,5 @@ export const queryHandlerMap: QueryHandlerMap = {
document_get: new DocumentGetQueryHandler(), document_get: new DocumentGetQueryHandler(),
document_state_get: new DocumentStateGetQueryHandler(), document_state_get: new DocumentStateGetQueryHandler(),
document_updates_list: new DocumentUpdatesListQueryHandler(), document_updates_list: new DocumentUpdatesListQueryHandler(),
account_metadata_list: new AccountMetadataListQueryHandler(),
}; };

View File

@@ -41,16 +41,14 @@ export class WorkspaceMetadataListQueryHandler
} }
if ( if (
event.type === 'workspace_metadata_updated' && event.type === 'workspace_metadata_saved' &&
event.accountId === input.accountId && event.accountId === input.accountId &&
event.workspaceId === input.workspaceId event.workspaceId === input.workspaceId
) { ) {
const newOutput = output.map((metadata) => { const newOutput = [
if (metadata.key === event.metadata.key) { ...output.filter((metadata) => metadata.key !== event.metadata.key),
return event.metadata; event.metadata,
} ];
return metadata;
});
return { return {
hasChanges: true, hasChanges: true,

View File

@@ -69,7 +69,7 @@ export class MetadataService {
} }
eventBus.publish({ eventBus.publish({
type: 'app_metadata_updated', type: 'app_metadata_saved',
metadata: mapAppMetadata(createdMetadata), metadata: mapAppMetadata(createdMetadata),
}); });
} }

View File

@@ -1,27 +1,32 @@
import { Outlet } from 'react-router-dom'; import { useState, useEffect } from 'react';
import React from 'react';
import { AppContext } from './contexts/app';
import { AppContext } from '@/renderer/contexts/app';
import { DelayedComponent } from '@/renderer/components/ui/delayed-component'; import { DelayedComponent } from '@/renderer/components/ui/delayed-component';
import { AppLoader } from '@/renderer/app-loader'; import { AppLoader } from '@/renderer/app-loader';
import { useQuery } from '@/renderer/hooks/use-query'; import { useQuery } from '@/renderer/hooks/use-query';
import { RadarProvider } from '@/renderer/radar-provider'; import { RadarProvider } from '@/renderer/radar-provider';
import { Account } from '@/renderer/components/accounts/account';
import { Login } from '@/renderer/components/accounts/login';
export const App = () => { 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', type: 'app_metadata_list',
}); });
React.useEffect(() => { const { data: accounts, isPending: isPendingAccounts } = useQuery({
type: 'account_list',
});
useEffect(() => {
window.colanode.init().then(() => { window.colanode.init().then(() => {
setInitialized(true); setInitialized(true);
}); });
}, []); }, []);
if (!initialized || isPending) { if (!initialized || isPendingMetadata || isPendingAccounts) {
return ( return (
<DelayedComponent> <DelayedComponent>
<AppLoader /> <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 ( return (
<AppContext.Provider <AppContext.Provider
value={{ value={{
getMetadata: (key: string) => { getMetadata: (key) => {
return data?.find((metadata) => metadata.key === key)?.value; 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> <RadarProvider>
<Outlet /> {!openLogin && account ? (
<Account key={account.id} account={account} />
) : (
<Login />
)}
</RadarProvider> </RadarProvider>
</AppContext.Provider> </AppContext.Provider>
); );

View File

@@ -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>
);
};

View File

@@ -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;
};

View File

@@ -1,42 +1,86 @@
import React from 'react'; import React from 'react';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import { AccountLogout } from '@/renderer/components/accounts/account-logout'; import { AccountLogout } from '@/renderer/components/accounts/account-logout';
import { AccountSettingsDialog } from '@/renderer/components/accounts/account-settings-dialog'; import { AccountSettingsDialog } from '@/renderer/components/accounts/account-settings-dialog';
import { AccountContext } from '@/renderer/contexts/account'; 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 { useQuery } from '@/renderer/hooks/use-query';
import { WorkspaceCreate } from '@/renderer/components/workspaces/workspace-create';
import { Workspace } from '@/renderer/components/workspaces/workspace';
export const Account = () => { interface AccountProps {
const { accountId } = useParams<{ accountId: string }>(); account: AccountType;
const navigate = useNavigate(); }
export const Account = ({ account }: AccountProps) => {
const [openSettings, setOpenSettings] = React.useState(false); const [openSettings, setOpenSettings] = React.useState(false);
const [openLogout, setOpenLogout] = React.useState(false); const [openLogout, setOpenLogout] = React.useState(false);
const [openCreateWorkspace, setOpenCreateWorkspace] = React.useState(false);
const { data, isPending } = useQuery({ const { data: metadata, isPending: isPendingMetadata } = useQuery({
type: 'account_get', type: 'account_metadata_list',
accountId: accountId!, accountId: account.id,
}); });
if (isPending) { const { data: workspaces, isPending: isPendingWorkspaces } = useQuery({
type: 'workspace_list',
accountId: account.id,
});
if (isPendingMetadata || isPendingWorkspaces) {
return null; return null;
} }
if (!data) { const workspaceMetadata = metadata?.find(
return <AccountNotFound />; (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 ( return (
<AccountContext.Provider <AccountContext.Provider
value={{ value={{
...account, ...account,
openSettings: () => setOpenSettings(true), openSettings: () => setOpenSettings(true),
openLogout: () => setOpenLogout(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 && ( {openSettings && (
<AccountSettingsDialog <AccountSettingsDialog
open={true} open={true}
@@ -48,7 +92,6 @@ export const Account = () => {
onCancel={() => setOpenLogout(false)} onCancel={() => setOpenLogout(false)}
onLogout={() => { onLogout={() => {
setOpenLogout(false); setOpenLogout(false);
navigate('/');
}} }}
/> />
)} )}

View File

@@ -1,6 +1,6 @@
import React from 'react'; 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 { EmailLogin } from '@/renderer/components/accounts/email-login';
import { EmailRegister } from '@/renderer/components/accounts/email-register'; import { EmailRegister } from '@/renderer/components/accounts/email-register';
import { EmailVerify } from '@/renderer/components/accounts/email-verify'; import { EmailVerify } from '@/renderer/components/accounts/email-verify';
@@ -31,8 +31,7 @@ type VerifyPanelState = {
type PanelState = LoginPanelState | RegisterPanelState | VerifyPanelState; type PanelState = LoginPanelState | RegisterPanelState | VerifyPanelState;
export const LoginForm = ({ accounts, servers }: LoginFormProps) => { export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
const navigate = useNavigate(); const app = useApp();
const [server, setServer] = React.useState<Server>(servers[0]!); const [server, setServer] = React.useState<Server>(servers[0]!);
const [panel, setPanel] = React.useState<PanelState>({ const [panel, setPanel] = React.useState<PanelState>({
type: 'login', type: 'login',
@@ -52,8 +51,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
server={server} server={server}
onSuccess={(output) => { onSuccess={(output) => {
if (output.type === 'success') { if (output.type === 'success') {
const userId = output.workspaces[0]?.id ?? ''; app.openAccount(output.account.id);
navigate(`/${output.account.id}/${userId}`);
} else if (output.type === 'verify') { } else if (output.type === 'verify') {
setPanel({ setPanel({
type: 'verify', type: 'verify',
@@ -81,8 +79,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
server={server} server={server}
onSuccess={(output) => { onSuccess={(output) => {
if (output.type === 'success') { if (output.type === 'success') {
const userId = output.workspaces[0]?.id ?? ''; app.openAccount(output.account.id);
navigate(`/${output.account.id}/${userId}`);
} else if (output.type === 'verify') { } else if (output.type === 'verify') {
setPanel({ setPanel({
type: 'verify', type: 'verify',
@@ -113,8 +110,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
expiresAt={panel.expiresAt} expiresAt={panel.expiresAt}
onSuccess={(output) => { onSuccess={(output) => {
if (output.type === 'success') { if (output.type === 'success') {
const userId = output.workspaces[0]?.id ?? ''; app.openAccount(output.account.id);
navigate(`/${output.account.id}/${userId}`);
} }
}} }}
/> />
@@ -137,7 +133,7 @@ export const LoginForm = ({ accounts, servers }: LoginFormProps) => {
<p <p
className="text-center text-sm text-muted-foreground hover:cursor-pointer hover:underline" className="text-center text-sm text-muted-foreground hover:cursor-pointer hover:underline"
onClick={() => { onClick={() => {
navigate(-1); app.closeLogin();
}} }}
> >
Cancel Cancel

View File

@@ -1,7 +1,7 @@
import { Check, LogOut, Plus, Settings } from 'lucide-react'; import { Check, LogOut, Plus, Settings } from 'lucide-react';
import React from '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 { Avatar } from '@/renderer/components/avatars/avatar';
import { NotificationBadge } from '@/renderer/components/ui/notification-badge'; import { NotificationBadge } from '@/renderer/components/ui/notification-badge';
import { import {
@@ -18,8 +18,8 @@ import { useQuery } from '@/renderer/hooks/use-query';
import { AccountReadState } from '@/shared/types/radars'; import { AccountReadState } from '@/shared/types/radars';
export function SidebarMenuFooter() { export function SidebarMenuFooter() {
const app = useApp();
const account = useAccount(); const account = useAccount();
const navigate = useNavigate();
const radar = useRadar(); const radar = useRadar();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -112,7 +112,7 @@ export function SidebarMenuFooter() {
key={accountItem.id} key={accountItem.id}
className="p-0" className="p-0"
onClick={() => { onClick={() => {
navigate(`/${accountItem.id}`); app.openAccount(accountItem.id);
}} }}
> >
<AccountContext.Provider <AccountContext.Provider
@@ -120,6 +120,8 @@ export function SidebarMenuFooter() {
...accountItem, ...accountItem,
openSettings: () => {}, openSettings: () => {},
openLogout: () => {}, openLogout: () => {},
openWorkspace: () => {},
openWorkspaceCreate: () => {},
}} }}
> >
<div className="w-full flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <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 <DropdownMenuItem
className="flex items-center gap-2 text-muted-foreground hover:text-foreground" className="flex items-center gap-2 text-muted-foreground hover:text-foreground"
onClick={() => { onClick={() => {
navigate(`/login`); app.openLogin();
}} }}
> >
<Plus className="size-4" /> <Plus className="size-4" />

View File

@@ -1,6 +1,5 @@
import { Bell, Check, Plus, Settings } from 'lucide-react'; import { Bell, Check, Plus, Settings } from 'lucide-react';
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Avatar } from '@/renderer/components/avatars/avatar'; import { Avatar } from '@/renderer/components/avatars/avatar';
import { NotificationBadge } from '@/renderer/components/ui/notification-badge'; import { NotificationBadge } from '@/renderer/components/ui/notification-badge';
@@ -20,7 +19,6 @@ import { useQuery } from '@/renderer/hooks/use-query';
export const SidebarMenuHeader = () => { export const SidebarMenuHeader = () => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const account = useAccount(); const account = useAccount();
const navigate = useNavigate();
const radar = useRadar(); const radar = useRadar();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -104,7 +102,7 @@ export const SidebarMenuHeader = () => {
key={workspaceItem.id} key={workspaceItem.id}
className="p-0" className="p-0"
onClick={() => { 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"> <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 <DropdownMenuItem
className="gap-2 p-2 text-muted-foreground hover:text-foreground" className="gap-2 p-2 text-muted-foreground hover:text-foreground"
onClick={() => { onClick={() => {
navigate(`/${account.id}/create`); account.openWorkspaceCreate();
}} }}
> >
<Plus className="size-4" /> <Plus className="size-4" />

View File

@@ -1,25 +1,20 @@
import { useNavigate } from 'react-router-dom';
import { WorkspaceForm } from '@/renderer/components/workspaces/workspace-form'; import { WorkspaceForm } from '@/renderer/components/workspaces/workspace-form';
import { useAccount } from '@/renderer/contexts/account'; import { useAccount } from '@/renderer/contexts/account';
import { useMutation } from '@/renderer/hooks/use-mutation'; import { useMutation } from '@/renderer/hooks/use-mutation';
import { useQuery } from '@/renderer/hooks/use-query';
import { toast } from '@/renderer/hooks/use-toast'; 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 account = useAccount();
const navigate = useNavigate();
const { mutate, isPending } = useMutation(); const { mutate, isPending } = useMutation();
const { data } = useQuery({
type: 'workspace_list',
accountId: account.id,
});
const workspaces = data ?? [];
const handleCancel = workspaces.length > 0 ? () => navigate('/') : undefined;
return ( return (
<div className="container flex flex-row justify-center"> <div className="container flex flex-row justify-center">
<div className="w-full max-w-[700px]"> <div className="w-full max-w-[700px]">
@@ -39,7 +34,7 @@ export const WorkspaceCreate = () => {
avatar: values.avatar ?? null, avatar: values.avatar ?? null,
}, },
onSuccess(output) { onSuccess(output) {
navigate(`/${account.id}/${output.id}`); onSuccess(output.id);
}, },
onError(error) { onError(error) {
toast({ toast({
@@ -51,7 +46,7 @@ export const WorkspaceCreate = () => {
}); });
}} }}
isSaving={isPending} isSaving={isPending}
onCancel={handleCancel} onCancel={onCancel}
saveText="Create" saveText="Create"
/> />
</div> </div>

View File

@@ -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>
);
};

View File

@@ -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;
};

View File

@@ -1,47 +1,34 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom';
import { Layout } from '@/renderer/components/layouts/layout'; import { Layout } from '@/renderer/components/layouts/layout';
import { WorkspaceSettingsDialog } from '@/renderer/components/workspaces/workspace-settings-dialog'; 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 { useAccount } from '@/renderer/contexts/account';
import { WorkspaceContext } from '@/renderer/contexts/workspace'; import { WorkspaceContext } from '@/renderer/contexts/workspace';
import { useQuery } from '@/renderer/hooks/use-query'; import { useQuery } from '@/renderer/hooks/use-query';
import { import {
WorkspaceMetadataKey, WorkspaceMetadataKey,
WorkspaceMetadataMap, WorkspaceMetadataMap,
Workspace as WorkspaceType,
} from '@/shared/types/workspaces'; } from '@/shared/types/workspaces';
export const Workspace = () => { interface WorkspaceProps {
const { workspaceId } = useParams<{ workspaceId: string }>(); workspace: WorkspaceType;
}
export const Workspace = ({ workspace }: WorkspaceProps) => {
const account = useAccount(); const account = useAccount();
const [openSettings, setOpenSettings] = React.useState(false); 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({ const { data: metadata, isPending: isPendingMetadata } = useQuery({
type: 'workspace_metadata_list', type: 'workspace_metadata_list',
accountId: account.id, accountId: account.id,
workspaceId: workspaceId!, workspaceId: workspace.id,
}); });
if (isPendingWorkspace || isPendingMetadata) { if (isPendingMetadata) {
return null; return null;
} }
if (!workspace) {
return <WorkspaceNotFound />;
}
if (!workspace) {
return <WorkspaceNotFound />;
}
return ( return (
<WorkspaceContext.Provider <WorkspaceContext.Provider
value={{ value={{
@@ -68,22 +55,22 @@ export const Workspace = () => {
window.colanode.executeMutation({ window.colanode.executeMutation({
type: 'workspace_metadata_save', type: 'workspace_metadata_save',
accountId: account.id, accountId: account.id,
workspaceId: workspaceId!, workspaceId: workspace.id,
key, key,
value: JSON.stringify(value), value,
}); });
}, },
deleteMetadata(key: string) { deleteMetadata(key: string) {
window.colanode.executeMutation({ window.colanode.executeMutation({
type: 'workspace_metadata_delete', type: 'workspace_metadata_delete',
accountId: account.id, accountId: account.id,
workspaceId: workspaceId!, workspaceId: workspace.id,
key, key,
}); });
}, },
}} }}
> >
<Layout key={workspaceId} /> <Layout key={workspace.id} />
{openSettings && ( {openSettings && (
<WorkspaceSettingsDialog <WorkspaceSettingsDialog
open={openSettings} open={openSettings}

View File

@@ -5,6 +5,8 @@ import { Account } from '@/shared/types/accounts';
interface AccountContext extends Account { interface AccountContext extends Account {
openSettings: () => void; openSettings: () => void;
openLogout: () => void; openLogout: () => void;
openWorkspace: (id: string) => void;
openWorkspaceCreate: () => void;
} }
export const AccountContext = createContext<AccountContext>( export const AccountContext = createContext<AccountContext>(

View File

@@ -6,6 +6,14 @@ interface AppContext {
getMetadata: <K extends AppMetadataKey>( getMetadata: <K extends AppMetadataKey>(
key: K key: K
) => AppMetadataMap[K]['value'] | undefined; ) => 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); export const AppContext = createContext<AppContext>({} as AppContext);

View File

@@ -4,56 +4,14 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { createHashRouter, RouterProvider } from 'react-router-dom';
import { HTML5Backend } from '@/shared/lib/dnd-backend'; import { HTML5Backend } from '@/shared/lib/dnd-backend';
import { App } from '@/renderer/app'; 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 { Toaster } from '@/renderer/components/ui/toaster';
import { TooltipProvider } from '@/renderer/components/ui/tooltip'; 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 { useEventBus } from '@/renderer/hooks/use-event-bus';
import { Event } from '@/shared/types/events'; 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({ export const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
@@ -102,7 +60,7 @@ const Root = () => {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<TooltipProvider> <TooltipProvider>
<RouterProvider router={router} /> <App />
</TooltipProvider> </TooltipProvider>
<Toaster /> <Toaster />
</DndProvider> </DndProvider>

View File

@@ -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;
};
}
}

View File

@@ -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;
};
}
}

View File

@@ -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;
};
}
}

View 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;
};
}
}

View File

@@ -1,9 +1,14 @@
import {
WorkspaceMetadataMap,
WorkspaceMetadataKey,
} from '@/shared/types/workspaces';
export type WorkspaceMetadataSaveMutationInput = { export type WorkspaceMetadataSaveMutationInput = {
type: 'workspace_metadata_save'; type: 'workspace_metadata_save';
accountId: string; accountId: string;
workspaceId: string; workspaceId: string;
key: string; key: WorkspaceMetadataKey;
value: string; value: WorkspaceMetadataMap[WorkspaceMetadataKey]['value'];
}; };
export type WorkspaceMetadataSaveMutationOutput = { export type WorkspaceMetadataSaveMutationOutput = {

View File

@@ -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[];
};
}
}

View File

@@ -10,3 +10,18 @@ export type Account = {
updatedAt: string | null; updatedAt: string | null;
syncedAt: 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;
};

View File

@@ -34,10 +34,18 @@ export type AppWindowSizeMetadata = {
updatedAt: string | null; updatedAt: string | null;
}; };
export type AppAccountMetadata = {
key: 'account';
value: string;
createdAt: string;
updatedAt: string | null;
};
export type AppMetadata = export type AppMetadata =
| AppPlatformMetadata | AppPlatformMetadata
| AppVersionMetadata | AppVersionMetadata
| AppWindowSizeMetadata; | AppWindowSizeMetadata
| AppAccountMetadata;
export type AppMetadataKey = AppMetadata['key']; export type AppMetadataKey = AppMetadata['key'];
@@ -45,4 +53,5 @@ export type AppMetadataMap = {
platform: AppPlatformMetadata; platform: AppPlatformMetadata;
version: AppVersionMetadata; version: AppVersionMetadata;
window_size: AppWindowSizeMetadata; window_size: AppWindowSizeMetadata;
account: AppAccountMetadata;
}; };

View File

@@ -1,7 +1,7 @@
import { Message } from '@colanode/core'; import { Message } from '@colanode/core';
import { AppMetadata } from '@/shared/types/apps'; 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 { Server } from '@/shared/types/servers';
import { Workspace, WorkspaceMetadata } from '@/shared/types/workspaces'; import { Workspace, WorkspaceMetadata } from '@/shared/types/workspaces';
import { User } from '@/shared/types/users'; import { User } from '@/shared/types/users';
@@ -169,8 +169,8 @@ export type AccountConnectionMessageEvent = {
message: Message; message: Message;
}; };
export type AppMetadataUpdatedEvent = { export type AppMetadataSavedEvent = {
type: 'app_metadata_updated'; type: 'app_metadata_saved';
metadata: AppMetadata; metadata: AppMetadata;
}; };
@@ -179,8 +179,20 @@ export type AppMetadataDeletedEvent = {
metadata: AppMetadata; metadata: AppMetadata;
}; };
export type WorkspaceMetadataUpdatedEvent = { export type AccountMetadataSavedEvent = {
type: 'workspace_metadata_updated'; 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; accountId: string;
workspaceId: string; workspaceId: string;
metadata: WorkspaceMetadata; metadata: WorkspaceMetadata;
@@ -256,9 +268,11 @@ export type Event =
| AccountConnectionOpenedEvent | AccountConnectionOpenedEvent
| AccountConnectionClosedEvent | AccountConnectionClosedEvent
| AccountConnectionMessageEvent | AccountConnectionMessageEvent
| AppMetadataUpdatedEvent | AppMetadataSavedEvent
| AppMetadataDeletedEvent | AppMetadataDeletedEvent
| WorkspaceMetadataUpdatedEvent | AccountMetadataSavedEvent
| AccountMetadataDeletedEvent
| WorkspaceMetadataSavedEvent
| WorkspaceMetadataDeletedEvent | WorkspaceMetadataDeletedEvent
| DocumentUpdatedEvent | DocumentUpdatedEvent
| DocumentDeletedEvent | DocumentDeletedEvent

68
package-lock.json generated
View File

@@ -102,7 +102,6 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.53.2", "react-hook-form": "^7.53.2",
"react-intersection-observer": "^9.16.0", "react-intersection-observer": "^9.16.0",
"react-router-dom": "^7.5.0",
"semver": "^7.7.1", "semver": "^7.7.1",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@@ -6922,12 +6921,6 @@
"@types/node": "*" "@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": { "node_modules/@types/cors": {
"version": "2.8.17", "version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "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": { "node_modules/react-style-singleton": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -17462,12 +17406,6 @@
"node": ">= 0.8.0" "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": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -19097,12 +19035,6 @@
"linux" "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": { "node_modules/turbo-windows-64": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.0.tgz", "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-2.5.0.tgz",