diff --git a/dev/prod/src/index.ejs b/dev/prod/src/index.ejs index 96a11cc5810..87229f8a45e 100644 --- a/dev/prod/src/index.ejs +++ b/dev/prod/src/index.ejs @@ -4,10 +4,35 @@ Huly - + + + - \ No newline at end of file + diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 943b4a1529e..2bfa1f67b70 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -454,7 +454,12 @@ export async function configurePlatform() { for (const link of links) { const htmlLink = document.createElement('link') htmlLink.rel = link.rel - htmlLink.href = link.href + if (link.rel.split(/\s+/).includes('icon') && document.getElementById('workspace-favicon') !== null) { + // Preserve the startup icon until workspace settings are available. + htmlLink.dataset.defaultHref = link.href + } else { + htmlLink.href = link.href + } if (link.type !== undefined) { htmlLink.type = link.type diff --git a/models/setting/src/index.ts b/models/setting/src/index.ts index 9778abb96e0..f0c0038ea82 100644 --- a/models/setting/src/index.ts +++ b/models/setting/src/index.ts @@ -134,6 +134,9 @@ export class TOfficeSettings extends TConfiguration implements OfficeSettings { @Model(setting.class.WorkspaceSetting, core.class.Doc, DOMAIN_SETTING) export class TWorkspaceSetting extends TDoc implements WorkspaceSetting { icon?: Ref + identificationColor?: string | null + syncWorkspaceLogo?: boolean + identificationColorEnabled?: boolean } @Mixin(setting.mixin.SpaceTypeEditor, core.class.Class) diff --git a/packages/presentation/src/___tests___/workspaceIdentity.test.ts b/packages/presentation/src/___tests___/workspaceIdentity.test.ts new file mode 100644 index 00000000000..c6169b6f816 --- /dev/null +++ b/packages/presentation/src/___tests___/workspaceIdentity.test.ts @@ -0,0 +1,106 @@ +// Copyright © 2026 Huly Contributors. Licensed under the Eclipse Public License, Version 2.0. + +import { mixLogoColor, normalizeIdentityColor, renderWorkspaceIdentity } from '../workspaceIdentity' +import { readFileSync } from 'fs' +import { resolve } from 'path' +import { runInNewContext } from 'vm' + +describe('workspace favicon before application startup', () => { + const icon = 'data:image/png;base64,iVBORw0KGgo=' + const template = readFileSync(resolve(__dirname, '../../../../dev/prod/src/index.ejs'), 'utf8') + + function bootstrap (pathname: string, cached?: string, denied = false): { fallback: any, icons: any[] } { + const fallback = { href: '', dataset: { defaultHref: '/huly/favicon.ico' } } + const icons: any[] = [] + const script = template.match(/ + +
+
+ +
+ {#key saveRevision} + save({ syncWorkspaceLogo: event.detail })} /> + {/key} +
+
+
+ +
+ {#key saveRevision} + save({ identificationColorEnabled: event.detail })} /> + {/key} + {#if showColor} +
+ + +
+ {/if} +
+
+ {#if imageError}
{/if} + {#if error}{/if} +
+ + diff --git a/plugins/setting/src/index.ts b/plugins/setting/src/index.ts index bf8d3ef6a0a..d7a65ffeacf 100644 --- a/plugins/setting/src/index.ts +++ b/plugins/setting/src/index.ts @@ -165,6 +165,10 @@ export interface OfficeSettings extends Configuration { */ export interface WorkspaceSetting extends Doc { icon?: Ref | null + /** A manual colour override. Missing/null follows the current logo automatically. */ + identificationColor?: string | null + syncWorkspaceLogo?: boolean + identificationColorEnabled?: boolean } export enum IntegrationError { @@ -255,6 +259,11 @@ export default plugin(settingId, { Setting: '' as IntlString, Spaces: '' as IntlString, WorkspaceSettings: '' as IntlString, + IdentificationColor: '' as IntlString, + SyncWorkspaceLogo: '' as IntlString, + ColorDefault: '' as IntlString, + ColorLogoUnavailable: '' as IntlString, + ColorSaveFailed: '' as IntlString, Integrations: '' as IntlString, Support: '' as IntlString, Privacy: '' as IntlString, diff --git a/plugins/workbench-resources/src/components/Workbench.svelte b/plugins/workbench-resources/src/components/Workbench.svelte index 1f81828e780..0e27b2ecf41 100644 --- a/plugins/workbench-resources/src/components/Workbench.svelte +++ b/plugins/workbench-resources/src/components/Workbench.svelte @@ -111,6 +111,7 @@ import AppSwitcher from './AppSwitcher.svelte' import Applications from './Applications.svelte' import Logo from './Logo.svelte' + import WorkspaceFavicon from './WorkspaceFavicon.svelte' import NavFooter from './NavFooter.svelte' import NavHeader from './NavHeader.svelte' import Navigator from './Navigator.svelte' @@ -839,6 +840,7 @@ /> {:else if $myEmployeeStore || account.role === AccountRole.Owner || isAdminUser()} + diff --git a/plugins/workbench-resources/src/components/WorkspaceFavicon.svelte b/plugins/workbench-resources/src/components/WorkspaceFavicon.svelte new file mode 100644 index 00000000000..f12ffac7bfe --- /dev/null +++ b/plugins/workbench-resources/src/components/WorkspaceFavicon.svelte @@ -0,0 +1,25 @@ + + diff --git a/plugins/workbench-resources/src/utils.ts b/plugins/workbench-resources/src/utils.ts index a2e5b067356..1aaec0636d3 100644 --- a/plugins/workbench-resources/src/utils.ts +++ b/plugins/workbench-resources/src/utils.ts @@ -29,7 +29,7 @@ import type { import core, { hasAccountRole } from '@hcengineering/core' import login from '@hcengineering/login' import { getMetadata, getResource, setMetadata } from '@hcengineering/platform' -import presentation, { closeClient, getClient, setPresentationCookie } from '@hcengineering/presentation' +import presentation, { clearWorkspaceFaviconCache, closeClient, getClient, setPresentationCookie } from '@hcengineering/presentation' import { closePanel, getCurrentLocation, @@ -217,6 +217,7 @@ export async function logIn (loginInfo: { account: string, token?: string }): Pr } export async function logOut (): Promise { + clearWorkspaceFaviconCache() const accountsUrl = getMetadata(login.metadata.AccountsUrl) try { await getAccountClient(accountsUrl).deleteCookie()