Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-10-10 21:22:28 +07:00
commit ac4c5455d3
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
19 changed files with 230 additions and 134 deletions

View File

@ -32,7 +32,7 @@ export async function syncFiles (
): Promise<void> { ): Promise<void> {
if (exAdapter.adapters === undefined) return if (exAdapter.adapters === undefined) return
for (const [name, adapter] of exAdapter.adapters.entries()) { for (const [name, adapter] of [...exAdapter.adapters.entries()].reverse()) {
await adapter.make(ctx, workspaceId) await adapter.make(ctx, workspaceId)
await retryOnFailure(ctx, 5, async () => { await retryOnFailure(ctx, 5, async () => {
@ -47,7 +47,12 @@ export async function syncFiles (
for (const data of dataBulk) { for (const data of dataBulk) {
const blob = await exAdapter.stat(ctx, workspaceId, data._id) const blob = await exAdapter.stat(ctx, workspaceId, data._id)
if (blob !== undefined) continue if (blob !== undefined) {
if (blob.provider !== name && name === exAdapter.defaultAdapter) {
await exAdapter.syncBlobFromStorage(ctx, workspaceId, data._id, exAdapter.defaultAdapter)
}
continue
}
await exAdapter.syncBlobFromStorage(ctx, workspaceId, data._id, name) await exAdapter.syncBlobFromStorage(ctx, workspaceId, data._id, name)
@ -167,6 +172,13 @@ async function processAdapter (
let targetBlob: Blob | ListBlobResult | undefined = targetBlobs.get(data._id) let targetBlob: Blob | ListBlobResult | undefined = targetBlobs.get(data._id)
if (targetBlob !== undefined) { if (targetBlob !== undefined) {
console.log('Target blob already exists', targetBlob._id) console.log('Target blob already exists', targetBlob._id)
const aggrBlob = await exAdapter.stat(ctx, workspaceId, data._id)
if (aggrBlob === undefined || aggrBlob?.provider !== targetBlob.provider) {
targetBlob = await exAdapter.syncBlobFromStorage(ctx, workspaceId, targetBlob._id, exAdapter.defaultAdapter)
}
// We could safely delete source blob
toRemove.push(data._id)
} }
if (targetBlob === undefined) { if (targetBlob === undefined) {
@ -176,32 +188,28 @@ async function processAdapter (
console.error('blob not found', data._id) console.error('blob not found', data._id)
continue continue
} }
await rateLimiter.exec(async () => { targetBlob = await rateLimiter.exec(async () => {
try { try {
await retryOnFailure( const result = await retryOnFailure(
ctx, ctx,
5, 5,
async () => { async () => {
await processFile(ctx, source, target, workspaceId, sourceBlob) await processFile(ctx, source, target, workspaceId, sourceBlob)
// We need to sync and update aggregator table for now. // We need to sync and update aggregator table for now.
targetBlob = await exAdapter.syncBlobFromStorage( return await exAdapter.syncBlobFromStorage(ctx, workspaceId, sourceBlob._id, exAdapter.defaultAdapter)
ctx,
workspaceId,
sourceBlob._id,
exAdapter.defaultAdapter
)
}, },
50 50
) )
movedCnt += 1 movedCnt += 1
movedBytes += sourceBlob.size movedBytes += sourceBlob.size
batchBytes += sourceBlob.size batchBytes += sourceBlob.size
return result
} catch (err) { } catch (err) {
console.error('failed to process blob', data._id, err) console.error('failed to process blob', data._id, err)
} }
}) })
if (targetBlob !== undefined && 'size' in targetBlob && (targetBlob as Blob).size === sourceBlob.size) { if (targetBlob !== undefined) {
// We could safely delete source blob // We could safely delete source blob
toRemove.push(sourceBlob._id) toRemove.push(sourceBlob._id)
} }
@ -233,7 +241,10 @@ async function processAdapter (
await rateLimiter.waitProcessing() await rateLimiter.waitProcessing()
if (toRemove.length > 0 && params.move) { if (toRemove.length > 0 && params.move) {
await source.remove(ctx, workspaceId, toRemove) while (toRemove.length > 0) {
const part = toRemove.splice(0, 500)
await source.remove(ctx, workspaceId, part)
}
} }
} finally { } finally {
await iterator.close() await iterator.close()

View File

@ -221,8 +221,12 @@ export function createModel (builder: Builder): void {
builder.createDoc<ActivityMessageControl<ChunterSpace>>(activity.class.ActivityMessageControl, core.space.Model, { builder.createDoc<ActivityMessageControl<ChunterSpace>>(activity.class.ActivityMessageControl, core.space.Model, {
objectClass: chunter.class.DirectMessage, objectClass: chunter.class.DirectMessage,
skip: [{ _class: core.class.TxMixin }, { _class: core.class.TxCreateDoc }, { _class: core.class.TxRemoveDoc }], skip: [
allowedFields: ['members'] { _class: core.class.TxMixin },
{ _class: core.class.TxCreateDoc },
{ _class: core.class.TxRemoveDoc },
{ _class: core.class.TxUpdateDoc }
]
}) })
builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, { builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, {
@ -241,16 +245,6 @@ export function createModel (builder: Builder): void {
} }
}) })
builder.createDoc(activity.class.DocUpdateMessageViewlet, core.space.Model, {
objectClass: chunter.class.DirectMessage,
action: 'update',
config: {
members: {
presenter: chunter.activity.MembersChangedMessage
}
}
})
builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityMessagePreview, { builder.mixin(chunter.class.ChatMessage, core.class.Class, activity.mixin.ActivityMessagePreview, {
presenter: chunter.component.ChatMessagePreview presenter: chunter.component.ChatMessagePreview
}) })

View File

@ -351,6 +351,17 @@ export const chunterOperation: MigrateOperation = {
func: async (client) => { func: async (client) => {
await removeDuplicatedDirects(client) await removeDuplicatedDirects(client)
} }
},
{
state: 'remove-direct-members-messages',
func: async (client) => {
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage,
attachedToClass: chunter.class.DirectMessage,
action: 'update',
'attributeUpdates.attrKey': 'members'
})
}
} }
]) ])
}, },

View File

@ -105,6 +105,7 @@ export class TTxSidebarEvent extends TTx implements TxSidebarEvent {
@UX(workbench.string.Tab) @UX(workbench.string.Tab)
export class TWorkbenchTab extends TPreference implements WorkbenchTab { export class TWorkbenchTab extends TPreference implements WorkbenchTab {
location!: string location!: string
name?: string
isPinned!: boolean isPinned!: boolean
} }

View File

@ -20,15 +20,16 @@
export let node: MarkupNode export let node: MarkupNode
export let preview = false export let preview = false
const is = diffview.component.Highlight
$: language = node.attrs?.language $: language = node.attrs?.language
$: content = node.content ?? [] $: content = node.content ?? []
$: value = content.map((node) => node.text).join('/n') $: value = content.map((node) => node.text).join('/n')
$: margin = preview ? '0' : null
$: props = { value, language }
</script> </script>
{#if node} {#if node}
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}> <pre class="proseCodeBlock" style:margin><code><Component {is} {props} /></code></pre>
<code>
<Component is={diffview.component.Highlight} props={{ value, language }} />
</code>
</pre>
{/if} {/if}

View File

@ -73,9 +73,10 @@
<div class="close-button {orientation}"> <div class="close-button {orientation}">
<ButtonIcon icon={IconClose} size="min" on:click={() => dispatch('close')} /> <ButtonIcon icon={IconClose} size="min" on:click={() => dispatch('close')} />
</div> </div>
{:else} {:else if $$slots.postfix === undefined}
<div /> <div />
{/if} {/if}
<slot name="postfix" />
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -98,11 +98,15 @@
<svelte:fragment slot="notify"> <svelte:fragment slot="notify">
{#if count != null && count > 0} {#if count != null && count > 0}
<div class="antiHSpacer" /> <div class="antiHSpacer" />
<NotifyMarker {count} /> <div class="notify">
<NotifyMarker {count} />
</div>
<div class="antiHSpacer" /> <div class="antiHSpacer" />
{:else if secondaryNotifyMarker} {:else if secondaryNotifyMarker}
<div class="antiHSpacer" /> <div class="antiHSpacer" />
<NotifyMarker count={0} kind="with-dot" /> <div class="notify">
<NotifyMarker count={0} kind="simple" size="xx-small" />
</div>
<div class="antiHSpacer" /> <div class="antiHSpacer" />
{/if} {/if}
</svelte:fragment> </svelte:fragment>
@ -126,4 +130,12 @@
background-color: var(--global-ui-highlight-BackgroundColor); background-color: var(--global-ui-highlight-BackgroundColor);
} }
} }
.notify {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
}
</style> </style>

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
export let count: number = 0 export let count: number = 0
export let kind: 'primary' | 'simple' | 'with-dot' = 'primary' export let kind: 'primary' | 'simple' = 'primary'
export let size: 'xx-small' | 'x-small' | 'small' | 'medium' = 'small' export let size: 'xx-small' | 'x-small' | 'small' | 'medium' = 'small'
const maxNumber = 9 const maxNumber = 9
@ -34,10 +34,6 @@
<div class="notifyMarker {size} {kind}" /> <div class="notifyMarker {size} {kind}" />
{/if} {/if}
{#if kind === 'with-dot'}
<div class="notifyMarker {size} {kind}"></div>
{/if}
<style lang="scss"> <style lang="scss">
.notifyMarker { .notifyMarker {
display: flex; display: flex;
@ -48,7 +44,6 @@
font-weight: 700; font-weight: 700;
&.simple, &.simple,
&.with-dot,
&.primary { &.primary {
background-color: var(--global-higlight-Color); background-color: var(--global-higlight-Color);
color: var(--global-on-accent-TextColor); color: var(--global-on-accent-TextColor);
@ -75,10 +70,5 @@
height: 1.25rem; height: 1.25rem;
font-size: 0.625rem; font-size: 0.625rem;
} }
&.with-dot {
font-weight: 400;
font-size: 0.25rem;
}
} }
</style> </style>

View File

@ -87,6 +87,9 @@
<path fill-rule="evenodd" clip-rule="evenodd" <path fill-rule="evenodd" clip-rule="evenodd"
d="M21.7071 2.29289C21.3166 1.90237 20.6834 1.90237 20.2929 2.29289C19.9024 2.68342 19.9024 3.31658 20.2929 3.70711L20.5858 4L14.9628 9.623C14.7214 9.86439 14.394 10 14.0526 10H13.2426C10.2609 10 7.4013 11.1845 5.29289 13.2929C4.90237 13.6834 4.90237 14.3166 5.29289 14.7071L10.5859 20.0001L2 28.5859V30H3.41436L12.0001 21.4143L17.2929 26.7071C17.6834 27.0976 18.3166 27.0976 18.7071 26.7071C20.8155 24.5987 22 21.7391 22 18.7574V17.9474C22 17.606 22.1356 17.2786 22.377 17.0372L28 11.4142L28.2929 11.7071C28.6834 12.0976 29.3166 12.0976 29.7071 11.7071C30.0976 11.3166 30.0976 10.6834 29.7071 10.2929L21.7071 2.29289ZM16.377 11.0372L22 5.41421L26.5858 10L20.9628 15.623C20.3463 16.2395 20 17.0756 20 17.9474V18.7574C20 20.873 19.2746 22.9139 17.9617 24.5474L7.45256 14.0383C9.08614 12.7254 11.127 12 13.2426 12H14.0526C14.9244 12 15.7605 11.6537 16.377 11.0372Z"/> d="M21.7071 2.29289C21.3166 1.90237 20.6834 1.90237 20.2929 2.29289C19.9024 2.68342 19.9024 3.31658 20.2929 3.70711L20.5858 4L14.9628 9.623C14.7214 9.86439 14.394 10 14.0526 10H13.2426C10.2609 10 7.4013 11.1845 5.29289 13.2929C4.90237 13.6834 4.90237 14.3166 5.29289 14.7071L10.5859 20.0001L2 28.5859V30H3.41436L12.0001 21.4143L17.2929 26.7071C17.6834 27.0976 18.3166 27.0976 18.7071 26.7071C20.8155 24.5987 22 21.7391 22 18.7574V17.9474C22 17.606 22.1356 17.2786 22.377 17.0372L28 11.4142L28.2929 11.7071C28.6834 12.0976 29.3166 12.0976 29.7071 11.7071C30.0976 11.3166 30.0976 10.6834 29.7071 10.2929L21.7071 2.29289ZM16.377 11.0372L22 5.41421L26.5858 10L20.9628 15.623C20.3463 16.2395 20 17.0756 20 17.9474V18.7574C20 20.873 19.2746 22.9139 17.9617 24.5474L7.45256 14.0383C9.08614 12.7254 11.127 12 13.2426 12H14.0526C14.9244 12 15.7605 11.6537 16.377 11.0372Z"/>
</symbol> </symbol>
<symbol id="pin-tack" viewBox="0 0 18 18">
<line x1="9" y1="16.25" x2="9" y2="12.25" stroke-linecap="round" stroke-linejoin="round" stroke="#212121"></line><path d="M14.25,12.25c-.089-.699-.318-1.76-.969-2.875-.335-.574-.703-1.028-1.031-1.375V3.75c0-1.105-.895-2-2-2h-2.5c-1.105,0-2,.895-2,2v4.25c-.329,.347-.697,.801-1.031,1.375-.65,1.115-.88,2.176-.969,2.875H14.25Z" stroke-linecap="round" stroke-linejoin="round"></path>
</symbol>
<symbol id="model" viewBox="0 0 24 24"> <symbol id="model" viewBox="0 0 24 24">
<path d="M17.2,7.8h3.6c1.1,0,2-0.9,2-2V4.2c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0H9.8V4c0-1.5-1.2-2.8-2.8-2.8H4 C2.5,1.2,1.2,2.5,1.2,4v2c0,1.5,1.2,2.8,2.8,2.8h3c1.5,0,2.8-1.2,2.8-2.8V5.8h2V18c0,1.5,1.2,2.8,2.8,2.8h0.8v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-0.8c-0.7,0-1.2-0.6-1.2-1.2v-4.8h2v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-2v-6h2v0C15.2,6.9,16.1,7.8,17.2,7.8z M8.2,6 c0,0.7-0.6,1.2-1.2,1.2H4C3.3,7.2,2.8,6.7,2.8,6V4c0-0.7,0.6-1.2,1.2-1.2h3c0.7,0,1.2,0.6,1.2,1.2V6z M16.8,19.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V19.2z M16.8,11.7 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V11.7z M16.8,4.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V4.2z"/> <path d="M17.2,7.8h3.6c1.1,0,2-0.9,2-2V4.2c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0H9.8V4c0-1.5-1.2-2.8-2.8-2.8H4 C2.5,1.2,1.2,2.5,1.2,4v2c0,1.5,1.2,2.8,2.8,2.8h3c1.5,0,2.8-1.2,2.8-2.8V5.8h2V18c0,1.5,1.2,2.8,2.8,2.8h0.8v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-0.8c-0.7,0-1.2-0.6-1.2-1.2v-4.8h2v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-2v-6h2v0C15.2,6.9,16.1,7.8,17.2,7.8z M8.2,6 c0,0.7-0.6,1.2-1.2,1.2H4C3.3,7.2,2.8,6.7,2.8,6V4c0-0.7,0.6-1.2,1.2-1.2h3c0.7,0,1.2,0.6,1.2,1.2V6z M16.8,19.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V19.2z M16.8,11.7 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V11.7z M16.8,4.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V4.2z"/>
</symbol> </symbol>

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -61,5 +61,6 @@ loadMetadata(view.icon, {
Undo: `${icons}#undo`, Undo: `${icons}#undo`,
Video: `${icons}#video`, Video: `${icons}#video`,
Audio: `${icons}#audio`, Audio: `${icons}#audio`,
File: `${icons}#file` File: `${icons}#file`,
PinTack: `${icons}#pin-tack`
}) })

