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(
async (cmd: { workspace: string, move: string, blobLimit: string, concurrency: string, disabled: boolean }) => {
const params = {
blobSizeLimitMb: parseInt(cmd.blobLimit),
concurrency: parseInt(cmd.concurrency),
move: cmd.move === 'true'
}

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import core from '@hcengineering/model-core'
import view from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import { WidgetType } from '@hcengineering/workbench'
import contact from '@hcengineering/contact'
import chunter from './plugin'
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 }]
})
// 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)
defineNotifications(builder)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -107,9 +107,9 @@
on:keydown
>
{#if loading}
<div class="icon"><Spinner size={'small'} /></div>
<div class="icon no-gap"><Spinner size={'small'} /></div>
{: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 label}<span><Label {label} params={labelParams} /></span>{/if}
{#if title}<span>{title}</span>{/if}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,5 +8,5 @@
<path
{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"
/></svg
>
/>
</svg>

View File

@ -8,5 +8,5 @@
<path
{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"
/></svg
>
/>
</svg>

View File

@ -161,7 +161,7 @@
</button>
</div>
{/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" />
</div>
<div
@ -216,7 +216,7 @@
min-height: var(--status-bar-height);
height: var(--status-bar-height);
// min-width: 600px;
font-size: 12px;
font-size: 0.75rem;
line-height: 150%;
background-color: var(--theme-statusbar-color);
// border-bottom: 1px solid var(--theme-navpanel-divider);

View File

@ -47,7 +47,7 @@
$: localStorage.setItem('activity-filter', JSON.stringify(selectedFiltersRefs))
$: 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
if (saved !== null && saved !== undefined) {

View File

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

View File

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

View File

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

View File

@ -92,10 +92,8 @@
</svelte:fragment>
<svelte:fragment slot="header">
<div class="flex fs-title flex-gap-1">
<span class="over-underline" on:click={handleMove}>{space?.name}</span>><span
class="over-underline"
on:click={handleMove}>{state?.name}</span
>
<span class="over-underline" on:click={handleMove}>{space?.name}</span>>
<span class="over-underline" on:click={handleMove}>{state?.name}</span>
</div>
</svelte:fragment>
<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;
margin: 0.25rem 0;
height: 1.875rem;
font-size: 0.75rem;
&:not(:first-child)::after {
position: absolute;

View File

@ -126,8 +126,10 @@
use:tooltip={{
component: Label,
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">
<EmployeeBox
value={creatorPersonRef}

View File

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

View File

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

View File

@ -31,7 +31,12 @@ import { getChannelName, isThreadMessage } from './utils'
import chunter from './plugin'
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 id = encodeObjectURI(_id, _class)
@ -39,6 +44,10 @@ export function openChannel (_id: string, _class: Ref<Class<Doc>>, thread?: Ref<
return
}
if (forceApplication) {
loc.path[2] = chunterId
}
loc.path[3] = id
loc.query = { ...loc.query, message: null }
@ -317,7 +326,12 @@ export async function closeThreadInSidebarChannel (widget: Widget, tab: ChatWidg
}, 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 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,
_class: object?._class,
thread: message._id,
selectedMessageId,
channelName: name
}
}
@ -436,7 +451,7 @@ export async function locationDataResolver (loc: Location): Promise<LocationData
const linkProviders = client.getModel().findAllSync(view.mixin.LinkIdProvider, {})
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)) }
const titleIntl = client.getHierarchy().getClass(object._class).label

View File

@ -251,7 +251,9 @@ export default plugin(chunterId, {
},
function: {
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<
(
_id: Ref<Doc>,

View File

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

View File

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

View File

@ -102,6 +102,7 @@
"People": "Personnes",
"For": "Pour",
"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",
"For": "Para",
"SelectUsers": "Selecionar utilizadores",
"AddGuest": "Adicionar convidado"
"AddGuest": "Adicionar convidado",
"ViewProfile": "Ver perfil"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,103 +1,150 @@
<script lang="ts">
import { Employee, PersonAccount, getName, Status } from '@hcengineering/contact'
import { getCurrentAccount, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import Avatar from './Avatar.svelte'
import { Button, Label, resizeObserver, showPopup } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import { Employee } from '@hcengineering/contact'
import { Class, Doc, Ref } from '@hcengineering/core'
import { ModernButton, navigate, resizeObserver } from '@hcengineering/ui'
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 { employeeByIdStore } from '../utils'
import EmployeeSetStatusPopup from './EmployeeSetStatusPopup.svelte'
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte'
import Edit from './icons/Edit.svelte'
import Avatar from './Avatar.svelte'
import { employeeByIdStore, personAccountByPersonId, statusByUserStore } from '../utils'
import { EmployeePresenter } from '../index'
export let employeeId: Ref<Employee>
const client = getClient()
const me = (getCurrentAccount() as PersonAccount).person
$: 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 hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
function onEdit () {
showPopup(
EmployeeSetStatusPopup,
{
currentStatus: status
},
undefined,
() => {},
(newStatus: Status) => {
if (status && newStatus) {
client.updateDoc(contact.class.Status, status.space, status._id, { ...newStatus })
} else if (status && !newStatus) {
client.removeDoc(contact.class.Status, status.space, status._id)
} else {
client.addCollection(contact.class.Status, employee.space, employeeId, contact.mixin.Employee, 'statuses', {
name: newStatus.name,
dueDate: newStatus.dueDate
})
}
}
)
dispatch('close')
let employee: Employee | undefined = undefined
$: employee = $employeeByIdStore.get(employeeId)
$: accounts = $personAccountByPersonId.get(employeeId) ?? []
$: isOnline = accounts.some((account) => $statusByUserStore.get(account._id)?.online === true)
// const statusesQuery = createQuery()
// let editable = false
// let status: Status | undefined = undefined
// $: editable = employeeId === me
// statusesQuery.query(contact.class.Status, { attachedTo: employeeId }, (res) => {
// status = res[0]
// })
// function setStatus (): void {
// if (!employee) return
// showPopup(
// EmployeeSetStatusPopup,
// {
// currentStatus: status
// },
// 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>
<div
class="antiPopup p-4 flex-col"
class="root flex-col"
use:resizeObserver={() => {
dispatch('changeContent')
}}
>
{#if employee}
<div class="flex-col-center pb-2">
<Avatar size={'x-large'} person={employee} name={employee.name} />
<div class="flex-presenter flex-gap-2 p-2">
<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 class="pb-2">{getName(client.getHierarchy(), employee)}</div>
<DocNavLink object={employee}>
<Label label={contact.string.ViewFullProfile} />
</DocNavLink>
{#if status}
<div class="pb-2">
<Label label={contact.string.Status} />
<div class="flex-row-stretch statusContainer">
<div class="pr-2">
<EmployeeStatusPresenter {employee} withTooltip={false} />
</div>
{#if editable}
<div class="setStatusButton">
<Button icon={Edit} title={contact.string.SetStatus} on:click={onEdit} />
</div>
{/if}
</div>
</div>
{:else if editable}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-row-stretch over-underline pb-2" on:click={onEdit}>
<Label label={contact.string.SetStatus} />
</div>
{/if}
<div class="separator" />
<div class="flex-presenter flex-gap-2 p-2">
<ComponentExtensions extension={contact.extension.EmployeePopupActions} props={{ employee }} />
<ModernButton
label={contact.string.ViewProfile}
icon={contact.icon.Person}
size="small"
iconSize="small"
on:click={viewProfile}
/>
</div>
<!--{#if status}-->
<!-- <div class="pb-2">-->
<!-- <Label label={contact.string.Status} />-->
<!-- <div class="flex-row-stretch statusContainer">-->
<!-- <div class="pr-2">-->
<!-- <EmployeeStatusPresenter {employee} withTooltip={false} />-->
<!-- </div>-->
<!-- {#if editable}-->
<!-- <div class="setStatusButton">-->
<!-- <Button icon={Edit} title={contact.string.SetStatus} on:click={setStatus} />-->
<!-- </div>-->
<!-- {/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}
</div>
<style lang="scss">
.statusContainer {
.setStatusButton {
opacity: 0;
}
&:hover .setStatusButton {
opacity: 1;
}
.root {
display: flex;
flex-direction: column;
width: auto;
min-height: 0;
min-width: 0;
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>

View File

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

View File

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

View File

@ -37,6 +37,7 @@ import core, {
type Doc,
type DocumentQuery,
getCurrentAccount,
groupByArray,
type Hierarchy,
type IdMap,
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]))
})
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 personByIdStore = derived([personAccountPersonByIdStore, employeeByIdStore], (vals) => {

View File

@ -31,7 +31,7 @@ import {
import type { Asset, Metadata, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
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'
/**
@ -301,7 +301,8 @@ export const contactPlugin = plugin(contactId, {
Members: '' as IntlString,
Contacts: '' as IntlString,
Employees: '' as IntlString,
Persons: '' as IntlString
Persons: '' as IntlString,
ViewProfile: '' as IntlString
},
viewlet: {
TableMember: '' as Ref<Viewlet>,
@ -332,6 +333,9 @@ export const contactPlugin = plugin(contactId, {
},
ids: {
MentionCommonNotificationType: '' as Ref<Doc>
},
extension: {
EmployeePopupActions: '' as ComponentExtensionId
}
})

View File

@ -127,7 +127,8 @@ function getDocumentLinkId (doc: Document): string {
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('---')
if (parts.length > 1) {
return parts[parts.length - 1] as Ref<ControlledDocument>

View File

@ -19,16 +19,21 @@ import { hljsDefineSvelte } from './languages/svelte-hljs'
hljs.registerLanguage('svelte', hljsDefineSvelte)
export interface HighlightOptions {
auto?: boolean
language: string | undefined
}
export function highlightText (text: string, options: HighlightOptions): string {
// We should always use highlighter because it sanitizes the input
// 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 { 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)
}

View File

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

View File

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

View File

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

View File

@ -20,12 +20,12 @@
import love from '../plugin'
import VideoPopup from './VideoPopup.svelte'
export let widgetState: WidgetState
export let widgetState: WidgetState | 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)
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,9 +33,7 @@
preferences = res
})
$: widgetId = $sidebarStore.widget
$: widget = widgets.find((it) => it._id === widgetId)
$: size = $sidebarStore.variant === SidebarVariant.MINI ? 'mini' : widget?.size
$: size = $sidebarStore.variant === SidebarVariant.MINI ? 'mini' : undefined
function txListener (tx: Tx): void {
if (tx._class === workbench.class.TxSidebarEvent) {
@ -79,17 +77,5 @@
min-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>

View File

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

View File

@ -76,9 +76,7 @@ export class DocumentsPage extends CalendarPage {
async changeSpaceInCreateDocumentForm (space: string): Promise<void> {
await this.changeSpaceButton.click()
await this.page
.locator(`div.list-container.flex-col.flex-grow.svelte-15na0wa >> text=${space}`)
.click({ force: true })
await this.page.locator(`div.selectPopup >> div.list-container >> text=${space}`).click({ force: true })
}
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,
integration: IntegrationContainer
): Promise<void> {
const { repository } = await this.provider.getProjectAndRepository(event.repository.node_id)
if (repository === undefined) {
const { repository: repo } = await this.provider.getProjectAndRepository(event.repository.node_id)
if (repo === undefined) {
this.ctx.info('No project for repository', {
repository: event.repository,
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
switch (event.action) {
case 'created': {
await this.createSyncData(event, derivedClient, repository)
await this.createSyncData(event, derivedClient, repo)
break
}
case 'deleted': {
const syncData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.comment.url ?? '').toLowerCase()
})
if (syncData !== undefined) {
@ -183,6 +184,7 @@ export class CommentSyncManager implements DocSyncManager {
}
case 'edited': {
const commentData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (event.comment.url ?? '').toLowerCase()
})
@ -221,6 +223,7 @@ export class CommentSyncManager implements DocSyncManager {
repo: GithubIntegrationRepository
): Promise<void> {
const commentData = await this.client.findOne(github.class.DocSyncInfo, {
space: repo.githubProject as Ref<GithubProject>,
url: (createdEvent.comment.url ?? '').toLowerCase()
})
@ -266,6 +269,7 @@ export class CommentSyncManager implements DocSyncManager {
if (parent === undefined) {
// Find parent by issue url
parent = await this.client.findOne(github.class.DocSyncInfo, {
space: container.project._id,
url: (comment.html_url.split('#')?.[0] ?? '').toLowerCase()
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1114,7 +1114,7 @@ export class GithubWorker implements IntegrationManager {
}
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 docs = await this.ctx.with(
@ -1126,7 +1126,7 @@ export class GithubWorker implements IntegrationManager {
{
needSync: { $ne: githubSyncVersion },
externalVersion: { $in: [githubExternalSyncVersion, '#'] },
space: { $in: _projects },
space: { $in: Array.from(_projects.keys()) },
repository: { $in: [null, ..._repositories] }
},
{
@ -1142,18 +1142,21 @@ export class GithubWorker implements IntegrationManager {
this.previousWait += docs.length
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
}
async doSyncFor (docs: DocSyncInfo[]): Promise<void> {
async doSyncFor (docs: DocSyncInfo[], project: GithubProject): Promise<void> {
const byClass = this.groupByClass(docs)
// We need to reorder based on our sync mappers
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, {
_id: { $in: syncInfo.map((it) => it._id as Ref<Doc>) }
})
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[] }
})

View File

@ -335,7 +335,7 @@ test.describe('Content in the Documents tests', () => {
await documentContentPage.applyToolbarCommand('Line 16', 'btnBlockquote')
await documentContentPage.applyToolbarCommand('Line 17', 'btnCode')
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.addImage('Line 20')
await page.keyboard.type('Cat')