diff --git a/packages/ui/src/components/WorkspaceAvatar.svelte b/packages/ui/src/components/WorkspaceAvatar.svelte new file mode 100644 index 0000000000..f02292c19c --- /dev/null +++ b/packages/ui/src/components/WorkspaceAvatar.svelte @@ -0,0 +1,79 @@ + + + +
+
+ {getWorkspaceInitial(displayName)} +
+ {#if hasUnread} +
+ {/if} +
+ + diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 53d92fca99..afa70dc96f 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -67,6 +67,7 @@ export { default as StatusBadge } from './components/StatusBadge.svelte' export { default as StateTag } from './components/StateTag.svelte' export { default as Component } from './components/Component.svelte' export { default as Icon } from './components/Icon.svelte' +export { default as WorkspaceAvatar } from './components/WorkspaceAvatar.svelte' export { default as ActionIcon } from './components/ActionIcon.svelte' export { default as Toggle } from './components/Toggle.svelte' export { default as RadioButton } from './components/RadioButton.svelte' @@ -307,6 +308,7 @@ export * from './tooltips' export * from './panelup' export * from './components/calendar/internal/DateUtils' export * from './colors' +export * from './workspace' export * from './focus' export * from './resize' export * from './lazy' diff --git a/packages/ui/src/workspace.ts b/packages/ui/src/workspace.ts new file mode 100644 index 0000000000..76b67a5ed9 --- /dev/null +++ b/packages/ui/src/workspace.ts @@ -0,0 +1,35 @@ +// +// Copyright © 2026 TraceX SAS. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/** + * Shared helpers for rendering workspace selection UI (login page, in-app + * workspace switcher). Kept here so both places stay visually consistent + * without introducing a dependency between the plugins that own them. + * @public + */ +export function getWorkspaceInitial (name: string | undefined): string { + const initial = (name ?? '').trim().charAt(0).toUpperCase() + return initial === '' ? '?' : initial +} + +/** + * @public + */ +export function getWorkspaceLastVisitDays (lastVisit: number | undefined): number | undefined { + if (lastVisit === undefined || lastVisit === 0) { + return undefined + } + return Math.round((Date.now() - lastVisit) / (1000 * 3600 * 24)) +} diff --git a/plugins/login-resources/src/components/SelectWorkspace.svelte b/plugins/login-resources/src/components/SelectWorkspace.svelte index 9060e6b6ed..8c7b89c812 100644 --- a/plugins/login-resources/src/components/SelectWorkspace.svelte +++ b/plugins/login-resources/src/components/SelectWorkspace.svelte @@ -23,14 +23,16 @@ } from '@hcengineering/core' import { LoginInfo } from '@hcengineering/login' import { OK, Severity, Status } from '@hcengineering/platform' - import presentation, { MessageBox, NavLink, isAdminUser, reduceCalls } from '@hcengineering/presentation' + import presentation, { MessageBox, NavLink, reduceCalls } from '@hcengineering/presentation' import { Button, Label, Scroller, SearchEdit, Spinner, + WorkspaceAvatar, deviceOptionsStore as deviceInfo, + getWorkspaceLastVisitDays, showPopup, ticker } from '@hcengineering/ui' @@ -53,22 +55,6 @@ export let navigateUrl: string | undefined = undefined - // Workspaces have no icon/logo of their own, so we derive a stable - // colored initial as a lightweight stand-in for a real workspace icon. - const workspaceAvatarColors = ['#5B9BF0', '#63BC8B', '#E0A64D', '#D96B6B', '#9B7FE0', '#4DB6C7', '#E08FC0', '#8CC152'] - - function workspaceAvatarColor (id: string): string { - let hash = 0 - for (let i = 0; i < id.length; i++) { - hash = (hash * 31 + id.charCodeAt(i)) | 0 - } - return workspaceAvatarColors[Math.abs(hash) % workspaceAvatarColors.length] - } - - function workspaceInitial (name: string): string { - return name.trim().charAt(0).toUpperCase() || '?' - } - let workspaces: WorkspaceInfoWithStatus[] = [] let status = OK let accountPromise: Promise @@ -186,21 +172,19 @@ .filter((it) => search === '' || (it.name?.includes(search) ?? false) || it.url.includes(search)) .slice(0, 500) as workspace} {@const wsName = workspace.name ?? workspace.url} - {@const lastUsageDays = - workspace.lastVisit === undefined - ? 'N/A' - : Math.round((Date.now() - workspace.lastVisit) / (1000 * 3600 * 24))} + {@const lastUsageDays = getWorkspaceLastVisitDays(workspace.lastVisit)}
select(workspace.url)}> -
- {workspaceInitial(wsName)} -
+ {wsName} - {#if workspace.hasUnread === true} -
- {/if} {#if isArchivingMode(workspace.mode)} -
{/each} @@ -256,14 +240,6 @@ diff --git a/plugins/workbench-resources/src/plugin.ts b/plugins/workbench-resources/src/plugin.ts index 7d23bead95..3162a3305b 100644 --- a/plugins/workbench-resources/src/plugin.ts +++ b/plugins/workbench-resources/src/plugin.ts @@ -46,7 +46,9 @@ export default mergeIds(workbenchId, workbench, { AccessDenied: '' as IntlString, Widget: '' as IntlString, WidgetPreference: '' as IntlString, - Tab: '' as IntlString + Tab: '' as IntlString, + FailedToLoadWorkspaces: '' as IntlString, + NoWorkspacesFound: '' as IntlString }, component: { SpacePanel: '' as AnyComponent,