View File

@ -257,7 +257,8 @@ const view = plugin(viewId, {
Undo: '' as Asset, Undo: '' as Asset,
Video: '' as Asset, Video: '' as Asset,
Audio: '' as Asset, Audio: '' as Asset,
File: '' as Asset File: '' as Asset,
PinTack: '' as Asset
}, },
category: { category: {
General: '' as Ref<ActionCategory>, General: '' as Ref<ActionCategory>,

View File

@ -13,85 +13,51 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AnySvelteComponent, closePanel, getCurrentLocation, Location, ModernTab, navigate } from '@hcengineering/ui' import {
AnySvelteComponent,
closePanel,
getCurrentLocation,
Icon,
ModernTab,
navigate,
languageStore,
locationToUrl
} from '@hcengineering/ui'
import { ComponentExtensions, getClient } from '@hcengineering/presentation' import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import { Asset, getResource, IntlString } from '@hcengineering/platform' import { Asset, getResource, translate } from '@hcengineering/platform'
import { type Application, WorkbenchTab } from '@hcengineering/workbench' import { WorkbenchTab } from '@hcengineering/workbench'
import { Class, Doc, Ref } from '@hcengineering/core'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { parseLinkId, showMenu } from '@hcengineering/view-resources' import { showMenu } from '@hcengineering/view-resources'
import { Analytics } from '@hcengineering/analytics'
import { closeTab, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench' import { closeTab, getTabDataByLocation, getTabLocation, selectTab, tabIdStore, tabsStore } from '../workbench'
import workbench from '../plugin' import workbench from '../plugin'
export let tab: WorkbenchTab export let tab: WorkbenchTab
const client = getClient() const client = getClient()
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
let name: string | undefined = undefined let name: string | undefined = tab.name
let label: IntlString | undefined = undefined
let icon: Asset | AnySvelteComponent | undefined let icon: Asset | AnySvelteComponent | undefined
let iconProps: Record<string, any> | undefined let iconProps: Record<string, any> | undefined
async function getResolvedLocation (loc: Location, app?: Application): Promise<Location> { async function updateTabData (tab: WorkbenchTab, lang: string): Promise<void> {
if (app === undefined) return loc
if (app.locationResolver) {
const resolver = await getResource(app.locationResolver)
return (await resolver(loc))?.loc ?? loc
}
return loc
}
async function getTabName (loc: Location): Promise<string | undefined> {
if (loc.fragment == null) return
const hierarchy = client.getHierarchy()
const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
if (_class == null) return
const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle)
if (mixin === undefined) return
const titleProvider = await getResource(mixin.titleProvider)
try {
const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
return await titleProvider(client, _id)
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
}
}
async function updateTabData (tab: WorkbenchTab): Promise<void> {
const tabLoc = $tabIdStore === tab._id ? getCurrentLocation() : getTabLocation(tab) const tabLoc = $tabIdStore === tab._id ? getCurrentLocation() : getTabLocation(tab)
const alias = tabLoc.path[2] const data = await getTabDataByLocation(tabLoc)
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias })[0]
if (application?.locationDataResolver) { name = data.name ?? (await translate(data.label, {}, lang))
const resolver = await getResource(application.locationDataResolver) if (data.iconComponent !== undefined) {
const data = await resolver(tabLoc) icon = await getResource(data.iconComponent)
name = data.name
label = data.nameIntl ?? application.label ?? workbench.string.Tab
if (data.iconComponent) {
icon = await getResource(data.iconComponent)
} else {
icon = data.icon ?? application?.icon
}
iconProps = data.iconProps
} else { } else {
const special = tabLoc.path[3] icon = data.icon
const specialLabel = application?.navigatorModel?.specials?.find((s) => s.id === special)?.label }
const resolvedLoc = await getResolvedLocation(tabLoc, application) iconProps = data.iconProps
name = await getTabName(resolvedLoc)
label = specialLabel ?? application?.label ?? workbench.string.Tab if (tab.name !== name && tab.location === locationToUrl(tabLoc)) {
icon = application?.icon await client.update(tab, { name })
iconProps = undefined
} }
} }
$: void updateTabData(tab) $: void updateTabData(tab, $languageStore)
function handleClickTab (): void { function handleClickTab (): void {
selectTab(tab._id) selectTab(tab._id)
@ -118,7 +84,6 @@
</script> </script>
<ModernTab <ModernTab
labelIntl={label}
label={name} label={name}
{icon} {icon}
{iconProps} {iconProps}
@ -132,4 +97,9 @@
<svelte:fragment slot="prefix"> <svelte:fragment slot="prefix">
<ComponentExtensions extension={workbench.extensions.WorkbenchTabExtensions} props={{ tab }} /> <ComponentExtensions extension={workbench.extensions.WorkbenchTabExtensions} props={{ tab }} />
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="postfix">
{#if tab.isPinned}
<Icon icon={view.icon.PinTack} size="x-small" />
{/if}
</svelte:fragment>
</ModernTab> </ModernTab>

View File

@ -13,20 +13,26 @@
// limitations under the License. // limitations under the License.
// //
import { derived, get, writable } from 'svelte/store' import { derived, get, writable } from 'svelte/store'
import core, { concatLink, getCurrentAccount, type Ref } from '@hcengineering/core' import core, { type Class, concatLink, type Doc, getCurrentAccount, type Ref } from '@hcengineering/core'
import workbench, { type WorkbenchTab } from '@hcengineering/workbench' import { type Application, workbenchId, type WorkbenchTab } from '@hcengineering/workbench'
import { import {
location as locationStore, location as locationStore,
locationToUrl, locationToUrl,
parseLocation, parseLocation,
type Location, type Location,
navigate, navigate,
getCurrentLocation getCurrentLocation,
languageStore,
type AnyComponent
} from '@hcengineering/ui' } from '@hcengineering/ui'
import presentation, { getClient } from '@hcengineering/presentation' import presentation, { getClient } from '@hcengineering/presentation'
import { getMetadata } from '@hcengineering/platform' import view from '@hcengineering/view'
import { type Asset, type IntlString, getMetadata, getResource, translate } from '@hcengineering/platform'
import { parseLinkId } from '@hcengineering/view-resources'
import { notificationId } from '@hcengineering/notification'
import { workspaceStore } from './utils' import { workspaceStore } from './utils'
import workbench from './plugin'
export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>() export const tabIdStore = writable<Ref<WorkbenchTab> | undefined>()
export const tabsStore = writable<WorkbenchTab[]>([]) export const tabsStore = writable<WorkbenchTab[]>([])
@ -38,7 +44,14 @@ workspaceStore.subscribe((workspace) => {
tabIdStore.set(getTabFromLocalStorage(workspace ?? '')) tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
}) })
locationStore.subscribe((loc) => { locationStore.subscribe(() => {
void syncTabLoc()
})
tabIdStore.subscribe(saveTabToLocalStorage)
async function syncTabLoc (): Promise<void> {
const loc = getCurrentLocation()
const workspace = get(workspaceStore) const workspace = get(workspaceStore)
if (workspace == null || workspace === '') return if (workspace == null || workspace === '') return
const tab = get(currentTabStore) const tab = get(currentTabStore)
@ -51,11 +64,22 @@ locationStore.subscribe((loc) => {
return return
} }
if (loc.path[2] === '' || loc.path[2] == null) return if (loc.path[2] === '' || loc.path[2] == null) return
void getClient().update(tab, { location: locationToUrl(loc) }) const data = await getTabDataByLocation(loc)
}) const name = data.name ?? (await translate(data.label, {}, get(languageStore)))
tabIdStore.subscribe(saveTabToLocalStorage)
if (tab.name !== undefined && name !== tab.name && tab.isPinned) {
const me = getCurrentAccount()
const _id = await getClient().createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
location: locationToUrl(loc),
name,
attachedTo: me._id,
isPinned: false
})
selectTab(_id)
} else {
await getClient().update(tab, { location: locationToUrl(loc), name })
}
}
export function syncWorkbenchTab (): void { export function syncWorkbenchTab (): void {
const workspace = get(workspaceStore) const workspace = get(workspaceStore)
tabIdStore.set(getTabFromLocalStorage(workspace ?? '')) tabIdStore.set(getTabFromLocalStorage(workspace ?? ''))
@ -90,7 +114,7 @@ export function selectTab (_id: Ref<WorkbenchTab>): void {
tabIdStore.set(_id) tabIdStore.set(_id)
} }
export function getTabLocation (tab: WorkbenchTab): Location { export function getTabLocation (tab: Pick<WorkbenchTab, 'location'>): Location {
const base = `${window.location.protocol}//${window.location.host}` const base = `${window.location.protocol}//${window.location.host}`
const front = getMetadata(presentation.metadata.FrontUrl) ?? base const front = getMetadata(presentation.metadata.FrontUrl) ?? base
const url = new URL(concatLink(front, tab.location)) const url = new URL(concatLink(front, tab.location))
@ -120,13 +144,15 @@ export async function createTab (): Promise<void> {
const loc = getCurrentLocation() const loc = getCurrentLocation()
const client = getClient() const client = getClient()
const me = getCurrentAccount() const me = getCurrentAccount()
const defaultUrl = `${workbenchId}/${loc.path[1]}/${notificationId}`
const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, { const tab = await client.createDoc(workbench.class.WorkbenchTab, core.space.Workspace, {
attachedTo: me._id, attachedTo: me._id,
location: locationToUrl(loc), location: defaultUrl,
isPinned: false isPinned: false
}) })
selectTab(tab) selectTab(tab)
navigate(getTabLocation({ location: defaultUrl }))
} }
export function canCloseTab (tab: WorkbenchTab): boolean { export function canCloseTab (tab: WorkbenchTab): boolean {
@ -143,3 +169,66 @@ export async function unpinTab (tab: WorkbenchTab): Promise<void> {
const client = getClient() const client = getClient()
await client.update(tab, { isPinned: false }) await client.update(tab, { isPinned: false })
} }
export async function getTabDataByLocation (loc: Location): Promise<{
name?: string
label: IntlString
icon?: Asset
iconComponent?: AnyComponent
iconProps?: Record<string, any>
}> {
const client = getClient()
const appAlias = loc.path[2]
const application = client.getModel().findAllSync<Application>(workbench.class.Application, { alias: appAlias })[0]
let name: string | undefined
let label: IntlString | undefined
let icon: Asset | undefined
let iconComponent: AnyComponent | undefined
let iconProps: Record<string, any> | undefined
if (application?.locationDataResolver != null) {
const resolver = await getResource(application.locationDataResolver)
const data = await resolver(loc)
name = data.name
label = data.nameIntl ?? application.label ?? workbench.string.Tab
iconComponent = data.iconComponent
icon = data.icon ?? application.icon
iconProps = data.iconProps
} else {
const special = loc.path[3]
const specialLabel = application?.navigatorModel?.specials?.find((s) => s.id === special)?.label
const resolvedLoc = await getResolvedLocation(loc, application)
name = await getDefaultTabName(resolvedLoc)
label = specialLabel ?? application?.label ?? workbench.string.Tab
icon = application?.icon
}
return { name, label, icon, iconComponent, iconProps }
}
async function getResolvedLocation (loc: Location, app?: Application): Promise<Location> {
if (app?.locationResolver === undefined) return loc
const resolver = await getResource(app.locationResolver)
return (await resolver(loc))?.loc ?? loc
}
async function getDefaultTabName (loc: Location): Promise<string | undefined> {
if (loc.fragment == null) return
const client = getClient()
const hierarchy = client.getHierarchy()
const [, id, _class] = decodeURIComponent(loc.fragment).split('|')
if (_class == null) return
const mixin = hierarchy.classHierarchyMixin(_class as Ref<Class<Doc>>, view.mixin.ObjectTitle)
if (mixin === undefined) return
const titleProvider = await getResource(mixin.titleProvider)
try {
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const _id = await parseLinkId(linkProviders, id, _class as Ref<Class<Doc>>)
return await titleProvider(client, _id)
} catch (err: any) {
console.error(err)
}
}

View File

@ -113,6 +113,7 @@ export interface TxSidebarEvent<T extends Record<string, any> = Record<string, a
export interface WorkbenchTab extends Preference { export interface WorkbenchTab extends Preference {
location: string location: string
isPinned: boolean isPinned: boolean
name?: string
} }
/** /**

View File

@ -62,13 +62,13 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
} }
} }
const provider = this.adapters.get(current?.provider ?? this.defaultAdapter) const provider = this.adapters.get(providerId ?? current?.provider ?? this.defaultAdapter)
if (provider === undefined) { if (provider === undefined) {
throw new NoSuchKeyError('No such provider found') throw new NoSuchKeyError('No such provider found')
} }
const stat = await provider.stat(ctx, workspaceId, objectName) const stat = await provider.stat(ctx, workspaceId, objectName)
if (stat !== undefined) { if (stat !== undefined) {
stat.provider = current?.provider ?? this.defaultAdapter stat.provider = providerId ?? current?.provider ?? this.defaultAdapter
if (current !== undefined) { if (current !== undefined) {
await this.dbAdapter.clean(ctx, workspaceId, DOMAIN_BLOB, [current._id]) await this.dbAdapter.clean(ctx, workspaceId, DOMAIN_BLOB, [current._id])
} }

View File

@ -628,9 +628,17 @@ export abstract class IssueSyncManagerBase {
itemId: target.prjData?.id as string itemId: target.prjData?.id as string
}) })
} catch (err: any) { } catch (err: any) {
if (err.errors?.[0]?.type === 'NOT_FOUND') {
errors.push({ error: err, response })
return errors
}
Analytics.handleError(err) Analytics.handleError(err)
// Failed to update one particular value, skip it. // Failed to update one particular value, skip it.
this.ctx.error('error during field update', { error: err, response }) this.ctx.error('error during field update', {
error: err,
response,
workspace: this.provider.getWorkspaceId().name
})
errors.push({ error: err, response }) errors.push({ error: err, response })
} }
} }
@ -892,10 +900,9 @@ export abstract class IssueSyncManagerBase {
} }
if (fieldsUpdate.length > 0 && syncToProject && target.prjData !== undefined) { if (fieldsUpdate.length > 0 && syncToProject && target.prjData !== undefined) {
const errors = await this.updateIssueValues(target, okit, fieldsUpdate) const errors = await this.updateIssueValues(target, okit, fieldsUpdate)
if (errors.length > 0) { if (errors.length === 0) {
return { externalVersion: '', needUpdate: githubSyncVersion, error: errors } needExternalSync = true
} }
needExternalSync = true
} }
// TODO: Add support for labels, milestone, assignees // TODO: Add support for labels, milestone, assignees
} }

View File

@ -971,6 +971,10 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
return { needSync: githubSyncVersion } return { needSync: githubSyncVersion }
} }
if (info.repository == null) {
return { needSync: githubSyncVersion }
}
const pullRequestExternal = info.external as unknown as PullRequestExternalData const pullRequestExternal = info.external as unknown as PullRequestExternalData
if (info.externalVersion !== githubExternalSyncVersion) { if (info.externalVersion !== githubExternalSyncVersion) {

View File

@ -265,7 +265,14 @@ export class RepositorySyncMapper implements DocSyncManager {
let allRepos: GithubIntegrationRepository[] = [...allRepositories] let allRepos: GithubIntegrationRepository[] = [...allRepositories]
const githubRepos:
| Repository
| Endpoints['GET /installation/repositories']['response']['data']['repositories'][0][] = []
for await (const { repository } of iterable) { for await (const { repository } of iterable) {
githubRepos.push(repository)
}
for (const repository of githubRepos) {
const integrationRepo: GithubIntegrationRepository | undefined = allRepos.find( const integrationRepo: GithubIntegrationRepository | undefined = allRepos.find(
(it) => it.repositoryId === repository.id (it) => it.repositoryId === repository.id
) )
@ -325,13 +332,8 @@ export class RepositorySyncMapper implements DocSyncManager {
// Ok we have repos removed from integration, we need to delete them. // Ok we have repos removed from integration, we need to delete them.
for (const repo of allRepos) { for (const repo of allRepos) {
await this.client.remove(repo) // Mark as archived
const prj = projects.find((it) => it._id === repo.githubProject) await this.client.update(repo, { archived: true })
if (prj !== undefined) {
await this.client.update(prj, {
$pull: { repositories: repo._id }
})
}
} }
// We need to delete and disconnect missing repositories. // We need to delete and disconnect missing repositories.

View File

@ -395,9 +395,6 @@ export class GithubWorker implements IntegrationManager {
periodicSyncPromise: Promise<void> | undefined periodicSyncPromise: Promise<void> | undefined
async performPeriodicSync (): Promise<void> { async performPeriodicSync (): Promise<void> {
try { try {
for (const inst of this.integrations.values()) {
await this.repositoryManager.reloadRepositories(inst)
}
this.triggerUpdate() this.triggerUpdate()
} catch (err: any) { } catch (err: any) {
Analytics.handleError(err) Analytics.handleError(err)