diff --git a/client/src/components/organizations/OrganizationSwitchPopover.vue b/client/src/components/organizations/OrganizationSwitchPopover.vue new file mode 100644 index 00000000000..b15ad63b31e --- /dev/null +++ b/client/src/components/organizations/OrganizationSwitchPopover.vue @@ -0,0 +1,220 @@ + + + + + + + diff --git a/client/src/locales/en-US.json b/client/src/locales/en-US.json index c491e2ac167..db0f5403424 100644 --- a/client/src/locales/en-US.json +++ b/client/src/locales/en-US.json @@ -85,7 +85,8 @@ "sortByUserName": "User Name", "sortByLastLogin": "Last login", "labelSortBy": "Sort by", - "search": "Search" + "search": "Search", + "loggedIn": "Logged in" }, "noExistingOrganization": { "title": "Need to create or join an organization?", @@ -1187,5 +1188,10 @@ "missingToken": "Link doesn't include a token.", "invalidToken": "Link contains an invalid token." } + }, + "OrganizationSwitch": { + "loggedInOrgs": "Connected organizations", + "myOrgs": "My organizations", + "active": "Active" } } diff --git a/client/src/locales/fr-FR.json b/client/src/locales/fr-FR.json index 6e7fc29f3d5..318eb53c780 100644 --- a/client/src/locales/fr-FR.json +++ b/client/src/locales/fr-FR.json @@ -85,7 +85,8 @@ "sortByUserName": "Nom d'utilisateur", "sortByLastLogin": "Dernière connexion", "labelSortBy": "Trier par", - "search": "Rechercher" + "search": "Rechercher", + "loggedIn": "Connecté" }, "noExistingOrganization": { "title": "Besoin de créer ou rejoindre une organisation ?", @@ -1187,5 +1188,10 @@ "missingToken": "Le lien n'inclut pas de jeton.", "invalidToken": "Le lien contient un jeton invalide." } + }, + "OrganizationSwitch": { + "loggedInOrgs": "Organisations connectées", + "myOrgs": "Mes organisations", + "active": "Active" } } diff --git a/client/src/parsec/invitation.ts b/client/src/parsec/invitation.ts index 68cfc41cdb0..e9fad1e6f03 100644 --- a/client/src/parsec/invitation.ts +++ b/client/src/parsec/invitation.ts @@ -68,8 +68,9 @@ export async function listUserInvitations(): Promise, ListInvitationsError>>((resolve, _reject) => { - const ret: Array = [ + return { + ok: true, + value: [ { tag: InviteListItemTag.User, addr: 'parsec://parsec.example.com/MyOrg?action=claim_device&token=12346565645645654645645645645645', @@ -86,9 +87,8 @@ export async function listUserInvitations(): Promise = []; + +export async function getLoggedInDevices(): Promise> { + return loggedInDevices; +} + +export function isDeviceLoggedIn(device: AvailableDevice): boolean { + return loggedInDevices.find((info) => info.device.slug === device.slug) !== undefined; +} + +export function getDeviceHandle(device: AvailableDevice): ConnectionHandle | null { + const info = loggedInDevices.find((info) => info.device.slug === device.slug); + if (info) { + return info.handle; + } + return null; +} + export async function listAvailableDevices(): Promise> { return await libparsec.listAvailableDevices(window.getConfigDir()); } @@ -34,6 +57,11 @@ export async function login(device: AvailableDevice, password: string): Promise< console.log('Event received', event); } + const info = loggedInDevices.find((info) => info.device.slug === device.slug); + if (info !== undefined) { + return { ok: true, value: info.handle }; + } + if (!needsMocks()) { const clientConfig = getClientConfig(); const strategy: DeviceAccessStrategyPassword = { @@ -41,9 +69,14 @@ export async function login(device: AvailableDevice, password: string): Promise< password: password, keyFile: device.keyFilePath, }; - return await libparsec.clientStart(clientConfig, parsecEventCallback, strategy); + const result = await libparsec.clientStart(clientConfig, parsecEventCallback, strategy); + if (result.ok) { + loggedInDevices.push({ handle: result.value, device: device }); + } + return result; } else { if (password === 'P@ssw0rd.' || password === 'AVeryL0ngP@ssw0rd') { + loggedInDevices.push({ handle: DEFAULT_HANDLE, device: device }); return { ok: true, value: DEFAULT_HANDLE }; } return { @@ -60,8 +93,19 @@ export async function logout(): Promise> { const handle = getParsecHandle(); if (handle !== null && !needsMocks()) { - return await libparsec.clientStop(handle); + const result = await libparsec.clientStop(handle); + if (result.ok) { + const index = loggedInDevices.findIndex((info) => info.handle === handle); + if (index !== -1) { + loggedInDevices.splice(index, 1); + } + } + return result; } else { + const index = loggedInDevices.findIndex((info) => info.handle === handle); + if (index !== -1) { + loggedInDevices.splice(index, 1); + } return { ok: true, value: null }; } } diff --git a/client/src/router/index.ts b/client/src/router/index.ts index a1f42f9ea04..be69e4b6d3e 100644 --- a/client/src/router/index.ts +++ b/client/src/router/index.ts @@ -3,6 +3,7 @@ export * from '@/router/checks'; export * from '@/router/navigation'; export { + getConnectionHandle, getCurrentRouteName, getCurrentRouteParams, getCurrentRouteQuery, @@ -12,3 +13,4 @@ export { getWorkspaceId, } from '@/router/params'; export * from '@/router/types'; +export { watchOrganizationSwitch, watchRoute } from '@/router/watchers'; diff --git a/client/src/router/navigation.ts b/client/src/router/navigation.ts index 507d358d5e3..cece3099737 100644 --- a/client/src/router/navigation.ts +++ b/client/src/router/navigation.ts @@ -1,8 +1,9 @@ // Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS -import { startWorkspace, WorkspaceID } from '@/parsec'; +import { ConnectionHandle, startWorkspace, WorkspaceID } from '@/parsec'; import { getConnectionHandle } from '@/router/params'; -import { getRouter, Routes } from '@/router/types'; +import { getCurrentRoute, getRouter, Routes } from '@/router/types'; +import { organizationKey } from '@/router/watchers'; import { LocationQueryRaw, RouteParamsRaw } from 'vue-router'; export interface NavigationOptions { @@ -51,3 +52,59 @@ export async function routerGoBack(): Promise { const router = getRouter(); router.go(-1); } + +interface RouteBackup { + handle: ConnectionHandle; + data: { + route: Routes; + params: object; + query: object; + }; +} + +const routesBackup: Array = []; + +export async function switchOrganization(handle: ConnectionHandle | null, backup = true): Promise { + if (backup) { + const currentHandle = getConnectionHandle(); + + if (currentHandle === null) { + console.error('No current handle'); + return; + } + if (handle === currentHandle) { + console.error('Cannot switch to same organization'); + return; + } + // Backup the current route + const currentRoute = getCurrentRoute(); + const index = routesBackup.findIndex((bk) => bk.handle === currentHandle); + if (index !== -1) { + routesBackup.splice(index, 1); + } + console.log('Saving', currentRoute.value.name, currentRoute.value.params, currentRoute.value.query); + routesBackup.push({ + handle: currentHandle, + data: { + route: currentRoute.value.name as Routes, + params: currentRoute.value.params, + query: currentRoute.value.query, + }, + }); + } + + // No handle, navigate to organization list + if (!handle) { + await navigateTo(Routes.Home, { skipHandle: true, replace: true }); + organizationKey.value += 1; + } else { + const backup = routesBackup.find((bk) => bk.handle === handle); + if (!backup) { + console.error('No backup, organization was not connected'); + return; + } + console.log('Restoring', backup.data.route, backup.data.params, backup.data.query); + await navigateTo(backup.data.route, { params: backup.data.params, query: backup.data.query, skipHandle: true, replace: true }); + organizationKey.value += 1; + } +} diff --git a/client/src/router/types.ts b/client/src/router/types.ts index 5edb33196d1..42487c4235b 100644 --- a/client/src/router/types.ts +++ b/client/src/router/types.ts @@ -1,7 +1,7 @@ // Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS import { createRouter, createWebHistory } from '@ionic/vue-router'; -import { Ref, WatchStopHandle, watch } from 'vue'; +import { Ref } from 'vue'; import { RouteLocationNormalizedLoaded, RouteRecordRaw, Router } from 'vue-router'; export enum Routes { @@ -134,8 +134,3 @@ export function getRouter(): Router { export function getCurrentRoute(): Ref { return (router as Router).currentRoute; } - -export function watchRoute(callback: () => Promise): WatchStopHandle { - const currentRoute = getCurrentRoute(); - return watch(currentRoute, callback); -} diff --git a/client/src/router/watchers.ts b/client/src/router/watchers.ts new file mode 100644 index 00000000000..3af0a60e283 --- /dev/null +++ b/client/src/router/watchers.ts @@ -0,0 +1,15 @@ +// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS + +import { getCurrentRoute } from '@/router/types'; +import { Ref, WatchStopHandle, ref, watch } from 'vue'; + +export const organizationKey: Ref = ref(0); + +export function watchRoute(callback: () => Promise): WatchStopHandle { + const currentRoute = getCurrentRoute(); + return watch(currentRoute, callback); +} + +export function watchOrganizationSwitch(callback: () => Promise): WatchStopHandle { + return watch(organizationKey, callback); +} diff --git a/client/src/views/files/FoldersPage.vue b/client/src/views/files/FoldersPage.vue index c12113796c5..0a5f613ff0a 100644 --- a/client/src/views/files/FoldersPage.vue +++ b/client/src/views/files/FoldersPage.vue @@ -237,6 +237,9 @@ const fileImports: Ref> = ref([]); const routeWatchCancel = watchRoute(async () => { const newPath = getDocumentPath(); + if (newPath === '') { + return; + } if (newPath !== currentPath.value) { currentPath.value = newPath; } @@ -260,6 +263,10 @@ const ownRole: Ref = ref(parsec.WorkspaceRole.Reader); onMounted(async () => { ownRole.value = await parsec.getWorkspaceRole(getWorkspaceId()); callbackId = await importManager.registerCallback(onFileImportState); + const path = getDocumentPath(); + if (path !== '') { + currentPath.value = path; + } await listFolder(); }); diff --git a/client/src/views/header/HeaderPage.vue b/client/src/views/header/HeaderPage.vue index d93231c434f..4a8c2f0b21d 100644 --- a/client/src/views/header/HeaderPage.vue +++ b/client/src/views/header/HeaderPage.vue @@ -7,7 +7,7 @@ import HeaderBackButton from '@/components/header/HeaderBackButton.vue'; import HeaderBreadcrumbs, { RouterPathNode } from '@/components/header/HeaderBreadcrumbs.vue'; -import { ClientInfo, Path, WorkspaceName, getClientInfo, getWorkspaceName } from '@/parsec'; +import { ClientInfo, Path, WorkspaceName, getClientInfo, getWorkspaceName, isMobile } from '@/parsec'; import { Routes, currentRouteIs, @@ -133,7 +133,6 @@ import { IonPage, IonRouterOutlet, IonToolbar, - isPlatform, popoverController, } from '@ionic/vue'; import { home, menu, notifications, search } from 'ionicons/icons'; @@ -145,6 +144,12 @@ const userInfo: Ref = ref(null); const fullPath: Ref = ref([]); const routeWatchCancel = watchRoute(async () => { + const result = await getClientInfo(); + if (result.ok) { + userInfo.value = result.value; + } else { + console.log('Could not get user info', result.error); + } await updateRoute(); }); diff --git a/client/src/views/home/HomePage.vue b/client/src/views/home/HomePage.vue index c0c95af0150..d1eab238542 100644 --- a/client/src/views/home/HomePage.vue +++ b/client/src/views/home/HomePage.vue @@ -57,8 +57,8 @@