mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 12:35:02 +02:00
Merge remote-tracking branch 'origin/develop' into staging-new
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Vendored
+1
@@ -617,6 +617,7 @@
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"sourceMaps": true,
|
||||
"nodeVersionHint": 22,
|
||||
"cwd": "${workspaceRoot}/services/github/pod-github",
|
||||
"protocol": "inspector",
|
||||
"outputCapture": "std"
|
||||
|
||||
+50
-3
@@ -1319,9 +1319,41 @@ async function migrateAccount (
|
||||
accountDB: AccountDB,
|
||||
dryRun = true
|
||||
): Promise<AccountUuid | undefined> {
|
||||
const primaryKey: SocialKey = {
|
||||
type: SocialIdType.EMAIL,
|
||||
value: account.email
|
||||
let primaryKey: SocialKey
|
||||
let secondaryKey: SocialKey | undefined
|
||||
|
||||
if (account.githubId != null) {
|
||||
if (account.githubUser == null) {
|
||||
console.log('No github user found for github id', account.githubId)
|
||||
return
|
||||
}
|
||||
|
||||
primaryKey = {
|
||||
type: SocialIdType.GITHUB,
|
||||
value: account.githubUser
|
||||
}
|
||||
secondaryKey = !account.email.startsWith('github:')
|
||||
? {
|
||||
type: SocialIdType.EMAIL,
|
||||
value: account.email
|
||||
}
|
||||
: undefined
|
||||
} else if (account.openId != null) {
|
||||
primaryKey = {
|
||||
type: SocialIdType.OIDC,
|
||||
value: account.openId
|
||||
}
|
||||
secondaryKey = !account.email.startsWith('openid:')
|
||||
? {
|
||||
type: SocialIdType.EMAIL,
|
||||
value: account.email
|
||||
}
|
||||
: undefined
|
||||
} else {
|
||||
primaryKey = {
|
||||
type: SocialIdType.EMAIL,
|
||||
value: account.email
|
||||
}
|
||||
}
|
||||
|
||||
let personUuid: PersonUuid
|
||||
@@ -1389,6 +1421,21 @@ async function migrateAccount (
|
||||
}
|
||||
}
|
||||
|
||||
if (secondaryKey != null) {
|
||||
const existingSecondary = await accountDB.socialId.findOne(secondaryKey)
|
||||
if (existingSecondary == null) {
|
||||
if (!dryRun) {
|
||||
await accountDB.socialId.insertOne({
|
||||
...secondaryKey,
|
||||
personUuid,
|
||||
...verified
|
||||
})
|
||||
} else {
|
||||
console.log('Creating secondary social id', { personUuid, confirmed: account.confirmed, secondaryKey })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return personUuid as AccountUuid
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export function createModel (builder: Builder): void {
|
||||
icon: aiAssistant.component.IconHulyAssistant,
|
||||
allowMultiple: false,
|
||||
createComponent: aiAssistant.component.Connect,
|
||||
configureComponent: aiAssistant.component.Connect,
|
||||
onDisconnect: aiAssistant.handler.DisconnectHandler,
|
||||
onDisconnectAll: aiAssistant.handler.DisconnectAllHandler,
|
||||
reconnectComponent: aiAssistant.component.Connect,
|
||||
|
||||
@@ -116,9 +116,6 @@
|
||||
{ type: communication.type.Direct, order: 2 }
|
||||
],
|
||||
fixedTypes: [chat.masterTag.Thread, communication.type.Direct],
|
||||
specialSorting: {
|
||||
[communication.type.Direct]: 'alphabetical'
|
||||
},
|
||||
allowCreate: true,
|
||||
defaultSorting: 'recent',
|
||||
lookback: '2w',
|
||||
|
||||
@@ -72,7 +72,7 @@ class RequestPromise {
|
||||
reject!: (reason?: any) => void
|
||||
reconnect?: () => void
|
||||
|
||||
// Required to proeprly handle rate limits
|
||||
// Required to properly handle rate limits
|
||||
sendData: () => void = () => {}
|
||||
|
||||
constructor (
|
||||
@@ -92,6 +92,11 @@ class RequestPromise {
|
||||
|
||||
const globalRPCHandler: RPCHandler = new RPCHandler()
|
||||
|
||||
interface OnConnectHandler {
|
||||
resolve: () => void
|
||||
reject: (err: Error) => void
|
||||
}
|
||||
|
||||
class Connection implements ClientConnection {
|
||||
private websocket: ClientSocket | null = null
|
||||
binaryMode = false
|
||||
@@ -99,7 +104,7 @@ class Connection implements ClientConnection {
|
||||
private readonly requests = new Map<ReqId, RequestPromise>()
|
||||
private lastId = 0
|
||||
private interval: number | undefined
|
||||
private dialTimer: any | undefined
|
||||
private dialTimer: number | undefined
|
||||
|
||||
private sockets = 0
|
||||
private openAction: any
|
||||
@@ -166,12 +171,13 @@ class Connection implements ClientConnection {
|
||||
}
|
||||
|
||||
private schedulePing (socketId: number): void {
|
||||
clearInterval(this.interval)
|
||||
this.pingResponse = Date.now()
|
||||
const wsocket = this.websocket
|
||||
const interval = setInterval(() => {
|
||||
|
||||
clearInterval(this.interval)
|
||||
this.interval = setInterval(() => {
|
||||
if (wsocket !== this.websocket) {
|
||||
clearInterval(interval)
|
||||
clearInterval(this.interval)
|
||||
return
|
||||
}
|
||||
if (!this.upgrading && this.pingResponse !== 0 && Date.now() - this.pingResponse > hangTimeout) {
|
||||
@@ -203,7 +209,6 @@ class Connection implements ClientConnection {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
}, pingTimeout)
|
||||
this.interval = interval
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
@@ -211,6 +216,12 @@ class Connection implements ClientConnection {
|
||||
clearTimeout(this.openAction)
|
||||
clearTimeout(this.dialTimer)
|
||||
clearInterval(this.interval)
|
||||
for (const handler of this.onConnectHandlers) {
|
||||
handler.reject(new Error('Connection closed'))
|
||||
}
|
||||
for (const req of this.requests.values()) {
|
||||
req.reject(new Error('Connection closed'))
|
||||
}
|
||||
if (this.websocket !== null) {
|
||||
this.websocket.close(1000)
|
||||
this.websocket = null
|
||||
@@ -222,7 +233,7 @@ class Connection implements ClientConnection {
|
||||
}
|
||||
|
||||
delay = 0
|
||||
onConnectHandlers: (() => void)[] = []
|
||||
onConnectHandlers: OnConnectHandler[] = []
|
||||
|
||||
private waitOpenConnection (ctx: MeasureContext): Promise<void> | undefined {
|
||||
if (this.isConnected()) {
|
||||
@@ -233,9 +244,10 @@ class Connection implements ClientConnection {
|
||||
'wait-connection',
|
||||
{},
|
||||
(ctx) =>
|
||||
new Promise((resolve) => {
|
||||
this.onConnectHandlers.push(() => {
|
||||
resolve()
|
||||
new Promise((resolve, reject) => {
|
||||
this.onConnectHandlers.push({
|
||||
resolve,
|
||||
reject
|
||||
})
|
||||
// Websocket is null for first time
|
||||
this.scheduleOpen(ctx, false)
|
||||
@@ -294,7 +306,7 @@ class Connection implements ClientConnection {
|
||||
if (resp.error !== undefined) {
|
||||
if (resp.error?.code === UNAUTHORIZED.code || resp.terminate === true) {
|
||||
if (
|
||||
resp.error.code !== platform.status.WorkspaceArchived ||
|
||||
resp.error.code !== platform.status.WorkspaceArchived &&
|
||||
resp.error.code !== platform.status.WorkspaceNotFound
|
||||
) {
|
||||
Analytics.handleError(new PlatformError(resp.error))
|
||||
@@ -350,7 +362,7 @@ class Connection implements ClientConnection {
|
||||
|
||||
// We need to clear dial timer, since we recieve hello response.
|
||||
clearTimeout(this.dialTimer)
|
||||
this.dialTimer = null
|
||||
this.dialTimer = undefined
|
||||
this.lastHash = (resp as HelloResponse).lastHash
|
||||
|
||||
const serverVersion = helloResp.serverVersion
|
||||
@@ -374,7 +386,7 @@ class Connection implements ClientConnection {
|
||||
// Notify all waiting connection listeners
|
||||
const handlers = this.onConnectHandlers.splice(0, this.onConnectHandlers.length)
|
||||
for (const h of handlers) {
|
||||
h()
|
||||
h.resolve()
|
||||
}
|
||||
|
||||
for (const [, v] of this.requests.entries()) {
|
||||
@@ -540,12 +552,10 @@ class Connection implements ClientConnection {
|
||||
return
|
||||
}
|
||||
this.websocket = wsocket
|
||||
const opened = false
|
||||
|
||||
if (this.dialTimer != null) {
|
||||
if (this.dialTimer === undefined) {
|
||||
this.dialTimer = setTimeout(() => {
|
||||
this.dialTimer = null
|
||||
if (!opened && !this.closed) {
|
||||
this.dialTimer = undefined
|
||||
if (!this.closed) {
|
||||
void this.opt?.onDialTimeout?.()?.catch((err) => {
|
||||
this.ctx.error('failed to handle dial timeout', { err })
|
||||
})
|
||||
@@ -652,16 +662,15 @@ class Connection implements ClientConnection {
|
||||
ctx.withSync('send-hello', {}, () => this.websocket?.send(this.rpcHandler.serialize(helloRequest, false)))
|
||||
}
|
||||
|
||||
wsocket.onerror = (event: any) => {
|
||||
// FIX: remove undefined variable 'opened'
|
||||
wsocket.onerror = () => {
|
||||
if (this.websocket !== wsocket) {
|
||||
return
|
||||
}
|
||||
if (this.delay < 3) {
|
||||
this.delay += 1
|
||||
}
|
||||
if (opened) {
|
||||
console.error('client websocket error:', socketId, this.url, this.workspace, this.user)
|
||||
}
|
||||
console.error('client websocket error:', socketId, this.url, this.workspace, this.user)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,7 +770,7 @@ class Connection implements ClientConnection {
|
||||
|
||||
getAccount (): Promise<Account> {
|
||||
if (this.account !== undefined) {
|
||||
return clone(this.account)
|
||||
return Promise.resolve(clone(this.account))
|
||||
}
|
||||
return this.sendRequest({ method: 'getAccount', params: [] })
|
||||
}
|
||||
|
||||
+36
-46
@@ -25,7 +25,8 @@
|
||||
TableOfContents,
|
||||
TableOfContentsContent,
|
||||
getNodeElement,
|
||||
highlightUpdateCommand
|
||||
highlightUpdateCommand,
|
||||
selectNode
|
||||
} from '@hcengineering/text-editor-resources'
|
||||
import { EditBox, Label, Scroller } from '@hcengineering/ui'
|
||||
import { getCollaborationUser } from '@hcengineering/view-resources'
|
||||
@@ -41,8 +42,8 @@
|
||||
$documentCommentHighlightedLocation as documentCommentHighlightedLocation,
|
||||
$documentComments as documentComments,
|
||||
documentCommentsDisplayRequested,
|
||||
documentCommentsHighlightUpdated,
|
||||
documentCommentsLocationNavigateRequested,
|
||||
documentCommentsAddCanceled,
|
||||
$isEditable as isEditable
|
||||
} from '../../stores/editors/document'
|
||||
import DocumentPrintTitlePage from '../print/DocumentPrintTitlePage.svelte'
|
||||
@@ -58,19 +59,16 @@
|
||||
let headings: Heading[] = []
|
||||
let textEditor: CollaboratorEditor
|
||||
let selectedNodeId: string | null | undefined = undefined
|
||||
let isFocused = false
|
||||
let editor: Editor
|
||||
let title = $controlledDocument?.title ?? ''
|
||||
|
||||
$: isTemplate =
|
||||
$controlledDocument != null && hierarchy.hasMixin($controlledDocument, documents.mixin.DocumentTemplate)
|
||||
|
||||
function handleRefreshHighlight () {
|
||||
if (!textEditor) {
|
||||
return
|
||||
}
|
||||
$: commentUuids = $documentComments.map((p) => p.nodeId).filter((id) => id != null)
|
||||
|
||||
textEditor.commands()?.command(highlightUpdateCommand())
|
||||
function handleRefreshHighlight (): void {
|
||||
textEditor?.commands()?.command(highlightUpdateCommand())
|
||||
}
|
||||
|
||||
const unsubscribeHighlightRefresh = merge([documentCommentHighlightedLocation, documentComments.updates]).subscribe({
|
||||
@@ -82,21 +80,28 @@
|
||||
const unsubscribeNavigateToLocation = documentCommentsLocationNavigateRequested.subscribe({
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
next: async ({ nodeId }) => {
|
||||
if (!nodeId) {
|
||||
if (nodeId == null) {
|
||||
handleRefreshHighlight()
|
||||
return
|
||||
}
|
||||
|
||||
if (!textEditor) {
|
||||
return
|
||||
selectedNodeId = nodeId
|
||||
|
||||
if (editor !== undefined) {
|
||||
await tick()
|
||||
|
||||
const element = getNodeElement(editor, nodeId)
|
||||
element?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await tick()
|
||||
|
||||
const element = getNodeElement(editor, nodeId)
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
const unsubscribeCommentsAddCanceled = documentCommentsAddCanceled.subscribe({
|
||||
next: ({ nodeId }) => {
|
||||
if (editor !== undefined && nodeId != null) {
|
||||
if (selectNode(editor, nodeId)) {
|
||||
editor.commands.unsetQMSInlineCommentMark()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -104,6 +109,7 @@
|
||||
onDestroy(() => {
|
||||
unsubscribeHighlightRefresh()
|
||||
unsubscribeNavigateToLocation()
|
||||
unsubscribeCommentsAddCanceled()
|
||||
})
|
||||
|
||||
const handleUpdateTitle = async () => {
|
||||
@@ -137,15 +143,9 @@
|
||||
return null
|
||||
}
|
||||
|
||||
function handleShowDocumentComments (uuid: string) {
|
||||
if (!uuid) {
|
||||
return
|
||||
}
|
||||
|
||||
documentCommentsDisplayRequested({
|
||||
element: getNodeElement(editor, uuid),
|
||||
nodeId: uuid
|
||||
})
|
||||
function handleShowDocumentComments (nodeId: string): void {
|
||||
const element = getNodeElement(editor, nodeId)
|
||||
documentCommentsDisplayRequested({ element, nodeId })
|
||||
}
|
||||
|
||||
async function createEmbedding (file: File): Promise<{ file: Ref<Blob>, type: string } | undefined> {
|
||||
@@ -241,32 +241,22 @@
|
||||
qmsInlineComment: {
|
||||
isHighlightModeOn: () => $canViewDocumentComments || $canAddDocumentComments,
|
||||
getNodeHighlight: handleNodeHighlight,
|
||||
onNodeSelected: (uuid) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
if (isFocused) {
|
||||
documentCommentsHighlightUpdated(selectedNodeId !== null ? { nodeId: selectedNodeId } : null)
|
||||
}
|
||||
},
|
||||
onNodeClicked: (uuid) => {
|
||||
if (selectedNodeId !== uuid) {
|
||||
selectedNodeId = uuid
|
||||
}
|
||||
onNodeClicked: (uuids) => {
|
||||
// filter out those uuids that are not in comments
|
||||
uuids = Array.isArray(uuids) ? uuids : [uuids]
|
||||
uuids = uuids.filter((id) => commentUuids.includes(id)).sort()
|
||||
|
||||
if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId) {
|
||||
// scroll through the comments as user clicks on the same node
|
||||
const currIndex = selectedNodeId != null ? uuids.indexOf(selectedNodeId) : -1
|
||||
const nextIndex = currIndex === -1 ? 0 : (currIndex + 1) % uuids.length
|
||||
selectedNodeId = uuids[nextIndex]
|
||||
|
||||
if (!$arePopupsOpened && $canViewDocumentComments && selectedNodeId != null) {
|
||||
handleShowDocumentComments(selectedNodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
focus: {
|
||||
onFocus: (focused) => {
|
||||
isFocused = focused
|
||||
}
|
||||
}
|
||||
},
|
||||
toc: {
|
||||
onChange: (h) => {
|
||||
headings = h
|
||||
|
||||
+16
-4
@@ -11,17 +11,29 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let messageId: Ref<ChatMessage> = generateId()
|
||||
|
||||
async function handleMessage (event: CustomEvent<string>): Promise<void> {
|
||||
const messageId: Ref<ChatMessage> = generateId()
|
||||
const comment = await addDocumentCommentFx({ content: event.detail, messageId, nodeId })
|
||||
messageId = generateId()
|
||||
|
||||
dispatch('close', comment)
|
||||
}
|
||||
|
||||
let popup: HTMLDivElement | undefined
|
||||
|
||||
function handleClick (event: MouseEvent): void {
|
||||
if (event.target instanceof Node) {
|
||||
if (popup !== undefined && !popup.contains(event.target)) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch('close', undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-editor-popup w-85">
|
||||
<svelte:window on:click|capture={handleClick} />
|
||||
|
||||
<div class="text-editor-popup w-85" bind:this={popup}>
|
||||
<ReferenceInput
|
||||
autofocus
|
||||
focusable
|
||||
|
||||
@@ -96,6 +96,10 @@ export const savedAttachmentsUpdated = createEvent<Array<Ref<Attachment>>>(
|
||||
generateActionName('savedAttachmentsUpdated')
|
||||
)
|
||||
|
||||
export const documentCommentsAddCanceled = createEvent<{
|
||||
nodeId?: string | null
|
||||
}>(generateActionName('documentCommentsAddCanceled'))
|
||||
|
||||
export const documentCommentsDisplayRequested = createEvent<{
|
||||
nodeId?: string | null
|
||||
element?: PopupAlignment
|
||||
|
||||
+6
-2
@@ -19,9 +19,10 @@ import { type CompAndProps, type PopupAlignment, popupstore, showPopup } from '@
|
||||
import documents, { type Document, type DocumentComment } from '@hcengineering/controlled-documents'
|
||||
import { isDocumentCommentAttachedTo } from '../../../utils'
|
||||
import {
|
||||
DocumentCommentPopupCategory,
|
||||
type DocumentCommentsFilter,
|
||||
DocumentCommentPopupCategory,
|
||||
documentCommentPopupsOpened,
|
||||
documentCommentsAddCanceled,
|
||||
documentCommentsDisplayRequested,
|
||||
documentCommentsHighlightCleared,
|
||||
documentCommentsHighlightUpdated,
|
||||
@@ -31,7 +32,8 @@ import {
|
||||
documentCommentsSortByChanged,
|
||||
documentCommentsUpdated,
|
||||
controlledDocumentClosed,
|
||||
savedAttachmentsUpdated
|
||||
savedAttachmentsUpdated,
|
||||
controlledDocumentOpened
|
||||
} from './actions'
|
||||
|
||||
export const $areDocumentCommentPopupsOpened = createStore(false).on(
|
||||
@@ -120,6 +122,7 @@ export const showAddCommentPopupFx = createEffect((payload: { element?: PopupAli
|
||||
payload.element,
|
||||
(result) => {
|
||||
if (result === null || result === undefined) {
|
||||
documentCommentsAddCanceled({ nodeId: payload.nodeId })
|
||||
documentCommentsHighlightCleared()
|
||||
} else {
|
||||
documentCommentsDisplayRequested(payload)
|
||||
@@ -187,4 +190,5 @@ export const $savedAttachments = createStore<Array<Ref<Attachment>>>([])
|
||||
.on(savedAttachmentsUpdated, (_, payload) => payload)
|
||||
.reset(controlledDocumentClosed)
|
||||
|
||||
forward({ from: controlledDocumentOpened, to: documentCommentsHighlightCleared })
|
||||
forward({ from: documentCommentsLocationNavigateRequested, to: documentCommentsHighlightUpdated })
|
||||
|
||||
@@ -24,7 +24,7 @@ import { getCurrentEmployee } from '@hcengineering/contact'
|
||||
import { RequestStatus } from '@hcengineering/request'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { type ActionContext } from '@hcengineering/text-editor'
|
||||
import { getNodeElement, selectNode, nodeUuidName } from '@hcengineering/text-editor-resources'
|
||||
import { getNodeElement, selectNode } from '@hcengineering/text-editor-resources'
|
||||
|
||||
import { showAddCommentPopupFx } from './stores/editors/document'
|
||||
import { $editorMode } from './stores/editors/document/editor'
|
||||
@@ -127,39 +127,20 @@ async function canAddDocumentComments (doc: ControlledDocument, mode: EditorMode
|
||||
return false
|
||||
}
|
||||
|
||||
function setQMSInlineCommentMark (editor: Editor): string | undefined {
|
||||
if (editor === undefined) {
|
||||
export async function comment (editor: Editor, event: MouseEvent, ctx: ActionContext): Promise<void> {
|
||||
const { objectId, objectClass } = ctx
|
||||
|
||||
if (editor === undefined || objectId === undefined || objectClass === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const nodeId = generateId()
|
||||
editor.commands.setQMSInlineCommentMark(nodeId)
|
||||
|
||||
return nodeId
|
||||
}
|
||||
const element = getNodeElement(editor, nodeId)
|
||||
await showAddCommentPopupFx({ element, nodeId })
|
||||
|
||||
export async function comment (editor: Editor, event: MouseEvent, ctx: ActionContext): Promise<void> {
|
||||
const { objectId, objectClass } = ctx
|
||||
if (objectId === undefined || objectClass === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let selectedNodeId = editor.extensionStorage[nodeUuidName].activeNodeUuid
|
||||
|
||||
if (selectedNodeId == null) {
|
||||
selectedNodeId = setQMSInlineCommentMark(editor)
|
||||
}
|
||||
|
||||
if (selectedNodeId == null) {
|
||||
return
|
||||
}
|
||||
|
||||
await showAddCommentPopupFx({
|
||||
element: getNodeElement(editor, selectedNodeId),
|
||||
nodeId: selectedNodeId
|
||||
})
|
||||
|
||||
selectNode(editor, selectedNodeId)
|
||||
selectNode(editor, nodeId)
|
||||
}
|
||||
|
||||
export async function isCommentVisible (editor: Editor, ctx: ActionContext): Promise<boolean> {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
export let context: Context
|
||||
|
||||
const client = getClient()
|
||||
const func = client.getModel().findObject(contextValue.func)
|
||||
$: func = client.getModel().findObject(contextValue.func)
|
||||
</script>
|
||||
|
||||
{#if func !== undefined}
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Process, State } from '@hcengineering/process'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { Button, IconAdd, Label } from '@hcengineering/ui'
|
||||
import { SortableDocList } from '@hcengineering/view-resources'
|
||||
import plugin from '../../plugin'
|
||||
import StateInlineEditor from './StateInlineEditor.svelte'
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
import { SortableList } from '@hcengineering/view-resources'
|
||||
|
||||
export let process: Process
|
||||
export let states: State[]
|
||||
@@ -49,11 +49,11 @@
|
||||
<div class="header w-full p-4">
|
||||
<Label label={plugin.string.States} />
|
||||
</div>
|
||||
<SortableList _class={plugin.class.State} query={{ process: process._id }} isAddButtonHidden>
|
||||
<SortableDocList _class={plugin.class.State} query={{ process: process._id }}>
|
||||
<svelte:fragment slot="object" let:value>
|
||||
<StateInlineEditor value={toState(value)} />
|
||||
</svelte:fragment>
|
||||
</SortableList>
|
||||
</SortableDocList>
|
||||
{#if !readonly}
|
||||
<Button kind={'ghost'} width={'100%'} icon={IconAdd} label={plugin.string.AddState} on:click={addState} />
|
||||
{/if}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
import Navigator from './Navigator.svelte'
|
||||
import TransitionPresenter from './TransitionPresenter.svelte'
|
||||
import TriggerPresenter from './TriggerPresenter.svelte'
|
||||
import { SortableList } from '@hcengineering/view-resources'
|
||||
|
||||
export let _id: Ref<Transition>
|
||||
export let visibleSecondNav: boolean = true
|
||||
@@ -136,6 +137,11 @@
|
||||
function edit (): void {
|
||||
$settingsStore = { id: _id, component: AsideTransitionEditor, props: { process, transition: value } }
|
||||
}
|
||||
|
||||
async function moveHadler (): Promise<void> {
|
||||
if (value === undefined) return
|
||||
await client.update(value, { actions: value.actions })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent-content__container columns">
|
||||
@@ -169,20 +175,22 @@
|
||||
<div class="label w-full p-4">
|
||||
<Label label={plugin.string.Actions} />
|
||||
</div>
|
||||
{#each value.actions as action}
|
||||
<Button
|
||||
justify="left"
|
||||
kind="ghost"
|
||||
width="100%"
|
||||
on:click={() => {
|
||||
editAction(action)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<ActionPresenter {action} {process} readonly={false} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/each}
|
||||
<SortableList bind:items={value.actions} on:move={moveHadler}>
|
||||
<svelte:fragment slot="object" let:value>
|
||||
<Button
|
||||
justify="left"
|
||||
kind="ghost"
|
||||
width="100%"
|
||||
on:click={() => {
|
||||
editAction(value)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<ActionPresenter action={value} {process} readonly={false} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</SortableList>
|
||||
<Button
|
||||
kind={'ghost'}
|
||||
width={'100%'}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import AddTransitionPopup from './AddTransitionPopup.svelte'
|
||||
import TransitionPresenter from './TransitionPresenter.svelte'
|
||||
import TriggerPresenter from './TriggerPresenter.svelte'
|
||||
import { SortableList } from '@hcengineering/view-resources'
|
||||
import { SortableDocList } from '@hcengineering/view-resources'
|
||||
|
||||
export let process: Process
|
||||
export let readonly: boolean
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="header w-full p-4">
|
||||
<Label label={plugin.string.Transitions} />
|
||||
</div>
|
||||
<SortableList _class={plugin.class.Transition} query={{ process: process._id }} isAddButtonHidden>
|
||||
<SortableDocList _class={plugin.class.Transition} query={{ process: process._id }}>
|
||||
<svelte:fragment slot="object" let:value>
|
||||
{@const transition = toTransition(value)}
|
||||
<Button
|
||||
@@ -66,7 +66,7 @@
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</SortableList>
|
||||
</SortableDocList>
|
||||
{#if !readonly}
|
||||
<Button kind={'ghost'} width={'100%'} icon={IconAdd} label={plugin.string.AddTransition} on:click={addTransition} />
|
||||
{/if}
|
||||
|
||||
@@ -22,7 +22,8 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
import {
|
||||
QMSInlineCommentMark,
|
||||
type QMSInlineCommentMarkOptions,
|
||||
findQMSInlineCommentMark
|
||||
findQMSInlineCommentMark,
|
||||
getMarkUuid
|
||||
} from './qmsInlineCommentMark'
|
||||
|
||||
export enum CommentHighlightType {
|
||||
@@ -233,24 +234,24 @@ const createDecorations = (
|
||||
): DecorationSet => {
|
||||
const decorations: Decoration[] = []
|
||||
|
||||
doc.descendants((node, pos) => {
|
||||
const qmsInlineCommentMark = findQMSInlineCommentMark(node)
|
||||
doc.descendants((descendant, pos) => {
|
||||
descendant.marks.forEach((mark) => {
|
||||
const uuid = getMarkUuid(mark)
|
||||
if (uuid !== undefined) {
|
||||
const attributes = generateAttributes(uuid, options)
|
||||
if (attributes === null || attributes === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (qmsInlineCommentMark !== null && qmsInlineCommentMark !== undefined) {
|
||||
const nodeUuid = qmsInlineCommentMark.attrs[QMSInlineCommentMark.name]
|
||||
const attributes = generateAttributes(nodeUuid, options)
|
||||
if (attributes === null || attributes === undefined) {
|
||||
return
|
||||
// the first pos does not contain the mark, so we need to add 1 (pos + 1) to get the correct range
|
||||
const range = getMarkRange(doc.resolve(pos + 1), markType, mark.attrs)
|
||||
if (!isRange(range)) {
|
||||
return
|
||||
}
|
||||
|
||||
decorations.push(Decoration.inline(range.from, range.to, attributes))
|
||||
}
|
||||
|
||||
// the first pos does not contain the mark, so we need to add 1 (pos + 1) to get the correct range
|
||||
const range = getMarkRange(doc.resolve(pos + 1), markType, qmsInlineCommentMark.attrs)
|
||||
if (!isRange(range)) {
|
||||
return
|
||||
}
|
||||
|
||||
decorations.push(Decoration.inline(range.from, range.to, attributes))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return DecorationSet.create(doc, decorations)
|
||||
|
||||
+89
-88
@@ -13,25 +13,16 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import {
|
||||
type CommandProps,
|
||||
type Editor,
|
||||
Mark,
|
||||
getMarkRange,
|
||||
getMarkType,
|
||||
getMarksBetween,
|
||||
mergeAttributes
|
||||
} from '@tiptap/core'
|
||||
import { type Node, type Mark as ProseMirrorMark } from '@tiptap/pm/model'
|
||||
import { type EditorState, Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'
|
||||
import { type CommandProps, type Editor, Mark, getMarkRange, mergeAttributes } from '@tiptap/core'
|
||||
import { type Node, type Mark as ProseMirrorMark, Fragment, Slice } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'
|
||||
|
||||
export const qmsInlineCommentMarkName = 'node-uuid'
|
||||
export const nodeElementQuerySelector = (nodeUuid: string): string => `span[${qmsInlineCommentMarkName}='${nodeUuid}']`
|
||||
|
||||
export interface QMSInlineCommentMarkOptions {
|
||||
HTMLAttributes: Record<string, any>
|
||||
onNodeSelected?: (uuid: string | null) => void
|
||||
onNodeClicked?: (uuid: string) => void
|
||||
onNodeClicked?: (uuid: string | string[]) => void
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
@@ -43,32 +34,6 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
export interface QMSInlineCommentMarkStorage {
|
||||
activeQMSInlineComment: string | null
|
||||
}
|
||||
|
||||
const findSelectionQMSInlineCommentMark = (state: EditorState): ProseMirrorMark | undefined => {
|
||||
const { doc, selection } = state
|
||||
|
||||
if (selection === null || selection === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
let nodeUuidMark: ProseMirrorMark | undefined
|
||||
for (const range of selection.ranges) {
|
||||
if (nodeUuidMark === undefined) {
|
||||
doc.nodesBetween(range.$from.pos, range.$to.pos, (node) => {
|
||||
if (nodeUuidMark !== undefined) {
|
||||
return false
|
||||
}
|
||||
nodeUuidMark = findQMSInlineCommentMark(node)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return nodeUuidMark
|
||||
}
|
||||
|
||||
export const findQMSInlineCommentMark = (node: Node): ProseMirrorMark | undefined => {
|
||||
if (node === null || node === undefined) {
|
||||
return
|
||||
@@ -85,13 +50,14 @@ export function getNodeElement (editor: Editor, uuid: string): Element | null {
|
||||
return editor.view.dom.querySelector(nodeElementQuerySelector(uuid))
|
||||
}
|
||||
|
||||
export function selectNode (editor: Editor, uuid: string): void {
|
||||
export function selectNode (editor: Editor, uuid: string): boolean {
|
||||
if (editor === undefined) {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
const { doc, schema, tr } = editor.view.state
|
||||
let foundNode = false
|
||||
|
||||
const { doc, schema, tr } = editor.view.state
|
||||
doc.descendants((node, pos) => {
|
||||
if (foundNode) {
|
||||
return false
|
||||
@@ -118,12 +84,17 @@ export function selectNode (editor: Editor, uuid: string): void {
|
||||
editor?.view.dispatch(tr.setSelection(new TextSelection($start, $end)))
|
||||
editor.commands.focus()
|
||||
})
|
||||
|
||||
return foundNode
|
||||
}
|
||||
|
||||
export const QMSInlineCommentMark = Mark.create<QMSInlineCommentMarkOptions, QMSInlineCommentMarkStorage>({
|
||||
export const QMSInlineCommentMark = Mark.create<QMSInlineCommentMarkOptions>({
|
||||
name: qmsInlineCommentMarkName,
|
||||
exitable: true,
|
||||
inclusive: false,
|
||||
// set to empty string to allow multiple marks of the same type
|
||||
// https://prosemirror.net/docs/ref/#model.MarkSpec.excludes
|
||||
excludes: '',
|
||||
addOptions () {
|
||||
return {
|
||||
HTMLAttributes: {}
|
||||
@@ -161,37 +132,28 @@ export const QMSInlineCommentMark = Mark.create<QMSInlineCommentMarkOptions, QMS
|
||||
|
||||
addProseMirrorPlugins () {
|
||||
const options = this.options
|
||||
const storage: QMSInlineCommentMarkStorage = this.storage
|
||||
|
||||
const plugins = [
|
||||
...(this.parent?.() ?? []),
|
||||
new Plugin({
|
||||
key: new PluginKey('handle-qms-inline-comment-click-plugin'),
|
||||
props: {
|
||||
handleClick (view, pos) {
|
||||
const from = Math.max(0, pos - 1)
|
||||
const to = Math.min(view.state.doc.content.size, pos + 1)
|
||||
const markRanges =
|
||||
getMarksBetween(from, to, view.state.doc)?.filter(
|
||||
(markRange) =>
|
||||
markRange.mark.type.name === qmsInlineCommentMarkName && markRange.from <= pos && markRange.to >= pos
|
||||
) ?? []
|
||||
let nodeUuid: string | null = null
|
||||
const marks = view.state.selection.$head
|
||||
.marks()
|
||||
.filter((mark) => mark.type.name === qmsInlineCommentMarkName)
|
||||
|
||||
if (markRanges.length > 0) {
|
||||
nodeUuid = markRanges[0].mark.attrs[qmsInlineCommentMarkName]
|
||||
}
|
||||
const uuids = marks
|
||||
.map((mark) => mark.attrs[qmsInlineCommentMarkName])
|
||||
.filter((uuid) => uuid !== null && uuid !== undefined && uuid.length > 0)
|
||||
|
||||
if (nodeUuid !== null) {
|
||||
options.onNodeClicked?.(nodeUuid)
|
||||
}
|
||||
|
||||
if (storage.activeQMSInlineComment !== nodeUuid) {
|
||||
storage.activeQMSInlineComment = nodeUuid
|
||||
options.onNodeSelected?.(storage.activeQMSInlineComment)
|
||||
if (uuids.length !== 0) {
|
||||
options.onNodeClicked?.(uuids)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
QmsInlineCommentPastePlugin()
|
||||
]
|
||||
|
||||
return plugins
|
||||
@@ -202,14 +164,9 @@ export const QMSInlineCommentMark = Mark.create<QMSInlineCommentMarkOptions, QMS
|
||||
setQMSInlineCommentMark:
|
||||
(uuid: string) =>
|
||||
({ commands, state }: CommandProps) => {
|
||||
const { doc, selection } = state
|
||||
if (selection.empty) {
|
||||
if (state.selection.empty) {
|
||||
return false
|
||||
}
|
||||
if (doc.rangeHasMark(selection.from, selection.to, getMarkType(qmsInlineCommentMarkName, state.schema))) {
|
||||
return false
|
||||
}
|
||||
|
||||
return commands.setMark(this.name, { [qmsInlineCommentMarkName]: uuid })
|
||||
},
|
||||
unsetQMSInlineCommentMark:
|
||||
@@ -217,24 +174,68 @@ export const QMSInlineCommentMark = Mark.create<QMSInlineCommentMarkOptions, QMS
|
||||
({ commands }: CommandProps) =>
|
||||
commands.unsetMark(this.name)
|
||||
}
|
||||
},
|
||||
|
||||
addStorage () {
|
||||
return {
|
||||
activeQMSInlineComment: null
|
||||
}
|
||||
},
|
||||
|
||||
onSelectionUpdate () {
|
||||
const activeQMSInlineCommentMark = findSelectionQMSInlineCommentMark(this.editor.state)
|
||||
const activeQMSInlineComment =
|
||||
activeQMSInlineCommentMark !== null && activeQMSInlineCommentMark !== undefined
|
||||
? activeQMSInlineCommentMark.attrs[qmsInlineCommentMarkName]
|
||||
: null
|
||||
|
||||
if (this.storage.activeQMSInlineComment !== activeQMSInlineComment) {
|
||||
this.storage.activeQMSInlineComment = activeQMSInlineComment
|
||||
this.options.onNodeSelected?.(this.storage.activeQMSInlineComment)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function removeMarkFromNode (node: Node, name: string): Node {
|
||||
if (node.isText) {
|
||||
return node.mark(node.marks.filter((mark) => mark.type.name !== name))
|
||||
}
|
||||
|
||||
if (node.content.size > 0) {
|
||||
const nodes: Node[] = []
|
||||
node.content.forEach((child) => {
|
||||
nodes.push(removeMarkFromNode(child, name))
|
||||
})
|
||||
return node.copy(Fragment.fromArray(nodes))
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
export function getMarkUuid (mark: ProseMirrorMark): string | undefined {
|
||||
return mark.type.name === qmsInlineCommentMarkName ? mark.attrs[qmsInlineCommentMarkName] : undefined
|
||||
}
|
||||
|
||||
export function QmsInlineCommentPastePlugin (): Plugin {
|
||||
return new Plugin({
|
||||
key: new PluginKey('qms-inline-comment-paste-plugin'),
|
||||
props: {
|
||||
transformPasted: (slice, view) => {
|
||||
const pastedUuids = new Set<string>()
|
||||
slice.content.forEach((node) => {
|
||||
node.descendants((descendant) => {
|
||||
descendant.marks.forEach((mark) => {
|
||||
const uuid = getMarkUuid(mark)
|
||||
if (uuid !== undefined) {
|
||||
pastedUuids.add(uuid)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
let hasDuplicatedUuids = false
|
||||
view.state.doc.descendants((node) => {
|
||||
if (hasDuplicatedUuids) return false
|
||||
for (const mark of node.marks) {
|
||||
const uuid = getMarkUuid(mark)
|
||||
if (uuid !== undefined && pastedUuids.has(uuid)) {
|
||||
hasDuplicatedUuids = true
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (hasDuplicatedUuids) {
|
||||
const nodes: Node[] = []
|
||||
slice.content.forEach((node) => {
|
||||
nodes.push(removeMarkFromNode(node, qmsInlineCommentMarkName))
|
||||
})
|
||||
return new Slice(Fragment.fromArray(nodes), slice.openStart, slice.openEnd)
|
||||
}
|
||||
|
||||
return slice
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,8 +95,7 @@ export {
|
||||
QMSInlineCommentMark as NodeUuidExtension,
|
||||
qmsInlineCommentMarkName as nodeUuidName,
|
||||
selectNode,
|
||||
type QMSInlineCommentMarkOptions as NodeUuidOptions,
|
||||
type QMSInlineCommentMarkStorage as NodeUuidStorage
|
||||
type QMSInlineCommentMarkOptions as NodeUuidOptions
|
||||
} from './components/extension/qms/qmsInlineCommentMark'
|
||||
export { referenceConfig, ReferenceExtension } from './components/extension/reference'
|
||||
export { type Provider } from './provider/types'
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { DocWithRank, makeRank } from '@hcengineering/task'
|
||||
import { IconSize } from '@hcengineering/ui'
|
||||
import { SvelteComponent } from 'svelte'
|
||||
import { getListItemPresenter, getObjectPresenter } from '../../utils'
|
||||
import SortableList from './SortableList.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let query: DocumentQuery<Doc> = {}
|
||||
export let queryOptions: FindOptions<Doc> | undefined = undefined
|
||||
export let presenterProps: Record<string, any> = {}
|
||||
export let direction: 'row' | 'column' = 'column'
|
||||
export let flipDuration = 200
|
||||
export let itemsCount = 0
|
||||
export let icon: Asset | undefined = undefined
|
||||
export let iconSize: IconSize = 'small'
|
||||
|
||||
const SORTING_ORDER = SortingOrder.Ascending
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const itemsQuery = createQuery()
|
||||
|
||||
let isPresenterLoading = false
|
||||
let areItemsloading = true
|
||||
let areItemsSorting = false
|
||||
|
||||
let presenter: typeof SvelteComponent | undefined
|
||||
let items: Doc[] = []
|
||||
|
||||
async function updatePresenter (classRef: Ref<Class<Doc>>) {
|
||||
try {
|
||||
isPresenterLoading = true
|
||||
|
||||
const listItemPresenter = await getListItemPresenter(client, classRef)
|
||||
if (listItemPresenter) {
|
||||
presenter = await getResource(listItemPresenter)
|
||||
return
|
||||
}
|
||||
|
||||
const objectModel = await getObjectPresenter(client, classRef, { key: '' })
|
||||
if (objectModel?.presenter) {
|
||||
presenter = objectModel.presenter
|
||||
}
|
||||
} finally {
|
||||
isPresenterLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function updateItems (newItems: FindResult<Doc>): void {
|
||||
items = newItems
|
||||
areItemsloading = false
|
||||
}
|
||||
|
||||
$: !$$slots.object && updatePresenter(_class)
|
||||
$: itemsQuery.query(_class, query, updateItems, {
|
||||
...(isSortable ? { sort: { rank: SORTING_ORDER } } : {}),
|
||||
...(queryOptions ?? {}),
|
||||
limit: Math.max(queryOptions?.limit ?? 0, 200)
|
||||
})
|
||||
|
||||
$: isSortable = hierarchy.getAllAttributes(_class).has('rank')
|
||||
|
||||
async function handleMove (
|
||||
e: CustomEvent<{ item: DocWithRank, prev: DocWithRank | undefined, next: DocWithRank | undefined }>
|
||||
): Promise<void> {
|
||||
if (!isSortable) return
|
||||
const { item, prev, next } = e.detail
|
||||
const sortingOrder = queryOptions?.sort?.rank ?? SORTING_ORDER
|
||||
const rank =
|
||||
sortingOrder === SortingOrder.Ascending ? makeRank(prev?.rank, next?.rank) : makeRank(next?.rank, prev?.rank)
|
||||
|
||||
try {
|
||||
areItemsSorting = true
|
||||
await client.update(item, { rank })
|
||||
} finally {
|
||||
areItemsSorting = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SortableList {items} {label} {direction} {flipDuration} {icon} {iconSize} bind:itemsCount on:move={handleMove}>
|
||||
<svelte:fragment slot="object" let:value>
|
||||
{#if $$slots.object}
|
||||
<slot name="object" {value} />
|
||||
{:else if presenter}
|
||||
<svelte:component this={presenter} {...presenterProps} {value} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SortableList>
|
||||
@@ -13,92 +13,24 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { Asset, getResource, IntlString } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { DocWithRank, makeRank } from '@hcengineering/task'
|
||||
import { Button, Component, Icon, IconAdd, IconSize, Label, Loading } from '@hcengineering/ui'
|
||||
import view, { ObjectFactory } from '@hcengineering/view'
|
||||
import { Asset, IntlString } from '@hcengineering/platform'
|
||||
import { Icon, IconSize, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { getListItemPresenter, getObjectPresenter } from '../../utils'
|
||||
import { SvelteComponent } from 'svelte'
|
||||
|
||||
/*
|
||||
How to use:
|
||||
|
||||
We must add presenter for the "_class" via "ListItemPresenter" / "AttributePresenter"
|
||||
mixins or render the "object" slot to be able display the rows list.
|
||||
|
||||
To create a new items, we should add "ObjectFactory" mixin also.
|
||||
|
||||
We can create a custom list items or editor based on "SortableListItem"
|
||||
|
||||
Important: the "ObjectFactory" component must emit the "close" event
|
||||
*/
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let items: any[]
|
||||
export let label: IntlString | undefined = undefined
|
||||
export let query: DocumentQuery<Doc> = {}
|
||||
export let queryOptions: FindOptions<Doc> | undefined = undefined
|
||||
export let presenterProps: Record<string, any> = {}
|
||||
export let direction: 'row' | 'column' = 'column'
|
||||
export let flipDuration = 200
|
||||
export let isAddButtonHidden = false
|
||||
export let isAddButtonDisabled = false
|
||||
export let itemsCount = 0
|
||||
export let icon: Asset | undefined = undefined
|
||||
export let iconSize: IconSize = 'small'
|
||||
|
||||
const SORTING_ORDER = SortingOrder.Ascending
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const itemsQuery = createQuery()
|
||||
|
||||
let isPresenterLoading = false
|
||||
let areItemsloading = true
|
||||
let areItemsSorting = false
|
||||
|
||||
let presenter: typeof SvelteComponent | undefined
|
||||
let objectFactory: ObjectFactory | undefined
|
||||
let items: Doc[] = []
|
||||
const areItemsSorting = false
|
||||
|
||||
let draggingIndex: number | null = null
|
||||
let hoveringIndex: number | null = null
|
||||
|
||||
let isCreating = false
|
||||
|
||||
async function updatePresenter (classRef: Ref<Class<Doc>>) {
|
||||
try {
|
||||
isPresenterLoading = true
|
||||
|
||||
const listItemPresenter = await getListItemPresenter(client, classRef)
|
||||
if (listItemPresenter) {
|
||||
presenter = await getResource(listItemPresenter)
|
||||
return
|
||||
}
|
||||
|
||||
const objectModel = await getObjectPresenter(client, classRef, { key: '' })
|
||||
if (objectModel?.presenter) {
|
||||
presenter = objectModel.presenter
|
||||
}
|
||||
} finally {
|
||||
isPresenterLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function updateObjectFactory (objectFactoryClassRef: Ref<Class<Doc>>) {
|
||||
const objectFactoryClass = hierarchy.getClass(objectFactoryClassRef)
|
||||
|
||||
if (hierarchy.hasMixin(objectFactoryClass, view.mixin.ObjectFactory)) {
|
||||
objectFactory = hierarchy.as(objectFactoryClass, view.mixin.ObjectFactory)
|
||||
}
|
||||
}
|
||||
|
||||
function updateItems (newItems: FindResult<Doc>): void {
|
||||
items = newItems
|
||||
areItemsloading = false
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, itemIndex: number) {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
@@ -114,63 +46,38 @@
|
||||
hoveringIndex = itemIndex
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
async function handleDrop (itemIndex: number) {
|
||||
if (isSortable && items && draggingIndex !== null && draggingIndex !== itemIndex) {
|
||||
const item = items[draggingIndex] as DocWithRank
|
||||
if (draggingIndex !== null && draggingIndex !== itemIndex) {
|
||||
const item = items[draggingIndex]
|
||||
const [prev, next] = [
|
||||
items[draggingIndex < itemIndex ? itemIndex : itemIndex - 1] as DocWithRank,
|
||||
items[draggingIndex < itemIndex ? itemIndex + 1 : itemIndex] as DocWithRank
|
||||
items[draggingIndex < itemIndex ? itemIndex : itemIndex - 1],
|
||||
items[draggingIndex < itemIndex ? itemIndex + 1 : itemIndex]
|
||||
]
|
||||
|
||||
const sortingOrder = queryOptions?.sort?.rank ?? SORTING_ORDER
|
||||
const rank =
|
||||
sortingOrder === SortingOrder.Ascending ? makeRank(prev?.rank, next?.rank) : makeRank(next?.rank, prev?.rank)
|
||||
items.splice(itemIndex, 0, items.splice(draggingIndex, 1)[0])
|
||||
|
||||
try {
|
||||
areItemsSorting = true
|
||||
await client.update(item, { rank })
|
||||
} finally {
|
||||
areItemsSorting = false
|
||||
}
|
||||
dispatch('move', { item, prev, next, items })
|
||||
}
|
||||
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
async function create () {
|
||||
if (objectFactory?.create) {
|
||||
const createFn = await getResource(objectFactory.create)
|
||||
await createFn(query)
|
||||
return
|
||||
}
|
||||
|
||||
isCreating = true
|
||||
}
|
||||
|
||||
function resetDrag () {
|
||||
draggingIndex = null
|
||||
hoveringIndex = null
|
||||
}
|
||||
|
||||
$: !$$slots.object && updatePresenter(_class)
|
||||
$: updateObjectFactory(_class)
|
||||
$: itemsQuery.query(_class, query, updateItems, {
|
||||
...(isSortable ? { sort: { rank: SORTING_ORDER } } : {}),
|
||||
...(queryOptions ?? {}),
|
||||
limit: Math.max(queryOptions?.limit ?? 0, 200)
|
||||
})
|
||||
|
||||
$: isLoading = isPresenterLoading || areItemsloading
|
||||
$: isSortable = hierarchy.getAllAttributes(_class).has('rank')
|
||||
$: itemsCount = items?.length ?? 0
|
||||
|
||||
$: isVertical = direction === 'column'
|
||||
|
||||
$: isDraggable = isSortable && items.length > 1 && !areItemsSorting
|
||||
$: isDraggable = items.length > 1 && !areItemsSorting
|
||||
</script>
|
||||
|
||||
<div class="flex-col" class:w-full={isVertical}>
|
||||
{#if label || !isAddButtonHidden}
|
||||
{#if label}
|
||||
<div class="flex mb-4">
|
||||
{#if icon}
|
||||
<div class="mr-2 flex-center">
|
||||
@@ -184,25 +91,10 @@
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !isAddButtonHidden}
|
||||
<div class="ml-auto">
|
||||
<Button
|
||||
showTooltip={{ label: presentation.string.Add }}
|
||||
disabled={isAddButtonDisabled || isLoading || !objectFactory}
|
||||
width="min-content"
|
||||
icon={IconAdd}
|
||||
size="small"
|
||||
kind="ghost"
|
||||
on:click={create}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<Loading />
|
||||
{:else if ($$slots.object ?? presenter) && items}
|
||||
{#if $$slots.object && items}
|
||||
<div class="flex-gap-1" class:flex-col={isVertical} class:flex={!isVertical} class:flex-wrap={!isVertical}>
|
||||
{#each items as item, index (item._id)}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
@@ -225,18 +117,9 @@
|
||||
}}
|
||||
on:dragend={resetDrag}
|
||||
>
|
||||
{#if $$slots.object}
|
||||
<slot name="object" value={item} {isDraggable} />
|
||||
{:else if presenter}
|
||||
<svelte:component this={presenter} {isDraggable} {...presenterProps} value={item} />
|
||||
{/if}
|
||||
<slot name="object" value={item} {isDraggable} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if objectFactory?.component && isCreating}
|
||||
<!-- Important: the "close" event must be specified -->
|
||||
<Component is={objectFactory.component} props={query} showLoading on:close={() => (isCreating = false)} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,7 @@ import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelt
|
||||
import DividerPresenter from './components/list/DividerPresenter.svelte'
|
||||
import GrowPresenter from './components/list/GrowPresenter.svelte'
|
||||
import ListView from './components/list/ListView.svelte'
|
||||
import SortableDocList from './components/list/SortableDocList.svelte'
|
||||
import SortableList from './components/list/SortableList.svelte'
|
||||
import SortableListItem from './components/list/SortableListItem.svelte'
|
||||
import TreeElement from './components/navigator/TreeElement.svelte'
|
||||
@@ -236,6 +237,7 @@ export {
|
||||
ObjectIcon,
|
||||
ObjectMention,
|
||||
SortableList,
|
||||
SortableDocList,
|
||||
SortableListItem,
|
||||
SpaceHeader,
|
||||
SpacePresenter,
|
||||
|
||||
@@ -380,6 +380,37 @@ async function OnCardUpdate (ctx: TxUpdateDoc<Card>[], control: TriggerControl):
|
||||
})
|
||||
}
|
||||
|
||||
res.push(...(await updatePeers(control, doc, updateTx)))
|
||||
return res
|
||||
}
|
||||
|
||||
async function updatePeers (control: TriggerControl, doc: Card, updateTx: TxUpdateDoc<Card>): Promise<Tx[]> {
|
||||
if (updateTx.space === core.space.DerivedTx) return []
|
||||
const isDirect = control.hierarchy.isDerived(doc._class, communication.type.Direct)
|
||||
const isThreadFromDirect = (doc.parentInfo ?? []).some((it) =>
|
||||
control.hierarchy.isDerived(it._class, communication.type.Direct)
|
||||
)
|
||||
|
||||
if (!isDirect && !isThreadFromDirect) return []
|
||||
|
||||
delete updateTx.operations.title
|
||||
delete updateTx.operations.parentInfo
|
||||
delete updateTx.operations.parent
|
||||
delete updateTx.operations.$inc
|
||||
|
||||
const peers = (
|
||||
(
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, {
|
||||
findPeers: { params: { kind: 'card', cardId: doc._id } }
|
||||
})
|
||||
).value as CardPeer[]
|
||||
).flatMap((it) => it.members)
|
||||
|
||||
const res: Tx[] = []
|
||||
for (const peer of peers) {
|
||||
res.push(control.txFactory.createTxUpdateDoc(doc._class, peer.extra.space, peer.cardId, updateTx.operations))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@ export CLIENT_SECRET="$POD_GITHUB_CLIENT_SECRET"
|
||||
export PRIVATE_KEY="$POD_GITHUB_PRIVATE_KEY"
|
||||
export SERVER_SECRET=secret
|
||||
export ACCOUNTS_URL=http://localhost:3000
|
||||
export COLLABORATOR_URL=http://localhost:3078
|
||||
export MINIO_ACCESS_KEY=minioadminchmo
|
||||
export MINIO_SECRET_KEY=minioadmin
|
||||
export MINIO_ENDPOINT=localhost
|
||||
export MONGO_URL=mongodb://localhost:27017
|
||||
export COLLABORATOR_URL=ws://huly.local:3078
|
||||
export STORAGE_CONFIG="datalake|http://huly.local:4030"
|
||||
export OTEL_EXPORTER_OTLP_ENDPOINT=http://huly.local:4318/v1/traces
|
||||
rush bundle --to @hcengineering/pod-github
|
||||
node $@ bundle/bundle.js $@
|
||||
@@ -214,6 +214,8 @@ export class PlatformWorker {
|
||||
let oldErrors = ''
|
||||
let sameErrors = 1
|
||||
|
||||
let lastTimeout: any
|
||||
|
||||
while (!this.canceled) {
|
||||
let errors: string[] = []
|
||||
try {
|
||||
@@ -227,6 +229,7 @@ export class PlatformWorker {
|
||||
this.triggerCheckWorkspaces = () => {
|
||||
this.ctx.info('Workspaces check triggered')
|
||||
this.triggerCheckWorkspaces = () => {}
|
||||
clearTimeout(lastTimeout)
|
||||
resolve()
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
@@ -320,9 +323,10 @@ export class PlatformWorker {
|
||||
try {
|
||||
;({ client } = await createPlatformClient(ctx, oldWorkspace, 30000))
|
||||
await this.removeInstallationFromWorkspace(oldWorker, installationId)
|
||||
await client.close()
|
||||
} catch (err: any) {
|
||||
ctx.error('failed to remove old installation from workspace', { workspace: oldWorkspace, installationId })
|
||||
} finally {
|
||||
await client?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -944,26 +948,18 @@ export class PlatformWorker {
|
||||
|
||||
checkedWorkspaces = new Set<string>()
|
||||
|
||||
async checkWorkspaceIsActive (
|
||||
workspace: WorkspaceUuid,
|
||||
workspaceInfo?: WorkspaceInfoWithStatus,
|
||||
needRecheck = false
|
||||
): Promise<{ workspaceInfo: WorkspaceInfoWithStatus | undefined, needRecheck: boolean }> {
|
||||
if (workspaceInfo === undefined && needRecheck) {
|
||||
const token = generateToken(systemAccountUuid, workspace, { service: 'github', mode: 'github' })
|
||||
workspaceInfo = await getAccountClient(config.AccountsURL, token).getWorkspaceInfo()
|
||||
}
|
||||
checkWorkspaceIsActive (workspace: WorkspaceUuid, workspaceInfo: WorkspaceInfoWithStatus): boolean {
|
||||
if (workspaceInfo?.uuid === undefined) {
|
||||
this.ctx.error('No workspace exists for workspaceId', { workspace })
|
||||
return { workspaceInfo: undefined, needRecheck: false }
|
||||
return false
|
||||
}
|
||||
if (workspaceInfo?.isDisabled === true || isDeletingMode(workspaceInfo?.mode)) {
|
||||
this.ctx.warn('Workspace is disabled', { workspace })
|
||||
return { workspaceInfo: undefined, needRecheck: false }
|
||||
return false
|
||||
}
|
||||
if (!isActiveMode(workspaceInfo?.mode)) {
|
||||
this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace, mode: workspaceInfo?.mode })
|
||||
return { workspaceInfo: undefined, needRecheck: true }
|
||||
return true
|
||||
}
|
||||
|
||||
const lastVisit = (Date.now() - (workspaceInfo.lastVisit ?? 0)) / (3600 * 24 * 1000) // In days
|
||||
@@ -973,9 +969,38 @@ export class PlatformWorker {
|
||||
this.checkedWorkspaces.add(workspace)
|
||||
this.ctx.warn('Workspace is inactive for too long, skipping for now.', { workspace })
|
||||
}
|
||||
return { workspaceInfo: undefined, needRecheck: true }
|
||||
return true
|
||||
}
|
||||
return { workspaceInfo, needRecheck: true }
|
||||
return false
|
||||
}
|
||||
|
||||
checkReconnect (workspace: WorkspaceUuid, event: ClientConnectEvent, worker: GithubWorker): void {
|
||||
if (event === ClientConnectEvent.Refresh || event === ClientConnectEvent.Upgraded) {
|
||||
void this.clients
|
||||
.get(workspace)
|
||||
?.refreshClient(event === ClientConnectEvent.Upgraded)
|
||||
?.catch((err) => {
|
||||
worker.ctx.error('Failed to refresh', { error: err })
|
||||
})
|
||||
}
|
||||
|
||||
// We need to check if workspace is inactive
|
||||
const token = generateToken(systemAccountUuid, workspace, { service: 'github', mode: 'github' })
|
||||
getAccountClient(config.AccountsURL, token)
|
||||
.getWorkspaceInfo()
|
||||
.then((wsInfo) => {
|
||||
const res = this.checkWorkspaceIsActive(workspace, wsInfo)
|
||||
if (!res) {
|
||||
this.ctx.warn('Workspace is inactive, removing from clients list.', { workspace })
|
||||
this.clients.delete(workspace)
|
||||
void worker?.close().catch((err) => {
|
||||
this.ctx.error('Failed to close workspace', { workspace, error: err })
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.ctx.error('Failed to check workspace is active', { workspace, error: err })
|
||||
})
|
||||
}
|
||||
|
||||
private async checkWorkspaces (): Promise<string[]> {
|
||||
@@ -1008,6 +1033,7 @@ export class PlatformWorker {
|
||||
this.ctx.info('connecting to workspace', { workspace: c, time: Date.now() - d.time, version: d.version })
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
try {
|
||||
const token = generateToken(systemAccountUuid, undefined, { service: 'github', mode: 'github' })
|
||||
const infos = new Map(
|
||||
@@ -1022,82 +1048,57 @@ export class PlatformWorker {
|
||||
toDelete.delete(workspace)
|
||||
continue
|
||||
}
|
||||
const returnedInfo = infos.get(workspace)
|
||||
if (returnedInfo === undefined) {
|
||||
rechecks.push(workspace)
|
||||
continue
|
||||
}
|
||||
const needRecheck = this.checkWorkspaceIsActive(workspace, returnedInfo)
|
||||
if (needRecheck) {
|
||||
rechecks.push(workspace)
|
||||
continue
|
||||
}
|
||||
await rateLimiter.add(async () => {
|
||||
const { workspaceInfo, needRecheck } = await this.checkWorkspaceIsActive(workspace, infos.get(workspace))
|
||||
if (workspaceInfo === undefined) {
|
||||
if (needRecheck) {
|
||||
rechecks.push(workspace)
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const branding = Object.values(this.brandingMap).find((b) => b.key === workspaceInfo?.branding) ?? null
|
||||
const workerCtx = this.ctx.newChild('worker', { workspace: workspaceInfo.uuid }, { span: false })
|
||||
const branding = Object.values(this.brandingMap).find((b) => b.key === returnedInfo?.branding) ?? null
|
||||
const workerCtx = this.ctx.newChild('worker', { workspace: returnedInfo.uuid }, { span: false })
|
||||
|
||||
connecting.set(workspaceInfo.uuid, {
|
||||
connecting.set(returnedInfo.uuid, {
|
||||
time: Date.now(),
|
||||
version: versionToString({
|
||||
major: workspaceInfo.versionMajor,
|
||||
minor: workspaceInfo.versionMinor,
|
||||
patch: workspaceInfo.versionPatch
|
||||
major: returnedInfo.versionMajor,
|
||||
minor: returnedInfo.versionMinor,
|
||||
patch: returnedInfo.versionPatch
|
||||
})
|
||||
})
|
||||
workerCtx.info('************************* Register worker ************************* ', {
|
||||
workspaceId: workspaceInfo.uuid,
|
||||
workspaceUrl: workspaceInfo.url,
|
||||
versionMajor: workspaceInfo.versionMajor,
|
||||
versionMinor: workspaceInfo.versionMinor,
|
||||
versionPatch: workspaceInfo.versionPatch,
|
||||
mode: workspaceInfo.mode,
|
||||
workspaceId: returnedInfo.uuid,
|
||||
workspaceUrl: returnedInfo.url,
|
||||
versionMajor: returnedInfo.versionMajor,
|
||||
versionMinor: returnedInfo.versionMinor,
|
||||
versionPatch: returnedInfo.versionPatch,
|
||||
mode: returnedInfo.mode,
|
||||
index: widx,
|
||||
total: workspaces.length
|
||||
})
|
||||
|
||||
let initialized = false
|
||||
const worker = await GithubWorker.create(
|
||||
this,
|
||||
workerCtx,
|
||||
this.installations,
|
||||
{
|
||||
dataId: workspaceInfo.dataId,
|
||||
url: workspaceInfo.url,
|
||||
uuid: workspaceInfo.uuid
|
||||
dataId: returnedInfo.dataId,
|
||||
url: returnedInfo.url,
|
||||
uuid: returnedInfo.uuid
|
||||
},
|
||||
branding,
|
||||
this.app,
|
||||
this.storageAdapter,
|
||||
(workspace, event) => {
|
||||
if (event === ClientConnectEvent.Refresh || event === ClientConnectEvent.Upgraded) {
|
||||
void this.clients
|
||||
.get(workspace)
|
||||
?.refreshClient(event === ClientConnectEvent.Upgraded)
|
||||
?.catch((err) => {
|
||||
workerCtx.error('Failed to refresh', { error: err })
|
||||
})
|
||||
}
|
||||
if (initialized) {
|
||||
// We need to check if workspace is inactive
|
||||
void this.checkWorkspaceIsActive(workspace, undefined, true)
|
||||
.then((res) => {
|
||||
if (res === undefined) {
|
||||
this.ctx.warn('Workspace is inactive, removing from clients list.', { workspace })
|
||||
this.clients.delete(workspace)
|
||||
void worker?.close().catch((err) => {
|
||||
this.ctx.error('Failed to close workspace', { workspace, error: err })
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
this.ctx.error('Failed to check workspace is active', { workspace, error: err })
|
||||
})
|
||||
}
|
||||
}
|
||||
this.storageAdapter
|
||||
)
|
||||
if (worker !== undefined) {
|
||||
initialized = true
|
||||
workerCtx.info('************************* Register worker Done ************************* ', {
|
||||
workspaceId: workspaceInfo.uuid,
|
||||
workspaceUrl: workspaceInfo.url,
|
||||
workspaceId: returnedInfo.uuid,
|
||||
workspaceUrl: returnedInfo.url,
|
||||
index: widx,
|
||||
total: workspaces.length
|
||||
})
|
||||
@@ -1107,12 +1108,12 @@ export class PlatformWorker {
|
||||
workerCtx.info(
|
||||
'************************* Failed Register worker, timeout or integrations removed *************************',
|
||||
{
|
||||
workspaceId: workspaceInfo.uuid,
|
||||
workspaceUrl: workspaceInfo.url,
|
||||
versionMajor: workspaceInfo.versionMajor,
|
||||
versionMinor: workspaceInfo.versionMinor,
|
||||
versionPatch: workspaceInfo.versionPatch,
|
||||
lastVisit: (Date.now() - (workspaceInfo.lastVisit ?? 0)) / (24 * 60 * 60 * 1000),
|
||||
workspaceId: returnedInfo.uuid,
|
||||
workspaceUrl: returnedInfo.url,
|
||||
versionMajor: returnedInfo.versionMajor,
|
||||
versionMinor: returnedInfo.versionMinor,
|
||||
versionPatch: returnedInfo.versionPatch,
|
||||
lastVisit: (Date.now() - (returnedInfo.lastVisit ?? 0)) / (24 * 60 * 60 * 1000),
|
||||
index: widx,
|
||||
total: workspaces.length
|
||||
}
|
||||
@@ -1124,7 +1125,7 @@ export class PlatformWorker {
|
||||
this.ctx.info("Couldn't create WS worker", { workspace, error: e })
|
||||
rechecks.push(workspace)
|
||||
} finally {
|
||||
connecting.delete(workspaceInfo.uuid)
|
||||
connecting.delete(returnedInfo.uuid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1766,13 +1766,13 @@ export class GithubWorker implements IntegrationManager {
|
||||
workspace: WorkspaceIds,
|
||||
branding: Branding | null,
|
||||
app: App,
|
||||
storageAdapter: StorageAdapter,
|
||||
reconnect: (workspaceId: WorkspaceUuid, event: ClientConnectEvent) => void
|
||||
storageAdapter: StorageAdapter
|
||||
): Promise<GithubWorker | undefined> {
|
||||
ctx.info('Connecting to', { workspace })
|
||||
let client: Client | undefined
|
||||
let endpoint: string | undefined
|
||||
let maitenanceState = false
|
||||
let worker: GithubWorker | undefined
|
||||
try {
|
||||
;({ client, endpoint } = await createPlatformClient(
|
||||
ctx,
|
||||
@@ -1784,7 +1784,9 @@ export class GithubWorker implements IntegrationManager {
|
||||
maitenanceState = true
|
||||
throw new Error('Workspace in maintenance')
|
||||
}
|
||||
reconnect(workspace.uuid, event)
|
||||
if (worker !== undefined) {
|
||||
platformWorker.checkReconnect(workspace.uuid, event, worker)
|
||||
}
|
||||
}
|
||||
))
|
||||
ctx.info('connected to github', { workspace: workspace.uuid, endpoint })
|
||||
@@ -1796,7 +1798,7 @@ export class GithubWorker implements IntegrationManager {
|
||||
|
||||
await GithubWorker.checkIntegrations(client, installations)
|
||||
|
||||
const worker = new GithubWorker(
|
||||
worker = new GithubWorker(
|
||||
ctx,
|
||||
platformWorker.getRateLimiter(endpoint ?? ''),
|
||||
platformWorker,
|
||||
@@ -1808,10 +1810,16 @@ export class GithubWorker implements IntegrationManager {
|
||||
branding
|
||||
)
|
||||
ctx.info('Init worker', { workspace: workspace.url, workspaceId: workspace.uuid })
|
||||
void worker.init()
|
||||
void worker.init().catch((err) => {
|
||||
ctx.error('failed to init worker', { error: err, workspace: workspace.uuid })
|
||||
void client?.close().catch((err) => {
|
||||
ctx.error('failed to close client after init error', { error: err, workspace: workspace.uuid })
|
||||
})
|
||||
})
|
||||
return worker
|
||||
} catch (err: any) {
|
||||
await client?.close()
|
||||
void worker?.close()
|
||||
if (maitenanceState) {
|
||||
ctx.info('workspace in maintenance, schedule recheck', { workspace: workspace.uuid, endpoint })
|
||||
return
|
||||
|
||||
@@ -37,6 +37,7 @@ import process, {
|
||||
parseError,
|
||||
processError,
|
||||
ProcessError,
|
||||
ProcessToDo,
|
||||
State,
|
||||
Step,
|
||||
Transition
|
||||
@@ -169,6 +170,10 @@ async function processExecution (control: ProcessControl, record: ProcessMessage
|
||||
if (transition !== undefined) {
|
||||
await execute(execution, transition, control)
|
||||
} else {
|
||||
if (record.event === process.trigger.OnToDoRemove) {
|
||||
const rollbackResult = await checkRollback(control, record, execution)
|
||||
if (rollbackResult) return
|
||||
}
|
||||
control.ctx.info('No transition found for event', {
|
||||
event: record.event,
|
||||
execution: execution._id,
|
||||
@@ -177,6 +182,23 @@ async function processExecution (control: ProcessControl, record: ProcessMessage
|
||||
}
|
||||
}
|
||||
|
||||
async function checkRollback (control: ProcessControl, record: ProcessMessage, execution: Execution): Promise<boolean> {
|
||||
const todo = record.context.todo as ProcessToDo
|
||||
if (!todo?.withRollback) return false
|
||||
const rollbackTxes = execution.rollback.pop() ?? []
|
||||
for (const tx of rollbackTxes) {
|
||||
const timeout = setTimeout(() => {
|
||||
control.ctx.warn('TX HANG', tx)
|
||||
}, 30000)
|
||||
await control.client.tx(tx)
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
await control.client.update(execution, {
|
||||
rollback: execution.rollback
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
async function getTriggerRollback (triggger: TriggerImpl, control: ProcessControl): Promise<Tx | undefined> {
|
||||
if (triggger.rollbackFunc !== undefined) {
|
||||
const rollbackFunc = await getResource(triggger.rollbackFunc)
|
||||
@@ -284,13 +306,12 @@ async function executeTransition (execution: Execution, transition: Transition,
|
||||
execution.rollback.push(rollback)
|
||||
}
|
||||
const executionUpdate = getDiffUpdate(execution, {
|
||||
rollback: execution.rollback.length > 30 ? execution.rollback.slice(-30) : execution.rollback,
|
||||
context: execution.context,
|
||||
currentState: state._id,
|
||||
status: isDone ? ExecutionStatus.Done : ExecutionStatus.Active,
|
||||
error: null
|
||||
})
|
||||
executionUpdate.context = execution.context
|
||||
executionUpdate.rollback = execution.rollback.length > 30 ? execution.rollback.slice(-30) : execution.rollback
|
||||
res.push(client.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, executionUpdate))
|
||||
res.push(
|
||||
client.txFactory.createTxCreateDoc(process.class.ExecutionLog, execution.space, {
|
||||
|
||||
Reference in New Issue
Block a user