mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-30 05:25:00 +02:00
Fix validation issues (#11050)
* Fix formatting Signed-off-by: Artyom Savchenko <armisav@gmail.com> * Fix svelte-check Signed-off-by: Artyom Savchenko <armisav@gmail.com> * Fix build Signed-off-by: Artyom Savchenko <armisav@gmail.com> * Fix formatting Signed-off-by: Artyom Savchenko <armisav@gmail.com> * Fix print resources Signed-off-by: Artyom Savchenko <armisav@gmail.com> --------- Signed-off-by: Artyom Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -16,10 +16,12 @@ describe('workspace favicon before application startup', () => {
|
||||
expect(script).toBeDefined()
|
||||
runInNewContext(script ?? '', {
|
||||
location: { pathname },
|
||||
localStorage: { getItem: (key: string) => {
|
||||
if (denied) throw new Error('Storage denied')
|
||||
return key === 'huly.workspace-favicon:/workbench/company-a' ? cached ?? null : null
|
||||
} },
|
||||
localStorage: {
|
||||
getItem: (key: string) => {
|
||||
if (denied) throw new Error('Storage denied')
|
||||
return key === 'huly.workspace-favicon:/workbench/company-a' ? (cached ?? null) : null
|
||||
}
|
||||
},
|
||||
document: {
|
||||
getElementById: () => fallback,
|
||||
createElement: () => ({}),
|
||||
@@ -76,31 +78,43 @@ describe('workspace identification colour', () => {
|
||||
|
||||
describe('optional workspace favicon', () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
afterEach(() => { globalThis.fetch = originalFetch })
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it('does not load images when both options are disabled', async () => {
|
||||
const fetchIcon = jest.fn()
|
||||
globalThis.fetch = fetchIcon
|
||||
await expect(renderWorkspaceIdentity('/workspace.png', null, undefined, {
|
||||
syncLogo: false, showColor: false
|
||||
})).resolves.toEqual({ color: '#64748b' })
|
||||
await expect(
|
||||
renderWorkspaceIdentity('/workspace.png', null, undefined, {
|
||||
syncLogo: false,
|
||||
showColor: false
|
||||
})
|
||||
).resolves.toEqual({ color: '#64748b' })
|
||||
expect(fetchIcon).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not wait for a workspace logo when applying a manual badge to the site icon', async () => {
|
||||
const fetchIcon = jest.fn().mockRejectedValue(new Error('Site icon unavailable'))
|
||||
globalThis.fetch = fetchIcon
|
||||
await expect(renderWorkspaceIdentity('/workspace.png', '#ff8800', undefined, {
|
||||
syncLogo: false, showColor: true, defaultIconUrl: '/site.ico'
|
||||
})).rejects.toThrow('Site icon unavailable')
|
||||
await expect(
|
||||
renderWorkspaceIdentity('/workspace.png', '#ff8800', undefined, {
|
||||
syncLogo: false,
|
||||
showColor: true,
|
||||
defaultIconUrl: '/site.ico'
|
||||
})
|
||||
).rejects.toThrow('Site icon unavailable')
|
||||
expect(fetchIcon).toHaveBeenCalledTimes(1)
|
||||
expect(fetchIcon).toHaveBeenCalledWith('/site.ico', { signal: undefined })
|
||||
})
|
||||
|
||||
it('falls back to the unchanged site favicon if the workspace logo is unavailable', async () => {
|
||||
globalThis.fetch = jest.fn().mockRejectedValue(new Error('Workspace logo unavailable'))
|
||||
await expect(renderWorkspaceIdentity('/workspace.png', null, undefined, {
|
||||
syncLogo: true, showColor: false
|
||||
})).resolves.toEqual({ color: '#64748b' })
|
||||
await expect(
|
||||
renderWorkspaceIdentity('/workspace.png', null, undefined, {
|
||||
syncLogo: true,
|
||||
showColor: false
|
||||
})
|
||||
).resolves.toEqual({ color: '#64748b' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ async function resumeAudioContext (context: AudioContext): Promise<boolean> {
|
||||
|
||||
// Calling resume before the document has received user activation can leave
|
||||
// its promise pending indefinitely because of the browser autoplay policy.
|
||||
if (navigator.userActivation?.hasBeenActive === false) return false
|
||||
if (!navigator.userActivation?.hasBeenActive) return false
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
|
||||
@@ -22,11 +22,18 @@ export function mixLogoColor (pixels: Uint8ClampedArray): string {
|
||||
}
|
||||
}
|
||||
if (weight === 0) return defaultIdentityColor
|
||||
return '#' + total.map((sum) => {
|
||||
const linear = sum / weight
|
||||
const srgb = linear <= 0.0031308 ? linear * 12.92 : 1.055 * linear ** (1 / 2.4) - 0.055
|
||||
return Math.round(Math.max(0, Math.min(1, srgb)) * 255).toString(16).padStart(2, '0')
|
||||
}).join('')
|
||||
return (
|
||||
'#' +
|
||||
total
|
||||
.map((sum) => {
|
||||
const linear = sum / weight
|
||||
const srgb = linear <= 0.0031308 ? linear * 12.92 : 1.055 * linear ** (1 / 2.4) - 0.055
|
||||
return Math.round(Math.max(0, Math.min(1, srgb)) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0')
|
||||
})
|
||||
.join('')
|
||||
)
|
||||
}
|
||||
|
||||
/** @public */
|
||||
@@ -90,15 +97,16 @@ export function clearWorkspaceFaviconCache (ownerDocument: Document = document):
|
||||
function getOriginalIcons (ownerDocument: Document): HTMLLinkElement[] {
|
||||
let icons = originalIcons.get(ownerDocument)
|
||||
if (icons === undefined) {
|
||||
icons = Array.from(ownerDocument.head.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]:not(#workspace-favicon)'))
|
||||
.map((icon) => {
|
||||
// The initial page keeps fallback icons inert while a cached workspace icon is visible.
|
||||
if (icon.dataset.defaultHref === undefined) return icon
|
||||
const original = icon.cloneNode(true) as HTMLLinkElement
|
||||
original.href = new URL(icon.dataset.defaultHref, ownerDocument.baseURI).href
|
||||
delete original.dataset.defaultHref
|
||||
return original
|
||||
})
|
||||
icons = Array.from(
|
||||
ownerDocument.head.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]:not(#workspace-favicon)')
|
||||
).map((icon) => {
|
||||
// The initial page keeps fallback icons inert while a cached workspace icon is visible.
|
||||
if (icon.dataset.defaultHref === undefined) return icon
|
||||
const original = icon.cloneNode(true) as HTMLLinkElement
|
||||
original.href = new URL(icon.dataset.defaultHref, ownerDocument.baseURI).href
|
||||
delete original.dataset.defaultHref
|
||||
return original
|
||||
})
|
||||
originalIcons.set(ownerDocument, icons)
|
||||
}
|
||||
return icons
|
||||
@@ -192,7 +200,8 @@ export function createWorkspaceFavicon (ownerDocument: Document = document): {
|
||||
const defaults = getOriginalIcons(ownerDocument)
|
||||
const cacheKey = faviconCacheKey(ownerDocument)
|
||||
const cacheRevision = cacheRevisions.get(ownerDocument) ?? 0
|
||||
const link = ownerDocument.querySelector<HTMLLinkElement>('link#workspace-favicon') ?? ownerDocument.createElement('link')
|
||||
const link =
|
||||
ownerDocument.querySelector<HTMLLinkElement>('link#workspace-favicon') ?? ownerDocument.createElement('link')
|
||||
link.rel = 'icon'
|
||||
link.type = 'image/png'
|
||||
link.sizes.value = '32x32'
|
||||
@@ -225,8 +234,14 @@ export function createWorkspaceFavicon (ownerDocument: Document = document): {
|
||||
request?.abort()
|
||||
request = new AbortController()
|
||||
try {
|
||||
const result = await renderWorkspaceIdentity(logoUrl, color, request.signal,
|
||||
options === undefined ? undefined : { ...options, defaultIconUrl: getDefaultWorkspaceFaviconUrl(ownerDocument) })
|
||||
const result = await renderWorkspaceIdentity(
|
||||
logoUrl,
|
||||
color,
|
||||
request.signal,
|
||||
options === undefined
|
||||
? undefined
|
||||
: { ...options, defaultIconUrl: getDefaultWorkspaceFaviconUrl(ownerDocument) }
|
||||
)
|
||||
if (disposed || current !== revision || faviconCacheKey(ownerDocument) !== cacheKey) return
|
||||
if (cacheRevision !== (cacheRevisions.get(ownerDocument) ?? 0)) return
|
||||
if (result.favicon === undefined) {
|
||||
|
||||
@@ -28,7 +28,15 @@
|
||||
* a prefix is only aliased (transformation 1), never wrapped (transformation
|
||||
* 2). This matches user intent — they specified a field explicitly.
|
||||
*/
|
||||
import { escapeRegExp, fullTextSearchFields } from '@hcengineering/core'
|
||||
import * as core from '@hcengineering/core'
|
||||
|
||||
// Keep standalone package checks independent of generated dependency declarations.
|
||||
// Both exports are part of the current core source and runtime bundle, while an
|
||||
// older ignored `core/types` directory may not describe them yet.
|
||||
const { escapeRegExp, fullTextSearchFields } = core as typeof core & {
|
||||
escapeRegExp: (value: string) => string
|
||||
fullTextSearchFields: readonly string[]
|
||||
}
|
||||
|
||||
export type SearchScope = 'title' | 'title-description' | 'all'
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const MILLISECONDS_IN_MINUTE = 60000
|
||||
export const MILLISECONDS_IN_DAY = 86400000
|
||||
export const MILLISECONDS_IN_WEEK = DAYS_IN_WEEK * MILLISECONDS_IN_DAY
|
||||
|
||||
export function firstDay(date: Date, firstDay: number = 1): Date {
|
||||
export function firstDay (date: Date, firstDay: number = 1): Date {
|
||||
const firstDayOfMonth = new Date(new Date(date).setHours(0, 0, 0, 0))
|
||||
firstDayOfMonth.setDate(1) // First day of month
|
||||
const result = new Date(firstDayOfMonth)
|
||||
@@ -32,23 +32,23 @@ export function firstDay(date: Date, firstDay: number = 1): Date {
|
||||
return result
|
||||
}
|
||||
|
||||
export function getWeek(date: Date): number {
|
||||
export function getWeek (date: Date): number {
|
||||
const onejan = new Date(date.getFullYear(), 0, 1)
|
||||
return Math.ceil(((date.getTime() - onejan.getTime()) / MILLISECONDS_IN_DAY + onejan.getDay() + 1) / DAYS_IN_WEEK)
|
||||
}
|
||||
|
||||
export function daysInMonth(date: Date): number {
|
||||
export function daysInMonth (date: Date): number {
|
||||
return 33 - new Date(date.getFullYear(), date.getMonth(), 33).getDate()
|
||||
}
|
||||
|
||||
export function getWeekDayName(weekDay: Date, weekFormat: 'narrow' | 'short' | 'long' | undefined = 'short'): string {
|
||||
export function getWeekDayName (weekDay: Date, weekFormat: 'narrow' | 'short' | 'long' | undefined = 'short'): string {
|
||||
const locale = new Intl.NumberFormat().resolvedOptions().locale
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
weekday: weekFormat
|
||||
}).format(weekDay)
|
||||
}
|
||||
|
||||
export function getWeekDayNames(): Map<number, string> {
|
||||
export function getWeekDayNames (): Map<number, string> {
|
||||
const today: Date = new Date()
|
||||
const offset: number = 0 - today.getDay()
|
||||
const startDate: number = today.setTime(today.getTime() + MILLISECONDS_IN_DAY * offset)
|
||||
@@ -59,15 +59,15 @@ export function getWeekDayNames(): Map<number, string> {
|
||||
return result
|
||||
}
|
||||
|
||||
export function day(firstDay: Date, offset: number): Date {
|
||||
export function day (firstDay: Date, offset: number): Date {
|
||||
return new Date(new Date(firstDay).setDate(firstDay.getDate() + offset))
|
||||
}
|
||||
|
||||
export function weekday(firstDay: Date, w: number, d: number): Date {
|
||||
export function weekday (firstDay: Date, w: number, d: number): Date {
|
||||
return day(firstDay, w * DAYS_IN_WEEK + d)
|
||||
}
|
||||
|
||||
export function areDatesEqual(firstDate: Date | undefined, secondDate: Date | undefined): boolean {
|
||||
export function areDatesEqual (firstDate: Date | undefined, secondDate: Date | undefined): boolean {
|
||||
if (firstDate === undefined || secondDate === undefined) {
|
||||
return false
|
||||
}
|
||||
@@ -78,19 +78,19 @@ export function areDatesEqual(firstDate: Date | undefined, secondDate: Date | un
|
||||
)
|
||||
}
|
||||
|
||||
export function fromCurrentToTz(date: Date | number, tz: string): Date {
|
||||
export function fromCurrentToTz (date: Date | number, tz: string): Date {
|
||||
return utcToZonedTime(zonedTimeToUtc(date, getUserTimezone()), tz)
|
||||
}
|
||||
|
||||
export function fromTzToCurrent(date: Date | number, tz: string): Date {
|
||||
export function fromTzToCurrent (date: Date | number, tz: string): Date {
|
||||
return utcToZonedTime(zonedTimeToUtc(date, tz), getUserTimezone())
|
||||
}
|
||||
|
||||
export function isWeekend(date: Date): boolean {
|
||||
export function isWeekend (date: Date): boolean {
|
||||
return date.getDay() === 0 || date.getDay() === 6
|
||||
}
|
||||
|
||||
export function getMonthName(date: Date, option: 'narrow' | 'short' | 'long' | 'numeric' | '2-digit' = 'long'): string {
|
||||
export function getMonthName (date: Date, option: 'narrow' | 'short' | 'long' | 'numeric' | '2-digit' = 'long'): string {
|
||||
try {
|
||||
const locale = new Intl.NumberFormat().resolvedOptions().locale
|
||||
return new Intl.DateTimeFormat(locale, { month: option }).format(date)
|
||||
@@ -106,24 +106,24 @@ export interface ICell {
|
||||
classes: TCellStyle
|
||||
}
|
||||
|
||||
export function getLocalWeekStart(): number {
|
||||
export function getLocalWeekStart (): number {
|
||||
const locale = new Intl.Locale(navigator.language)
|
||||
return typeof (locale as any)?.getWeekInfo === 'function'
|
||||
? (locale as any)?.getWeekInfo()?.firstDay
|
||||
: ((locale as any).weekInfo?.firstDay ?? 1)
|
||||
}
|
||||
|
||||
export function hasLocalWeekStart(): boolean {
|
||||
export function hasLocalWeekStart (): boolean {
|
||||
const locale = new Intl.Locale(navigator.language)
|
||||
return typeof (locale as any)?.getWeekInfo === 'function' || (locale as any).weekInfo?.firstDay !== undefined
|
||||
}
|
||||
|
||||
export function getWeekStart(date: Date = new Date(), firstDay: number = 1): Date {
|
||||
export function getWeekStart (date: Date = new Date(), firstDay: number = 1): Date {
|
||||
date = new Date(new Date(date).setHours(0, 0, 0, 0))
|
||||
return new Date(date.setDate(date.getDate() - ((date.getDay() - firstDay + 7) % 7)))
|
||||
}
|
||||
|
||||
export function addZero(value: number): string {
|
||||
export function addZero (value: number): string {
|
||||
if (value < 10) {
|
||||
return `0${value}`
|
||||
}
|
||||
@@ -155,11 +155,11 @@ export const getDueDateIconModifier = (
|
||||
return 'normal'
|
||||
}
|
||||
|
||||
export function getFormattedDate(value: number | null, options?: Intl.DateTimeFormatOptions): string {
|
||||
export function getFormattedDate (value: number | null, options?: Intl.DateTimeFormatOptions): string {
|
||||
return value === null ? '' : new Date(value).toLocaleString('default', options ?? { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
export function formatDate(date: Date, showYear: boolean): string {
|
||||
export function formatDate (date: Date, showYear: boolean): string {
|
||||
const locale = new Intl.NumberFormat().resolvedOptions().locale
|
||||
const options: Intl.DateTimeFormatOptions = showYear
|
||||
? { year: 'numeric', month: 'short', day: 'numeric' }
|
||||
@@ -185,7 +185,7 @@ export const convertTimeZone = (tz: string): TimeZone => {
|
||||
}
|
||||
}
|
||||
|
||||
export function getUserTimezone(): string {
|
||||
export function getUserTimezone (): string {
|
||||
if (window.Intl !== undefined) {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
} catch (err) {
|
||||
if (!controller.signal.aborted && file === value) {
|
||||
failed = true
|
||||
Analytics.handleError(err)
|
||||
Analytics.handleError(err instanceof Error ? err : new Error(String(err)))
|
||||
}
|
||||
} finally {
|
||||
if (!controller.signal.aborted && file === value) {
|
||||
|
||||
@@ -110,9 +110,13 @@
|
||||
|
||||
const client = getClient()
|
||||
const workspaceSettingsQuery = createQuery()
|
||||
workspaceSettingsQuery.query(settingsRes.class.WorkspaceSetting, { _id: settingsRes.ids.WorkspaceSetting }, (result) => {
|
||||
workspaceSettings = result[0]
|
||||
})
|
||||
workspaceSettingsQuery.query(
|
||||
settingsRes.class.WorkspaceSetting,
|
||||
{ _id: settingsRes.ids.WorkspaceSetting },
|
||||
(result) => {
|
||||
workspaceSettings = result[0]
|
||||
}
|
||||
)
|
||||
|
||||
async function handleAvatarDone (): Promise<void> {
|
||||
const existing = await client.findOne(settingsRes.class.WorkspaceSetting, { _id: settingsRes.ids.WorkspaceSetting })
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
<script lang="ts">
|
||||
import core, { AccountRole, getCurrentAccount } from '@hcengineering/core'
|
||||
import {
|
||||
defaultIdentityColor, getClient, getFileUrl, getDefaultWorkspaceFaviconUrl, normalizeIdentityColor,
|
||||
renderWorkspaceIdentity, type WorkspaceIdentityImage
|
||||
defaultIdentityColor,
|
||||
getClient,
|
||||
getFileUrl,
|
||||
getDefaultWorkspaceFaviconUrl,
|
||||
normalizeIdentityColor,
|
||||
renderWorkspaceIdentity,
|
||||
type WorkspaceIdentityImage
|
||||
} from '@hcengineering/presentation'
|
||||
import setting, { type WorkspaceSetting } from '@hcengineering/setting'
|
||||
import { Button, Label, Toggle } from '@hcengineering/ui'
|
||||
@@ -27,7 +32,12 @@
|
||||
$: logoUrl = workspaceSetting?.icon != null ? getFileUrl(workspaceSetting.icon) : undefined
|
||||
$: void updatePreview(logoUrl, manualColor, syncLogo, showColor)
|
||||
|
||||
async function updatePreview (url: string | undefined, color: string | undefined, syncLogo: boolean, showColor: boolean): Promise<void> {
|
||||
async function updatePreview (
|
||||
url: string | undefined,
|
||||
color: string | undefined,
|
||||
syncLogo: boolean,
|
||||
showColor: boolean
|
||||
): Promise<void> {
|
||||
const current = ++revision
|
||||
request?.abort()
|
||||
request = new AbortController()
|
||||
@@ -48,7 +58,9 @@
|
||||
if (color !== undefined) await save({ identificationColor: color })
|
||||
}
|
||||
|
||||
async function save (patch: Partial<Pick<WorkspaceSetting, 'identificationColor' | 'syncWorkspaceLogo' | 'identificationColorEnabled'>>): Promise<void> {
|
||||
async function save (
|
||||
patch: Partial<Pick<WorkspaceSetting, 'identificationColor' | 'syncWorkspaceLogo' | 'identificationColorEnabled'>>
|
||||
): Promise<void> {
|
||||
if (!canEdit || busy) return
|
||||
const focused = document.activeElement as HTMLElement | null
|
||||
const focusLabel = focused?.closest('label')?.getAttribute('aria-labelledby')
|
||||
@@ -57,8 +69,12 @@
|
||||
try {
|
||||
const existing = await client.findOne(setting.class.WorkspaceSetting, { _id: setting.ids.WorkspaceSetting })
|
||||
if (existing === undefined) {
|
||||
await client.createDoc(setting.class.WorkspaceSetting, core.space.Workspace,
|
||||
patch, setting.ids.WorkspaceSetting)
|
||||
await client.createDoc(
|
||||
setting.class.WorkspaceSetting,
|
||||
core.space.Workspace,
|
||||
patch,
|
||||
setting.ids.WorkspaceSetting
|
||||
)
|
||||
} else {
|
||||
await client.diffUpdate(existing, patch)
|
||||
}
|
||||
@@ -90,8 +106,12 @@
|
||||
<span id="workspace-sync-logo-label"><Label label={setting.string.SyncWorkspaceLogo} /></span>
|
||||
<div class="controls">
|
||||
{#key saveRevision}
|
||||
<Toggle on={syncLogo} disabled={!canEdit || busy} aria-labelledby="workspace-sync-logo-label"
|
||||
on:change={(event) => save({ syncWorkspaceLogo: event.detail })} />
|
||||
<Toggle
|
||||
on={syncLogo}
|
||||
disabled={!canEdit || busy}
|
||||
aria-labelledby="workspace-sync-logo-label"
|
||||
on:change={(event) => save({ syncWorkspaceLogo: event.detail })}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,8 +119,12 @@
|
||||
<span id="workspace-color-label"><Label label={setting.string.IdentificationColor} /></span>
|
||||
<div class="controls">
|
||||
{#key saveRevision}
|
||||
<Toggle on={showColor} disabled={!canEdit || busy} aria-labelledby="workspace-color-label"
|
||||
on:change={(event) => save({ identificationColorEnabled: event.detail })} />
|
||||
<Toggle
|
||||
on={showColor}
|
||||
disabled={!canEdit || busy}
|
||||
aria-labelledby="workspace-color-label"
|
||||
on:change={(event) => save({ identificationColorEnabled: event.detail })}
|
||||
/>
|
||||
{/key}
|
||||
{#if showColor}
|
||||
<div class="color-control" role="group" aria-labelledby="workspace-color-label">
|
||||
@@ -108,12 +132,23 @@
|
||||
<span class="color-swatch" style:background={preview.color} />
|
||||
<code>{preview.color.toUpperCase()}</code>
|
||||
<span class="sr-only"><Label label={setting.string.IdentificationColor} /></span>
|
||||
<input bind:this={colorInput} type="color" value={preview.color} disabled={!canEdit || busy}
|
||||
on:change={(event) => saveColor(event.currentTarget.value)} />
|
||||
<input
|
||||
bind:this={colorInput}
|
||||
type="color"
|
||||
value={preview.color}
|
||||
disabled={!canEdit || busy}
|
||||
on:change={(event) => saveColor(event.currentTarget.value)}
|
||||
/>
|
||||
</label>
|
||||
<span class="color-reset">
|
||||
<Button kind="ghost" size="small" label={setting.string.ColorDefault} padding="0 .5rem"
|
||||
disabled={!canEdit || busy || manualColor === undefined} on:click={() => saveColor(null)} />
|
||||
<Button
|
||||
kind="ghost"
|
||||
size="small"
|
||||
label={setting.string.ColorDefault}
|
||||
padding="0 .5rem"
|
||||
disabled={!canEdit || busy || manualColor === undefined}
|
||||
on:click={() => saveColor(null)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -124,10 +159,30 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.identity { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.setting-row { display: grid; grid-template-columns: min(11rem, 45%) minmax(0, 1fr); align-items: center; gap: 0.75rem; min-height: 2rem; }
|
||||
.controls { display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem; min-width: 0; }
|
||||
.setting-row :global(.toggle:focus-within) { outline: 2px solid var(--primary-button-outline); outline-offset: 3px; border-radius: 1rem; }
|
||||
.identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.setting-row {
|
||||
display: grid;
|
||||
grid-template-columns: min(11rem, 45%) minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-height: 2rem;
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.setting-row :global(.toggle:focus-within) {
|
||||
outline: 2px solid var(--primary-button-outline);
|
||||
outline-offset: 3px;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
.color-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -145,7 +200,10 @@
|
||||
border-radius: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.color-value:focus-within { outline: 2px solid var(--theme-content-color); outline-offset: 2px; }
|
||||
.color-value:focus-within {
|
||||
outline: 2px solid var(--theme-content-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.color-swatch {
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
@@ -153,9 +211,31 @@
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: inset 0 0 0 1px var(--theme-divider-color);
|
||||
}
|
||||
.color-reset { border-left: 1px solid var(--theme-divider-color); padding-left: 0.125rem; }
|
||||
input[type='color'] { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
|
||||
code { font-size: 0.8rem; }
|
||||
.error { color: var(--theme-error-color, #e05252); font-size: 0.8125rem; }
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||
.color-reset {
|
||||
border-left: 1px solid var(--theme-divider-color);
|
||||
padding-left: 0.125rem;
|
||||
}
|
||||
input[type='color'] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
code {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.error {
|
||||
color: var(--theme-error-color, #e05252);
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ describe('buildRecipientBundles — end-to-end', () => {
|
||||
expect(bundles.size).toBe(1)
|
||||
const bundle = bundles.get(userB) ?? []
|
||||
expect(bundle.length).toBe(2)
|
||||
expect(bundle.map((b) => b.identifier).sort()).toEqual(['PROJ-2', 'PROJ-3'])
|
||||
expect(bundle.map((b) => b.identifier).sort((a, b) => a.localeCompare(b))).toEqual(['PROJ-2', 'PROJ-3'])
|
||||
})
|
||||
|
||||
it('produces zero bundles when only the trigger user has stake in any shift', () => {
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
dispatch('update', { key: model.key, value })
|
||||
}
|
||||
|
||||
type DependentViewOption = ViewOptionModel & { dependsOn?: string }
|
||||
|
||||
// checking if selector provides multiple choice options
|
||||
const hasMultipleSelections = (varTocheck: any) => {
|
||||
if (!varTocheck) return false
|
||||
@@ -95,8 +97,9 @@
|
||||
$: visibleOthers = config.other.filter((p) => {
|
||||
if (p.hidden?.(viewOptions) === true) return false
|
||||
if (hideKeys.includes(p.key)) return false
|
||||
if (p.dependsOn != null) {
|
||||
const parentValue = (viewOptions as Record<string, unknown>)?.[p.dependsOn]
|
||||
const { dependsOn } = p as DependentViewOption
|
||||
if (dependsOn != null) {
|
||||
const parentValue = (viewOptions as Record<string, unknown>)?.[dependsOn]
|
||||
if (parentValue !== true) return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -19,7 +19,14 @@ import { type PresentationMiddlewareCreator } from '@hcengineering/presentation/
|
||||
import { type AnyComponent } from '@hcengineering/ui/src/types'
|
||||
import view, { viewId } from '@hcengineering/view'
|
||||
|
||||
export default mergeIds(viewId, view, {
|
||||
// Keep standalone package checks independent of generated dependency declarations.
|
||||
// ConfigureColumns is already registered by the current @hcengineering/view
|
||||
// runtime; an older ignored `view/types` directory may not describe it yet.
|
||||
const typedView = view as typeof view & {
|
||||
string: typeof view.string & { ConfigureColumns: IntlString }
|
||||
}
|
||||
|
||||
export default mergeIds(viewId, typedView, {
|
||||
component: {
|
||||
ObjectFilter: '' as AnyComponent,
|
||||
DateFilter: '' as AnyComponent,
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
settingsLoaded = true
|
||||
})
|
||||
$: logoUrl = workspaceSetting?.icon != null ? getFileUrl(workspaceSetting.icon) : undefined
|
||||
$: if (settingsLoaded) void favicon?.update(logoUrl, workspaceSetting?.identificationColor, {
|
||||
syncLogo: workspaceSetting?.syncWorkspaceLogo === true,
|
||||
showColor: workspaceSetting?.identificationColorEnabled === true
|
||||
})
|
||||
$: if (settingsLoaded) {
|
||||
void favicon?.update(logoUrl, workspaceSetting?.identificationColor, {
|
||||
syncLogo: workspaceSetting?.syncWorkspaceLogo === true,
|
||||
showColor: workspaceSetting?.identificationColorEnabled === true
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
favicon = createWorkspaceFavicon()
|
||||
|
||||
@@ -29,7 +29,12 @@ import type {
|
||||
import core, { hasAccountRole } from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { getMetadata, getResource, setMetadata } from '@hcengineering/platform'
|
||||
import presentation, { clearWorkspaceFaviconCache, closeClient, getClient, setPresentationCookie } from '@hcengineering/presentation'
|
||||
import presentation, {
|
||||
clearWorkspaceFaviconCache,
|
||||
closeClient,
|
||||
getClient,
|
||||
setPresentationCookie
|
||||
} from '@hcengineering/presentation'
|
||||
import {
|
||||
closePanel,
|
||||
getCurrentLocation,
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('rest-api-server', () => {
|
||||
const person = await accountClient.getPerson()
|
||||
const socialIds: SocialId[] = await accountClient.getSocialIds(true)
|
||||
|
||||
// Ensure employee is created
|
||||
// Employee mixin creation is privileged; use the system token only for test setup.
|
||||
|
||||
await ensureEmployee(
|
||||
testCtx,
|
||||
@@ -99,7 +99,7 @@ describe('rest-api-server', () => {
|
||||
socialIds: socialIds.map((si) => si._id),
|
||||
fullSocialIds: socialIds
|
||||
},
|
||||
connect(),
|
||||
connect(apiWorkspace1, true),
|
||||
socialIds,
|
||||
async () => person
|
||||
)
|
||||
@@ -113,7 +113,7 @@ describe('rest-api-server', () => {
|
||||
socialIds: socialIds.map((si) => si._id),
|
||||
fullSocialIds: socialIds
|
||||
},
|
||||
connect(apiWorkspace2),
|
||||
connect(apiWorkspace2, true),
|
||||
socialIds,
|
||||
async () => person
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user