diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 54141344df..8ab1bd2927 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -420,7 +420,9 @@ export async function configurePlatform() { setMetadata(login.metadata.TransactorOverride, config.TRANSACTOR_OVERRIDE) // Use binary response transfer for faster performance and small transfer sizes. - setMetadata(client.metadata.UseBinaryProtocol, config.USE_BINARY_PROTOCOL ?? true) + const binaryOverride = localStorage.getItem(client.metadata.UseBinaryProtocol) + setMetadata(client.metadata.UseBinaryProtocol, binaryOverride != null ? binaryOverride === 'true' : (config.USE_BINARY_PROTOCOL ?? true)) + // Disable for now, since it causes performance issues on linux/docker/kubernetes boxes for now. setMetadata(client.metadata.UseProtocolCompression, true) diff --git a/dev/tool/package.json b/dev/tool/package.json index 0e1da1edcc..440618bb89 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -160,7 +160,6 @@ "csv-parse": "~5.1.0", "email-addresses": "^5.0.0", "fast-equals": "^5.0.1", - "got": "^11.8.3", "libphonenumber-js": "^1.9.46", "mime-types": "~2.1.34", "mongodb": "^6.12.0", diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 1082186475..cbffad5fc0 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -69,7 +69,7 @@ import { registerTxAdapterFactory } from '@hcengineering/server-pipeline' import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token' -import { FileModelLogger } from '@hcengineering/server-tool' +import { FileModelLogger, buildModel } from '@hcengineering/server-tool' import { createWorkspace, upgradeWorkspace } from '@hcengineering/workspace-service' import path from 'path' @@ -143,7 +143,7 @@ import { moveFromMongoToPG, moveWorkspaceFromMongoToPG } from './db' -import { restoreControlledDocContentMongo, restoreWikiContentMongo } from './markup' +import { restoreControlledDocContentMongo, restoreWikiContentMongo, restoreMarkupRefsMongo } from './markup' import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin' import { fixAccountEmails, renameAccount } from './renameAccount' import { copyToDatalake, moveFiles, showLostFiles } from './storage' @@ -1345,6 +1345,61 @@ export function devTool ( }) }) + program + .command('restore-markup-ref-mongo') + .description('restore markup document content refs') + .option('-w, --workspace ', 'Selected workspace only', '') + .option('-f, --force', 'Force update', false) + .action(async (cmd: { workspace: string, force: boolean }) => { + const { txes, version } = prepareTools() + + const { hierarchy } = await buildModel(toolCtx, txes) + + let workspaces: Workspace[] = [] + await withAccountDatabase(async (db) => { + workspaces = await listWorkspacesPure(db) + workspaces = workspaces + .filter((p) => isActiveMode(p.mode)) + .filter((p) => cmd.workspace === '' || p.workspace === cmd.workspace) + .sort((a, b) => b.lastVisit - a.lastVisit) + }) + + console.log('found workspaces', workspaces.length) + + await withStorage(async (storageAdapter) => { + const mongodbUri = getMongoDBUrl() + const client = getMongoClient(mongodbUri) + const _client = await client.getClient() + + try { + const count = workspaces.length + let index = 0 + for (const workspace of workspaces) { + index++ + + toolCtx.info('processing workspace', { + workspace: workspace.workspace, + version: workspace.version, + index, + count + }) + + if (!cmd.force && (workspace.version === undefined || !deepEqual(workspace.version, version))) { + console.log(`upgrade to ${versionToString(version)} is required`) + continue + } + + const workspaceId = getWorkspaceId(workspace.workspace) + const wsDb = getWorkspaceMongoDB(_client, { name: workspace.workspace }) + + await restoreMarkupRefsMongo(toolCtx, wsDb, workspaceId, hierarchy, storageAdapter) + } + } finally { + client.close() + } + }) + }) + program .command('confirm-email ') .description('confirm user email') diff --git a/dev/tool/src/markup.ts b/dev/tool/src/markup.ts index d939e2cb48..776be29e79 100644 --- a/dev/tool/src/markup.ts +++ b/dev/tool/src/markup.ts @@ -13,10 +13,17 @@ // limitations under the License. // -import { loadCollabYdoc, saveCollabYdoc, yDocCopyXmlField } from '@hcengineering/collaboration' +import { + loadCollabYdoc, + saveCollabJson, + saveCollabYdoc, + yDocCopyXmlField, + yDocFromBuffer +} from '@hcengineering/collaboration' import core, { type Blob, type Doc, + type Hierarchy, type MeasureContext, type Ref, type TxCreateDoc, @@ -24,6 +31,7 @@ import core, { type WorkspaceId, DOMAIN_TX, SortingOrder, + makeCollabId, makeCollabYdocId, makeDocCollabId } from '@hcengineering/core' @@ -290,3 +298,65 @@ export async function restoreControlledDocContentForDoc ( return true } + +export async function restoreMarkupRefsMongo ( + ctx: MeasureContext, + db: Db, + workspaceId: WorkspaceId, + hierarchy: Hierarchy, + storageAdapter: StorageAdapter +): Promise { + const classes = hierarchy.getDescendants(core.class.Doc) + for (const _class of classes) { + const domain = hierarchy.findDomain(_class) + if (domain === undefined) continue + + const allAttributes = hierarchy.getAllAttributes(_class) + const attributes = Array.from(allAttributes.values()).filter((attribute) => { + return hierarchy.isDerived(attribute.type._class, core.class.TypeCollaborativeDoc) + }) + + if (attributes.length === 0) continue + if (hierarchy.isMixin(_class) && attributes.every((p) => p.attributeOf !== _class)) continue + + ctx.info('processing', { _class, attributes: attributes.map((p) => p.name) }) + + const collection = db.collection(domain) + const iterator = collection.find({ _class }) + try { + while (true) { + const doc = await iterator.next() + if (doc === null) { + break + } + + for (const attribute of attributes) { + const isMixin = hierarchy.isMixin(attribute.attributeOf) + + const attributeName = isMixin ? `${attribute.attributeOf}.${attribute.name}` : attribute.name + + const value = isMixin + ? ((doc as any)[attribute.attributeOf]?.[attribute.name] as string) + : ((doc as any)[attribute.name] as string) + + if (typeof value === 'string') { + continue + } + + const collabId = makeCollabId(doc._class, doc._id, attribute.name) + const ydocId = makeCollabYdocId(collabId) + + try { + const buffer = await storageAdapter.read(ctx, workspaceId, ydocId) + const ydoc = yDocFromBuffer(Buffer.concat(buffer as any)) + + const jsonId = await saveCollabJson(ctx, storageAdapter, workspaceId, collabId, ydoc) + await collection.updateOne({ _id: doc._id }, { $set: { [attributeName]: jsonId } }) + } catch {} + } + } + } finally { + await iterator.close() + } + } +} diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 0d01744fff..d5d0d1eb47 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -213,7 +213,8 @@ export function createModel (builder: Builder): void { ['assigned', view.string.Assigned, {}], ['created', view.string.Created, {}], ['subscribed', view.string.Subscribed, {}] - ] + ], + descriptors: [view.viewlet.List, view.viewlet.Table, task.viewlet.Kanban] } }, { diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index b765a8479f..7a52ae250f 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -510,6 +510,14 @@ export function createModel (builder: Builder): void { value: true }) + builder.mixin(tracker.class.Milestone, core.class.Class, setting.mixin.Editable, { + value: true + }) + + builder.mixin(tracker.class.Component, core.class.Class, setting.mixin.Editable, { + value: true + }) + builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.LinkProvider, { encode: tracker.function.GetIssueLinkFragment }) diff --git a/packages/core/src/classes.ts b/packages/core/src/classes.ts index 94dd4b02a3..86bc962760 100644 --- a/packages/core/src/classes.ts +++ b/packages/core/src/classes.ts @@ -661,6 +661,7 @@ export type WorkspaceMode = | 'pending-deletion' // -> 'deleting' | 'deleting' // -> "deleted" | 'active' + | 'deleted' | 'archiving-pending-backup' // -> 'cleaning' | 'archiving-backup' // -> 'archiving-pending-clean' | 'archiving-pending-clean' // -> 'archiving-clean' diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 8d56516f27..c5e490d3d8 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -21,12 +21,10 @@ import { Hierarchy } from './hierarchy' import { MeasureContext, MeasureMetricsContext } from './measurements' import { ModelDb } from './memdb' import type { DocumentQuery, FindOptions, FindResult, FulltextStorage, Storage, TxResult, WithLookup } from './storage' -import { SearchOptions, SearchQuery, SearchResult, SortingOrder } from './storage' -import { Tx, TxCUD } from './tx' +import { SearchOptions, SearchQuery, SearchResult } from './storage' +import { Tx, TxCUD, type TxWorkspaceEvent } from './tx' import { toFindResult } from './utils' -const transactionThreshold = 500 - /** * @public */ @@ -85,11 +83,13 @@ export interface ClientConnection extends Storage, FulltextStorage, BackupClient isConnected: () => boolean close: () => Promise - onConnect?: (event: ClientConnectEvent, data: any) => Promise + onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise // If hash is passed, will return LoadModelResponse loadModel: (last: Timestamp, hash?: string) => Promise getAccount: () => Promise + + getLastHash?: (ctx: MeasureContext) => Promise } class ClientImpl implements AccountClient, BackupClient { @@ -236,7 +236,7 @@ export async function createClient ( let hierarchy = new Hierarchy() let model = new ModelDb(hierarchy) - let lastTx: number = 0 + let lastTx: string | undefined function txHandler (...tx: Tx[]): void { if (tx == null || tx.length === 0) { @@ -248,7 +248,11 @@ export async function createClient ( // eslint-disable-next-line @typescript-eslint/no-floating-promises client.updateFromRemote(...tx) } - lastTx = tx.reduce((cur, it) => (it.modifiedOn > cur ? it.modifiedOn : cur), 0) + for (const t of tx) { + if (t._class === core.class.TxWorkspaceEvent) { + lastTx = (t as TxWorkspaceEvent).params.lastTx + } + } } const conn = await ctx.with('connect', {}, () => connect(txHandler)) @@ -264,11 +268,14 @@ export async function createClient ( txHandler(...txBuffer) txBuffer = undefined - const oldOnConnect: ((event: ClientConnectEvent, data: any) => Promise) | undefined = conn.onConnect - conn.onConnect = async (event, data) => { + const oldOnConnect: + | ((event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise) + | undefined = conn.onConnect + conn.onConnect = async (event, _lastTx, data) => { console.log('Client: onConnect', event) if (event === ClientConnectEvent.Maintenance) { - await oldOnConnect?.(ClientConnectEvent.Maintenance, data) + lastTx = _lastTx + await oldOnConnect?.(ClientConnectEvent.Maintenance, _lastTx, data) return } // Find all new transactions and apply @@ -282,51 +289,27 @@ export async function createClient ( model = new ModelDb(hierarchy) await ctx.with('build-model', {}, (ctx) => buildModel(ctx, loadModelResponse, modelFilter, hierarchy, model)) - await oldOnConnect?.(ClientConnectEvent.Upgraded, data) - + await oldOnConnect?.(ClientConnectEvent.Upgraded, _lastTx, data) + lastTx = _lastTx // No need to fetch more stuff since upgrade was happened. return } - if (event === ClientConnectEvent.Connected) { + if (event === ClientConnectEvent.Connected && _lastTx !== lastTx && lastTx === undefined) { // No need to do anything here since we connected. - await oldOnConnect?.(event, data) + await oldOnConnect?.(event, _lastTx, data) + lastTx = _lastTx return } - // We need to look for last {transactionThreshold} transactions and if it is more since lastTx one we receive, we need to perform full refresh. - if (lastTx === 0) { - await oldOnConnect?.(ClientConnectEvent.Refresh, data) + if (_lastTx === lastTx) { + // Same lastTx, no need to refresh + await oldOnConnect?.(ClientConnectEvent.Reconnected, _lastTx, data) return } - const atxes = await ctx.with('find-atx', {}, () => - conn.findAll( - core.class.Tx, - { modifiedOn: { $gt: lastTx }, objectSpace: { $ne: core.space.Model } }, - { sort: { modifiedOn: SortingOrder.Ascending, _id: SortingOrder.Ascending }, limit: transactionThreshold } - ) - ) - - let needFullRefresh = false - // if we have attachment document create/delete we need to full refresh, since some derived data could be missing - for (const tx of atxes) { - if ( - (tx as TxCUD).attachedTo !== undefined && - (tx._class === core.class.TxCreateDoc || tx._class === core.class.TxRemoveDoc) - ) { - needFullRefresh = true - break - } - } - - if (atxes.length < transactionThreshold && !needFullRefresh) { - console.log('applying input transactions', atxes.length) - txHandler(...atxes) - await oldOnConnect?.(ClientConnectEvent.Reconnected, data) - } else { - // We need to trigger full refresh on queries, etc. - await oldOnConnect?.(ClientConnectEvent.Refresh, data) - } + lastTx = _lastTx + // We need to trigger full refresh on queries, etc. + await oldOnConnect?.(ClientConnectEvent.Refresh, lastTx, data) } return client @@ -344,6 +327,10 @@ async function tryLoadModel ( hash: '' } + if (conn.getLastHash !== undefined && (await conn.getLastHash(ctx)) === current.hash) { + // We have same model hash. + return current + } const lastTxTime = getLastTxTime(current.transactions) const result = await ctx.with('connection-load-model', { hash: current.hash !== '' }, (ctx) => conn.loadModel(lastTxTime, current.hash) @@ -357,21 +344,23 @@ async function tryLoadModel ( hash: '' } } - - // Save concatenated - void ctx - .with('persistence-store', {}, (ctx) => - persistence?.store({ - ...result, - transactions: !result.full ? current.transactions.concat(result.transactions) : result.transactions + const transactions = current.transactions.concat(result.transactions) + if (result.hash !== current.hash) { + // Save concatenated, if have some more of them. + void ctx + .with('persistence-store', {}, (ctx) => + persistence?.store({ + ...result, + transactions: !result.full ? transactions : result.transactions + }) + ) + .catch((err) => { + Analytics.handleError(err) }) - ) - .catch((err) => { - Analytics.handleError(err) - }) + } if (!result.full && !reload) { - result.transactions = current.transactions.concat(result.transactions) + result.transactions = transactions } return result diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts index 13df6a1b16..956cf15972 100644 --- a/packages/core/src/tx.ts +++ b/packages/core/src/tx.ts @@ -51,7 +51,8 @@ export enum WorkspaceEvent { IndexingUpdate, SecurityChange, MaintenanceNotification, - BulkUpdate + BulkUpdate, + LastTx } /** diff --git a/packages/text/src/markdown/serializer.ts b/packages/text/src/markdown/serializer.ts index 6a11631e2d..f3d8fa58fc 100644 --- a/packages/text/src/markdown/serializer.ts +++ b/packages/text/src/markdown/serializer.ts @@ -140,7 +140,22 @@ export const storeNodes: Record = { image: (state, node) => { const attrs = nodeAttrs(node) - if (attrs['file-id'] != null) { + if (attrs.token != null && attrs['file-id'] != null) { + // Convert image to token format + state.write( + '![' + + state.esc(`${attrs.alt ?? ''}`) + + '](' + + (state.imageUrl + + `${attrs['file-id']}` + + `?file=${attrs['file-id']}` + + (attrs.width != null ? '&width=' + state.esc(`${attrs.width}`) : '') + + (attrs.height != null ? '&height=' + state.esc(`${attrs.height}`) : '') + + (attrs.token != null ? '&token=' + state.esc(`${attrs.token}`) : '')) + + (attrs.title != null ? ' ' + state.quote(`${attrs.title}`) : '') + + ')' + ) + } else if (attrs['file-id'] != null) { // Convert image to fileid format state.write( '![' + diff --git a/packages/theme/styles/prose.scss b/packages/theme/styles/prose.scss index 10f59a06d7..927528fa0e 100644 --- a/packages/theme/styles/prose.scss +++ b/packages/theme/styles/prose.scss @@ -172,6 +172,7 @@ table.proseTable { &__selected { left: 0; + &::before { right: 0; top: 0; @@ -203,8 +204,7 @@ table.proseTable { } &:hover { - &:not(.table-row-handle__selected) { - } + &:not(.table-row-handle__selected) {} button { opacity: 1; @@ -213,6 +213,7 @@ table.proseTable { &__selected { top: 0; + &::before { bottom: 0; left: 0; @@ -246,6 +247,27 @@ table.proseTable { } } + .column-resize-handle { + position: absolute; + right: -1px; + top: -1px; + bottom: -1px; + width: 1px; + z-index: 100; + background-color: var(--primary-button-focused); + + &::after { + content: ''; + position: absolute; + top: 0; + left: -5px; + right: -5px; + bottom: 0; + cursor: col-resize; + z-index: 100; + } + } + .table-row-insert { display: flex; flex-direction: row; @@ -291,13 +313,13 @@ table.proseTable { } &:hover+.table-insert-marker { - opacity: 1; + display: block; } } .table-insert-marker { background-color: var(--primary-button-focused); - opacity: 0; + display: none; } } @@ -526,7 +548,7 @@ pre.proseCodeBlock>pre.proseCode { border-bottom: 2px solid rgba(255, 203, 0, .35); padding-bottom: 2px; transition: background 0.2s ease, border 0.2s ease; - + &.active { transition-delay: 150ms; background: rgba(255, 203, 0, .24); diff --git a/packages/ui/src/components/DropdownLabels.svelte b/packages/ui/src/components/DropdownLabels.svelte index 2f58b2d8cd..5228af1abe 100644 --- a/packages/ui/src/components/DropdownLabels.svelte +++ b/packages/ui/src/components/DropdownLabels.svelte @@ -50,9 +50,11 @@ let container: HTMLElement let opened: boolean = false - $: selectedItem = multiselect ? items.filter((p) => selected?.includes(p.id)) : items.find((x) => x.id === selected) - $: if (autoSelect && selected === undefined && items[0] !== undefined) { - selected = multiselect ? [items[0].id] : items[0].id + $: selectedItem = multiselect + ? (items ?? []).filter((p) => selected?.includes(p.id)) + : (items ?? []).find((x) => x.id === selected) + $: if (autoSelect && selected === undefined && items?.[0] !== undefined) { + selected = multiselect ? [items?.[0]?.id] : items?.[0]?.id } const dispatch = createEventDispatcher() @@ -111,7 +113,7 @@ {:else if Array.isArray(selectedItem)} {#if selectedItem.length > 0} - {#each selectedItem as seleceted, i} + {#each selectedItem as seleceted} {seleceted.label} {/each} {:else} diff --git a/packages/ui/src/components/Separator.svelte b/packages/ui/src/components/Separator.svelte index c00f94984d..172891708c 100644 --- a/packages/ui/src/components/Separator.svelte +++ b/packages/ui/src/components/Separator.svelte @@ -504,6 +504,27 @@ if (parentElement != null && typeof float === 'string') parentElement.setAttribute('data-float', float) } + const clearContainer = (container: HTMLElement): void => { + if (container === null) return + if (container.hasAttribute('data-float')) container.removeAttribute('data-float') + if (container.hasAttribute('data-size')) container.removeAttribute('data-size') + container.style.width = '' + container.style.minWidth = '' + container.style.maxWidth = '' + } + const clearSibling = (): void => { + if (separators != null && prevElement != null && separators[index].float !== undefined) { + clearContainer(prevElement) + } + if (separators != null && nextElement != null && separators[index + 1].float !== undefined) { + clearContainer(nextElement) + } + } + const clearParent = (): void => { + if (parentElement === null && separator != null) parentElement = separator.parentElement as HTMLElement + if (parentElement != null && typeof float === 'string') clearContainer(parentElement) + } + const calculateSeparators = (): void => { if (parentElement != null) { const elements: Element[] = Array.from(parentElement.children) @@ -600,6 +621,10 @@ } }) onDestroy(() => { + if (mounted) { + if (sState === SeparatorState.FLOAT) clearParent() + else if (sState === SeparatorState.NORMAL) clearSibling() + } window.removeEventListener('resize', resizeDocument) if (sState !== SeparatorState.FLOAT && $separatorsStore.filter((f) => f === name).length > 0) { $separatorsStore = $separatorsStore.filter((f) => f !== name) diff --git a/packages/ui/src/components/internal/Root.svelte b/packages/ui/src/components/internal/Root.svelte index ccdfc62486..b5c7c9f6ca 100644 --- a/packages/ui/src/components/internal/Root.svelte +++ b/packages/ui/src/components/internal/Root.svelte @@ -140,7 +140,7 @@ updateDeviceSize() $: secondRow = checkAdaptiveMatching($deviceInfo.size, 'xs') - $: asideFloat = $deviceInfo.aside.float + $: asideFloat = checkAdaptiveMatching($deviceInfo.size, 'sm') $: asideOpen = $deviceInfo.aside.visible $: appsMini = $deviceInfo.isMobile && diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte index 1c8758b1ea..74fa2b1234 100644 --- a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte +++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte @@ -14,7 +14,7 @@ -->
@@ -166,7 +144,7 @@ bind:value={search} collapsed on:change={() => { - updateResultQuery(search, documentIds, doneStates, mode) + updateResultQuery(search, doneStates, mode) }} /> @@ -179,18 +157,19 @@
(resultQuery = e.detail)} /> - - -{#if viewlet} - {#if loading} - - {:else} - - {/if} +{#if loading || !viewlet || !viewlet?.$lookup?.descriptor?.component} + +{:else} + {/if} diff --git a/plugins/task-resources/src/components/kanban/KanbanView.svelte b/plugins/task-resources/src/components/kanban/KanbanView.svelte index c3b9054f34..f9868b5bac 100644 --- a/plugins/task-resources/src/components/kanban/KanbanView.svelte +++ b/plugins/task-resources/src/components/kanban/KanbanView.svelte @@ -56,7 +56,7 @@ export let space: Ref | undefined = undefined export let baseMenuClass: Ref> | undefined = undefined export let query: DocumentQuery = {} - export let viewOptionsConfig: ViewOptionModel[] | undefined + export let viewOptionsConfig: ViewOptionModel[] | undefined = undefined export let viewOptions: ViewOptions export let viewlet: Viewlet export let config: (string | BuildModelKey)[] diff --git a/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte b/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte index c37fa94106..a1f18726fb 100644 --- a/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte +++ b/plugins/task-resources/src/components/taskTypes/TaskKindSelector.svelte @@ -12,6 +12,9 @@ export let baseClass: Ref> | undefined = undefined export let kind: ButtonKind = 'regular' export let size: ButtonSize = 'medium' + export let justify: 'left' | 'center' = 'center' + export let width: string | undefined = undefined + export let showAlways: boolean = false export let allTypes = false const client = getClient() @@ -46,12 +49,14 @@ } -{#if projectType !== undefined && items.length > 1} +{#if projectType !== undefined && (items.length > 1 || showAlways)} +
- - -
- - {#if editable && focused} -
- -
- - -
-
- +
+ + + +
+ {#if editable && focused} +
+
-
- - -
-
- + +
+
+ +
-
- {/if} + +
+
+ +
+
+ {/if} +
diff --git a/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte b/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte index fd4f44e38c..a549505441 100644 --- a/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte +++ b/plugins/workbench-resources/src/components/sidebar/SidebarExpanded.svelte @@ -22,7 +22,8 @@ Location, Header, Breadcrumbs, - getCurrentLocation + getCurrentLocation, + Separator } from '@hcengineering/ui' import { onDestroy, onMount } from 'svelte' @@ -32,6 +33,7 @@ export let widgets: Widget[] = [] export let preferences: WidgetPreference[] = [] + export let float: boolean = false let widgetId: Ref | undefined = undefined let widget: Widget | undefined = undefined @@ -96,70 +98,92 @@ } - + {/if} +
+ {#if widget !== undefined && tabs.length > 0} + { + void handleTabClose(e.detail, widget) + }} + on:open={(e) => { + handleTabOpen(e.detail, widget) + }} + /> {/if}
-{#if widget !== undefined && tabs.length > 0} - { - void handleTabClose(e.detail, widget) - }} - on:open={(e) => { - handleTabOpen(e.detail, widget) - }} - /> -{/if} - +