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-07 16:01:16 +07:00
commit f3246b00bc
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
81 changed files with 527 additions and 276 deletions

View File

@ -1110,7 +1110,6 @@ export function devTool (
.action( .action(
async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string, disabled: boolean }) => { async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string, disabled: boolean }) => {
const params = { const params = {
blobSizeLimitMb: parseInt(cmd.blobLimit),
concurrency: parseInt(cmd.concurrency), concurrency: parseInt(cmd.concurrency),
move: cmd.move === 'true' move: cmd.move === 'true'
} }

View File

@ -21,7 +21,6 @@ import { type Db } from 'mongodb'
import { PassThrough } from 'stream' import { PassThrough } from 'stream'
export interface MoveFilesParams { export interface MoveFilesParams {
blobSizeLimitMb: number
concurrency: number concurrency: number
move: boolean move: boolean
} }
@ -86,7 +85,7 @@ export async function moveFiles (
for (const [name, adapter] of exAdapter.adapters.entries()) { for (const [name, adapter] of exAdapter.adapters.entries()) {
if (name === exAdapter.defaultAdapter) continue if (name === exAdapter.defaultAdapter) continue
console.log('moving from', name, 'limit', params.blobSizeLimitMb, 'concurrency', params.concurrency) console.log('moving from', name, 'limit', 'concurrency', params.concurrency)
// we attempt retry the whole process in case of failure // we attempt retry the whole process in case of failure
// files that were already moved will be skipped // files that were already moved will be skipped
@ -129,10 +128,13 @@ async function processAdapter (
workspaceId: WorkspaceId, workspaceId: WorkspaceId,
params: MoveFilesParams params: MoveFilesParams
): Promise<void> { ): Promise<void> {
if (source === target) {
// Just in case
return
}
let time = Date.now() let time = Date.now()
let processedCnt = 0 let processedCnt = 0
let processedBytes = 0 let processedBytes = 0
let skippedCnt = 0
let movedCnt = 0 let movedCnt = 0
let movedBytes = 0 let movedBytes = 0
let batchBytes = 0 let batchBytes = 0
@ -140,47 +142,62 @@ async function processAdapter (
const rateLimiter = new RateLimiter(params.concurrency) const rateLimiter = new RateLimiter(params.concurrency)
const iterator = await source.listStream(ctx, workspaceId) const iterator = await source.listStream(ctx, workspaceId)
const toRemove: string[] = []
try { try {
while (true) { while (true) {
const dataBulk = await iterator.next() const dataBulk = await iterator.next()
if (dataBulk.length === 0) break if (dataBulk.length === 0) break
for (const data of dataBulk) { for (const data of dataBulk) {
const blob = let targetBlob = await target.stat(ctx, workspaceId, data._id)
(await exAdapter.stat(ctx, workspaceId, data._id)) ?? (await source.stat(ctx, workspaceId, data._id)) const sourceBlob = await source.stat(ctx, workspaceId, data._id)
if (blob === undefined) { if (sourceBlob === undefined) {
console.error('blob not found', data._id) console.error('blob not found', data._id)
continue continue
} }
if (targetBlob !== undefined) {
console.log('Target blob already exists', targetBlob._id, targetBlob.contentType)
}
if (blob.provider !== exAdapter.defaultAdapter) { if (targetBlob === undefined) {
if (blob.size <= params.blobSizeLimitMb * 1024 * 1024) { await rateLimiter.exec(async () => {
await rateLimiter.exec(async () => { try {
try { await retryOnFailure(
await retryOnFailure( ctx,
ctx, 5,
5, async () => {
async () => { await processFile(ctx, source, target, workspaceId, sourceBlob)
await processFile(ctx, source, params.move ? exAdapter : target, workspaceId, blob) // We need to sync and update aggregator table for now.
}, await exAdapter.syncBlobFromStorage(ctx, workspaceId, sourceBlob._id, exAdapter.defaultAdapter)
50 },
) 50
movedCnt += 1 )
movedBytes += blob.size movedCnt += 1
batchBytes += blob.size movedBytes += sourceBlob.size
} catch (err) { batchBytes += sourceBlob.size
console.error('failed to process blob', data._id, err) } catch (err) {
} console.error('failed to process blob', data._id, err)
}) }
} else { })
skippedCnt += 1 }
console.log('skipping large blob', data._id, Math.round(blob.size / 1024 / 1024))
} if (targetBlob === undefined) {
targetBlob = await target.stat(ctx, workspaceId, data._id)
}
if (
targetBlob !== undefined &&
targetBlob.size === sourceBlob.size &&
targetBlob.contentType === sourceBlob.contentType
) {
// We could safely delete source blob
toRemove.push(sourceBlob._id)
} }
processedCnt += 1 processedCnt += 1
processedBytes += blob.size processedBytes += sourceBlob.size
if (processedCnt % 100 === 0) { if (processedCnt % 100 === 0) {
await rateLimiter.waitProcessing() await rateLimiter.waitProcessing()
@ -195,8 +212,6 @@ async function processAdapter (
movedCnt, movedCnt,
Math.round(movedBytes / 1024 / 1024) + 'MB', Math.round(movedBytes / 1024 / 1024) + 'MB',
'+' + Math.round(batchBytes / 1024 / 1024) + 'MB', '+' + Math.round(batchBytes / 1024 / 1024) + 'MB',
'skipped',
skippedCnt,
Math.round(duration / 1000) + 's' Math.round(duration / 1000) + 's'
) )
@ -207,6 +222,9 @@ async function processAdapter (
} }
await rateLimiter.waitProcessing() await rateLimiter.waitProcessing()
if (toRemove.length > 0 && params.move) {
await source.remove(ctx, workspaceId, toRemove)
}
} finally { } finally {
await iterator.close() await iterator.close()
} }

View File

@ -46,8 +46,8 @@ export function defineActions (builder: Builder): void {
createAction(builder, { createAction(builder, {
action: chunter.actionImpl.StartConversation, action: chunter.actionImpl.StartConversation,
label: chunter.string.StartConversation, label: chunter.string.Message,
icon: chunter.icon.Thread, icon: view.icon.Bubble,
input: 'focus', input: 'focus',
category: chunter.category.Chunter, category: chunter.category.Chunter,
target: contact.mixin.Employee, target: contact.mixin.Employee,

View File

@ -21,6 +21,7 @@ import core from '@hcengineering/model-core'
import view from '@hcengineering/model-view' import view from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench' import workbench from '@hcengineering/model-workbench'
import { WidgetType } from '@hcengineering/workbench' import { WidgetType } from '@hcengineering/workbench'
import contact from '@hcengineering/contact'
import chunter from './plugin' import chunter from './plugin'
import { defineActions } from './actions' import { defineActions } from './actions'
@ -303,6 +304,16 @@ export function createModel (builder: Builder): void {
disabled: [{ _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }] disabled: [{ _class: 1 }, { space: 1 }, { modifiedBy: 1 }, { createdBy: 1 }, { createdOn: -1 }]
}) })
// Extensions
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: contact.extension.EmployeePopupActions,
component: chunter.component.DirectMessageButton
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: activity.extension.ActivityEmployeePresenter,
component: chunter.component.EmployeePresenter
})
defineActions(builder) defineActions(builder)
defineNotifications(builder) defineNotifications(builder)
} }

View File

@ -35,7 +35,9 @@ export default mergeIds(chunterId, chunter, {
ChatMessageNotificationLabel: '' as AnyComponent, ChatMessageNotificationLabel: '' as AnyComponent,
ThreadNotificationPresenter: '' as AnyComponent, ThreadNotificationPresenter: '' as AnyComponent,
JoinChannelNotificationPresenter: '' as AnyComponent, JoinChannelNotificationPresenter: '' as AnyComponent,
WorkbenchTabExtension: '' as AnyComponent WorkbenchTabExtension: '' as AnyComponent,
DirectMessageButton: '' as AnyComponent,
EmployeePresenter: '' as AnyComponent
}, },
action: { action: {
MarkCommentUnread: '' as Ref<Action>, MarkCommentUnread: '' as Ref<Action>,

View File

@ -185,8 +185,7 @@ export function createModel (builder: Builder): void {
label: love.string.MeetingRoom, label: love.string.MeetingRoom,
type: WidgetType.Flexible, type: WidgetType.Flexible,
icon: love.icon.Cam, icon: love.icon.Cam,
component: love.component.VideoWidget, component: love.component.VideoWidget
size: 'medium'
}, },
love.ids.VideoWidget love.ids.VideoWidget
) )

View File

@ -76,8 +76,9 @@
use:tooltip={{ use:tooltip={{
component: Label, component: Label,
props: { label: attribute.label } props: { label: attribute.label }
}}><Label label={attribute.label} /></span }}
> ><Label label={attribute.label} />
</span>
<div class="flex flex-grow min-w-0"> <div class="flex flex-grow min-w-0">
<svelte:component <svelte:component
this={editor} this={editor}

View File

@ -26,7 +26,9 @@
</script> </script>
{#if node} {#if node}
<pre class="proseCodeBlock" style:margin={preview ? '0' : null}><code <pre class="proseCodeBlock" style:margin={preview ? '0' : null}>
><Component is={diffview.component.Highlight} props={{ value, language }} /></code <code>
></pre> <Component is={diffview.component.Highlight} props={{ value, language }} />
</code>
</pre>
{/if} {/if}

View File

@ -17,7 +17,7 @@ import { textblockTypeInputRule } from '@tiptap/core'
import CodeBlock, { CodeBlockOptions } from '@tiptap/extension-code-block' import CodeBlock, { CodeBlockOptions } from '@tiptap/extension-code-block'
export const codeBlockOptions: CodeBlockOptions = { export const codeBlockOptions: CodeBlockOptions = {
defaultLanguage: null, defaultLanguage: 'plaintext',
languageClassPrefix: 'language-', languageClassPrefix: 'language-',
exitOnArrowDown: true, exitOnArrowDown: true,
exitOnTripleEnter: true, exitOnTripleEnter: true,

View File

@ -316,6 +316,7 @@ input.search {
border-radius: 50%; border-radius: 50%;
} }
&:not(.small-gap, .large-gap) { margin-right: .375rem; } &:not(.small-gap, .large-gap) { margin-right: .375rem; }
&.no-gap { margin-right: 0; }
&.small-gap { margin-right: .25rem; } &.small-gap { margin-right: .25rem; }
&.large-gap { margin-right: .5rem; } &.large-gap { margin-right: .5rem; }
&.flow:last-child { margin-right: 0; } &.flow:last-child { margin-right: 0; }

View File

@ -270,6 +270,9 @@
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 50%; border-radius: 50%;
&.relative {
position: relative;
}
&.xx-small, &.xx-small,
&.inline, &.inline,
&.tiny, &.tiny,

View File

@ -107,9 +107,9 @@
on:keydown on:keydown
> >
{#if loading} {#if loading}
<div class="icon"><Spinner size={'small'} /></div> <div class="icon no-gap"><Spinner size={'small'} /></div>
{:else if icon} {:else if icon}
<div class="icon"><Icon {icon} {iconProps} size={actualIconSize} /></div> <div class="icon no-gap"><Icon {icon} {iconProps} size={actualIconSize} /></div>
{/if} {/if}
{#if label}<span><Label {label} params={labelParams} /></span>{/if} {#if label}<span><Label {label} params={labelParams} /></span>{/if}
{#if title}<span>{title}</span>{/if} {#if title}<span>{title}</span>{/if}

View File

@ -121,7 +121,6 @@
</div> </div>
</div> </div>
</div> </div>
>
<style lang="scss"> <style lang="scss">
.img { .img {

View File

@ -87,7 +87,6 @@
{/if} {/if}
</div> </div>
</div> </div>
>
<style lang="scss"> <style lang="scss">
.editbox { .editbox {

View File

@ -28,6 +28,7 @@
export let lazy = false export let lazy = false
export let minHeight: string | null = null export let minHeight: string | null = null
export let highlightIndex: number | undefined = undefined export let highlightIndex: number | undefined = undefined
export let items: any[] = []
export let getKey: (index: number) => string = (index) => index.toString() export let getKey: (index: number) => string = (index) => index.toString()
const refs: HTMLElement[] = [] const refs: HTMLElement[] = []
@ -65,6 +66,8 @@
r?.scrollIntoView({ behavior: 'auto', block: 'nearest' }) r?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
} }
} }
$: array = items.length > 0 ? items : Array(count)
</script> </script>
{#if count} {#if count}
@ -75,7 +78,7 @@
dispatch('changeContent') dispatch('changeContent')
}} }}
> >
{#each Array(count) as _, row (getKey(row))} {#each array as _, row (getKey(row))}
{#if lazy} {#if lazy}
<div style="min-height: {minHeight}"> <div style="min-height: {minHeight}">
<Lazy> <Lazy>
@ -145,6 +148,6 @@
.list-container { .list-container {
min-width: 0; min-width: 0;
// border-radius: 0.25rem; // border-radius: 0.25rem;
user-select: none; //user-select: none;
} }
</style> </style>

View File

@ -81,6 +81,7 @@
<style lang="scss"> <style lang="scss">
.container { .container {
font-weight: 500; font-weight: 500;
font-size: 0.75rem;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 0.25rem; border-radius: 0.25rem;
cursor: pointer; cursor: pointer;

View File

@ -157,7 +157,6 @@
</div> </div>
</div> </div>
</div> </div>
>
<style lang="scss"> <style lang="scss">
.editbox-container { .editbox-container {

View File

@ -73,7 +73,6 @@
on:keypress on:keypress
on:blur on:blur
/> />
>
<style lang="scss"> <style lang="scss">
.root { .root {

View File

@ -86,7 +86,6 @@
<IconClose size={'small'} /> <IconClose size={'small'} />
</button> </button>
</label> </label>
>
<style lang="scss"> <style lang="scss">
.searchInput-wrapper { .searchInput-wrapper {

View File

@ -55,7 +55,6 @@
on:blur on:blur
/> />
</div> </div>
>
<style lang="scss"> <style lang="scss">
.textarea { .textarea {

View File

@ -84,4 +84,3 @@
> >
{time} {time}
</span> </span>
span>

View File

@ -89,9 +89,9 @@
{#if selectedTZ} {#if selectedTZ}
<div class="header flex-col"> <div class="header flex-col">
<div class="flex-between min-h-4" style:margin-right={'-.5rem'}> <div class="flex-between min-h-4" style:margin-right={'-.5rem'}>
<span class="text-xs font-medium uppercase content-darker-color flex-grow" <span class="text-xs font-medium uppercase content-darker-color flex-grow">
><Label label={ui.string.Selected} /></span <Label label={ui.string.Selected} />
> </span>
{#if reset !== null} {#if reset !== null}
<ActionIcon <ActionIcon
icon={IconUndo} icon={IconUndo}

View File

@ -112,9 +112,9 @@
{#if viewDate} {#if viewDate}
<div class="caption"> <div class="caption">
{#each [...Array(7).keys()] as dayOfWeek} {#each [...Array(7).keys()] as dayOfWeek}
<span class="weekdays ui-regular-12" <span class="weekdays ui-regular-12">
>{capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}</span {capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}
> </span>
{/each} {/each}
</div> </div>
<div class="calendar"> <div class="calendar">

View File

@ -134,9 +134,9 @@
{#if viewDate} {#if viewDate}
<div class="calendar" class:noPadding> <div class="calendar" class:noPadding>
{#each [...Array(7).keys()] as dayOfWeek} {#each [...Array(7).keys()] as dayOfWeek}
<span class="caption" <span class="caption">
>{capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}</span {capitalizeFirstLetter(getWeekDayName(day(firstDayOfCurrentMonth, dayOfWeek), 'short'))}
> </span>
{/each} {/each}
{#each [...Array(displayedWeeksCount).keys()] as weekIndex} {#each [...Array(displayedWeeksCount).keys()] as weekIndex}

View File

@ -8,5 +8,5 @@
<path <path
{opacity} {opacity}
d="M 8 1.320313 L 0.660156 8.132813 L 1.339844 8.867188 L 2 8.253906 L 2 14 L 7 14 L 7 9 L 9 9 L 9 14 L 14 14 L 14 8.253906 L 14.660156 8.867188 L 15.339844 8.132813 Z M 8 2.679688 L 13 7.328125 L 13 13 L 10 13 L 10 8 L 6 8 L 6 13 L 3 13 L 3 7.328125 Z" d="M 8 1.320313 L 0.660156 8.132813 L 1.339844 8.867188 L 2 8.253906 L 2 14 L 7 14 L 7 9 L 9 9 L 9 14 L 14 14 L 14 8.253906 L 14.660156 8.867188 L 15.339844 8.132813 Z M 8 2.679688 L 13 7.328125 L 13 13 L 10 13 L 10 8 L 6 8 L 6 13 L 3 13 L 3 7.328125 Z"
/></svg />
> </svg>

View File

@ -8,5 +8,5 @@
<path <path
{opacity} {opacity}
d="M 14.5 2.792969 L 5.5 11.792969 L 1.851563 8.148438 L 1.5 7.792969 L 0.792969 8.5 L 1.148438 8.851563 L 5.5 13.207031 L 15.207031 3.5 Z" d="M 14.5 2.792969 L 5.5 11.792969 L 1.851563 8.148438 L 1.5 7.792969 L 0.792969 8.5 L 1.148438 8.851563 L 5.5 13.207031 L 15.207031 3.5 Z"
/></svg />
> </svg>

View File

@ -161,7 +161,7 @@
</button> </button>
</div> </div>
{/if} {/if}
<div class="flex-row-center left-items" style:-webkit-app-region={'no-drag'}> <div class="flex-row-center left-items flex-gap-0-5" style:-webkit-app-region={'no-drag'}>
<RootBarExtension position="left" /> <RootBarExtension position="left" />
</div> </div>
<div <div
@ -216,7 +216,7 @@
min-height: var(--status-bar-height); min-height: var(--status-bar-height);
height: var(--status-bar-height); height: var(--status-bar-height);
// min-width: 600px; // min-width: 600px;
font-size: 12px; font-size: 0.75rem;
line-height: 150%; line-height: 150%;
background-color: var(--theme-statusbar-color); background-color: var(--theme-statusbar-color);
// border-bottom: 1px solid var(--theme-navpanel-divider); // border-bottom: 1px solid var(--theme-navpanel-divider);

View File

@ -47,7 +47,7 @@
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs)) $: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
$: localStorage.setItem('activity-newest-first', JSON.stringify(isNewestFirst)) $: localStorage.setItem('activity-newest-first', JSON.stringify(isNewestFirst))
client.findAll(activity.class.ActivityMessagesFilter, {}).then((res) => { void client.findAll(activity.class.ActivityMessagesFilter, {}).then((res) => {
filters = res filters = res
if (saved !== null && saved !== undefined) { if (saved !== null && saved !== undefined) {

View File

@ -14,15 +14,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import { getClient, LiteMessageViewer } from '@hcengineering/presentation' import { ComponentExtensions, getClient, LiteMessageViewer } from '@hcengineering/presentation'
import { Person, type PersonAccount } from '@hcengineering/contact' import { Person, type PersonAccount } from '@hcengineering/contact'
import { import { Avatar, personAccountByIdStore, personByIdStore, SystemAvatar } from '@hcengineering/contact-resources'
Avatar,
EmployeePresenter,
personAccountByIdStore,
personByIdStore,
SystemAvatar
} from '@hcengineering/contact-resources'
import core, { Account, Doc, Ref, Timestamp, type WithLookup } from '@hcengineering/core' import core, { Account, Doc, Ref, Timestamp, type WithLookup } from '@hcengineering/core'
import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui' import { Icon, Label, resizeObserver, TimeSince, tooltip } from '@hcengineering/ui'
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform' import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
@ -131,7 +125,7 @@
/> />
</DocNavLink> </DocNavLink>
{:else if person} {:else if person}
<EmployeePresenter value={person} shouldShowAvatar={false} compact showStatus={false} /> <ComponentExtensions extension={activity.extension.ActivityEmployeePresenter} props={{ person }} />
{:else} {:else}
<Label label={core.string.System} /> <Label label={core.string.System} />
{/if} {/if}

View File

@ -19,9 +19,9 @@
ActivityMessageViewType ActivityMessageViewType
} from '@hcengineering/activity' } from '@hcengineering/activity'
import { Person } from '@hcengineering/contact' import { Person } from '@hcengineering/contact'
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources' import { Avatar, SystemAvatar } from '@hcengineering/contact-resources'
import core, { Ref } from '@hcengineering/core' import core, { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import { Action, Icon, Label } from '@hcengineering/ui' import { Action, Icon, Label } from '@hcengineering/ui'
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources' import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
@ -209,7 +209,7 @@
<div class="header clear-mins"> <div class="header clear-mins">
{#if person} {#if person}
<div class="username"> <div class="username">
<EmployeePresenter value={person} shouldShowAvatar={false} compact /> <ComponentExtensions extension={activity.extension.ActivityEmployeePresenter} props={{ person }} />
</div> </div>
{:else} {:else}
<div class="strong"> <div class="strong">

View File

@ -30,7 +30,7 @@ import {
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform' import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import { Preference } from '@hcengineering/preference' import { Preference } from '@hcengineering/preference'
import type { AnyComponent } from '@hcengineering/ui' import type { AnyComponent, ComponentExtensionId } from '@hcengineering/ui'
/** /**
* @public * @public
@ -334,6 +334,9 @@ export default plugin(activityId, {
AllFilter: '' as Ref<ActivityMessagesFilter>, AllFilter: '' as Ref<ActivityMessagesFilter>,
MentionNotification: '' as Ref<Doc> MentionNotification: '' as Ref<Doc>
}, },
extension: {
ActivityEmployeePresenter: '' as ComponentExtensionId
},
function: { function: {
ShouldScrollToActivity: '' as Resource<() => boolean> ShouldScrollToActivity: '' as Resource<() => boolean>
}, },

View File

@ -92,10 +92,8 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<div class="flex fs-title flex-gap-1"> <div class="flex fs-title flex-gap-1">
<span class="over-underline" on:click={handleMove}>{space?.name}</span>><span <span class="over-underline" on:click={handleMove}>{space?.name}</span>>
class="over-underline" <span class="over-underline" on:click={handleMove}>{state?.name}</span>
on:click={handleMove}>{state?.name}</span
>
</div> </div>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="tools"> <svelte:fragment slot="tools">

View File

@ -0,0 +1,57 @@
<script lang="ts">
import contact, { Person, Employee } from '@hcengineering/contact'
import { EmployeePresenter } from '@hcengineering/contact-resources'
import { getClient } from '@hcengineering/presentation'
import { getCurrentLocation, location, Location } from '@hcengineering/ui'
import { decodeObjectURI } from '@hcengineering/view'
import { Ref } from '@hcengineering/core'
import { chunterId } from '@hcengineering/chunter'
import { notificationId } from '@hcengineering/notification'
import { createDirect } from '../utils'
import { openChannel } from '../navigation'
import chunter from '../plugin'
export let person: Person | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
function canNavigateToDirect (location: Location, person: Person | undefined): boolean {
const app = location.path[2]
if (app !== chunterId && app !== notificationId) {
return false
}
if (person === undefined) {
return false
}
return hierarchy.hasMixin(person, contact.mixin.Employee) && (person as Employee).active
}
async function openEmployeeDirect (): Promise<void> {
if (person === undefined) return
const dm = await createDirect([person._id as Ref<Employee>])
if (dm === undefined) {
return
}
const loc = getCurrentLocation()
const [_id] = decodeObjectURI(loc.path[3]) ?? []
if (_id === dm) {
return
}
openChannel(dm, chunter.class.DirectMessage, undefined, true)
}
</script>
<EmployeePresenter
value={person}
shouldShowAvatar={false}
compact
onEmployeeEdit={canNavigateToDirect($location, person) ? openEmployeeDirect : undefined}
/>

View File

@ -0,0 +1,49 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// 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.
-->
<script lang="ts">
import { ModernButton, getCurrentLocation } from '@hcengineering/ui'
import view, { decodeObjectURI } from '@hcengineering/view'
import { Employee } from '@hcengineering/contact'
import chunter from '../plugin'
import { createDirect } from '../utils'
import { openChannelInSidebar } from '../navigation'
export let employee: Employee
async function openDirect (): Promise<void> {
const dm = await createDirect([employee._id])
if (dm === undefined) {
return
}
const loc = getCurrentLocation()
const [_id] = decodeObjectURI(loc.path[3]) ?? []
if (_id === dm) {
return
}
await openChannelInSidebar(dm, chunter.class.DirectMessage, undefined, undefined, true)
}
</script>
<ModernButton
label={chunter.string.Message}
icon={view.icon.Bubble}
size="small"
iconSize="small"
on:click={openDirect}
/>

View File

@ -60,6 +60,7 @@
flex-shrink: 0; flex-shrink: 0;
margin: 0.25rem 0; margin: 0.25rem 0;
height: 1.875rem; height: 1.875rem;
font-size: 0.75rem;
&:not(:first-child)::after { &:not(:first-child)::after {
position: absolute; position: absolute;

View File

@ -126,8 +126,10 @@
use:tooltip={{ use:tooltip={{
component: Label, component: Label,
props: { label: core.string.CreatedBy } props: { label: core.string.CreatedBy }
}}><Label label={core.string.CreatedBy} /></span }}
> >
<Label label={core.string.CreatedBy} />
</span>
<div class="flex flex-grow min-w-0"> <div class="flex flex-grow min-w-0">
<EmployeeBox <EmployeeBox
value={creatorPersonRef} value={creatorPersonRef}

View File

@ -102,7 +102,7 @@
<div class="antiHSpacer" /> <div class="antiHSpacer" />
{:else if secondaryNotifyMarker} {:else if secondaryNotifyMarker}
<div class="antiHSpacer" /> <div class="antiHSpacer" />
<NotifyMarker count={0} kind="secondary" size="x-small" /> <NotifyMarker count={0} kind="simple" />
<div class="antiHSpacer" /> <div class="antiHSpacer" />
{/if} {/if}
</svelte:fragment> </svelte:fragment>

View File

@ -53,6 +53,8 @@ import ThreadViewPanel from './components/threads/ThreadViewPanel.svelte'
import ChatWidget from './components/ChatWidget.svelte' import ChatWidget from './components/ChatWidget.svelte'
import ChatWidgetTab from './components/ChatWidgetTab.svelte' import ChatWidgetTab from './components/ChatWidgetTab.svelte'
import WorkbenchTabExtension from './components/WorkbenchTabExtension.svelte' import WorkbenchTabExtension from './components/WorkbenchTabExtension.svelte'
import DirectMessageButton from './components/DirectMessageButton.svelte'
import EmployeePresenter from './components/ChunterEmployeePresenter.svelte'
import { import {
chunterSpaceLinkFragmentProvider, chunterSpaceLinkFragmentProvider,
@ -181,7 +183,9 @@ export default async (): Promise<Resources> => ({
JoinChannelNotificationPresenter, JoinChannelNotificationPresenter,
ChatWidget, ChatWidget,
ChatWidgetTab, ChatWidgetTab,
WorkbenchTabExtension WorkbenchTabExtension,
DirectMessageButton,
EmployeePresenter
}, },
activity: { activity: {
ChannelCreatedMessage, ChannelCreatedMessage,

View File

@ -31,7 +31,12 @@ import { getChannelName, isThreadMessage } from './utils'
import chunter from './plugin' import chunter from './plugin'
import { threadMessagesStore } from './stores' import { threadMessagesStore } from './stores'
export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<ActivityMessage>): void { export function openChannel (
_id: string,
_class: Ref<Class<Doc>>,
thread?: Ref<ActivityMessage>,
forceApplication = false
): void {
const loc = getCurrentLocation() const loc = getCurrentLocation()
const id = encodeObjectURI(_id, _class) const id = encodeObjectURI(_id, _class)
@ -39,6 +44,10 @@ export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<
return return
} }
if (forceApplication) {
loc.path[2] = chunterId
}
loc.path[3] = id loc.path[3] = id
loc.query = { ...loc.query, message: null } loc.query = { ...loc.query, message: null }
@ -317,7 +326,12 @@ export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidg
}, 100) }, 100)
} }
export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc): Promise<void> { export async function openThreadInSidebar (
_id: Ref<ActivityMessage>,
msg?: ActivityMessage,
doc?: Doc,
selectedMessageId?: Ref<ActivityMessage>
): Promise<void> {
const client = getClient() const client = getClient()
const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0] const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: chunter.ids.ChatWidget })[0]
@ -362,6 +376,7 @@ export async function openThreadInSidebar (_id: Ref<ActivityMessage>, msg?: Acti
_id: object?._id, _id: object?._id,
_class: object?._class, _class: object?._class,
thread: message._id, thread: message._id,
selectedMessageId,
channelName: name channelName: name
} }
} }
@ -436,7 +451,7 @@ export async function locationDataResolver (loc: Location): Promise<LocationData
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {}) const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
const _id: Ref<Doc> | undefined = await parseLinkId(linkProviders, id, _class) const _id: Ref<Doc> | undefined = await parseLinkId(linkProviders, id, _class)
const object = await client.findOne(_class, { _id }) const object = hierarchy.hasClass(_class) ? await client.findOne(_class, { _id }) : undefined
if (object === undefined) return { name: await translate(chunter.string.Chat, {}, get(languageStore)) } if (object === undefined) return { name: await translate(chunter.string.Chat, {}, get(languageStore)) }
const titleIntl = client.getHierarchy().getClass(object._class).label const titleIntl = client.getHierarchy().getClass(object._class).label

View File

@ -251,7 +251,9 @@ export default plugin(chunterId, {
}, },
function: { function: {
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>, CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
OpenThreadInSidebar: '' as Resource<(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc) => Promise<void>>, OpenThreadInSidebar: '' as Resource<
(_id: Ref<ActivityMessage>, msg?: ActivityMessage, doc?: Doc, selectedId?: Ref<ActivityMessage>) => Promise<void>
>,
OpenChannelInSidebar: '' as Resource< OpenChannelInSidebar: '' as Resource<
( (
_id: Ref<Doc>, _id: Ref<Doc>,

View File

@ -102,6 +102,7 @@
"People": "People", "People": "People",
"For": "For", "For": "For",
"SelectUsers": "Select users", "SelectUsers": "Select users",
"AddGuest": "Add guest" "AddGuest": "Add guest",
"ViewProfile": "View profile"
} }
} }

View File

@ -102,6 +102,7 @@
"People": "Personas", "People": "Personas",
"For": "Para", "For": "Para",
"SelectUsers": "Seleccionar usuarios", "SelectUsers": "Seleccionar usuarios",
"AddGuest": "Añadir invitado" "AddGuest": "Añadir invitado",
"ViewProfile": "Ver perfil"
} }
} }

View File

@ -102,6 +102,7 @@
"People": "Personnes", "People": "Personnes",
"For": "Pour", "For": "Pour",
"SelectUsers": "Sélectionner des utilisateurs", "SelectUsers": "Sélectionner des utilisateurs",
"AddGuest": "Ajouter un invité" "AddGuest": "Ajouter un invité",
"ViewProfile": "Voir le profil"
} }
} }

View File

@ -102,6 +102,7 @@
"People": "Pessoas", "People": "Pessoas",
"For": "Para", "For": "Para",
"SelectUsers": "Selecionar utilizadores", "SelectUsers": "Selecionar utilizadores",
"AddGuest": "Adicionar convidado" "AddGuest": "Adicionar convidado",
"ViewProfile": "Ver perfil"
} }
} }

View File

@ -102,6 +102,7 @@
"People": "Люди", "People": "Люди",
"For": "Для", "For": "Для",
"SelectUsers": "Выберите пользователей", "SelectUsers": "Выберите пользователей",
"AddGuest": "Добавить гостя" "AddGuest": "Добавить гостя",
"ViewProfile": "Посмотреть профиль"
} }
} }

View File

@ -102,6 +102,7 @@
"People": "人员", "People": "人员",
"For": "为", "For": "为",
"SelectUsers": "选择用户", "SelectUsers": "选择用户",
"AddGuest": "添加访客" "AddGuest": "添加访客",
"ViewProfile": "查看资料"
} }
} }

View File

@ -186,8 +186,10 @@
if (openable) { if (openable) {
dispatch('update', 'open') dispatch('update', 'open')
} }
}}>{value}</span }}
> >
{value}
</span>
<Button <Button
focusIndex={3} focusIndex={3}
kind={'ghost'} kind={'ghost'}

View File

@ -2,12 +2,14 @@
import { Employee, Person } from '@hcengineering/contact' import { Employee, Person } from '@hcengineering/contact'
import { Ref, WithLookup } from '@hcengineering/core' import { Ref, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform' import { IntlString } from '@hcengineering/platform'
import ui, { IconSize } from '@hcengineering/ui' import ui, { IconSize, LabelAndProps } from '@hcengineering/ui'
import { PersonLabelTooltip, employeeByIdStore, personByIdStore } from '..' import { PersonLabelTooltip, employeeByIdStore, personByIdStore } from '..'
import PersonPresenter from '../components/PersonPresenter.svelte' import PersonPresenter from '../components/PersonPresenter.svelte'
import contact from '../plugin' import contact from '../plugin'
import EmployeePreviewPopup from './EmployeePreviewPopup.svelte'
export let value: Ref<Person> | WithLookup<Person> | null | undefined export let value: Ref<Person> | WithLookup<Person> | null | undefined
export let showPopup: boolean = true
export let tooltipLabels: PersonLabelTooltip | undefined = undefined export let tooltipLabels: PersonLabelTooltip | undefined = undefined
export let shouldShowAvatar: boolean = true export let shouldShowAvatar: boolean = true
export let shouldShowName: boolean = true export let shouldShowName: boolean = true
@ -24,16 +26,26 @@
export let compact: boolean = false export let compact: boolean = false
export let showStatus: boolean = false export let showStatus: boolean = false
$: employeeValue = typeof value === 'string' ? $personByIdStore.get(value) : value $: employeeValue = typeof value === 'string' ? ($personByIdStore.get(value) as Employee) : (value as Employee)
$: active = $: active = employeeValue !== undefined ? $employeeByIdStore.get(employeeValue?._id)?.active ?? false : false
employeeValue !== undefined ? $employeeByIdStore.get(employeeValue?._id as Ref<Employee>)?.active ?? false : false
function getPreviewPopup (active: boolean, value: Employee | undefined): LabelAndProps | undefined {
if (!active || value === undefined || !showPopup) {
return undefined
}
return {
component: EmployeePreviewPopup,
props: { employeeId: value._id }
}
}
</script> </script>
<PersonPresenter <PersonPresenter
value={employeeValue} value={employeeValue}
{tooltipLabels} {tooltipLabels}
onEdit={onEmployeeEdit} onEdit={onEmployeeEdit}
customTooltip={getPreviewPopup(active, employeeValue)}
{shouldShowAvatar} {shouldShowAvatar}
{shouldShowName} {shouldShowName}
{avatarSize} {avatarSize}

View File

@ -1,103 +1,150 @@
<script lang="ts"> <script lang="ts">
import { Employee, PersonAccount, getName, Status } from '@hcengineering/contact' import { Employee } from '@hcengineering/contact'
import { getCurrentAccount, Ref } from '@hcengineering/core' import { Class, Doc, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation' import { ModernButton, navigate, resizeObserver } from '@hcengineering/ui'
import Avatar from './Avatar.svelte'
import { Button, Label, resizeObserver, showPopup } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import view from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { ComponentExtensions, getClient } from '@hcengineering/presentation'
import contact from '../plugin' import contact from '../plugin'
import { employeeByIdStore } from '../utils' import Avatar from './Avatar.svelte'
import EmployeeSetStatusPopup from './EmployeeSetStatusPopup.svelte' import { employeeByIdStore, personAccountByPersonId, statusByUserStore } from '../utils'
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte' import { EmployeePresenter } from '../index'
import Edit from './icons/Edit.svelte'
export let employeeId: Ref<Employee> export let employeeId: Ref<Employee>
const client = getClient() const client = getClient()
const me = (getCurrentAccount() as PersonAccount).person const hierarchy = client.getHierarchy()
$: editable = employeeId === me
const statusesQuery = createQuery()
let status: Status | undefined = undefined
$: employee = $employeeByIdStore.get(employeeId) as Employee
statusesQuery.query(contact.class.Status, { attachedTo: employeeId }, (res) => {
status = res[0]
})
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
function onEdit () { let employee: Employee | undefined = undefined
showPopup(
EmployeeSetStatusPopup, $: employee = $employeeByIdStore.get(employeeId)
{ $: accounts = $personAccountByPersonId.get(employeeId) ?? []
currentStatus: status $: isOnline = accounts.some((account) => $statusByUserStore.get(account._id)?.online === true)
},
undefined, // const statusesQuery = createQuery()
() => {}, // let editable = false
(newStatus: Status) => { // let status: Status | undefined = undefined
if (status && newStatus) { // $: editable = employeeId === me
client.updateDoc(contact.class.Status, status.space, status._id, { ...newStatus }) // statusesQuery.query(contact.class.Status, { attachedTo: employeeId }, (res) => {
} else if (status && !newStatus) { // status = res[0]
client.removeDoc(contact.class.Status, status.space, status._id) // })
} else {
client.addCollection(contact.class.Status, employee.space, employeeId, contact.mixin.Employee, 'statuses', { // function setStatus (): void {
name: newStatus.name, // if (!employee) return
dueDate: newStatus.dueDate // showPopup(
}) // EmployeeSetStatusPopup,
} // {
} // currentStatus: status
) // },
dispatch('close') // undefined,
// () => {},
// async (newStatus: Status) => {
// if (status && newStatus) {
// await client.updateDoc(contact.class.Status, status.space, status._id, { ...newStatus })
// } else if (status && !newStatus) {
// await client.removeDoc(contact.class.Status, status.space, status._id)
// } else {
// await client.addCollection(contact.class.Status, employee.space, employeeId, contact.mixin.Employee, 'statuses', {
// name: newStatus.name,
// dueDate: newStatus.dueDate
// })
// }
// }
// )
// dispatch('close')
// }
async function viewProfile (): Promise<void> {
if (employee === undefined) return
const panelComponent = hierarchy.classHierarchyMixin(employee._class as Ref<Class<Doc>>, view.mixin.ObjectPanel)
const comp = panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, employee, {}, comp)
navigate(loc)
} }
</script> </script>
<div <div
class="antiPopup p-4 flex-col" class="root flex-col"
use:resizeObserver={() => { use:resizeObserver={() => {
dispatch('changeContent') dispatch('changeContent')
}} }}
> >
{#if employee} {#if employee}
<div class="flex-col-center pb-2"> <div class="flex-presenter flex-gap-2 p-2">
<Avatar size={'x-large'} person={employee} name={employee.name} /> <Avatar size="large" person={employee} name={employee.name} />
<span class="username">
<EmployeePresenter value={employee} shouldShowAvatar={false} showPopup={false} compact />
</span>
<span class="hulyAvatar-statusMarker small relative mt-0-5" class:online={isOnline} class:offline={!isOnline} />
</div> </div>
<div class="pb-2">{getName(client.getHierarchy(), employee)}</div> <div class="separator" />
<DocNavLink object={employee}> <div class="flex-presenter flex-gap-2 p-2">
<Label label={contact.string.ViewFullProfile} /> <ComponentExtensions extension={contact.extension.EmployeePopupActions} props={{ employee }} />
</DocNavLink> <ModernButton
{#if status} label={contact.string.ViewProfile}
<div class="pb-2"> icon={contact.icon.Person}
<Label label={contact.string.Status} /> size="small"
<div class="flex-row-stretch statusContainer"> iconSize="small"
<div class="pr-2"> on:click={viewProfile}
<EmployeeStatusPresenter {employee} withTooltip={false} /> />
</div> </div>
{#if editable}
<div class="setStatusButton"> <!--{#if status}-->
<Button icon={Edit} title={contact.string.SetStatus} on:click={onEdit} /> <!-- <div class="pb-2">-->
</div> <!-- <Label label={contact.string.Status} />-->
{/if} <!-- <div class="flex-row-stretch statusContainer">-->
</div> <!-- <div class="pr-2">-->
</div> <!-- <EmployeeStatusPresenter {employee} withTooltip={false} />-->
{:else if editable} <!-- </div>-->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- {#if editable}-->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- <div class="setStatusButton">-->
<div class="flex-row-stretch over-underline pb-2" on:click={onEdit}> <!-- <Button icon={Edit} title={contact.string.SetStatus} on:click={setStatus} />-->
<Label label={contact.string.SetStatus} /> <!-- </div>-->
</div> <!-- {/if}-->
{/if} <!-- </div>-->
<!-- </div>-->
<!--{:else if editable}-->
<!-- &lt;!&ndash; svelte-ignore a11y-click-events-have-key-events &ndash;&gt;-->
<!-- &lt;!&ndash; svelte-ignore a11y-no-static-element-interactions &ndash;&gt;-->
<!-- <div class="flex-row-stretch over-underline pb-2" on:click={setStatus}>-->
<!-- <Label label={contact.string.SetStatus} />-->
<!-- </div>-->
<!--{/if}-->
{/if} {/if}
</div> </div>
<style lang="scss"> <style lang="scss">
.statusContainer { .root {
.setStatusButton { display: flex;
opacity: 0; flex-direction: column;
} width: auto;
min-height: 0;
&:hover .setStatusButton { min-width: 0;
opacity: 1; max-width: 30rem;
} background: var(--theme-popup-color);
user-select: none;
} }
.separator {
height: 1px;
width: 100%;
background: var(--global-ui-BorderColor);
}
.username {
font-weight: 500;
}
//.statusContainer {
// .setStatusButton {
// opacity: 0;
// }
//
// &:hover .setStatusButton {
// opacity: 1;
// }
//}
</style> </style>

View File

@ -47,9 +47,9 @@
<div class="icon circle"> <div class="icon circle">
<Company size={'small'} /> <Company size={'small'} />
</div> </div>
<span class="overflow-label label" class:no-underline={noUnderline || disabled} class:fs-bold={accent} <span class="overflow-label label" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
>{value.name}</span {value.name}
> </span>
</div> </div>
</DocNavLink> </DocNavLink>
{:else if type === 'text'} {:else if type === 'text'}

View File

@ -34,6 +34,7 @@
export let defaultName: IntlString | undefined = ui.string.NotSelected export let defaultName: IntlString | undefined = ui.string.NotSelected
export let statusLabel: IntlString | undefined = undefined export let statusLabel: IntlString | undefined = undefined
export let tooltipLabels: PersonLabelTooltip | undefined = undefined export let tooltipLabels: PersonLabelTooltip | undefined = undefined
export let customTooltip: LabelAndProps | undefined = undefined
export let avatarSize: IconSize = 'x-small' export let avatarSize: IconSize = 'x-small'
export let onEdit: ((event: MouseEvent) => void) | undefined = undefined export let onEdit: ((event: MouseEvent) => void) | undefined = undefined
// export let element: HTMLElement | undefined = undefined // export let element: HTMLElement | undefined = undefined
@ -81,7 +82,7 @@
{#if value || shouldShowPlaceholder} {#if value || shouldShowPlaceholder}
<PersonContent <PersonContent
showTooltip={getTooltip(tooltipLabels, personValue)} showTooltip={customTooltip ?? getTooltip(tooltipLabels, personValue)}
value={personValue} value={personValue}
{inline} {inline}
{onEdit} {onEdit}

View File

@ -37,6 +37,7 @@ import core, {
type Doc, type Doc,
type DocumentQuery, type DocumentQuery,
getCurrentAccount, getCurrentAccount,
groupByArray,
type Hierarchy, type Hierarchy,
type IdMap, type IdMap,
matchQuery, matchQuery,
@ -310,6 +311,10 @@ export const personIdByAccountId = derived(personAccountByIdStore, (vals) => {
return new Map<Ref<PersonAccount>, Ref<Person>>(Array.from(vals.values()).map((it) => [it._id, it.person])) return new Map<Ref<PersonAccount>, Ref<Person>>(Array.from(vals.values()).map((it) => [it._id, it.person]))
}) })
export const personAccountByPersonId = derived(personAccountByIdStore, (vals) => {
return groupByArray(Array.from(vals.values()), (it) => it.person)
})
export const statusByUserStore = writable<Map<Ref<Account>, UserStatus>>(new Map()) export const statusByUserStore = writable<Map<Ref<Account>, UserStatus>>(new Map())
export const personByIdStore = derived([personAccountPersonByIdStore, employeeByIdStore], (vals) => { export const personByIdStore = derived([personAccountPersonByIdStore, employeeByIdStore], (vals) => {

View File

@ -31,7 +31,7 @@ import {
import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform' import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform'
import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates' import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
import type { AnyComponent, ColorDefinition, ResolvedLocation, Location } from '@hcengineering/ui' import type { AnyComponent, ColorDefinition, ResolvedLocation, Location, ComponentExtensionId } from '@hcengineering/ui'
import { Action, FilterMode, Viewlet } from '@hcengineering/view' import { Action, FilterMode, Viewlet } from '@hcengineering/view'
/** /**
@ -301,7 +301,8 @@ export const contactPlugin = plugin(contactId, {
Members: '' as IntlString, Members: '' as IntlString,
Contacts: '' as IntlString, Contacts: '' as IntlString,
Employees: '' as IntlString, Employees: '' as IntlString,
Persons: '' as IntlString Persons: '' as IntlString,
ViewProfile: '' as IntlString
}, },
viewlet: { viewlet: {
TableMember: '' as Ref<Viewlet>, TableMember: '' as Ref<Viewlet>,
@ -332,6 +333,9 @@ export const contactPlugin = plugin(contactId, {
}, },
ids: { ids: {
MentionCommonNotificationType: '' as Ref<Doc> MentionCommonNotificationType: '' as Ref<Doc>
},
extension: {
EmployeePopupActions: '' as ComponentExtensionId
} }
}) })

View File

@ -127,7 +127,8 @@ function getDocumentLinkId (doc: Document): string {
return `${slug}---${doc._id}` return `${slug}---${doc._id}`
} }
function parseDocumentId (shortLink: string): Ref<ControlledDocument> | undefined { function parseDocumentId (shortLink?: string): Ref<ControlledDocument> | undefined {
if (shortLink === undefined) return undefined
const parts = shortLink.split('---') const parts = shortLink.split('---')
if (parts.length > 1) { if (parts.length > 1) {
return parts[parts.length - 1] as Ref<ControlledDocument> return parts[parts.length - 1] as Ref<ControlledDocument>

View File

@ -19,16 +19,21 @@ import { hljsDefineSvelte } from './languages/svelte-hljs'
hljs.registerLanguage('svelte', hljsDefineSvelte) hljs.registerLanguage('svelte', hljsDefineSvelte)
export interface HighlightOptions { export interface HighlightOptions {
auto?: boolean
language: string | undefined language: string | undefined
} }
export function highlightText (text: string, options: HighlightOptions): string { export function highlightText (text: string, options: HighlightOptions): string {
// We should always use highlighter because it sanitizes the input // We should always use highlighter because it sanitizes the input
// We have to always use highlighter to ensure that the input is sanitized // We have to always use highlighter to ensure that the input is sanitized
const { language } = options const { language, auto } = options
const validLanguage = language !== undefined && hljs.getLanguage(language) !== undefined const validLanguage = language !== undefined && hljs.getLanguage(language) !== undefined
const { value: highlighted } = validLanguage ? hljs.highlight(text, { language }) : hljs.highlightAuto(text) const { value: highlighted } = validLanguage
? hljs.highlight(text, { language })
: auto === true
? hljs.highlightAuto(text)
: { value: text }
return normalizeHighlightTags(highlighted) return normalizeHighlightTags(highlighted)
} }

View File

@ -39,9 +39,9 @@
{#if shouldShowAvatar} {#if shouldShowAvatar}
<div class="icon"><Icon icon={lead.icon.Lead} size={'small'} /></div> <div class="icon"><Icon icon={lead.icon.Lead} size={'small'} /></div>
{/if} {/if}
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent} <span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
>{value.identifier}</span {value.identifier}
> </span>
</div> </div>
</DocNavLink> </DocNavLink>
{:else if type === 'text'} {:else if type === 'text'}

View File

@ -164,8 +164,10 @@
setMetadataLocalStorage(login.metadata.LoginEndpoint, null) setMetadataLocalStorage(login.metadata.LoginEndpoint, null)
setMetadataLocalStorage(login.metadata.LoginEmail, null) setMetadataLocalStorage(login.metadata.LoginEmail, null)
goTo('login') goTo('login')
}}><Label label={login.string.ChangeAccount} /></NavLink }}
> >
<Label label={login.string.ChangeAccount} />
</NavLink>
</div> </div>
</div> </div>
{/await} {/await}

View File

@ -361,7 +361,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
width: 15rem; width: 100%;
max-width: 100%;
user-select: none; user-select: none;
&:not(.isDock) { &:not(.isDock) {

View File

@ -20,12 +20,12 @@
import love from '../plugin' import love from '../plugin'
import VideoPopup from './VideoPopup.svelte' import VideoPopup from './VideoPopup.svelte'
export let widgetState: WidgetState export let widgetState: WidgetState | undefined
let room: Ref<TypeRoom> | undefined = undefined let room: Ref<TypeRoom> | undefined = undefined
$: room = widgetState.data?.room $: room = widgetState?.data?.room
$: if (widgetState.data?.room === undefined) { $: if (widgetState?.data?.room === undefined) {
closeWidget(love.ids.VideoWidget) closeWidget(love.ids.VideoWidget)
} }
</script> </script>

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' | 'secondary' | 'simple' = '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
@ -30,10 +30,6 @@
</div> </div>
{/if} {/if}
{#if kind === 'secondary'}
<div class="notifyMarker {size} {kind}" />
{/if}
{#if kind === 'simple'} {#if kind === 'simple'}
<div class="notifyMarker {size} {kind}" /> <div class="notifyMarker {size} {kind}" />
{/if} {/if}
@ -53,10 +49,6 @@
color: var(--global-on-accent-TextColor); color: var(--global-on-accent-TextColor);
} }
&.secondary {
background-color: var(--global-subtle-BackgroundColor);
}
&.xx-small { &.xx-small {
width: 0.5rem; width: 0.5rem;
height: 0.5rem; height: 0.5rem;

View File

@ -192,7 +192,7 @@
if (thread !== undefined) { if (thread !== undefined) {
const fn = await getResource(chunter.function.OpenThreadInSidebar) const fn = await getResource(chunter.function.OpenThreadInSidebar)
void fn(thread) void fn(thread, undefined, undefined, selectedMessageId)
} }
if (selectedMessageId !== undefined) { if (selectedMessageId !== undefined) {

View File

@ -107,6 +107,7 @@
bind:this={list} bind:this={list}
bind:selection={listSelection} bind:selection={listSelection}
count={displayData.length} count={displayData.length}
items={displayData}
highlightIndex={displayData.findIndex(([context]) => context === selectedContext)} highlightIndex={displayData.findIndex(([context]) => context === selectedContext)}
noScroll noScroll
minHeight="5.625rem" minHeight="5.625rem"

View File

@ -70,12 +70,12 @@
<tr class="antiTable-body__row"> <tr class="antiTable-body__row">
<td><PersonRefPresenter value={requested.employee} /></td> <td><PersonRefPresenter value={requested.employee} /></td>
<td><BooleanIcon value={requested.decision} /></td> <td><BooleanIcon value={requested.decision} /></td>
<td <td>
>{#if requested.comment} {#if requested.comment}
<ShowMore limit={126} fixed> <ShowMore limit={126} fixed>
<MessageViewer message={requested.comment.message} /> <MessageViewer message={requested.comment.message} />
</ShowMore>{/if}</td </ShowMore>{/if}
> </td>
</tr> </tr>
{/each} {/each}
</tbody> </tbody>

View File

@ -24,5 +24,7 @@
<span class="label nowrap"> <span class="label nowrap">
{#if shortLabel} {#if shortLabel}
{shortLabel}-{/if}{value.name}</span {shortLabel}-
> {/if}
{value.name}
</span>

View File

@ -33,9 +33,9 @@
<div class="icon"> <div class="icon">
<Icon icon={task.icon.Task} size={'small'} /> <Icon icon={task.icon.Task} size={'small'} />
</div> </div>
<span class="label nowrap" <span class="label nowrap">
>{#if shortLabel}{shortLabel}-{/if}{value.number}</span {#if shortLabel}{shortLabel}-{/if}{value.number}
> </span>
</div> </div>
</DocNavLink> </DocNavLink>
{/if} {/if}

View File

@ -112,24 +112,24 @@
maxDigitsAfterPoint={3} maxDigitsAfterPoint={3}
kind={'editbox'} kind={'editbox'}
/> />
<Button kind={'link-bordered'} on:click={() => (data.value = 1)} <Button kind={'link-bordered'} on:click={() => (data.value = 1)}>
><span slot="content">1<Label label={tracker.string.HourLabel} /></span></Button <span slot="content">1<Label label={tracker.string.HourLabel} /></span>
> </Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 2)} <Button kind={'link-bordered'} on:click={() => (data.value = 2)}>
><span slot="content">2<Label label={tracker.string.HourLabel} /></span></Button <span slot="content">2<Label label={tracker.string.HourLabel} /></span>
> </Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 4)} <Button kind={'link-bordered'} on:click={() => (data.value = 4)}>
><span slot="content">4<Label label={tracker.string.HourLabel} /></span></Button <span slot="content">4<Label label={tracker.string.HourLabel} /></span>
> </Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 6)} <Button kind={'link-bordered'} on:click={() => (data.value = 6)}>
><span slot="content">6<Label label={tracker.string.HourLabel} /></span></Button <span slot="content">6<Label label={tracker.string.HourLabel} /></span>
> </Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 7)} <Button kind={'link-bordered'} on:click={() => (data.value = 7)}>
><span slot="content">7<Label label={tracker.string.HourLabel} /></span></Button <span slot="content">7<Label label={tracker.string.HourLabel} /></span>
> </Button>
<Button kind={'link-bordered'} on:click={() => (data.value = 8)} <Button kind={'link-bordered'} on:click={() => (data.value = 8)}>
><span slot="content">8<Label label={tracker.string.HourLabel} /></span></Button <span slot="content">8<Label label={tracker.string.HourLabel} /></span>
> </Button>
</div> </div>
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} /> <EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
<svelte:fragment slot="pool"> <svelte:fragment slot="pool">

View File

@ -109,6 +109,6 @@
<DatePresenter value={report.date} kind={'ghost'} size={'small'} /> <DatePresenter value={report.date} kind={'ghost'} size={'small'} />
</FixedColumn> </FixedColumn>
</div> </div>
</div></svelte:fragment </div>
> </svelte:fragment>
</ListView> </ListView>

View File

@ -33,9 +33,7 @@
preferences = res preferences = res
}) })
$: widgetId = $sidebarStore.widget $: size = $sidebarStore.variant === SidebarVariant.MINI ? 'mini' : undefined
$: widget = widgets.find((it) => it._id === widgetId)
$: size = $sidebarStore.variant === SidebarVariant.MINI ? 'mini' : widget?.size
function txListener (tx: Tx): void { function txListener (tx: Tx): void {
if (tx._class === workbench.class.TxSidebarEvent) { if (tx._class === workbench.class.TxSidebarEvent) {
@ -79,17 +77,5 @@
min-width: 3.5rem !important; min-width: 3.5rem !important;
max-width: 3.5rem !important; max-width: 3.5rem !important;
} }
&.size-small {
width: 10rem !important;
min-width: 10rem !important;
max-width: 10rem !important;
}
&.size-medium {
width: 20rem !important;
min-width: 20rem !important;
max-width: 20rem !important;
}
} }
</style> </style>

View File

@ -71,7 +71,6 @@ export interface Widget extends Doc {
label: IntlString label: IntlString
icon: Asset icon: Asset
type: WidgetType type: WidgetType
size?: 'small' | 'medium'
component: AnyComponent component: AnyComponent
tabComponent?: AnyComponent tabComponent?: AnyComponent

View File

@ -76,9 +76,7 @@ export class DocumentsPage extends CalendarPage {
async changeSpaceInCreateDocumentForm (space: string): Promise<void> { async changeSpaceInCreateDocumentForm (space: string): Promise<void> {
await this.changeSpaceButton.click() await this.changeSpaceButton.click()
await this.page await this.page.locator(`div.selectPopup >> div.list-container >> text=${space}`).click({ force: true })
.locator(`div.list-container.flex-col.flex-grow.svelte-15na0wa >> text=${space}`)
.click({ force: true })
} }
async createTemplate (title: string, description: string, category: string, spaceName: string): Promise<void> { async createTemplate (title: string, description: string, category: string, spaceName: string): Promise<void> {

View File

@ -156,8 +156,8 @@ export class CommentSyncManager implements DocSyncManager {
derivedClient: TxOperations, derivedClient: TxOperations,
integration: IntegrationContainer integration: IntegrationContainer
): Promise<void> { ): Promise<void> {
const { repository } = await this.provider.getProjectAndRepository(event.repository.node_id) const { repository: repo } = await this.provider.getProjectAndRepository(event.repository.node_id)
if (repository === undefined) { if (repo === undefined) {
this.ctx.info('No project for repository', { this.ctx.info('No project for repository', {
repository: event.repository, repository: event.repository,
workspace: this.provider.getWorkspaceId().name workspace: this.provider.getWorkspaceId().name
@ -168,11 +168,12 @@ export class CommentSyncManager implements DocSyncManager {
const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System
switch (event.action) { switch (event.action) {
case 'created': { case 'created': {
await this.createSyncData(event, derivedClient, repository) await this.createSyncData(event, derivedClient, repo)
break break
} }
case 'deleted': { case 'deleted': {
const syncData = await this.client.findOne(github.class.DocSyncInfo, { const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.comment.url ?? '').toLowerCase() url: (event.comment.url ?? '').toLowerCase()
}) })
if (syncData !== undefined) { if (syncData !== undefined) {
@ -183,6 +184,7 @@ export class CommentSyncManager implements DocSyncManager {
} }
case 'edited': { case 'edited': {
const commentData = await this.client.findOne(github.class.DocSyncInfo, { const commentData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.comment.url ?? '').toLowerCase() url: (event.comment.url ?? '').toLowerCase()
}) })
@ -221,6 +223,7 @@ export class CommentSyncManager implements DocSyncManager {
repo: GithubIntegrationRepository repo: GithubIntegrationRepository
): Promise<void> { ): Promise<void> {
const commentData = await this.client.findOne(github.class.DocSyncInfo, { const commentData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (createdEvent.comment.url ?? '').toLowerCase() url: (createdEvent.comment.url ?? '').toLowerCase()
}) })
@ -266,6 +269,7 @@ export class CommentSyncManager implements DocSyncManager {
if (parent === undefined) { if (parent === undefined) {
// Find parent by issue url // Find parent by issue url
parent = await this.client.findOne(github.class.DocSyncInfo, { parent = await this.client.findOne(github.class.DocSyncInfo, {
space: container.project._id,
url: (comment.html_url.split('#')?.[0] ?? '').toLowerCase() url: (comment.html_url.split('#')?.[0] ?? '').toLowerCase()
}) })
} }

View File

@ -201,6 +201,7 @@ export abstract class IssueSyncManagerBase {
} }
)) as any )) as any
const syncData = await this.client.findOne(github.class.DocSyncInfo, { const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: prj._id,
url: (actualContent.node.content.url ?? '').toLowerCase() url: (actualContent.node.content.url ?? '').toLowerCase()
}) })
@ -353,7 +354,8 @@ export abstract class IssueSyncManagerBase {
} }
syncData = syncData =
syncData ?? (await this.client.findOne(github.class.DocSyncInfo, { url: (external.url ?? '').toLowerCase() })) syncData ??
(await this.client.findOne(github.class.DocSyncInfo, { space: prj._id, url: (external.url ?? '').toLowerCase() }))
if (syncData !== undefined) { if (syncData !== undefined) {
const doc: Issue | undefined = await this.client.findOne<Issue>(syncData.objectClass, { const doc: Issue | undefined = await this.client.findOne<Issue>(syncData.objectClass, {
@ -1262,7 +1264,10 @@ export abstract class IssueSyncManagerBase {
err: any, err: any,
_class: Ref<Class<Doc>> = tracker.class.Issue _class: Ref<Class<Doc>> = tracker.class.Issue
): Promise<void> { ): Promise<void> {
const syncData = await this.client.findOne(github.class.DocSyncInfo, { url: url.toLowerCase() }) const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: url.toLowerCase()
})
if (syncData === undefined) { if (syncData === undefined) {
await derivedClient?.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref<GithubProject>, { await derivedClient?.createDoc(github.class.DocSyncInfo, repo.githubProject as Ref<GithubProject>, {
url, url,

View File

@ -203,6 +203,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
} }
case 'deleted': { case 'deleted': {
const syncData = await this.client.findOne(github.class.DocSyncInfo, { const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.issue.html_url ?? '').toLowerCase() url: (event.issue.html_url ?? '').toLowerCase()
}) })
if (syncData !== undefined) { if (syncData !== undefined) {
@ -291,6 +292,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
repo: GithubIntegrationRepository repo: GithubIntegrationRepository
): Promise<void> { ): Promise<void> {
const issueSyncData = await this.client.findOne(github.class.DocSyncInfo, { const issueSyncData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (issueExternal.url ?? '').toLowerCase() url: (issueExternal.url ?? '').toLowerCase()
}) })
if (issueSyncData === undefined) { if (issueSyncData === undefined) {
@ -452,7 +454,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
break break
} }
await this.provider.doSyncFor(attachedDocs) await this.provider.doSyncFor(attachedDocs, container.project)
for (const child of attachedDocs) { for (const child of attachedDocs) {
await derivedClient.update(child, { createId }) await derivedClient.update(child, { createId })
} }

View File

@ -302,6 +302,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
} }
case 'synchronize': { case 'synchronize': {
const syncData = await this.client.findOne(github.class.DocSyncInfo, { const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (externalData.url ?? '').toLowerCase() url: (externalData.url ?? '').toLowerCase()
}) })
if (syncData !== undefined) { if (syncData !== undefined) {

View File

@ -213,6 +213,7 @@ export class ReviewCommentSyncManager implements DocSyncManager {
} }
case 'deleted': { case 'deleted': {
const reviewData = await this.client.findOne(github.class.DocSyncInfo, { const reviewData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.comment.html_url ?? '').toLowerCase() url: (event.comment.html_url ?? '').toLowerCase()
}) })
if (reviewData !== undefined) { if (reviewData !== undefined) {
@ -232,6 +233,7 @@ export class ReviewCommentSyncManager implements DocSyncManager {
} }
case 'edited': { case 'edited': {
const reviewData = await this.client.findOne(github.class.DocSyncInfo, { const reviewData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.comment.html_url ?? '').toLowerCase() url: (event.comment.html_url ?? '').toLowerCase()
}) })
@ -280,6 +282,7 @@ export class ReviewCommentSyncManager implements DocSyncManager {
externalData: ReviewCommentExternalData externalData: ReviewCommentExternalData
): Promise<void> { ): Promise<void> {
const reviewData = await this.client.findOne(github.class.DocSyncInfo, { const reviewData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (createdEvent.comment.html_url ?? '').toLowerCase() url: (createdEvent.comment.html_url ?? '').toLowerCase()
}) })
@ -331,9 +334,10 @@ export class ReviewCommentSyncManager implements DocSyncManager {
if (info.reviewThreadId === undefined && reviewComment.replyTo?.url !== undefined) { if (info.reviewThreadId === undefined && reviewComment.replyTo?.url !== undefined) {
const rthread = await derivedClient.findOne(github.class.GithubReviewComment, { const rthread = await derivedClient.findOne(github.class.GithubReviewComment, {
space: container.project._id,
url: reviewComment.replyTo?.url?.toLowerCase() url: reviewComment.replyTo?.url?.toLowerCase()
}) })
if (rthread !== undefined) { if (rthread !== undefined && info.reviewThreadId !== rthread.reviewThreadId) {
info.reviewThreadId = rthread.reviewThreadId info.reviewThreadId = rthread.reviewThreadId
await derivedClient.update(info, { reviewThreadId: info.reviewThreadId }) await derivedClient.update(info, { reviewThreadId: info.reviewThreadId })
} }
@ -519,8 +523,10 @@ export class ReviewCommentSyncManager implements DocSyncManager {
external: reviewExternal, external: reviewExternal,
current: existing, current: existing,
repository: repo._id, repository: repo._id,
parent: parent.url.toLocaleLowerCase(),
needSync: githubSyncVersion, needSync: githubSyncVersion,
externalVersion: githubExternalSyncVersion externalVersion: githubExternalSyncVersion,
reviewThreadId: info.reviewThreadId ?? existingReview.reviewThreadId
} }
// We need to update in current promise, to prevent event changes. // We need to update in current promise, to prevent event changes.
await derivedClient.update(info, upd) await derivedClient.update(info, upd)

View File

@ -205,6 +205,7 @@ export class ReviewThreadSyncManager implements DocSyncManager {
case 'unresolved': { case 'unresolved': {
const isResolved = event.action === 'resolved' const isResolved = event.action === 'resolved'
const reviewData = await this.client.findOne(github.class.DocSyncInfo, { const reviewData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: event.thread.node_id.toLocaleLowerCase() url: event.thread.node_id.toLocaleLowerCase()
}) })
@ -241,6 +242,7 @@ export class ReviewThreadSyncManager implements DocSyncManager {
} }
const reviewPR = await this.client.findOne(github.class.DocSyncInfo, { const reviewPR = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (reviewData.parent ?? '').toLowerCase() url: (reviewData.parent ?? '').toLowerCase()
}) })
if (reviewPR !== undefined) { if (reviewPR !== undefined) {
@ -516,6 +518,7 @@ export class ReviewThreadSyncManager implements DocSyncManager {
.map((it) => (it.parent ?? '').toLowerCase()) .map((it) => (it.parent ?? '').toLowerCase())
.filter((it, idx, arr) => it != null && arr.indexOf(it) === idx) .filter((it, idx, arr) => it != null && arr.indexOf(it) === idx)
const parents = await derivedClient.findAll(github.class.DocSyncInfo, { const parents = await derivedClient.findAll(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: { url: {
$in: allParents $in: allParents
} }

View File

@ -198,6 +198,7 @@ export class ReviewSyncManager implements DocSyncManager {
await this.createSyncData(event, derivedClient, repo, externalData) await this.createSyncData(event, derivedClient, repo, externalData)
const parentDoc = await this.client.findOne(github.class.DocSyncInfo, { const parentDoc = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.pull_request.html_url ?? '').toLowerCase() url: (event.pull_request.html_url ?? '').toLowerCase()
}) })
if (parentDoc !== undefined) { if (parentDoc !== undefined) {
@ -211,6 +212,7 @@ export class ReviewSyncManager implements DocSyncManager {
} }
case 'dismissed': { case 'dismissed': {
const reviewData = await this.client.findOne(github.class.DocSyncInfo, { const reviewData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.review.html_url ?? '').toLowerCase() url: (event.review.html_url ?? '').toLowerCase()
}) })
@ -254,6 +256,7 @@ export class ReviewSyncManager implements DocSyncManager {
externalData: ReviewExternalData externalData: ReviewExternalData
): Promise<void> { ): Promise<void> {
const reviewData = await this.client.findOne(github.class.DocSyncInfo, { const reviewData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (createdEvent.review.html_url ?? '').toLowerCase() url: (createdEvent.review.html_url ?? '').toLowerCase()
}) })

View File

@ -340,6 +340,7 @@ export async function syncDerivedDocuments<T extends { url: string }> (
extra?: any extra?: any
): Promise<void> { ): Promise<void> {
const childDocsOfClass = await derivedClient.findAll(github.class.DocSyncInfo, { const childDocsOfClass = await derivedClient.findAll(github.class.DocSyncInfo, {
space: prj._id,
objectClass, objectClass,
parent: (parentDoc.url ?? '').toLowerCase(), parent: (parentDoc.url ?? '').toLowerCase(),
...query ...query
@ -352,7 +353,7 @@ export async function syncDerivedDocuments<T extends { url: string }> (
if (existing === undefined) { if (existing === undefined) {
await derivedClient.createDoc<DocSyncInfo>(github.class.DocSyncInfo, prj._id, { await derivedClient.createDoc<DocSyncInfo>(github.class.DocSyncInfo, prj._id, {
objectClass, objectClass,
url: r.url.toLowerCase(), url: (r.url ?? '').toLowerCase(),
needSync: '', // we need to sync to retrieve patch in background needSync: '', // we need to sync to retrieve patch in background
githubNumber: 0, githubNumber: 0,
repository: repo._id, repository: repo._id,

View File

@ -111,7 +111,7 @@ export interface IntegrationManager {
event: T event: T
) => Promise<void> ) => Promise<void>
doSyncFor: (docs: DocSyncInfo[]) => Promise<void> doSyncFor: (docs: DocSyncInfo[], project: GithubProject) => Promise<void>
getWorkspaceId: () => WorkspaceIdWithUrl getWorkspaceId: () => WorkspaceIdWithUrl
getBranding: () => Branding | null getBranding: () => Branding | null

View File

@ -1114,7 +1114,7 @@ export class GithubWorker implements IntegrationManager {
} }
private async performSync (projects: GithubProject[], repositories: GithubIntegrationRepository[]): Promise<boolean> { private async performSync (projects: GithubProject[], repositories: GithubIntegrationRepository[]): Promise<boolean> {
const _projects = projects.map((it) => it._id) const _projects = toIdMap(projects)
const _repositories = repositories.map((it) => it._id) const _repositories = repositories.map((it) => it._id)
const docs = await this.ctx.with( const docs = await this.ctx.with(
@ -1126,7 +1126,7 @@ export class GithubWorker implements IntegrationManager {
{ {
needSync: { $ne: githubSyncVersion }, needSync: { $ne: githubSyncVersion },
externalVersion: { $in: [githubExternalSyncVersion, '#'] }, externalVersion: { $in: [githubExternalSyncVersion, '#'] },
space: { $in: _projects }, space: { $in: Array.from(_projects.keys()) },
repository: { $in: [null, ..._repositories] } repository: { $in: [null, ..._repositories] }
}, },
{ {
@ -1142,18 +1142,21 @@ export class GithubWorker implements IntegrationManager {
this.previousWait += docs.length this.previousWait += docs.length
this.ctx.info('Syncing', { docs: docs.length, workspace: this.workspace.name }) this.ctx.info('Syncing', { docs: docs.length, workspace: this.workspace.name })
await this.doSyncFor(docs) const bySpace = groupByArray(docs, (it) => it.space)
for (const [k, v] of bySpace.entries()) {
await this.doSyncFor(v, _projects.get(k as Ref<GithubProject>) as GithubProject)
}
} }
return docs.length !== 0 return docs.length !== 0
} }
async doSyncFor (docs: DocSyncInfo[]): Promise<void> { async doSyncFor (docs: DocSyncInfo[], project: GithubProject): Promise<void> {
const byClass = this.groupByClass(docs) const byClass = this.groupByClass(docs)
// We need to reorder based on our sync mappers // We need to reorder based on our sync mappers
for (const [_class, clDocs] of byClass.entries()) { for (const [_class, clDocs] of byClass.entries()) {
await this.syncClass(_class, clDocs) await this.syncClass(_class, clDocs, project)
} }
} }
@ -1220,12 +1223,13 @@ export class GithubWorker implements IntegrationManager {
} }
} }
async syncClass (_class: Ref<Class<Doc>>, syncInfo: DocSyncInfo[]): Promise<void> { async syncClass (_class: Ref<Class<Doc>>, syncInfo: DocSyncInfo[], project: GithubProject): Promise<void> {
const externalDocs = await this._client.findAll<Doc>(_class, { const externalDocs = await this._client.findAll<Doc>(_class, {
_id: { $in: syncInfo.map((it) => it._id as Ref<Doc>) } _id: { $in: syncInfo.map((it) => it._id as Ref<Doc>) }
}) })
const parents = await this._client.findAll<DocSyncInfo>(github.class.DocSyncInfo, { const parents = await this._client.findAll<DocSyncInfo>(github.class.DocSyncInfo, {
space: project._id,
url: { $in: syncInfo.map((it) => it.parent?.toLowerCase()).filter((it) => it) as string[] } url: { $in: syncInfo.map((it) => it.parent?.toLowerCase()).filter((it) => it) as string[] }
}) })

View File

@ -335,7 +335,7 @@ test.describe('Content in the Documents tests', () => {
await documentContentPage.applyToolbarCommand('Line 16', 'btnBlockquote') await documentContentPage.applyToolbarCommand('Line 16', 'btnBlockquote')
await documentContentPage.applyToolbarCommand('Line 17', 'btnCode') await documentContentPage.applyToolbarCommand('Line 17', 'btnCode')
await documentContentPage.applyToolbarCommand('Line 18', 'btnCodeBlock') await documentContentPage.applyToolbarCommand('Line 18', 'btnCodeBlock')
await documentContentPage.changeCodeBlockLanguage('Line 18', 'auto', 'css') await documentContentPage.changeCodeBlockLanguage('Line 18', 'plaintext', 'css')
await documentContentPage.applyNote('Line 19', 'warning', testNote) await documentContentPage.applyNote('Line 19', 'warning', testNote)
await documentContentPage.addImage('Line 20') await documentContentPage.addImage('Line 20')
await page.keyboard.type('Cat') await page.keyboard.type('Cat')