mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-25 09:30:27 +00:00
Merge branch 'develop' into staging-new
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
commit
8c09674c53
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { cardId, DOMAIN_CARD } from '@hcengineering/card'
|
import { type Card, cardId, DOMAIN_CARD } from '@hcengineering/card'
|
||||||
import core, { TxOperations, type Client, type Data, type Doc } from '@hcengineering/core'
|
import core, { TxOperations, type Client, type Data, type Doc } from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
tryMigrate,
|
tryMigrate,
|
||||||
@ -39,6 +39,11 @@ export const cardOperation: MigrateOperation = {
|
|||||||
state: 'migrate-spaces',
|
state: 'migrate-spaces',
|
||||||
mode: 'upgrade',
|
mode: 'upgrade',
|
||||||
func: migrateSpaces
|
func: migrateSpaces
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'migrate-childs-spaces',
|
||||||
|
mode: 'upgrade',
|
||||||
|
func: migrateChildsSpaces
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
@ -175,6 +180,14 @@ async function createDefaultProject (tx: TxOperations): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateChildsSpaces (client: MigrationClient): Promise<void> {
|
||||||
|
const toUpdate = await client.find<Card>(DOMAIN_CARD, { space: core.space.Workspace })
|
||||||
|
for (const doc of toUpdate) {
|
||||||
|
const parent = doc.parent != null ? (await client.find(DOMAIN_CARD, { _id: doc.parent }))[0] : undefined
|
||||||
|
await client.update(DOMAIN_CARD, { _id: doc._id }, { space: parent?.space ?? card.space.Default })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function migrateSpaces (client: MigrationClient): Promise<void> {
|
async function migrateSpaces (client: MigrationClient): Promise<void> {
|
||||||
await client.update(DOMAIN_CARD, { space: core.space.Workspace }, { space: card.space.Default })
|
await client.update(DOMAIN_CARD, { space: core.space.Workspace }, { space: card.space.Default })
|
||||||
}
|
}
|
||||||
|
@ -16,40 +16,43 @@
|
|||||||
import { saveCollabJson } from '@hcengineering/collaboration'
|
import { saveCollabJson } from '@hcengineering/collaboration'
|
||||||
import core, {
|
import core, {
|
||||||
buildSocialIdString,
|
buildSocialIdString,
|
||||||
|
configUserAccountUuid,
|
||||||
coreId,
|
coreId,
|
||||||
DOMAIN_MODEL_TX,
|
DOMAIN_MODEL_TX,
|
||||||
DOMAIN_SPACE,
|
DOMAIN_SPACE,
|
||||||
DOMAIN_STATUS,
|
DOMAIN_STATUS,
|
||||||
DOMAIN_TX,
|
DOMAIN_TX,
|
||||||
generateId,
|
generateId,
|
||||||
|
groupByArray,
|
||||||
makeCollabJsonId,
|
makeCollabJsonId,
|
||||||
makeCollabYdocId,
|
makeCollabYdocId,
|
||||||
makeDocCollabId,
|
makeDocCollabId,
|
||||||
MeasureMetricsContext,
|
MeasureMetricsContext,
|
||||||
RateLimiter,
|
RateLimiter,
|
||||||
SocialIdType,
|
SocialIdType,
|
||||||
type PersonId,
|
systemAccountUuid,
|
||||||
|
toIdMap,
|
||||||
|
TxProcessor,
|
||||||
|
type AccountUuid,
|
||||||
type AnyAttribute,
|
type AnyAttribute,
|
||||||
|
type AttachedDoc,
|
||||||
type Blob,
|
type Blob,
|
||||||
type Class,
|
type Class,
|
||||||
type Doc,
|
type Doc,
|
||||||
type Domain,
|
type Domain,
|
||||||
type MeasureContext,
|
type MeasureContext,
|
||||||
|
type PersonId,
|
||||||
type Ref,
|
type Ref,
|
||||||
|
type Role,
|
||||||
|
type SocialKey,
|
||||||
type Space,
|
type Space,
|
||||||
|
type SpaceType,
|
||||||
type Status,
|
type Status,
|
||||||
type TxCreateDoc,
|
type TxCreateDoc,
|
||||||
type TxCUD,
|
type TxCUD,
|
||||||
type SpaceType,
|
type TxMixin,
|
||||||
type TxUpdateDoc,
|
type TxUpdateDoc,
|
||||||
type Role,
|
type TypedSpace
|
||||||
toIdMap,
|
|
||||||
type TypedSpace,
|
|
||||||
TxProcessor,
|
|
||||||
type SocialKey,
|
|
||||||
type AccountUuid,
|
|
||||||
systemAccountUuid,
|
|
||||||
configUserAccountUuid
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import {
|
import {
|
||||||
createDefaultSpace,
|
createDefaultSpace,
|
||||||
@ -264,6 +267,41 @@ async function processMigrateContentFor (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateBackupMixins (client: MigrationClient): Promise<void> {
|
||||||
|
// Go via classes with domain and check if mixin exists and need to flush %hash%
|
||||||
|
const hierarchy = client.hierarchy
|
||||||
|
const curHash = Date.now().toString(16) // Current hash value
|
||||||
|
|
||||||
|
const txIterator = await client.traverse<TxMixin<Doc, AttachedDoc>>(DOMAIN_TX, { _class: core.class.TxMixin })
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const mixinOps = await txIterator.next(500)
|
||||||
|
if (mixinOps === null || mixinOps.length === 0) break
|
||||||
|
const _classes = groupByArray(mixinOps, (it) => it.objectClass)
|
||||||
|
|
||||||
|
for (const [_class, ops] of _classes.entries()) {
|
||||||
|
const domain = hierarchy.findDomain(_class)
|
||||||
|
if (domain === undefined) continue
|
||||||
|
let docs = await client.find(domain, { _id: { $in: ops.map((it) => it.objectId) } })
|
||||||
|
|
||||||
|
docs = docs.filter((it) => {
|
||||||
|
// Check if mixin is last operation by modifiedOn
|
||||||
|
const mops = ops.filter((mi) => mi.objectId === it._id)
|
||||||
|
if (mops.length === 0) return false
|
||||||
|
return mops.some((mi) => mi.modifiedOn === it.modifiedOn && mi.modifiedBy === it.modifiedBy)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (docs.length > 0) {
|
||||||
|
// Check if docs has mixins from list
|
||||||
|
const toUpdate = docs.filter((it) => hierarchy.findAllMixins(it).length > 0)
|
||||||
|
if (toUpdate.length > 0) {
|
||||||
|
await client.update(domain, { _id: { $in: toUpdate.map((it) => it._id) } }, { '%hash%': curHash })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function migrateCollaborativeDocsToJson (client: MigrationClient): Promise<void> {
|
async function migrateCollaborativeDocsToJson (client: MigrationClient): Promise<void> {
|
||||||
const ctx = new MeasureMetricsContext('migrateCollaborativeDocsToJson', {})
|
const ctx = new MeasureMetricsContext('migrateCollaborativeDocsToJson', {})
|
||||||
const storageAdapter = client.storageAdapter
|
const storageAdapter = client.storageAdapter
|
||||||
@ -921,6 +959,11 @@ export const coreOperation: MigrateOperation = {
|
|||||||
state: 'accounts-to-social-ids',
|
state: 'accounts-to-social-ids',
|
||||||
mode: 'upgrade',
|
mode: 'upgrade',
|
||||||
func: migrateAccounts
|
func: migrateAccounts
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: 'migrate-backup-mixins',
|
||||||
|
mode: 'upgrade',
|
||||||
|
func: migrateBackupMixins
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
import { type WorkspaceLoginInfo, getClient as getAccountClient } from '@hcengineering/account-client'
|
import { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||||
import client, { clientId } from '@hcengineering/client'
|
import client, { clientId } from '@hcengineering/client'
|
||||||
import {
|
import {
|
||||||
type Class,
|
type Class,
|
||||||
@ -51,6 +51,7 @@ import {
|
|||||||
createMarkupOperations
|
createMarkupOperations
|
||||||
} from './markup'
|
} from './markup'
|
||||||
import { type ConnectOptions, type PlatformClient, WithMarkup } from './types'
|
import { type ConnectOptions, type PlatformClient, WithMarkup } from './types'
|
||||||
|
import { getWorkspaceToken } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create platform client
|
* Create platform client
|
||||||
@ -282,39 +283,3 @@ class PlatformClientImpl implements PlatformClient {
|
|||||||
await this.close()
|
await this.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceToken {
|
|
||||||
endpoint: string
|
|
||||||
token: string
|
|
||||||
workspaceId: WorkspaceUuid
|
|
||||||
info: WorkspaceLoginInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getWorkspaceToken (
|
|
||||||
url: string,
|
|
||||||
options: ConnectOptions,
|
|
||||||
config?: ServerConfig
|
|
||||||
): Promise<WorkspaceToken> {
|
|
||||||
config ??= await loadServerConfig(url)
|
|
||||||
|
|
||||||
let token: string | undefined
|
|
||||||
|
|
||||||
if ('token' in options) {
|
|
||||||
token = options.token
|
|
||||||
} else {
|
|
||||||
const { email, password } = options
|
|
||||||
const loginInfo = await getAccountClient(config.ACCOUNTS_URL).login(email, password)
|
|
||||||
token = loginInfo.token
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token === undefined) {
|
|
||||||
throw new Error('Login failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const ws = await getAccountClient(config.ACCOUNTS_URL, token).selectWorkspace(options.workspace)
|
|
||||||
if (ws === undefined) {
|
|
||||||
throw new Error('Workspace not found')
|
|
||||||
}
|
|
||||||
|
|
||||||
return { endpoint: ws.endpoint, token: ws.token, workspaceId: ws.workspace, info: ws }
|
|
||||||
}
|
|
||||||
|
@ -19,3 +19,4 @@ export * from './socket'
|
|||||||
export * from './types'
|
export * from './types'
|
||||||
export * from './rest'
|
export * from './rest'
|
||||||
export * from './config'
|
export * from './config'
|
||||||
|
export * from './utils'
|
||||||
|
@ -13,6 +13,6 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
export { createRestClient } from './rest'
|
export { createRestClient, connectRest } from './rest'
|
||||||
export { createRestTxOperations } from './tx'
|
export { createRestTxOperations } from './tx'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
@ -39,12 +39,19 @@ import {
|
|||||||
import { PlatformError, unknownError } from '@hcengineering/platform'
|
import { PlatformError, unknownError } from '@hcengineering/platform'
|
||||||
|
|
||||||
import type { RestClient } from './types'
|
import type { RestClient } from './types'
|
||||||
|
import { AuthOptions } from '../types'
|
||||||
import { extractJson, withRetry } from './utils'
|
import { extractJson, withRetry } from './utils'
|
||||||
|
import { getWorkspaceToken } from '../utils'
|
||||||
|
|
||||||
export function createRestClient (endpoint: string, workspaceId: string, token: string): RestClient {
|
export function createRestClient (endpoint: string, workspaceId: string, token: string): RestClient {
|
||||||
return new RestClientImpl(endpoint, workspaceId, token)
|
return new RestClientImpl(endpoint, workspaceId, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function connectRest (url: string, options: AuthOptions): Promise<RestClient> {
|
||||||
|
const { endpoint, token, workspaceId } = await getWorkspaceToken(url, options)
|
||||||
|
return createRestClient(endpoint, workspaceId, token)
|
||||||
|
}
|
||||||
|
|
||||||
const rateLimitError = 'rate-limit'
|
const rateLimitError = 'rate-limit'
|
||||||
|
|
||||||
function isRLE (err: any): boolean {
|
function isRLE (err: any): boolean {
|
||||||
|
55
packages/api-client/src/utils.ts
Normal file
55
packages/api-client/src/utils.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2025 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.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { type WorkspaceLoginInfo, getClient as getAccountClient } from '@hcengineering/account-client'
|
||||||
|
import { WorkspaceUuid } from '@hcengineering/core'
|
||||||
|
import { AuthOptions } from './types'
|
||||||
|
import { loadServerConfig, ServerConfig } from './config'
|
||||||
|
|
||||||
|
export interface WorkspaceToken {
|
||||||
|
endpoint: string
|
||||||
|
token: string
|
||||||
|
workspaceId: WorkspaceUuid
|
||||||
|
info: WorkspaceLoginInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWorkspaceToken (
|
||||||
|
url: string,
|
||||||
|
options: AuthOptions,
|
||||||
|
config?: ServerConfig
|
||||||
|
): Promise<WorkspaceToken> {
|
||||||
|
config ??= await loadServerConfig(url)
|
||||||
|
|
||||||
|
let token: string | undefined
|
||||||
|
|
||||||
|
if ('token' in options) {
|
||||||
|
token = options.token
|
||||||
|
} else {
|
||||||
|
const { email, password } = options
|
||||||
|
const loginInfo = await getAccountClient(config.ACCOUNTS_URL).login(email, password)
|
||||||
|
token = loginInfo.token
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token === undefined) {
|
||||||
|
throw new Error('Login failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const ws = await getAccountClient(config.ACCOUNTS_URL, token).selectWorkspace(options.workspace)
|
||||||
|
if (ws === undefined) {
|
||||||
|
throw new Error('Workspace not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { endpoint: ws.endpoint, token: ws.token, workspaceId: ws.workspace, info: ws }
|
||||||
|
}
|
@ -127,9 +127,9 @@ p {
|
|||||||
user-select:inherit;
|
user-select:inherit;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
word-break: break-all;
|
// word-break: break-all;
|
||||||
word-break: break-word;
|
// word-break: break-word;
|
||||||
hyphens: auto;
|
// hyphens: auto;
|
||||||
color: var(--theme-link-color);
|
color: var(--theme-link-color);
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
|
@ -41,5 +41,5 @@
|
|||||||
{#if data.action === 'create'}
|
{#if data.action === 'create'}
|
||||||
<ActivityObjectValue {message} {card} />
|
<ActivityObjectValue {message} {card} />
|
||||||
{:else if data.update && data.action === 'update'}
|
{:else if data.update && data.action === 'update'}
|
||||||
<ActivityUpdateViewer update={data.update} model={attributeModel} />
|
<ActivityUpdateViewer update={data.update} model={attributeModel} content={message.content} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { IconDelete } from '@hcengineering/ui'
|
import { IconDelete } from '@hcengineering/ui'
|
||||||
import { ActivityTagUpdate } from '@hcengineering/communication-types'
|
import { ActivityTagUpdate, RichText } from '@hcengineering/communication-types'
|
||||||
import cardPlugin from '@hcengineering/card'
|
import cardPlugin from '@hcengineering/card'
|
||||||
|
|
||||||
import Icon from '../../Icon.svelte'
|
import Icon from '../../Icon.svelte'
|
||||||
@ -24,14 +24,16 @@
|
|||||||
import uiNext from '../../../plugin'
|
import uiNext from '../../../plugin'
|
||||||
|
|
||||||
export let update: ActivityTagUpdate
|
export let update: ActivityTagUpdate
|
||||||
|
export let content: RichText
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
$: mixin = hierarchy.getClass(update.tag)
|
$: mixin = hierarchy.hasClass(update.tag) ? hierarchy.getClass(update.tag) : undefined
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if update.action === 'add'}
|
{#if mixin !== undefined}
|
||||||
|
{#if update.action === 'add'}
|
||||||
<div class="flex-presenter overflow-label flex-gap-2">
|
<div class="flex-presenter overflow-label flex-gap-2">
|
||||||
<Icon icon={IconPlus} size="small" />
|
<Icon icon={IconPlus} size="small" />
|
||||||
<Label label={uiNext.string.Added} />
|
<Label label={uiNext.string.Added} />
|
||||||
@ -40,7 +42,7 @@
|
|||||||
<Label label={mixin.label} />
|
<Label label={mixin.label} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if update.action === 'remove'}
|
{:else if update.action === 'remove'}
|
||||||
<div class="flex-presenter overflow-label flex-gap-2">
|
<div class="flex-presenter overflow-label flex-gap-2">
|
||||||
<Icon icon={IconDelete} size="small" />
|
<Icon icon={IconDelete} size="small" />
|
||||||
<Label label={uiNext.string.Removed} />
|
<Label label={uiNext.string.Removed} />
|
||||||
@ -49,6 +51,16 @@
|
|||||||
<Label label={mixin.label} />
|
<Label label={mixin.label} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="flex-presenter overflow-label flex-gap-2">
|
||||||
|
{#if update.action === 'add'}
|
||||||
|
<Icon icon={IconPlus} size="small" />
|
||||||
|
{:else if update.action === 'remove'}
|
||||||
|
<Icon icon={IconDelete} size="small" />
|
||||||
|
{/if}
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -14,17 +14,18 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AttributeModel } from '@hcengineering/view'
|
import { AttributeModel } from '@hcengineering/view'
|
||||||
import { ActivityUpdate, ActivityUpdateType } from '@hcengineering/communication-types'
|
import { ActivityUpdate, ActivityUpdateType, RichText } from '@hcengineering/communication-types'
|
||||||
|
|
||||||
import ActivityUpdateTagViewer from './ActivityUpdateTagViewer.svelte'
|
import ActivityUpdateTagViewer from './ActivityUpdateTagViewer.svelte'
|
||||||
import ActivityUpdateAttributeViewer from './ActivityUpdateAttributeViewer.svelte'
|
import ActivityUpdateAttributeViewer from './ActivityUpdateAttributeViewer.svelte'
|
||||||
|
|
||||||
export let model: AttributeModel | undefined = undefined
|
export let model: AttributeModel | undefined = undefined
|
||||||
export let update: ActivityUpdate
|
export let update: ActivityUpdate
|
||||||
|
export let content: RichText
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if update.type === ActivityUpdateType.Attribute && model}
|
{#if update.type === ActivityUpdateType.Attribute && model}
|
||||||
<ActivityUpdateAttributeViewer {model} {update} />
|
<ActivityUpdateAttributeViewer {model} {update} />
|
||||||
{:else if update.type === ActivityUpdateType.Tag}
|
{:else if update.type === ActivityUpdateType.Tag}
|
||||||
<ActivityUpdateTagViewer {update} />
|
<ActivityUpdateTagViewer {update} {content} />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "Aktualizovaný {object}",
|
"UpdatedObject": "Aktualizovaný {object}",
|
||||||
"NewObjectType": "Nový {type}: {title}",
|
"NewObjectType": "Nový {type}: {title}",
|
||||||
"RemovedObjectType": "Odstraněný {type} : {title}",
|
"RemovedObjectType": "Odstraněný {type} : {title}",
|
||||||
"AttributeSetTo": "{name} nastaveno na {value}"
|
"AttributeSetTo": "{name} nastaveno na {value}",
|
||||||
|
"AddedTag": "Přidaný štítek: {title}",
|
||||||
|
"RemovedTag": "Odebráný štítek: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "{object} aktualisiert",
|
"UpdatedObject": "{object} aktualisiert",
|
||||||
"NewObjectType": "Neues {objectType}",
|
"NewObjectType": "Neues {objectType}",
|
||||||
"RemovedObjectType": "{objectType} entfernt",
|
"RemovedObjectType": "{objectType} entfernt",
|
||||||
"AttributeSetTo": "{attribute} auf {value} gesetzt"
|
"AttributeSetTo": "{attribute} auf {value} gesetzt",
|
||||||
|
"AddedTag": "Tag hinzugefügt: {title}",
|
||||||
|
"RemovedTag": "Tag entfernt: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "Updated {object}",
|
"UpdatedObject": "Updated {object}",
|
||||||
"NewObjectType": "New {type}: {title}",
|
"NewObjectType": "New {type}: {title}",
|
||||||
"RemovedObjectType": "Removed {type} : {title}",
|
"RemovedObjectType": "Removed {type} : {title}",
|
||||||
|
"AddedTag": "Added tag: {title}",
|
||||||
|
"RemovedTag": "Removed tag: {title}",
|
||||||
"AttributeSetTo": "{name} set to {value}"
|
"AttributeSetTo": "{name} set to {value}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -52,6 +52,8 @@
|
|||||||
"UpdatedObject": "Actualizado {object}",
|
"UpdatedObject": "Actualizado {object}",
|
||||||
"NewObjectType": "Nuevo {type}: {title}",
|
"NewObjectType": "Nuevo {type}: {title}",
|
||||||
"RemovedObjectType": "Eliminado {type} : {title}",
|
"RemovedObjectType": "Eliminado {type} : {title}",
|
||||||
"AttributeSetTo": "{name} establecido en {value}"
|
"AttributeSetTo": "{name} establecido en {value}",
|
||||||
|
"AddedTag": "Etiqueta añadida: {title}",
|
||||||
|
"RemovedTag": "Etiqueta eliminada: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "Mis à jour {object}",
|
"UpdatedObject": "Mis à jour {object}",
|
||||||
"NewObjectType": "Nouveau {type}: {title}",
|
"NewObjectType": "Nouveau {type}: {title}",
|
||||||
"RemovedObjectType": "Supprimé {type} : {title}",
|
"RemovedObjectType": "Supprimé {type} : {title}",
|
||||||
"AttributeSetTo": "{name} défini sur {value}"
|
"AttributeSetTo": "{name} défini sur {value}",
|
||||||
|
"AddedTag": "Tag ajouté: {title}",
|
||||||
|
"RemovedTag": "Tag supprimé: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "{object} aggiornato",
|
"UpdatedObject": "{object} aggiornato",
|
||||||
"NewObjectType": "Nuovo {type}: {title}",
|
"NewObjectType": "Nuovo {type}: {title}",
|
||||||
"RemovedObjectType": "{type} rimosso: {title}",
|
"RemovedObjectType": "{type} rimosso: {title}",
|
||||||
"AttributeSetTo": "{name} impostato su {value}"
|
"AttributeSetTo": "{name} impostato su {value}",
|
||||||
|
"AddedTag": "Tag aggiunto: {title}",
|
||||||
|
"RemovedTag": "Tag rimosso: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,9 @@
|
|||||||
"UpdatedObject": "{object} を更新しました",
|
"UpdatedObject": "{object} を更新しました",
|
||||||
"NewObjectType": "新しい {type}:{title}",
|
"NewObjectType": "新しい {type}:{title}",
|
||||||
"RemovedObjectType": "{type} を削除:{title}",
|
"RemovedObjectType": "{type} を削除:{title}",
|
||||||
"AttributeSetTo": "{name} を {value} に設定しました"
|
"AttributeSetTo": "{name} を {value} に設定しました",
|
||||||
|
"AddedTag": "タグを追加しました:{title}",
|
||||||
|
"RemovedTag": "タグを削除しました:{title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +52,8 @@
|
|||||||
"UpdatedObject": "Atualizado {object}",
|
"UpdatedObject": "Atualizado {object}",
|
||||||
"NewObjectType": "Novo {type}: {title}",
|
"NewObjectType": "Novo {type}: {title}",
|
||||||
"RemovedObjectType": "Removido {type} : {title}",
|
"RemovedObjectType": "Removido {type} : {title}",
|
||||||
"AttributeSetTo": "{name} definido para {value}"
|
"AttributeSetTo": "{name} definido para {value}",
|
||||||
|
"AddedTag": "Tag adicionado: {title}",
|
||||||
|
"RemovedTag": "Tag removido: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "Обновил(а) {object}",
|
"UpdatedObject": "Обновил(а) {object}",
|
||||||
"NewObjectType": "Новый(ые) {type}: {title}",
|
"NewObjectType": "Новый(ые) {type}: {title}",
|
||||||
"RemovedObjectType": "Удален(ы) {type} : {title}",
|
"RemovedObjectType": "Удален(ы) {type} : {title}",
|
||||||
"AttributeSetTo": "{name} установлен на {value}"
|
"AttributeSetTo": "{name} установлен на {value}",
|
||||||
|
"AddedTag": "Добавлен тег: {title}",
|
||||||
|
"RemovedTag": "Удален тег: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -53,6 +53,8 @@
|
|||||||
"UpdatedObject": "更新 {object}",
|
"UpdatedObject": "更新 {object}",
|
||||||
"NewObjectType": "新 {type}: {title}",
|
"NewObjectType": "新 {type}: {title}",
|
||||||
"RemovedObjectType": "移除 {type} : {title}",
|
"RemovedObjectType": "移除 {type} : {title}",
|
||||||
"AttributeSetTo": "{name} 设置为 {value}"
|
"AttributeSetTo": "{name} 设置为 {value}",
|
||||||
|
"AddedTag": "添加标签: {title}",
|
||||||
|
"RemovedTag": "移除标签: {title}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -320,7 +320,9 @@ export default plugin(activityId, {
|
|||||||
UpdatedObject: '' as IntlString,
|
UpdatedObject: '' as IntlString,
|
||||||
NewObjectType: '' as IntlString,
|
NewObjectType: '' as IntlString,
|
||||||
RemovedObjectType: '' as IntlString,
|
RemovedObjectType: '' as IntlString,
|
||||||
AttributeSetTo: '' as IntlString
|
AttributeSetTo: '' as IntlString,
|
||||||
|
AddedTag: '' as IntlString,
|
||||||
|
RemovedTag: '' as IntlString
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
Activity: '' as AnyComponent,
|
Activity: '' as AnyComponent,
|
||||||
|
@ -29,11 +29,11 @@
|
|||||||
closeTooltip,
|
closeTooltip,
|
||||||
deviceOptionsStore as deviceInfo,
|
deviceOptionsStore as deviceInfo,
|
||||||
day as getDay,
|
day as getDay,
|
||||||
getWeekStart,
|
|
||||||
getWeekDayName,
|
getWeekDayName,
|
||||||
|
getWeekStart,
|
||||||
|
isWeekend,
|
||||||
resizeObserver,
|
resizeObserver,
|
||||||
ticker,
|
ticker
|
||||||
isWeekend
|
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { showMenu } from '@hcengineering/view-resources'
|
import { showMenu } from '@hcengineering/view-resources'
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
@ -48,6 +48,7 @@
|
|||||||
import calendar from '../plugin'
|
import calendar from '../plugin'
|
||||||
import { isReadOnly, updateReccuringInstance } from '../utils'
|
import { isReadOnly, updateReccuringInstance } from '../utils'
|
||||||
import EventElement from './EventElement.svelte'
|
import EventElement from './EventElement.svelte'
|
||||||
|
import TimeDuration from './TimeDuration.svelte'
|
||||||
|
|
||||||
export let events: Event[]
|
export let events: Event[]
|
||||||
export let selectedDate: Date = new Date()
|
export let selectedDate: Date = new Date()
|
||||||
@ -342,6 +343,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const calcTime = (events: CalendarItem[]): number => {
|
||||||
|
if (events.length === 0) return 0
|
||||||
|
|
||||||
|
// Extract and sort intervals by start time
|
||||||
|
const intervals = events.map((it) => [it.date, it.dueDate]).sort((a, b) => a[0] - b[0])
|
||||||
|
|
||||||
|
// Merge overlapping intervals
|
||||||
|
const mergedIntervals = []
|
||||||
|
let currentStart = intervals[0][0]
|
||||||
|
let currentEnd = intervals[0][1]
|
||||||
|
|
||||||
|
for (let i = 1; i < intervals.length; i++) {
|
||||||
|
const [nextStart, nextEnd] = intervals[i]
|
||||||
|
|
||||||
|
// If intervals overlap, extend the current end if needed
|
||||||
|
if (nextStart <= currentEnd) {
|
||||||
|
currentEnd = Math.max(currentEnd, nextEnd)
|
||||||
|
} else {
|
||||||
|
// No overlap, add current interval to merged list and start a new one
|
||||||
|
mergedIntervals.push([currentStart, currentEnd])
|
||||||
|
currentStart = nextStart
|
||||||
|
currentEnd = nextEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last interval
|
||||||
|
mergedIntervals.push([currentStart, currentEnd])
|
||||||
|
|
||||||
|
// Calculate total duration in hours
|
||||||
|
const totalHours = mergedIntervals.reduce((sum, [start, end]) => sum + (end - start) / (1000 * 60 * 60), 0)
|
||||||
|
|
||||||
|
return totalHours * 1000 * 60 * 60
|
||||||
|
}
|
||||||
|
|
||||||
const checkIntersect = (date1: CalendarItem | CalendarElement, date2: CalendarItem | CalendarElement): boolean => {
|
const checkIntersect = (date1: CalendarItem | CalendarElement, date2: CalendarItem | CalendarElement): boolean => {
|
||||||
return (
|
return (
|
||||||
(date2.date <= date1.date && date2.dueDate > date1.date) ||
|
(date2.date <= date1.date && date2.dueDate > date1.date) ||
|
||||||
@ -818,6 +853,7 @@
|
|||||||
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
|
{#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
|
||||||
{@const day = getDay(weekStart, dayOfWeek)}
|
{@const day = getDay(weekStart, dayOfWeek)}
|
||||||
{@const tday = areDatesEqual(todayDate, day)}
|
{@const tday = areDatesEqual(todayDate, day)}
|
||||||
|
{@const tEvents = calcTime(toCalendar(events, day))}
|
||||||
<div class="sticky-header head title" class:center={displayedDaysCount > 1}>
|
<div class="sticky-header head title" class:center={displayedDaysCount > 1}>
|
||||||
<span class="day" class:today={tday}>{day.getDate()}</span>
|
<span class="day" class:today={tday}>{day.getDate()}</span>
|
||||||
{#if tday}
|
{#if tday}
|
||||||
@ -828,6 +864,11 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<span class="weekday">{getWeekDayName(day, weekFormat)}</span>
|
<span class="weekday">{getWeekDayName(day, weekFormat)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if tEvents !== 0}
|
||||||
|
<span style:min-width={'3rem'}>
|
||||||
|
<TimeDuration value={tEvents} />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -17,7 +17,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Schedule } from '@hcengineering/calendar'
|
import { Schedule } from '@hcengineering/calendar'
|
||||||
import { getCurrentEmployee } from '@hcengineering/contact'
|
import { getCurrentEmployee } from '@hcengineering/contact'
|
||||||
import presentation, { createQuery, getClient, getCurrentWorkspaceUrl, MessageBox } from '@hcengineering/presentation'
|
import presentation, {
|
||||||
|
copyTextToClipboard,
|
||||||
|
createQuery,
|
||||||
|
getClient,
|
||||||
|
getCurrentWorkspaceUrl,
|
||||||
|
MessageBox
|
||||||
|
} from '@hcengineering/presentation'
|
||||||
import { Action, ButtonIcon, IconAdd, IconDelete, IconLink, NavItem, showPopup } from '@hcengineering/ui'
|
import { Action, ButtonIcon, IconAdd, IconDelete, IconLink, NavItem, showPopup } from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
import view from '@hcengineering/view'
|
||||||
import { TreeElement } from '@hcengineering/view-resources'
|
import { TreeElement } from '@hcengineering/view-resources'
|
||||||
@ -91,7 +97,7 @@
|
|||||||
okLabel: presentation.string.CopyLink,
|
okLabel: presentation.string.CopyLink,
|
||||||
canSubmit: false,
|
canSubmit: false,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(link)
|
await copyTextToClipboard(link)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined
|
undefined
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { themeStore, formatDuration } from '@hcengineering/ui'
|
||||||
|
|
||||||
|
export let value: number
|
||||||
|
|
||||||
|
let duration: string
|
||||||
|
$: formatDuration(value, $themeStore.language).then((res) => {
|
||||||
|
duration = res
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{duration}
|
@ -93,6 +93,7 @@
|
|||||||
const _class = hierarchy.getClass(desc)
|
const _class = hierarchy.getClass(desc)
|
||||||
if (_class.label === undefined) continue
|
if (_class.label === undefined) continue
|
||||||
if (_class.kind !== ClassifierKind.CLASS) continue
|
if (_class.kind !== ClassifierKind.CLASS) continue
|
||||||
|
if ((_class as MasterTag).removed === true) continue
|
||||||
added.add(desc)
|
added.add(desc)
|
||||||
toAdd.push(_class)
|
toAdd.push(_class)
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,6 @@
|
|||||||
preferenceQuery.query(
|
preferenceQuery.query(
|
||||||
view.class.ViewletPreference,
|
view.class.ViewletPreference,
|
||||||
{
|
{
|
||||||
space: core.space.Workspace,
|
|
||||||
attachedTo: viewletId
|
attachedTo: viewletId
|
||||||
},
|
},
|
||||||
(res) => {
|
(res) => {
|
||||||
@ -129,7 +128,7 @@
|
|||||||
|
|
||||||
const filledData = fillDefaults(hierarchy, data, object._class)
|
const filledData = fillDefaults(hierarchy, data, object._class)
|
||||||
|
|
||||||
const _id = await client.createDoc(object._class, core.space.Workspace, filledData)
|
const _id = await client.createDoc(object._class, object.space, filledData)
|
||||||
|
|
||||||
Analytics.handleEvent(CardEvents.CardCreated)
|
Analytics.handleEvent(CardEvents.CardCreated)
|
||||||
|
|
||||||
|
@ -35,7 +35,13 @@
|
|||||||
const desc = hierarchy.getDescendants(_class)
|
const desc = hierarchy.getDescendants(_class)
|
||||||
for (const clazz of desc) {
|
for (const clazz of desc) {
|
||||||
const cls = hierarchy.getClass(clazz)
|
const cls = hierarchy.getClass(clazz)
|
||||||
if (cls.extends === _class && !cls.hidden && kind === cls.kind && cls.label !== undefined) {
|
if (
|
||||||
|
cls.extends === _class &&
|
||||||
|
!cls.hidden &&
|
||||||
|
kind === cls.kind &&
|
||||||
|
cls.label !== undefined &&
|
||||||
|
(cls as MasterTag).removed !== true
|
||||||
|
) {
|
||||||
result.push(clazz)
|
result.push(clazz)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,12 +24,7 @@ import aiBot from '@hcengineering/ai-bot'
|
|||||||
import { summarizeMessages as aiSummarizeMessages, translate as aiTranslate } from '@hcengineering/ai-bot-resources'
|
import { summarizeMessages as aiSummarizeMessages, translate as aiTranslate } from '@hcengineering/ai-bot-resources'
|
||||||
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
|
import { type Channel, type ChatMessage, type DirectMessage, type ThreadMessage } from '@hcengineering/chunter'
|
||||||
import contact, { getCurrentEmployee, getName, type Employee, type Person } from '@hcengineering/contact'
|
import contact, { getCurrentEmployee, getName, type Employee, type Person } from '@hcengineering/contact'
|
||||||
import {
|
import { employeeByAccountStore, employeeByIdStore, PersonIcon } from '@hcengineering/contact-resources'
|
||||||
employeeByAccountStore,
|
|
||||||
employeeByIdStore,
|
|
||||||
PersonIcon,
|
|
||||||
personRefByAccountUuidStore
|
|
||||||
} from '@hcengineering/contact-resources'
|
|
||||||
import core, {
|
import core, {
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
notEmpty,
|
notEmpty,
|
||||||
@ -562,29 +557,29 @@ export async function startConversationAction (docs?: Employee | Employee[]): Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDirect (employeeIds: Array<Ref<Person>>): Promise<Ref<DirectMessage>> {
|
export async function createDirect (employeeIds: Array<Ref<Employee>>): Promise<Ref<DirectMessage>> {
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const me = getCurrentEmployee()
|
const me = getCurrentEmployee()
|
||||||
const myAcc = getCurrentAccount()
|
const myAcc = getCurrentAccount()
|
||||||
|
|
||||||
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
|
const existingDms = await client.findAll(chunter.class.DirectMessage, {})
|
||||||
const newDirectPersons = employeeIds.includes(me) ? employeeIds : [...employeeIds, me]
|
const newDirectEmployeeIds = Array.from(new Set([...employeeIds, me]))
|
||||||
const newPersonsSet = new Set(newDirectPersons)
|
|
||||||
|
|
||||||
let direct: DirectMessage | undefined
|
let direct: DirectMessage | undefined
|
||||||
const personRefByAccountUuid = get(personRefByAccountUuidStore)
|
|
||||||
const employeeById = get(employeeByIdStore)
|
const employeeById = get(employeeByIdStore)
|
||||||
|
const newDirectAccounts = new Set(newDirectEmployeeIds.map((it) => employeeById.get(it)?.personUuid).filter(notEmpty))
|
||||||
|
|
||||||
for (const dm of existingDms) {
|
for (const dm of existingDms) {
|
||||||
const existPersonsSet = new Set(dm.members.map((acc) => personRefByAccountUuid.get(acc)).filter(notEmpty))
|
const existAccounts = new Set(dm.members)
|
||||||
|
|
||||||
if (existPersonsSet.size !== newPersonsSet.size) {
|
if (existAccounts.size !== newDirectAccounts.size) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let match = true
|
let match = true
|
||||||
for (const person of existPersonsSet) {
|
for (const acc of existAccounts) {
|
||||||
if (!newPersonsSet.has(person as Ref<Person>)) {
|
if (!newDirectAccounts.has(acc)) {
|
||||||
match = false
|
match = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -603,15 +598,7 @@ export async function createDirect (employeeIds: Array<Ref<Person>>): Promise<Re
|
|||||||
description: '',
|
description: '',
|
||||||
private: true,
|
private: true,
|
||||||
archived: false,
|
archived: false,
|
||||||
members: newDirectPersons.map((person) => {
|
members: Array.from(newDirectAccounts)
|
||||||
const employee = employeeById.get(person as Ref<Employee>)
|
|
||||||
|
|
||||||
if (employee?.personUuid === undefined) {
|
|
||||||
throw new Error(`Account id not found for person ${person}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return employee.personUuid
|
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const context = await client.findOne(notification.class.DocNotifyContext, {
|
const context = await client.findOne(notification.class.DocNotifyContext, {
|
||||||
|
@ -298,7 +298,7 @@ class Connection implements ClientConnection {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.rateLimit !== undefined && resp.rateLimit.remaining < 10) {
|
if (resp.rateLimit !== undefined && resp.rateLimit.remaining < 50) {
|
||||||
console.log(
|
console.log(
|
||||||
'Rate limits:',
|
'Rate limits:',
|
||||||
resp.rateLimit.remaining,
|
resp.rateLimit.remaining,
|
||||||
|
@ -45,6 +45,8 @@
|
|||||||
import { getProjectDocumentLink } from '../navigation'
|
import { getProjectDocumentLink } from '../navigation'
|
||||||
import documentRes from '../plugin'
|
import documentRes from '../plugin'
|
||||||
import {
|
import {
|
||||||
|
$approvalRequest as approvalRequest,
|
||||||
|
$reviewRequest as reviewRequest,
|
||||||
$activeRightPanelTab as activeRightPanelTab,
|
$activeRightPanelTab as activeRightPanelTab,
|
||||||
$availableEditorModes as availableEditorModes,
|
$availableEditorModes as availableEditorModes,
|
||||||
$availableRightPanelTabs as availableRightPanelTabs,
|
$availableRightPanelTabs as availableRightPanelTabs,
|
||||||
@ -140,7 +142,12 @@
|
|||||||
{
|
{
|
||||||
label: documentRes.string.TeamTab,
|
label: documentRes.string.TeamTab,
|
||||||
component: EditDocTeam,
|
component: EditDocTeam,
|
||||||
props: { controlledDoc: $controlledDocument, editable: $isDocumentOwner }
|
props: {
|
||||||
|
controlledDoc: $controlledDocument,
|
||||||
|
editable: $isDocumentOwner,
|
||||||
|
reviewRequest: $reviewRequest,
|
||||||
|
approvalRequest: $approvalRequest
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: documentRes.string.ReleaseTab,
|
label: documentRes.string.ReleaseTab,
|
||||||
|
@ -25,14 +25,13 @@
|
|||||||
export let canChangeReviewers: boolean = true
|
export let canChangeReviewers: boolean = true
|
||||||
export let canChangeApprovers: boolean = true
|
export let canChangeApprovers: boolean = true
|
||||||
export let canChangeCoAuthors: boolean = true
|
export let canChangeCoAuthors: boolean = true
|
||||||
|
export let reviewers: Ref<Employee>[] = controlledDoc?.reviewers ?? []
|
||||||
|
export let approvers: Ref<Employee>[] = controlledDoc?.approvers ?? []
|
||||||
|
export let coAuthors: Ref<Employee>[] = controlledDoc?.coAuthors ?? []
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const currentEmployee = getCurrentEmployee()
|
const currentEmployee = getCurrentEmployee()
|
||||||
|
|
||||||
$: reviewers = controlledDoc.reviewers
|
|
||||||
$: approvers = controlledDoc.approvers
|
|
||||||
$: coAuthors = controlledDoc.coAuthors
|
|
||||||
|
|
||||||
$: permissionsSpace = space === documents.space.UnsortedTemplates ? documents.space.QualityDocuments : space
|
$: permissionsSpace = space === documents.space.UnsortedTemplates ? documents.space.QualityDocuments : space
|
||||||
|
|
||||||
function getPermittedPersons (
|
function getPermittedPersons (
|
||||||
|
@ -13,37 +13,92 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Scroller } from '@hcengineering/ui'
|
import { Employee, Person } from '@hcengineering/contact'
|
||||||
|
import {
|
||||||
|
ControlledDocument,
|
||||||
|
ControlledDocumentState,
|
||||||
|
DocumentApprovalRequest,
|
||||||
|
DocumentReviewRequest,
|
||||||
|
DocumentState
|
||||||
|
} from '@hcengineering/controlled-documents'
|
||||||
import { Ref } from '@hcengineering/core'
|
import { Ref } from '@hcengineering/core'
|
||||||
import { ControlledDocument, ControlledDocumentState, DocumentState } from '@hcengineering/controlled-documents'
|
|
||||||
import { Employee } from '@hcengineering/contact'
|
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import { Scroller } from '@hcengineering/ui'
|
||||||
|
|
||||||
import DocTeam from './DocTeam.svelte'
|
import DocTeam from './DocTeam.svelte'
|
||||||
|
|
||||||
export let controlledDoc: ControlledDocument
|
export let controlledDoc: ControlledDocument
|
||||||
export let editable: boolean = true
|
export let editable: boolean = true
|
||||||
|
export let reviewRequest: DocumentReviewRequest | undefined
|
||||||
|
export let approvalRequest: DocumentApprovalRequest | undefined
|
||||||
|
|
||||||
$: canChangeCoAuthors =
|
$: controlledState = controlledDoc.controlledState ?? null
|
||||||
editable && controlledDoc.state === DocumentState.Draft && controlledDoc.controlledState == null
|
|
||||||
$: canChangeReviewers =
|
$: isEditableDraft = editable && controlledDoc.state === DocumentState.Draft
|
||||||
editable && controlledDoc.state === DocumentState.Draft && controlledDoc.controlledState == null
|
|
||||||
$: canChangeApprovers =
|
$: inCleanState = controlledState === null
|
||||||
editable &&
|
$: inReview = controlledState === ControlledDocumentState.InReview && reviewRequest !== undefined
|
||||||
((controlledDoc.state === DocumentState.Draft && controlledDoc.controlledState == null) ||
|
$: inApproval = controlledState === ControlledDocumentState.InApproval && approvalRequest !== undefined
|
||||||
controlledDoc.controlledState === ControlledDocumentState.InReview ||
|
$: isReviewed = controlledState === ControlledDocumentState.Reviewed
|
||||||
controlledDoc.controlledState === ControlledDocumentState.Reviewed)
|
|
||||||
|
$: canChangeCoAuthors = isEditableDraft && inCleanState
|
||||||
|
$: canChangeReviewers = isEditableDraft && (inCleanState || inReview)
|
||||||
|
$: canChangeApprovers = isEditableDraft && (inCleanState || inApproval || inReview || isReviewed)
|
||||||
|
|
||||||
|
$: reviewers = (reviewRequest?.requested as Ref<Employee>[]) ?? controlledDoc.reviewers
|
||||||
|
$: approvers = (approvalRequest?.requested as Ref<Employee>[]) ?? controlledDoc.approvers
|
||||||
|
$: coAuthors = controlledDoc.coAuthors
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
async function handleUpdate ({
|
async function handleUpdate ({
|
||||||
detail
|
detail
|
||||||
}: {
|
}: {
|
||||||
detail: { type: 'reviewers' | 'approvers', users: Ref<Employee>[] }
|
detail: { type: 'reviewers' | 'approvers', users: Ref<Person>[] }
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { type, users } = detail
|
const { type, users } = detail
|
||||||
|
|
||||||
await client.update(controlledDoc, { [type]: users })
|
const request = detail.type === 'reviewers' ? reviewRequest : approvalRequest
|
||||||
|
|
||||||
|
const ops = client.apply()
|
||||||
|
|
||||||
|
if (request?._id !== undefined) {
|
||||||
|
const requested = request.requested?.slice() ?? []
|
||||||
|
const requestedSet = new Set<Ref<Person>>(requested)
|
||||||
|
|
||||||
|
const addedPersons = new Set<Ref<Person>>()
|
||||||
|
const removedPersons = new Set<Ref<Person>>(requested)
|
||||||
|
|
||||||
|
for (const u of users) {
|
||||||
|
if (requestedSet.has(u)) {
|
||||||
|
removedPersons.delete(u)
|
||||||
|
} else {
|
||||||
|
addedPersons.add(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const approved = request.approved?.slice() ?? []
|
||||||
|
const approvedDates = request.approvedDates?.slice() ?? []
|
||||||
|
|
||||||
|
for (const u of removedPersons) {
|
||||||
|
const idx = approved.indexOf(u)
|
||||||
|
if (idx === -1) continue
|
||||||
|
approved.splice(idx, 1)
|
||||||
|
approvedDates.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredApprovesCount = users.length
|
||||||
|
|
||||||
|
await ops.update(request, {
|
||||||
|
requested: users,
|
||||||
|
approved,
|
||||||
|
approvedDates,
|
||||||
|
requiredApprovesCount
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await ops.update(controlledDoc, { [type]: users })
|
||||||
|
await ops.commit()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -56,6 +111,9 @@
|
|||||||
{canChangeCoAuthors}
|
{canChangeCoAuthors}
|
||||||
{canChangeReviewers}
|
{canChangeReviewers}
|
||||||
{canChangeApprovers}
|
{canChangeApprovers}
|
||||||
|
{reviewers}
|
||||||
|
{approvers}
|
||||||
|
{coAuthors}
|
||||||
on:update={handleUpdate}
|
on:update={handleUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,7 +18,8 @@ import {
|
|||||||
type ControlledDocument,
|
type ControlledDocument,
|
||||||
type DocumentComment,
|
type DocumentComment,
|
||||||
type DocumentTraining,
|
type DocumentTraining,
|
||||||
type Project
|
type Project,
|
||||||
|
type DocumentTemplate
|
||||||
} from '@hcengineering/controlled-documents'
|
} from '@hcengineering/controlled-documents'
|
||||||
import attachment from '@hcengineering/attachment'
|
import attachment from '@hcengineering/attachment'
|
||||||
import { type Class, type DocumentQuery, type Ref, SortingOrder } from '@hcengineering/core'
|
import { type Class, type DocumentQuery, type Ref, SortingOrder } from '@hcengineering/core'
|
||||||
@ -94,7 +95,7 @@ const queryDocumentVersionsFx = createEffect((payload: ControlledDocument) => {
|
|||||||
documents.class.ControlledDocument,
|
documents.class.ControlledDocument,
|
||||||
{
|
{
|
||||||
seqNumber: payload.seqNumber,
|
seqNumber: payload.seqNumber,
|
||||||
template: payload.template
|
template: (payload.template ?? null) as Ref<DocumentTemplate>
|
||||||
},
|
},
|
||||||
(result) => {
|
(result) => {
|
||||||
documentAllVersionsUpdated(result ?? [])
|
documentAllVersionsUpdated(result ?? [])
|
||||||
|
@ -3,7 +3,7 @@ import { connectMeeting, disconnectMeeting } from '@hcengineering/ai-bot-resourc
|
|||||||
import { Analytics } from '@hcengineering/analytics'
|
import { Analytics } from '@hcengineering/analytics'
|
||||||
import calendar, { type Event, type Schedule, getAllEvents } from '@hcengineering/calendar'
|
import calendar, { type Event, type Schedule, getAllEvents } from '@hcengineering/calendar'
|
||||||
import chunter from '@hcengineering/chunter'
|
import chunter from '@hcengineering/chunter'
|
||||||
import contact, { type Employee, getCurrentEmployee, getName, type Person } from '@hcengineering/contact'
|
import contact, { getCurrentEmployee, getName, type Person } from '@hcengineering/contact'
|
||||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||||
import core, {
|
import core, {
|
||||||
AccountRole,
|
AccountRole,
|
||||||
@ -956,7 +956,7 @@ export async function tryConnect (
|
|||||||
await client.update(invite, { status: invite.room === room._id ? RequestStatus.Approved : RequestStatus.Rejected })
|
await client.update(invite, { status: invite.room === room._id ? RequestStatus.Approved : RequestStatus.Rejected })
|
||||||
}
|
}
|
||||||
|
|
||||||
const isGuest = (currentPerson as Employee).role === AccountRole.Guest
|
const isGuest = client.getHierarchy().as(currentPerson, contact.mixin.Employee).role === AccountRole.Guest
|
||||||
if ((room.access === RoomAccess.Knock || isGuest) && (!isOffice(room) || room.person !== currentPerson._id)) {
|
if ((room.access === RoomAccess.Knock || isGuest) && (!isOffice(room) || room.person !== currentPerson._id)) {
|
||||||
const _id = await client.createDoc(love.class.JoinRequest, core.space.Workspace, {
|
const _id = await client.createDoc(love.class.JoinRequest, core.space.Workspace, {
|
||||||
person: currentPerson._id,
|
person: currentPerson._id,
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import WithTeamData from '../WithTeamData.svelte'
|
import WithTeamData from '../WithTeamData.svelte'
|
||||||
import { toSlots } from '../utils'
|
import { toSlots } from '../utils'
|
||||||
import DayPlan from './DayPlan.svelte'
|
import DayPlan from './DayPlan.svelte'
|
||||||
|
import { personRefByAccountUuidStore } from '@hcengineering/contact-resources'
|
||||||
|
|
||||||
export let space: Ref<Project>
|
export let space: Ref<Project>
|
||||||
export let currentDate: Date
|
export let currentDate: Date
|
||||||
@ -36,9 +37,22 @@
|
|||||||
todayFrom,
|
todayFrom,
|
||||||
todayTo
|
todayTo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$: persons = (project?.members ?? [])
|
||||||
|
.map((it) => $personRefByAccountUuidStore.get(it))
|
||||||
|
.filter((it) => it !== undefined)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithTeamData {space} fromDate={yesterdayFrom} toDate={todayTo} bind:project bind:todos bind:slots bind:events />
|
<WithTeamData
|
||||||
|
{space}
|
||||||
|
fromDate={yesterdayFrom}
|
||||||
|
toDate={todayTo}
|
||||||
|
bind:project
|
||||||
|
bind:todos
|
||||||
|
bind:slots
|
||||||
|
bind:events
|
||||||
|
{persons}
|
||||||
|
/>
|
||||||
|
|
||||||
<Header bind:currentDate />
|
<Header bind:currentDate />
|
||||||
{#if project}
|
{#if project}
|
||||||
|
@ -43,7 +43,9 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each gitem.events as event}
|
{#each gitem.events as event}
|
||||||
|
{#if event.overlap === 0}
|
||||||
<EventItem item={event} showTime={expanded} />
|
<EventItem item={event} showTime={expanded} />
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if gitem.busy.slots.length > 0}
|
{#if gitem.busy.slots.length > 0}
|
||||||
|
@ -16,8 +16,12 @@
|
|||||||
import calendar, { Event, getAllEvents } from '@hcengineering/calendar'
|
import calendar, { Event, getAllEvents } from '@hcengineering/calendar'
|
||||||
import { calendarByIdStore } from '@hcengineering/calendar-resources'
|
import { calendarByIdStore } from '@hcengineering/calendar-resources'
|
||||||
import { getCurrentEmployee, Person } from '@hcengineering/contact'
|
import { getCurrentEmployee, Person } from '@hcengineering/contact'
|
||||||
import { socialIdsByPersonRefStore, personRefByPersonIdStore } from '@hcengineering/contact-resources'
|
import {
|
||||||
import core, { Doc, IdMap, Ref, Timestamp, Tx, TxCUD, TxCreateDoc, TxUpdateDoc } from '@hcengineering/core'
|
personRefByAccountUuidStore,
|
||||||
|
personRefByPersonIdStore,
|
||||||
|
socialIdsByPersonRefStore
|
||||||
|
} from '@hcengineering/contact-resources'
|
||||||
|
import core, { Doc, IdMap, Ref, Timestamp, Tx, TxCreateDoc, TxCUD, TxUpdateDoc } from '@hcengineering/core'
|
||||||
import { Asset } from '@hcengineering/platform'
|
import { Asset } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { Project } from '@hcengineering/task'
|
import { Project } from '@hcengineering/task'
|
||||||
@ -43,7 +47,10 @@
|
|||||||
let slots: WorkSlot[] = []
|
let slots: WorkSlot[] = []
|
||||||
let events: Event[] = []
|
let events: Event[] = []
|
||||||
let todos: IdMap<ToDo> = new Map()
|
let todos: IdMap<ToDo> = new Map()
|
||||||
let persons: Ref<Person>[] = []
|
|
||||||
|
$: persons = (project?.members ?? [])
|
||||||
|
.map((it) => $personRefByAccountUuidStore.get(it))
|
||||||
|
.filter((it) => it !== undefined)
|
||||||
|
|
||||||
const txCreateQuery = createQuery()
|
const txCreateQuery = createQuery()
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
import { groupTeamData, toSlots } from '../utils'
|
import { groupTeamData, toSlots } from '../utils'
|
||||||
import EventElement from './EventElement.svelte'
|
import EventElement from './EventElement.svelte'
|
||||||
import PersonCalendar from './PersonCalendar.svelte'
|
import PersonCalendar from './PersonCalendar.svelte'
|
||||||
|
import { personRefByAccountUuidStore } from '@hcengineering/contact-resources'
|
||||||
|
|
||||||
export let space: Ref<Project>
|
export let space: Ref<Project>
|
||||||
export let currentDate: Date
|
export let currentDate: Date
|
||||||
@ -37,7 +38,10 @@
|
|||||||
let slots: WorkSlot[] = []
|
let slots: WorkSlot[] = []
|
||||||
let events: Event[] = []
|
let events: Event[] = []
|
||||||
let todos: IdMap<ToDo> = new Map()
|
let todos: IdMap<ToDo> = new Map()
|
||||||
let persons: Ref<Person>[] = []
|
|
||||||
|
$: persons = (project?.members ?? [])
|
||||||
|
.map((it) => $personRefByAccountUuidStore.get(it))
|
||||||
|
.filter((it) => it !== undefined)
|
||||||
|
|
||||||
function calcHourWidth (events: Event[], totalWidth: number): number[] {
|
function calcHourWidth (events: Event[], totalWidth: number): number[] {
|
||||||
const hours = new Map<number, number>()
|
const hours = new Map<number, number>()
|
||||||
|
@ -173,10 +173,10 @@ function crossWith (a: EventVars, b: EventVars): EventVars[] {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param events - without overlaps
|
* @param events - without overlaps
|
||||||
* @param event -
|
* @param event - any object with date and dueDate properties
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function calcOverlap (events: EventVars[], event: Event): { events: EventVars[], total: number } {
|
function calcOverlap (events: EventVars[], event: EventVars): { events: EventVars[], total: number } {
|
||||||
let tmp: EventVars[] = [{ date: event.date, dueDate: event.dueDate }]
|
let tmp: EventVars[] = [{ date: event.date, dueDate: event.dueDate }]
|
||||||
for (const a of events) {
|
for (const a of events) {
|
||||||
const newTmp: EventVars[] = []
|
const newTmp: EventVars[] = []
|
||||||
|
@ -162,11 +162,11 @@ async function getUpdateText (update: ActivityUpdate, card: Card, hierarchy: Hie
|
|||||||
const clazz = hierarchy.getClass(update.tag)
|
const clazz = hierarchy.getClass(update.tag)
|
||||||
if (update.action === 'add') {
|
if (update.action === 'add') {
|
||||||
const tagName = await translate(clazz.label, {})
|
const tagName = await translate(clazz.label, {})
|
||||||
return await translate(activity.string.NewObjectType, { type: 'tag', title: tagName })
|
return await translate(activity.string.AddedTag, { title: tagName })
|
||||||
}
|
}
|
||||||
if (update.action === 'remove') {
|
if (update.action === 'remove') {
|
||||||
const tagName = await translate(clazz.label, {})
|
const tagName = await translate(clazz.label, {})
|
||||||
return await translate(activity.string.RemovedObjectType, { type: 'tag', title: tagName })
|
return await translate(activity.string.RemovedTag, { title: tagName })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
|
@ -1401,7 +1401,8 @@ class MongoAdapter extends MongoAdapterBase {
|
|||||||
const filter = { _id: tx.objectId }
|
const filter = { _id: tx.objectId }
|
||||||
const modifyOp = {
|
const modifyOp = {
|
||||||
modifiedBy: tx.modifiedBy,
|
modifiedBy: tx.modifiedBy,
|
||||||
modifiedOn: tx.modifiedOn
|
modifiedOn: tx.modifiedOn,
|
||||||
|
'%hash%': this.curHash()
|
||||||
}
|
}
|
||||||
if (isOperator(tx.attributes)) {
|
if (isOperator(tx.attributes)) {
|
||||||
const update = { ...this.translateMixinAttrs(tx.mixin, tx.attributes), $set: { ...modifyOp } }
|
const update = { ...this.translateMixinAttrs(tx.mixin, tx.attributes), $set: { ...modifyOp } }
|
||||||
|
@ -40,15 +40,13 @@ export class SlidingWindowRateLimitter {
|
|||||||
// Update reset time
|
// Update reset time
|
||||||
rateLimit.resetTime = now + this.rateLimitWindow
|
rateLimit.resetTime = now + this.rateLimitWindow
|
||||||
|
|
||||||
|
if (rateLimit.requests.length <= this.rateLimitMax) {
|
||||||
rateLimit.requests.push(now + (rateLimit.rejectedRequests > this.rateLimitMax * 2 ? this.rateLimitWindow * 5 : 0))
|
rateLimit.requests.push(now + (rateLimit.rejectedRequests > this.rateLimitMax * 2 ? this.rateLimitWindow * 5 : 0))
|
||||||
|
}
|
||||||
|
|
||||||
if (rateLimit.requests.length > this.rateLimitMax) {
|
if (rateLimit.requests.length > this.rateLimitMax) {
|
||||||
rateLimit.rejectedRequests++
|
rateLimit.rejectedRequests++
|
||||||
|
|
||||||
if (rateLimit.requests.length > this.rateLimitMax * 2) {
|
|
||||||
// Keep only last requests
|
|
||||||
rateLimit.requests.splice(0, rateLimit.requests.length - this.rateLimitMax)
|
|
||||||
}
|
|
||||||
// Find when the oldest request will exit the window
|
// Find when the oldest request will exit the window
|
||||||
const someRequest = Math.round(Math.random() * rateLimit.requests.length)
|
const someRequest = Math.round(Math.random() * rateLimit.requests.length)
|
||||||
const nextAvailableTime = rateLimit.requests[someRequest] + this.rateLimitWindow
|
const nextAvailableTime = rateLimit.requests[someRequest] + this.rateLimitWindow
|
||||||
|
@ -123,7 +123,6 @@ describe('SlidingWindowRateLimitter', () => {
|
|||||||
clock += 10000
|
clock += 10000
|
||||||
|
|
||||||
const r2 = limiter.checkRateLimit('user1')
|
const r2 = limiter.checkRateLimit('user1')
|
||||||
expect(r2.remaining).toBe(0)
|
expect(r2.remaining).toBe(9)
|
||||||
expect(r2.retryAfter).toBeDefined()
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -88,6 +88,16 @@ export class SyncManager {
|
|||||||
await this.keyValueClient.setValue(historyKey, history)
|
await this.keyValueClient.setValue(historyKey, history)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getPageToken (userId: PersonId): Promise<string | null> {
|
||||||
|
const pageTokenKey = this.getPageTokenKey(userId)
|
||||||
|
return await this.keyValueClient.getValue<string>(pageTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setPageToken (userId: PersonId, pageToken: string): Promise<void> {
|
||||||
|
const pageTokenKey = this.getPageTokenKey(userId)
|
||||||
|
await this.keyValueClient.setValue(pageTokenKey, pageToken)
|
||||||
|
}
|
||||||
|
|
||||||
private async partSync (userId: PersonId, userEmail: string | undefined, historyId: string): Promise<void> {
|
private async partSync (userId: PersonId, userEmail: string | undefined, historyId: string): Promise<void> {
|
||||||
if (userEmail === undefined) {
|
if (userEmail === undefined) {
|
||||||
throw new Error('Cannot sync without user email')
|
throw new Error('Cannot sync without user email')
|
||||||
@ -151,7 +161,10 @@ export class SyncManager {
|
|||||||
if (userEmail === undefined) {
|
if (userEmail === undefined) {
|
||||||
throw new Error('Cannot sync without user email')
|
throw new Error('Cannot sync without user email')
|
||||||
}
|
}
|
||||||
const pageToken: string | undefined = undefined
|
|
||||||
|
// Get saved page token if exists to resume sync
|
||||||
|
let pageToken: string | undefined = (await this.getPageToken(userId)) ?? undefined
|
||||||
|
|
||||||
const query: gmail_v1.Params$Resource$Users$Messages$List = {
|
const query: gmail_v1.Params$Resource$Users$Messages$List = {
|
||||||
userId: 'me',
|
userId: 'me',
|
||||||
pageToken
|
pageToken
|
||||||
@ -160,49 +173,57 @@ export class SyncManager {
|
|||||||
query.q = q
|
query.q = q
|
||||||
}
|
}
|
||||||
let currentHistoryId: string | undefined
|
let currentHistoryId: string | undefined
|
||||||
|
let totalProcessedMessages = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messagesIds: string[] = []
|
// Process one page at a time
|
||||||
while (true) {
|
while (true) {
|
||||||
await this.rateLimiter.take(5)
|
await this.rateLimiter.take(5)
|
||||||
const messages = await this.gmail.messages.list(query)
|
const messages = await this.gmail.messages.list(query)
|
||||||
if (query.pageToken == null) {
|
|
||||||
this.ctx.info('Total messages', {
|
const ids = messages.data.messages?.map((p) => p.id).filter((id) => id != null) ?? []
|
||||||
|
this.ctx.info('Processing page', {
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
userId,
|
userId,
|
||||||
resultSizeEstimate: messages.data.resultSizeEstimate
|
messagesInPage: ids.length,
|
||||||
|
totalProcessed: totalProcessedMessages,
|
||||||
|
currentHistoryId,
|
||||||
|
pageToken: query.pageToken
|
||||||
})
|
})
|
||||||
}
|
|
||||||
const ids = messages.data.messages?.map((p) => p.id) ?? []
|
for (const id of ids) {
|
||||||
for (let index = 0; index < ids.length; index++) {
|
|
||||||
const id = ids[index]
|
|
||||||
if (id == null) continue
|
if (id == null) continue
|
||||||
messagesIds.push(id)
|
|
||||||
}
|
|
||||||
if (messages.data.nextPageToken == null) {
|
|
||||||
this.ctx.info('Break', { totalNewMessages: messagesIds.length })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
query.pageToken = messages.data.nextPageToken
|
|
||||||
}
|
|
||||||
for (let index = messagesIds.length - 1; index >= 0; index--) {
|
|
||||||
const id = messagesIds[index]
|
|
||||||
try {
|
try {
|
||||||
const message = await this.getMessage(id)
|
const message = await this.getMessage(id)
|
||||||
const historyId = message.data.historyId
|
const historyId = message.data.historyId
|
||||||
await this.messageManager.saveMessage(message, userEmail)
|
await this.messageManager.saveMessage(message, userEmail)
|
||||||
|
|
||||||
if (historyId != null && q === undefined) {
|
if (historyId != null && q === undefined) {
|
||||||
if (currentHistoryId == null || Number(currentHistoryId) < Number(historyId)) {
|
if (currentHistoryId == null || Number(currentHistoryId) < Number(historyId)) {
|
||||||
await this.setHistoryId(userId, historyId)
|
|
||||||
currentHistoryId = historyId
|
currentHistoryId = historyId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (index % 500 === 0) {
|
|
||||||
this.ctx.info('Remaining messages to sync', { workspace: this.workspace, userId, count: index })
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.ctx.error('Full sync message error', { workspace: this.workspace, userId, err })
|
this.ctx.error('Full sync message error', { workspace: this.workspace, userId, messageId: id, err })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalProcessedMessages += ids.length
|
||||||
|
|
||||||
|
if (messages.data.nextPageToken == null) {
|
||||||
|
this.ctx.info('Completed sync', { workspace: this.workspace, userId, totalMessages: totalProcessedMessages })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update page token for the next iteration
|
||||||
|
pageToken = messages.data.nextPageToken
|
||||||
|
query.pageToken = pageToken
|
||||||
|
await this.setPageToken(userId, pageToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHistoryId != null) {
|
||||||
|
await this.setHistoryId(userId, currentHistoryId)
|
||||||
|
}
|
||||||
this.ctx.info('Full sync finished', { workspaceUuid: this.workspace, userId, userEmail })
|
this.ctx.info('Full sync finished', { workspaceUuid: this.workspace, userId, userEmail })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.ctx.error('Full sync error', { workspace: this.workspace, userId, err })
|
this.ctx.error('Full sync error', { workspace: this.workspace, userId, err })
|
||||||
@ -243,4 +264,8 @@ export class SyncManager {
|
|||||||
private getHistoryKey (userId: PersonId): string {
|
private getHistoryKey (userId: PersonId): string {
|
||||||
return `history:${this.workspace}:${userId}`
|
return `history:${this.workspace}:${userId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPageTokenKey (userId: PersonId): string {
|
||||||
|
return `page-token:${this.workspace}:${userId}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user