mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 20:27:43 +02:00
Merge remote-tracking branch 'origin/develop' into staging
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Vendored
+4
-6
@@ -5,21 +5,19 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug notion import",
|
||||
"name": "Debug Notion import",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": ["src/__start.ts", "import-notion-to-teamspace", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-ws", "notion-test", "-u", "user1", "-pw", "1234", "-ts", "natalya"],
|
||||
// "args": ["src/__start.ts", "import-notion-with-teamspaces", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-ws", "ws1", "-u", "user1", "-pw", "1234"],
|
||||
// "args": ["src/__start.ts", "import-notion-to-teamspace", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-u", "user1", "-pw", "1234", "-ws", "ws1", "-ts", "notion", ],
|
||||
"args": ["src/__start.ts", "import-notion-with-teamspaces", "/home/anna/work/notion/natalya/Export-fad9ecb4-a1a5-4623-920d-df32dd423743", "-u", "user1", "-pw", "1234", "-ws", "ws1"],
|
||||
"env": {
|
||||
"SERVER_SECRET": "secret",
|
||||
"FRONT_URL": "http://localhost:8087",
|
||||
"ACCOUNTS_URL": "http://localhost:3000",
|
||||
},
|
||||
"runtimeVersion": "20",
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"sourceMaps": true,
|
||||
"outputCapture": "std",
|
||||
"cwd": "${workspaceRoot}/dev/tool"
|
||||
"cwd": "${workspaceRoot}/dev/import-tool"
|
||||
},
|
||||
{
|
||||
"address": "127.0.0.1",
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
"summary": "Build docker with platform",
|
||||
"description": "use to build all docker containers required for platform",
|
||||
"safeForSimultaneousRushProcesses": true,
|
||||
"shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot"
|
||||
"shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool"
|
||||
},
|
||||
{
|
||||
"commandKind": "global",
|
||||
|
||||
Generated
+570
-316
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM node:20
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
CMD [ "bash" ]
|
||||
@@ -27,7 +27,7 @@ rushx run-local import-notion-to-teamspace {dir} \
|
||||
* *dir* - path to the root of the extracted archive
|
||||
* *user* - your username or email
|
||||
* *password* - password
|
||||
* *workspace* - workspace name where the documents should be imported to
|
||||
* *workspace* - workspace url where the documents should be imported to
|
||||
* *teamspace* - teamspace to be created for newly imported docs
|
||||
|
||||
|
||||
@@ -58,11 +58,9 @@ rushx run-local import-notion-to-teamspace /home/john/extracted-notion-docs \
|
||||
* To import Notion workspace with teamspaces
|
||||
```
|
||||
docker run \
|
||||
-e SERVER_SECRET="" \
|
||||
-e ACCOUNTS_URL="https://account.huly.app" \
|
||||
-e FRONT_URL="https://huly.app" \
|
||||
-v $(pwd):/data \
|
||||
hardcoreeng/tool:latest \
|
||||
hardcoreeng/import-tool:latest \
|
||||
-- bundle.js import-notion-with-teamspaces /data \
|
||||
--user jane.doe@gmail.com \
|
||||
--password 4321qwe \
|
||||
@@ -71,14 +69,12 @@ docker run \
|
||||
* To import Notion workspace without teamspaces or a page with subpages.
|
||||
```
|
||||
docker run \
|
||||
-e SERVER_SECRET="" \
|
||||
-e ACCOUNTS_URL="https://account.huly.app" \
|
||||
-e FRONT_URL="https://huly.app" \
|
||||
-v $(pwd):/data \
|
||||
hardcoreeng/tool:latest \
|
||||
hardcoreeng/import-tool:latest \
|
||||
-- bundle.js import-notion-to-teamspace /data \
|
||||
--user jane.doe@gmail.com \
|
||||
--password 4321qwe \
|
||||
--workspace ws1 \
|
||||
--teamspace notion
|
||||
```
|
||||
```
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
# Copyright © 2021 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.
|
||||
#
|
||||
|
||||
rushx bundle
|
||||
rushx docker:build $@
|
||||
rushx docker:push
|
||||
@@ -0,0 +1,9 @@
|
||||
const esbuild = require('esbuild')
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
platform: 'node',
|
||||
outfile: 'bundle/bundle.js'
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@hcengineering/import-tool",
|
||||
"version": "0.1.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"template": "@hcengineering/node-package",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"start": "ts-node src/__start.ts",
|
||||
"_phase:bundle": "rushx bundle",
|
||||
"_phase:docker-build": "rushx docker:build",
|
||||
"_phase:docker-staging": "rushx docker:staging",
|
||||
"bundle": "mkdir -p bundle && esbuild src/__start.ts --bundle --keep-names --sourcemap=external --platform=node --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --define:process.env.GIT_REVISION=$(../../common/scripts/git_version.sh) --log-level=error --outfile=bundle/bundle.js",
|
||||
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/import-tool",
|
||||
"docker:tbuild": "docker build -t hardcoreeng/import-tool . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/import-tool",
|
||||
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/import-tool staging",
|
||||
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/import-tool",
|
||||
"run-local": "rush bundle --to @hcengineering/import-tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost MONGO_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service ELASTIC_URL=http://localhost:9200 REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --max-old-space-size=18000 ./bundle/bundle.js",
|
||||
"run-local-brk": "rush bundle --to @hcengineering/import-tool >/dev/null && cross-env SERVER_SECRET=secret ACCOUNTS_URL=http://localhost:3000 TRANSACTOR_URL=ws://localhost:3333 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin MINIO_ENDPOINT=localhost MONGO_URL=mongodb://localhost:27017 TELEGRAM_DATABASE=telegram-service ELASTIC_URL=http://localhost:9200 REKONI_URL=http://localhost:4004 MODEL_VERSION=$(node ../../common/scripts/show_version.js) GIT_REVISION=$(git describe --all --long) node --inspect-brk --enable-source-maps --max-old-space-size=18000 ./bundle/bundle.js",
|
||||
"run": "rush bundle --to @hcengineering/import-tool >/dev/null && cross-env node --max-old-space-size=8000 ./bundle/bundle.js",
|
||||
"upgrade": "rushx run-local upgrade",
|
||||
"format": "format src",
|
||||
"test": "jest --passWithNoTests --silent --forceExit",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent --forceExit",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "~7.0.3",
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint": "^8.54.0",
|
||||
"ts-node": "^10.8.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"@types/mime-types": "~2.1.1",
|
||||
"@types/node": "~20.11.16",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/attachment": "^0.6.14",
|
||||
"@hcengineering/collaboration": "^0.6.0",
|
||||
"@hcengineering/document": "^0.6.0",
|
||||
"@hcengineering/text": "^0.6.5",
|
||||
"@hcengineering/model-attachment": "^0.6.0",
|
||||
"@hcengineering/model-core": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/platform": "^0.6.11",
|
||||
"@hcengineering/server-tool": "^0.6.0",
|
||||
"@hcengineering/server-client": "^0.6.0",
|
||||
"commander": "^8.1.0",
|
||||
"mime-types": "~2.1.34"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { importTool } from '.'
|
||||
|
||||
importTool()
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { concatLink, TxOperations } from '@hcengineering/core'
|
||||
import serverClientPlugin, {
|
||||
createClient,
|
||||
getUserWorkspaces,
|
||||
login,
|
||||
selectWorkspace
|
||||
} from '@hcengineering/server-client'
|
||||
import { program } from 'commander'
|
||||
import { importNotion } from './notion'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function importTool (): void {
|
||||
function getFrontUrl (): string {
|
||||
const frontUrl = process.env.FRONT_URL
|
||||
if (frontUrl === undefined) {
|
||||
console.error('please provide front url')
|
||||
process.exit(1)
|
||||
}
|
||||
return frontUrl
|
||||
}
|
||||
|
||||
program.version('0.0.1')
|
||||
|
||||
// import-notion-with-teamspaces /home/anna/work/notion/pages/exported --workspace workspace
|
||||
program
|
||||
.command('import-notion-with-teamspaces <dir>')
|
||||
.description('import extracted archive exported from Notion as "Markdown & CSV"')
|
||||
.requiredOption('-u, --user <user>', 'user')
|
||||
.requiredOption('-pw, --password <password>', 'password')
|
||||
.requiredOption('-ws, --workspace <workspace>', 'workspace url where the documents should be imported to')
|
||||
.action(async (dir: string, cmd) => {
|
||||
await importFromNotion(dir, cmd.user, cmd.password, cmd.workspace)
|
||||
})
|
||||
|
||||
// import-notion-to-teamspace /home/anna/work/notion/pages/exported --workspace workspace --teamspace notion
|
||||
program
|
||||
.command('import-notion-to-teamspace <dir>')
|
||||
.description('import extracted archive exported from Notion as "Markdown & CSV"')
|
||||
.requiredOption('-u, --user <user>', 'user')
|
||||
.requiredOption('-pw, --password <password>', 'password')
|
||||
.requiredOption('-ws, --workspace <workspace>', 'workspace url where the documents should be imported to')
|
||||
.requiredOption('-ts, --teamspace <teamspace>', 'new teamspace name where the documents should be imported to')
|
||||
.action(async (dir: string, cmd) => {
|
||||
await importFromNotion(dir, cmd.user, cmd.password, cmd.workspace, cmd.teamspace)
|
||||
})
|
||||
|
||||
async function importFromNotion (
|
||||
dir: string,
|
||||
user: string,
|
||||
password: string,
|
||||
workspaceUrl: string,
|
||||
teamspace?: string
|
||||
): Promise<void> {
|
||||
if (workspaceUrl === '' || user === '' || password === '' || teamspace === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const config = await (await fetch(concatLink(getFrontUrl(), '/config.json'))).json()
|
||||
console.log('Setting up Accounts URL: ', config.ACCOUNTS_URL)
|
||||
setMetadata(serverClientPlugin.metadata.Endpoint, config.ACCOUNTS_URL)
|
||||
console.log('Trying to login user: ', user)
|
||||
const userToken = await login(user, password, workspaceUrl)
|
||||
if (userToken === undefined) {
|
||||
console.log('Login failed for user: ', user)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Looking for workspace: ', workspaceUrl)
|
||||
const allWorkspaces = await getUserWorkspaces(userToken)
|
||||
const workspaces = allWorkspaces.filter((ws) => ws.workspaceUrl === workspaceUrl)
|
||||
if (workspaces.length < 1) {
|
||||
console.log('Workspace not found: ', workspaceUrl)
|
||||
return
|
||||
}
|
||||
console.log('Workspace found')
|
||||
const selectedWs = await selectWorkspace(userToken, workspaces[0].workspace)
|
||||
console.log(selectedWs)
|
||||
|
||||
function uploader (token: string) {
|
||||
return (id: string, data: any) => {
|
||||
return fetch(concatLink(getFrontUrl(), config.UPLOAD_URL), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token
|
||||
},
|
||||
body: data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Connecting to Transactor URL: ', selectedWs.endpoint)
|
||||
const connection = await createClient(selectedWs.endpoint, selectedWs.token)
|
||||
const acc = connection.getModel().getAccountByEmail(user)
|
||||
if (acc === undefined) {
|
||||
console.log('Account not found for email: ', user)
|
||||
return
|
||||
}
|
||||
const client = new TxOperations(connection, acc._id)
|
||||
console.log('OK. Start the import directory: ', dir)
|
||||
await importNotion(client, uploader(selectedWs.token), dir, teamspace)
|
||||
await connection.close()
|
||||
}
|
||||
|
||||
program.parse(process.argv)
|
||||
}
|
||||
@@ -1,3 +1,17 @@
|
||||
//
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
generateId,
|
||||
type AttachedData,
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
+1
-87
@@ -46,14 +46,7 @@ import {
|
||||
createStorageBackupStorage,
|
||||
restore
|
||||
} from '@hcengineering/server-backup'
|
||||
import serverClientPlugin, {
|
||||
BlobClient,
|
||||
createClient,
|
||||
getTransactorEndpoint,
|
||||
getUserWorkspaces,
|
||||
login,
|
||||
selectWorkspace
|
||||
} from '@hcengineering/server-client'
|
||||
import serverClientPlugin, { BlobClient, createClient, getTransactorEndpoint } from '@hcengineering/server-client'
|
||||
import { getServerPipeline } from '@hcengineering/server-pipeline'
|
||||
import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token'
|
||||
import toolPlugin, { FileModelLogger } from '@hcengineering/server-tool'
|
||||
@@ -68,13 +61,11 @@ import { diffWorkspace, recreateElastic, updateField } from './workspace'
|
||||
|
||||
import core, {
|
||||
AccountRole,
|
||||
concatLink,
|
||||
generateId,
|
||||
getWorkspaceId,
|
||||
MeasureMetricsContext,
|
||||
metricsToString,
|
||||
systemAccountEmail,
|
||||
TxOperations,
|
||||
versionToString,
|
||||
type Data,
|
||||
type Doc,
|
||||
@@ -115,7 +106,6 @@ import { changeConfiguration } from './configuration'
|
||||
import { moveFromMongoToPG, moveWorkspaceFromMongoToPG } from './db'
|
||||
import { fixJsonMarkup, migrateMarkup, restoreLostMarkup } from './markup'
|
||||
import { fixMixinForeignAttributes, showMixinForeignAttributes } from './mixin'
|
||||
import { importNotion } from './notion'
|
||||
import { fixAccountEmails, renameAccount } from './renameAccount'
|
||||
import { moveFiles, showLostFiles, syncFiles } from './storage'
|
||||
|
||||
@@ -172,15 +162,6 @@ export function devTool (
|
||||
return elasticUrl
|
||||
}
|
||||
|
||||
function getFrontUrl (): string {
|
||||
const frontUrl = process.env.FRONT_URL
|
||||
if (frontUrl === undefined) {
|
||||
console.error('please provide front url')
|
||||
process.exit(1)
|
||||
}
|
||||
return frontUrl
|
||||
}
|
||||
|
||||
const initWS = process.env.INIT_WORKSPACE
|
||||
if (initWS !== undefined) {
|
||||
setMetadata(toolPlugin.metadata.InitWorkspace, initWS)
|
||||
@@ -242,73 +223,6 @@ export function devTool (
|
||||
})
|
||||
})
|
||||
|
||||
// import-notion-with-teamspaces /home/anna/work/notion/pages/exported --workspace workspace
|
||||
program
|
||||
.command('import-notion-with-teamspaces <dir>')
|
||||
.description('import extracted archive exported from Notion as "Markdown & CSV"')
|
||||
.requiredOption('-u, --user <user>', 'user')
|
||||
.requiredOption('-pw, --password <password>', 'password')
|
||||
.requiredOption('-ws, --workspace <workspace>', 'workspace where the documents should be imported to')
|
||||
.action(async (dir: string, cmd) => {
|
||||
await importFromNotion(dir, cmd.user, cmd.password, cmd.workspace)
|
||||
})
|
||||
|
||||
// import-notion-to-teamspace /home/anna/work/notion/pages/exported --workspace workspace --teamspace notion
|
||||
program
|
||||
.command('import-notion-to-teamspace <dir>')
|
||||
.description('import extracted archive exported from Notion as "Markdown & CSV"')
|
||||
.requiredOption('-u, --user <user>', 'user')
|
||||
.requiredOption('-pw, --password <password>', 'password')
|
||||
.requiredOption('-ws, --workspace <workspace>', 'workspace where the documents should be imported to')
|
||||
.requiredOption('-ts, --teamspace <teamspace>', 'new teamspace name where the documents should be imported to')
|
||||
.action(async (dir: string, cmd) => {
|
||||
await importFromNotion(dir, cmd.user, cmd.password, cmd.workspace, cmd.teamspace)
|
||||
})
|
||||
|
||||
async function importFromNotion (
|
||||
dir: string,
|
||||
user: string,
|
||||
password: string,
|
||||
workspace: string,
|
||||
teamspace?: string
|
||||
): Promise<void> {
|
||||
if (workspace === '' || user === '' || password === '' || teamspace === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const userToken = await login(user, password, workspace)
|
||||
const allWorkspaces = await getUserWorkspaces(userToken)
|
||||
const workspaces = allWorkspaces.filter((ws) => ws.workspace === workspace)
|
||||
if (workspaces.length < 1) {
|
||||
console.log('Workspace not found: ', workspace)
|
||||
return
|
||||
}
|
||||
const selectedWs = await selectWorkspace(userToken, workspaces[0].workspace)
|
||||
console.log(selectedWs)
|
||||
|
||||
function uploader (token: string) {
|
||||
return (id: string, data: any) => {
|
||||
return fetch(concatLink(getFrontUrl(), '/files'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token
|
||||
},
|
||||
body: data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const connection = await createClient(selectedWs.endpoint, selectedWs.token)
|
||||
const acc = connection.getModel().getAccountByEmail(user)
|
||||
if (acc === undefined) {
|
||||
console.log('Account not found for email: ', user)
|
||||
return
|
||||
}
|
||||
const client = new TxOperations(connection, acc._id)
|
||||
await importNotion(client, uploader(selectedWs.token), dir, teamspace)
|
||||
await connection.close()
|
||||
}
|
||||
|
||||
program
|
||||
.command('reset-account <email>')
|
||||
.description('create user and corresponding account in master database')
|
||||
|
||||
@@ -206,6 +206,8 @@ function defineChannelActions (builder: Builder): void {
|
||||
query: {
|
||||
archived: false
|
||||
},
|
||||
override: [view.action.Archive],
|
||||
visibilityTester: view.function.CanArchiveSpace,
|
||||
context: {
|
||||
mode: 'context',
|
||||
group: 'remove'
|
||||
|
||||
@@ -285,7 +285,7 @@ export const coreOperation: MigrateOperation = {
|
||||
func: migrateAllSpaceToTyped
|
||||
},
|
||||
{
|
||||
state: 'add-spaces-owner',
|
||||
state: 'add-spaces-owner-v1',
|
||||
func: migrateSpacesOwner
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
min-width: 0;
|
||||
border: 1px solid var(--theme-divider-color); // var(--global-surface-02-BorderColor);
|
||||
border-radius: var(--small-focus-BorderRadius);
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-right: 0;
|
||||
|
||||
&:not(.modal) {
|
||||
background-color: var(--theme-panel-color); // var(--global-surface-02-BackgroundColor);
|
||||
|
||||
@@ -368,6 +368,18 @@ pre.proseCodeBlock {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
pre.proseCodeBlock {
|
||||
button:not(.hovered) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fixes for MessageViewer
|
||||
pre.proseCodeBlock > pre.proseCode {
|
||||
padding: 0;
|
||||
|
||||
+2
@@ -41,6 +41,7 @@
|
||||
export let hoverable = true
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let hideLink = false
|
||||
export let readonly: boolean = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
$: personAccount = $personAccountByIdStore.get((value.createdBy ?? value.modifiedBy) as Ref<PersonAccount>)
|
||||
@@ -74,6 +75,7 @@
|
||||
{hoverable}
|
||||
{hoverStyles}
|
||||
viewlet={undefined}
|
||||
{readonly}
|
||||
{onClick}
|
||||
>
|
||||
<svelte:fragment slot="icon">
|
||||
|
||||
+2
@@ -39,6 +39,7 @@
|
||||
export let videoPreload = true
|
||||
export let hideLink = false
|
||||
export let compact = false
|
||||
export let readonly = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
@@ -72,6 +73,7 @@
|
||||
hideLink,
|
||||
type,
|
||||
compact,
|
||||
readonly,
|
||||
onClick
|
||||
}}
|
||||
/>
|
||||
|
||||
+3
-2
@@ -61,6 +61,7 @@
|
||||
export let type: ActivityMessageViewType = 'default'
|
||||
export let inlineActions: MessageInlineAction[] = []
|
||||
export let excludedActions: Ref<ViewAction>[] = []
|
||||
export let readonly: boolean = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
export let socialIcon: Asset | undefined = undefined
|
||||
@@ -107,8 +108,7 @@
|
||||
$: isHidden = !!viewlet?.onlyWithParent && parentMessage === undefined
|
||||
$: withActionMenu = withActions && !embedded && (actions.length > 0 || menuActionIds.length > 0)
|
||||
|
||||
let readonly: boolean = false
|
||||
$: readonly = $restrictionStore.disableComments
|
||||
$: readonly = readonly || $restrictionStore.disableComments
|
||||
|
||||
function canDisplayShort (type: ActivityMessageViewType, isSaved: boolean): boolean {
|
||||
return type === 'short' && !isSaved && (message.replies ?? 0) === 0
|
||||
@@ -143,6 +143,7 @@
|
||||
}
|
||||
|
||||
function handleContextMenu (event: MouseEvent): void {
|
||||
if (readonly) return
|
||||
const showCustomPopup = !isTextClicked(event.target as HTMLElement, event.clientX, event.clientY)
|
||||
if (showCustomPopup) {
|
||||
showMenu(event, { object: message, baseMenuClass: activity.class.ActivityMessage }, () => {
|
||||
|
||||
+2
@@ -42,6 +42,7 @@
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let hideLink = false
|
||||
export let compact = false
|
||||
export let readonly: boolean = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
@@ -96,6 +97,7 @@
|
||||
|
||||
<ActivityMessageTemplate
|
||||
message={value}
|
||||
{readonly}
|
||||
{person}
|
||||
{showNotify}
|
||||
{isHighlighted}
|
||||
|
||||
+2
@@ -54,6 +54,7 @@
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let hideLink = false
|
||||
export let type: ActivityMessageViewType = 'default'
|
||||
export let readonly = false
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
@@ -195,6 +196,7 @@
|
||||
{skipLabel}
|
||||
{hoverable}
|
||||
{hoverStyles}
|
||||
{readonly}
|
||||
type={viewlet?.label || getIsTextType(attributeModel) ? 'default' : type}
|
||||
showDatePreposition={hideLink}
|
||||
{onClick}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"Settings": "Settings",
|
||||
"ArchiveChannel": "Archive channel",
|
||||
"UnarchiveChannel": "Unarchive channel",
|
||||
"ArchiveConfirm": "Do you want to archive channel?",
|
||||
"ArchiveConfirm": "When you archive a channel, it’s archived for everyone.<br/><br/>No one will be able to send messages, but they’ll still have access to the channel’s history.<br/><br/>You’ll still be able to find the channel’s contents via search.<br/>And you can always unarchive the channel in the future, if needed.\n",
|
||||
"UnarchiveConfirm": "Do you want to unarchive channel?",
|
||||
"AddToSaved": "Add to saved",
|
||||
"RemoveFromSaved": "Remove from saved",
|
||||
@@ -127,6 +127,8 @@
|
||||
"Translate": "Translate",
|
||||
"ShowOriginal": "Show original",
|
||||
"Translating": "Translating...",
|
||||
"StartConversation": "Start conversation"
|
||||
"StartConversation": "Start conversation",
|
||||
"ViewingThreadFromArchivedChannel": "You are viewing a thread from an archived channel",
|
||||
"ViewingArchivedChannel": "You are viewing an archived channel"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
"Settings": "Ajustes",
|
||||
"ArchiveChannel": "Archivar canal",
|
||||
"UnarchiveChannel": "Desarchivar canal",
|
||||
"ArchiveConfirm": "¿Quieres archivar el canal?",
|
||||
"ArchiveConfirm": "Cuando archivas un canal, se archiva para todos.<br/><br/>Nadie podrá enviar mensajes, pero seguirán teniendo acceso al historial del canal.<br/><br/>Todavía podrás encontrar el contenido del canal a través de la búsqueda.<br/>Y siempre puedes desarchivar el canal en el futuro, si es necesario.\n",
|
||||
"UnarchiveConfirm": "¿Quieres desarchivar el canal?",
|
||||
"AddToSaved": "Añadir a guardados",
|
||||
"RemoveFromSaved": "Eliminar de guardados",
|
||||
@@ -127,6 +127,8 @@
|
||||
"Translate": "Traducir",
|
||||
"ShowOriginal": "Mostrar original",
|
||||
"Translating": "Traduciendo...",
|
||||
"StartConversation": "Iniciar conversación"
|
||||
"StartConversation": "Iniciar conversación",
|
||||
"ViewingThreadFromArchivedChannel": "Estás viendo un hilo de un canal archivado",
|
||||
"ViewingArchivedChannel": "Estás viendo un canal archivado"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
"Settings": "Paramètres",
|
||||
"ArchiveChannel": "Archiver le canal",
|
||||
"UnarchiveChannel": "Désarchiver le canal",
|
||||
"ArchiveConfirm": "Voulez-vous archiver le canal ?",
|
||||
"ArchiveConfirm": "Lorsque vous archivez un canal, il est archivé pour tout le monde.<br/><br/>Personne ne pourra envoyer de messages, mais ils auront toujours accès à l'historique du canal.<br/><br/>Vous pourrez toujours trouver le contenu du canal via la recherche.<br/>Et vous pourrez toujours désarchiver le canal à l'avenir, si nécessaire.\n",
|
||||
"UnarchiveConfirm": "Voulez-vous désarchiver le canal ?",
|
||||
"AddToSaved": "Ajouter aux favoris",
|
||||
"RemoveFromSaved": "Retirer des favoris",
|
||||
@@ -127,6 +127,8 @@
|
||||
"Translate": "Traduire",
|
||||
"ShowOriginal": "Afficher l'original",
|
||||
"Translating": "Traduction en cours...",
|
||||
"StartConversation": "Démarrer la conversation"
|
||||
"StartConversation": "Démarrer la conversation",
|
||||
"ViewingThreadFromArchivedChannel": "Vous consultez un fil de discussion d'un canal archivé",
|
||||
"ViewingArchivedChannel": "Vous consultez un canal archivé"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
"Settings": "Definições",
|
||||
"ArchiveChannel": "Arquivar canal",
|
||||
"UnarchiveChannel": "Desarquivar canal",
|
||||
"ArchiveConfirm": "Deseja arquivar o canal?",
|
||||
"ArchiveConfirm": "Quando arquivar um canal, ele é arquivado para todos.<br/><br/>Ninguém poderá enviar mensagens, mas ainda terão acesso ao histórico do canal.<br/><br/>Ainda poderá encontrar o conteúdo do canal através da pesquisa.<br/>E poderá sempre desarquivar o canal no futuro, se necessário.\n",
|
||||
"UnarchiveConfirm": "Deseja desarquivar o canal?",
|
||||
"AddToSaved": "Adicionar aos guardados",
|
||||
"RemoveFromSaved": "Remover dos guardados",
|
||||
@@ -127,6 +127,8 @@
|
||||
"Translate": "Traduzir",
|
||||
"ShowOriginal": "Mostrar original",
|
||||
"Translating": "A traduzir...",
|
||||
"StartConversation": "Iniciar conversa"
|
||||
"StartConversation": "Iniciar conversa",
|
||||
"ViewingThreadFromArchivedChannel": "Está a visualizar uma conversa em cadeia de um canal arquivado",
|
||||
"ViewingArchivedChannel": "Está a visualizar um canal arquivado"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
"Settings": "Настройки",
|
||||
"ArchiveChannel": "Архивировать канал",
|
||||
"UnarchiveChannel": "Разархивировать канал",
|
||||
"ArchiveConfirm": "Вы действительно хотите архивировать канал?",
|
||||
"ArchiveConfirm": "При архивации канала он архивируется для всех.<br/><br/>Никто не сможет отправлять сообщения, но у них все равно будет доступ к истории канала.<br/><br/>Вы все равно сможете найти содержимое канала через поиск.<br/>И вы всегда сможете разархивировать канал в будущем, если это понадобится.\n",
|
||||
"UnarchiveConfirm": "Вы действительно хотите разархивировать канал?",
|
||||
"AddToSaved": "Добавить в сохраненные",
|
||||
"RemoveFromSaved": "Удалить из сохраненных",
|
||||
@@ -127,6 +127,8 @@
|
||||
"Translate": "Перевести",
|
||||
"ShowOriginal": "Показать оригинал",
|
||||
"Translating": "Перевод...",
|
||||
"StartConversation": "Начать диалог"
|
||||
"StartConversation": "Начать диалог",
|
||||
"ViewingThreadFromArchivedChannel": "Вы просматриваете обсуждение из архивированного канала",
|
||||
"ViewingArchivedChannel": "Вы просматриваете архивированный канал"
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@
|
||||
"Settings": "设置",
|
||||
"ArchiveChannel": "归档频道",
|
||||
"UnarchiveChannel": "取消归档频道",
|
||||
"ArchiveConfirm": "你想要归档频道吗?",
|
||||
"ArchiveConfirm": "当你归档频道时,它会对所有人进行归档。<br/><br/>没有人能够发送消息,但他们仍然可以访问频道的历史记录。<br/><br/>你仍然可以通过搜索找到频道的内容。<br/>如果需要,你可以随时取消归档频道。\n",
|
||||
"UnarchiveConfirm": "你想要取消归档频道吗?",
|
||||
"AddToSaved": "添加到已保存",
|
||||
"RemoveFromSaved": "从已保存中移除",
|
||||
@@ -127,6 +127,8 @@
|
||||
"Translate": "翻译",
|
||||
"ShowOriginal": "显示原文",
|
||||
"Translating": "翻译中...",
|
||||
"StartConversation": "开始对话"
|
||||
"StartConversation": "开始对话",
|
||||
"ViewingThreadFromArchivedChannel": "你正在查看已归档频道的线程",
|
||||
"ViewingArchivedChannel": "你正在查看已归档频道"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
{#if dataProvider}
|
||||
<ChannelScrollView
|
||||
{object}
|
||||
channel={object}
|
||||
skipLabels={!isDocChannel}
|
||||
selectedFilters={filters}
|
||||
startFromBottom
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<!--
|
||||
// Copyright © 2024 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { ActivityExtension as ActivityExtensionComponent } from '@hcengineering/activity-resources'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import activity, { ActivityExtension } from '@hcengineering/activity'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { AnySvelteComponent, Icon, Label } from '@hcengineering/ui'
|
||||
import { Asset, getResource, translate } from '@hcengineering/platform'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import { getChannelName, getObjectIcon } from '../utils'
|
||||
import chunter from '../plugin'
|
||||
|
||||
export let object: Doc
|
||||
export let readonly = false
|
||||
export let boundary: HTMLElement | undefined | null = undefined
|
||||
export let collection: string | undefined
|
||||
export let isThread = false
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let extensions: ActivityExtension[] = []
|
||||
$: extensions = client.getModel().findAllSync(activity.class.ActivityExtension, { ofClass: object._class })
|
||||
|
||||
let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
let name: string | undefined = undefined
|
||||
|
||||
$: void updateIcon(object._class)
|
||||
$: void updateName(object)
|
||||
|
||||
async function updateIcon (_class: Ref<Class<Doc>>): Promise<void> {
|
||||
if (isThread) {
|
||||
return
|
||||
}
|
||||
const iconMixin = hierarchy.classHierarchyMixin(_class, view.mixin.ObjectIcon)
|
||||
let result: AnySvelteComponent | Asset | undefined = undefined
|
||||
|
||||
if (iconMixin?.component) {
|
||||
result = await getResource(iconMixin.component)
|
||||
} else {
|
||||
result = getObjectIcon(_class)
|
||||
}
|
||||
icon = result
|
||||
}
|
||||
|
||||
async function updateName (object: Doc): Promise<void> {
|
||||
const titleIntl = client.getHierarchy().getClass(object._class).label
|
||||
name = (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {}))
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !readonly}
|
||||
<div class="ref-input flex-col">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
{extensions}
|
||||
props={{ object, boundary, collection, autofocus: true, withTypingInfo: true }}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="message">
|
||||
{#if isThread}
|
||||
<Label label={chunter.string.ViewingThreadFromArchivedChannel} />
|
||||
{:else}
|
||||
<Label label={chunter.string.ViewingArchivedChannel} />
|
||||
<span class="info">
|
||||
{#if icon}
|
||||
<Icon {icon} size="x-small" />
|
||||
{/if}
|
||||
{#if name}
|
||||
{name}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.ref-input {
|
||||
flex-shrink: 0;
|
||||
margin: 1.25rem 1rem 0;
|
||||
max-height: 18.75rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 1rem;
|
||||
padding: 0.5rem 0;
|
||||
color: var(--global-primary-TextColor);
|
||||
background: var(--global-ui-BorderColor);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 0.25rem;
|
||||
gap: 0.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
export let ids: Ref<Person>[] = []
|
||||
export let disableRemoveFor: Ref<Person>[] = []
|
||||
export let readonly = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -36,22 +37,24 @@
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="item" style:padding="var(--spacing-1_5)" class:withoutBorder={persons.length === 0}>
|
||||
<ModernButton
|
||||
label={chunter.string.AddMembers}
|
||||
icon={IconAddMember}
|
||||
iconSize="small"
|
||||
kind="secondary"
|
||||
size="small"
|
||||
on:click={() => dispatch('add')}
|
||||
/>
|
||||
</div>
|
||||
{#if !readonly}
|
||||
<div class="item" style:padding="var(--spacing-1_5)" class:withoutBorder={persons.length === 0}>
|
||||
<ModernButton
|
||||
label={chunter.string.AddMembers}
|
||||
icon={IconAddMember}
|
||||
iconSize="small"
|
||||
kind="secondary"
|
||||
size="small"
|
||||
on:click={() => dispatch('add')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Scroller>
|
||||
{#each persons as person, index (person._id)}
|
||||
<div class="item" class:withoutBorder={index === persons.length - 1}>
|
||||
<div class="item__content" class:disabled={disableRemoveFor.includes(person._id)}>
|
||||
<div class="item__content" class:disabled={readonly || disableRemoveFor.includes(person._id)}>
|
||||
<UserDetails {person} showStatus />
|
||||
{#if !disableRemoveFor.includes(person._id)}
|
||||
{#if !readonly && !disableRemoveFor.includes(person._id)}
|
||||
<div class="item__action">
|
||||
<ButtonIcon
|
||||
icon={IconDelete}
|
||||
|
||||
@@ -13,20 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import activity, {
|
||||
ActivityExtension,
|
||||
ActivityMessage,
|
||||
ActivityMessagesFilter,
|
||||
DisplayActivityMessage
|
||||
} from '@hcengineering/activity'
|
||||
import activity, { ActivityMessage, ActivityMessagesFilter, DisplayActivityMessage } from '@hcengineering/activity'
|
||||
import {
|
||||
ActivityExtension as ActivityExtensionComponent,
|
||||
ActivityMessagePresenter,
|
||||
canGroupMessages,
|
||||
messageInFocus,
|
||||
sortActivityMessages
|
||||
} from '@hcengineering/activity-resources'
|
||||
import { Doc, getCurrentAccount, getDay, Ref, Timestamp } from '@hcengineering/core'
|
||||
import core, { Doc, getCurrentAccount, getDay, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import { DocNotifyContext } from '@hcengineering/notification'
|
||||
import { InboxNotificationsClientImpl } from '@hcengineering/notification-resources'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
@@ -48,9 +42,11 @@
|
||||
import ActivityMessagesSeparator from './ChannelMessagesSeparator.svelte'
|
||||
import JumpToDateSelector from './JumpToDateSelector.svelte'
|
||||
import HistoryLoading from './LoadingHistory.svelte'
|
||||
import ChannelInput from './ChannelInput.svelte'
|
||||
|
||||
export let provider: ChannelDataProvider
|
||||
export let object: Doc
|
||||
export let channel: Doc
|
||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||
export let scrollElement: HTMLDivElement | undefined | null = undefined
|
||||
export let startFromBottom = false
|
||||
@@ -74,6 +70,7 @@
|
||||
|
||||
const me = getCurrentAccount()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const contextByDocStore = inboxClient.contextByDoc
|
||||
const notificationsByContextStore = inboxClient.inboxNotificationsByContext
|
||||
@@ -94,7 +91,6 @@
|
||||
|
||||
let messages: ActivityMessage[] = []
|
||||
let displayMessages: DisplayActivityMessage[] = []
|
||||
let extensions: ActivityExtension[] = []
|
||||
|
||||
let scroller: Scroller | undefined | null = undefined
|
||||
let separatorElement: HTMLDivElement | undefined = undefined
|
||||
@@ -118,9 +114,8 @@
|
||||
$: messages = $messagesStore
|
||||
$: isLoading = $isLoadingStore
|
||||
|
||||
$: extensions = client.getModel().findAllSync(activity.class.ActivityExtension, { ofClass: doc._class })
|
||||
|
||||
$: notifyContext = $contextByDocStore.get(doc._id)
|
||||
$: readonly = hierarchy.isDerived(channel._class, core.class.Space) ? (channel as Space).archived : false
|
||||
|
||||
void client
|
||||
.getModel()
|
||||
@@ -809,7 +804,7 @@
|
||||
{/if}
|
||||
<slot name="header" />
|
||||
|
||||
{#if displayMessages.length === 0 && !embedded}
|
||||
{#if displayMessages.length === 0 && !embedded && !readonly}
|
||||
<BlankView
|
||||
icon={chunter.icon.Thread}
|
||||
header={chunter.string.NoMessagesInChannel}
|
||||
@@ -840,17 +835,12 @@
|
||||
attachmentImageSize="x-large"
|
||||
type={canGroup ? 'short' : 'default'}
|
||||
hideLink
|
||||
{readonly}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if !fixedInput}
|
||||
<div class="ref-input flex-col">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
{extensions}
|
||||
props={{ object, boundary: scrollElement, collection, autofocus: true, withTypingInfo: true }}
|
||||
/>
|
||||
</div>
|
||||
<ChannelInput {object} {readonly} boundary={scrollElement} {collection} isThread={embedded} />
|
||||
{/if}
|
||||
|
||||
{#if loadMoreAllowed && $canLoadNextForwardStore}
|
||||
@@ -859,7 +849,7 @@
|
||||
</Scroller>
|
||||
|
||||
{#if !embedded && showScrollDownButton}
|
||||
<div class="down-button absolute">
|
||||
<div class="down-button absolute" class:readonly>
|
||||
<ModernButton
|
||||
label={chunter.string.LatestMessages}
|
||||
shape="round"
|
||||
@@ -870,14 +860,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if fixedInput && object}
|
||||
<div class="ref-input flex-col">
|
||||
<ActivityExtensionComponent
|
||||
kind="input"
|
||||
{extensions}
|
||||
props={{ object, boundary: scrollElement, collection, autofocus: true, withTypingInfo: true }}
|
||||
/>
|
||||
</div>
|
||||
{#if fixedInput}
|
||||
<ChannelInput {object} {readonly} boundary={scrollElement} {collection} isThread={embedded} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -887,13 +871,6 @@
|
||||
flex-shrink: 5;
|
||||
}
|
||||
|
||||
.ref-input {
|
||||
flex-shrink: 0;
|
||||
margin: 1.25rem 1rem 1rem;
|
||||
margin-bottom: 0;
|
||||
max-height: 18.75rem;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -920,6 +897,10 @@
|
||||
animation: 0.5s fadeIn;
|
||||
animation-fill-mode: forwards;
|
||||
visibility: hidden;
|
||||
|
||||
&.readonly {
|
||||
bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
isThreadOpened = newLocation.path[4] != null
|
||||
})
|
||||
|
||||
$: readonly = hierarchy.isDerived(object._class, core.class.Space) ? (object as Space).archived : false
|
||||
$: showJoinOverlay = shouldShowJoinOverlay(object)
|
||||
$: isDocChat = !hierarchy.isDerived(object._class, chunter.class.ChunterSpace)
|
||||
$: withAside =
|
||||
@@ -114,7 +115,7 @@
|
||||
<div class="popupPanel-body" class:asideShown={withAside && isAsideShown}>
|
||||
<div class="popupPanel-body__main">
|
||||
{#key object._id}
|
||||
{#if shouldShowJoinOverlay(object)}
|
||||
{#if !readonly && shouldShowJoinOverlay(object)}
|
||||
<div class="body h-full w-full clear-mins flex-center">
|
||||
<div class="joinOverlay">
|
||||
<div class="an-element__label header">
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
export let videoPreload = true
|
||||
export let hideLink = false
|
||||
export let compact = false
|
||||
export let readonly = false
|
||||
export let type: ActivityMessageViewType = 'default'
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
@@ -245,6 +246,7 @@
|
||||
{skipLabel}
|
||||
{pending}
|
||||
{stale}
|
||||
{readonly}
|
||||
excludedActions={$shownTranslatedMessagesStore.has(value._id)
|
||||
? [chunter.action.TranslateMessage]
|
||||
: [chunter.action.ShowOriginalMessage]}
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
}
|
||||
)
|
||||
}
|
||||
$: readonly = object?.archived ?? false
|
||||
</script>
|
||||
|
||||
<DocAside {_class} {object}>
|
||||
@@ -146,6 +147,7 @@
|
||||
<ChannelMembers
|
||||
ids={Array.from(members)}
|
||||
disableRemoveFor={disabledRemoveFor}
|
||||
{readonly}
|
||||
on:add={openSelectUsersPopup}
|
||||
on:remove={removeMember}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, Mixin, Ref } from '@hcengineering/core'
|
||||
import core, { Class, Doc, Mixin, Ref, Space } from '@hcengineering/core'
|
||||
import {
|
||||
AttributeBarEditor,
|
||||
getClient,
|
||||
@@ -50,6 +50,8 @@
|
||||
)
|
||||
return filtredKeys.filter((key) => !isCollectionAttr(hierarchy, key))
|
||||
}
|
||||
|
||||
$: readonly = hierarchy.isDerived(_class, core.class.Space) ? (object as Space).archived : false
|
||||
</script>
|
||||
|
||||
<Scroller>
|
||||
@@ -59,7 +61,7 @@
|
||||
{object}
|
||||
ignoreKeys={objectChatPanel?.ignoreKeys ?? []}
|
||||
showHeader={false}
|
||||
readonly={false}
|
||||
{readonly}
|
||||
on:update
|
||||
/>
|
||||
<div class="popupPanel-body__aside-grid">
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
private: selectedVisibilityId === 'private',
|
||||
archived: false,
|
||||
members: [account._id],
|
||||
topic: description
|
||||
topic: description,
|
||||
owners: [account._id]
|
||||
})
|
||||
|
||||
openChannel(channelId, chunter.class.Channel)
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const inboxClient = InboxNotificationsClientImpl.getClient()
|
||||
const contextByDocStore = inboxClient.contextByDoc
|
||||
const contextsStore = inboxClient.contexts
|
||||
const objectsQueryByClass = new Map<Ref<Class<Doc>>, { query: LiveQuery, limit: number }>()
|
||||
|
||||
@@ -70,18 +69,17 @@
|
||||
sections = res
|
||||
})
|
||||
|
||||
$: shouldPushObject =
|
||||
object !== undefined && getObjectGroup(object) === model.id && !$contextByDocStore.has(object._id)
|
||||
$: shouldPushObject = object !== undefined && getObjectGroup(object) === model.id
|
||||
|
||||
function loadObjects (contexts: DocNotifyContext[]): void {
|
||||
const contextsByClass = groupByArray(contexts, ({ objectClass }) => objectClass)
|
||||
|
||||
for (const [_class, ctx] of contextsByClass.entries()) {
|
||||
const isChunterSpace = hierarchy.isDerived(_class, chunter.class.ChunterSpace)
|
||||
const isSpace = hierarchy.isDerived(_class, core.class.Space)
|
||||
const ids = ctx.map(({ objectId }) => objectId)
|
||||
const { query, limit } = objectsQueryByClass.get(_class) ?? {
|
||||
query: createQuery(),
|
||||
limit: isChunterSpace ? -1 : model.maxSectionItems ?? 5
|
||||
limit: isSpace ? -1 : model.maxSectionItems ?? 5
|
||||
}
|
||||
|
||||
objectsQueryByClass.set(_class, { query, limit: limit ?? model.maxSectionItems ?? 5 })
|
||||
@@ -90,7 +88,7 @@
|
||||
_class,
|
||||
{
|
||||
_id: { $in: limit !== -1 ? ids.slice(0, limit) : ids },
|
||||
...(isChunterSpace ? { space: core.space.Space } : {})
|
||||
...(isSpace ? { space: core.space.Space, archived: false } : {})
|
||||
},
|
||||
(res) => {
|
||||
objectsByClass = objectsByClass.set(_class, { docs: res, total: res.total })
|
||||
|
||||
@@ -3,15 +3,31 @@
|
||||
import ThreadParentMessage from './ThreadParentPresenter.svelte'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
import ChannelScrollView from '../ChannelScrollView.svelte'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import core, { Doc, Ref, Space } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
|
||||
import { ChannelDataProvider } from '../../channelDataProvider'
|
||||
import chunter from '../../plugin'
|
||||
|
||||
export let selectedMessageId: Ref<ActivityMessage> | undefined = undefined
|
||||
export let message: ActivityMessage
|
||||
|
||||
const query = createQuery()
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
let channel: Doc | undefined = undefined
|
||||
let dataProvider: ChannelDataProvider | undefined = undefined
|
||||
|
||||
$: query.query(
|
||||
message.attachedToClass,
|
||||
{ _id: message.attachedTo },
|
||||
(res) => {
|
||||
channel = res[0]
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
|
||||
$: if (message !== undefined && dataProvider === undefined) {
|
||||
dataProvider = new ChannelDataProvider(
|
||||
undefined,
|
||||
@@ -24,22 +40,26 @@
|
||||
}
|
||||
|
||||
$: messagesStore = dataProvider?.messagesStore
|
||||
$: readonly = hierarchy.isDerived(message.attachedToClass, core.class.Space)
|
||||
? (channel as Space)?.archived ?? false
|
||||
: false
|
||||
</script>
|
||||
|
||||
<div class="hulyComponent-content hulyComponent-content__container noShrink">
|
||||
{#if dataProvider !== undefined}
|
||||
{#if dataProvider !== undefined && channel !== undefined}
|
||||
<ChannelScrollView
|
||||
bind:selectedMessageId
|
||||
embedded
|
||||
skipLabels
|
||||
object={message}
|
||||
{channel}
|
||||
provider={dataProvider}
|
||||
fullHeight={false}
|
||||
fixedInput={false}
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="mt-3">
|
||||
<ThreadParentMessage {message} />
|
||||
<ThreadParentMessage {message} {readonly} />
|
||||
</div>
|
||||
|
||||
{#if (message.replies ?? $messagesStore?.length ?? 0) > 0}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let attachmentImageSize: AttachmentImageSize = 'x-large'
|
||||
export let videoPreload = true
|
||||
export let readonly = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
@@ -74,5 +75,6 @@
|
||||
{attachmentImageSize}
|
||||
{videoPreload}
|
||||
{onClick}
|
||||
{readonly}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { ActivityMessage } from '@hcengineering/activity'
|
||||
|
||||
export let message: ActivityMessage
|
||||
export let readonly = false
|
||||
</script>
|
||||
|
||||
<ActivityMessagePresenter
|
||||
@@ -26,4 +27,5 @@
|
||||
withShowMore={false}
|
||||
attachmentImageSize="x-large"
|
||||
skipLabel
|
||||
{readonly}
|
||||
/>
|
||||
|
||||
@@ -92,6 +92,7 @@ export async function ArchiveChannel (channel: Channel, evt: any, props?: { afte
|
||||
showPopup(MessageBox, {
|
||||
label: chunter.string.ArchiveChannel,
|
||||
message: chunter.string.ArchiveConfirm,
|
||||
richMessage: true,
|
||||
action: async () => {
|
||||
const client = getClient()
|
||||
|
||||
|
||||
@@ -215,7 +215,9 @@ export default plugin(chunterId, {
|
||||
Translate: '' as IntlString,
|
||||
ShowOriginal: '' as IntlString,
|
||||
Translating: '' as IntlString,
|
||||
StartConversation: '' as IntlString
|
||||
StartConversation: '' as IntlString,
|
||||
ViewingThreadFromArchivedChannel: '' as IntlString,
|
||||
ViewingArchivedChannel: '' as IntlString
|
||||
},
|
||||
ids: {
|
||||
DMNotification: '' as Ref<NotificationType>,
|
||||
|
||||
@@ -141,7 +141,7 @@ function createDecorations (doc: ProseMirrorNode, options: CodeBlockLowlightOpti
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleLangButtonClick(e, node, pos, view, options)
|
||||
handleLangButtonClick(e, node, pos, view, button, options)
|
||||
})
|
||||
} else {
|
||||
button.disabled = true
|
||||
@@ -181,6 +181,7 @@ function handleLangButtonClick (
|
||||
node: ProseMirrorNode,
|
||||
pos: number,
|
||||
view: EditorView,
|
||||
button: HTMLButtonElement,
|
||||
options: CodeBlockLowlightOptions
|
||||
): void {
|
||||
const language = node.attrs.language
|
||||
@@ -191,6 +192,8 @@ function handleLangButtonClick (
|
||||
label: language
|
||||
}))
|
||||
|
||||
button.classList.add('hovered')
|
||||
|
||||
showPopup(
|
||||
DropdownLabelsPopup,
|
||||
{
|
||||
@@ -199,6 +202,7 @@ function handleLangButtonClick (
|
||||
},
|
||||
getEventPositionElement(evt),
|
||||
(result) => {
|
||||
button.classList.remove('hovered')
|
||||
if (result != null) {
|
||||
const tr = view.state.tr.setNodeAttribute(pos, 'language', result)
|
||||
view.dispatch(tr)
|
||||
|
||||
@@ -196,7 +196,13 @@
|
||||
{#if isAdmin && ws.lastVisit != null && ws.lastVisit !== 0}
|
||||
<div class="text-sm">
|
||||
{#if ws.backupInfo != null}
|
||||
{ws.backupInfo.backupSize}Mb -
|
||||
{@const sz = ws.backupInfo.dataSize + ws.backupInfo.blobsSize}
|
||||
{@const szGb = Math.round((sz * 100) / 1024) / 100}
|
||||
{#if szGb > 0}
|
||||
{Math.round((sz * 100) / 1024) / 100}Gb -
|
||||
{:else}
|
||||
{Math.round(sz)}Mb -
|
||||
{/if}
|
||||
{/if}
|
||||
({lastUsageDays} days)
|
||||
</div>
|
||||
|
||||
@@ -910,6 +910,8 @@
|
||||
inset: 0;
|
||||
border: 1px solid var(--theme-divider-color);
|
||||
border-radius: var(--medium-BorderRadius);
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.antiPanel-application {
|
||||
|
||||
@@ -165,10 +165,7 @@
|
||||
width: calc(100% - 3.5rem);
|
||||
height: 100%;
|
||||
background: var(--theme-panel-color);
|
||||
|
||||
border-right: 1px solid var(--global-ui-BorderColor);
|
||||
border-top-left-radius: var(--small-focus-BorderRadius);
|
||||
border-bottom-left-radius: var(--small-focus-BorderRadius);
|
||||
}
|
||||
|
||||
.component {
|
||||
|
||||
@@ -23,4 +23,4 @@
|
||||
export let selected: Ref<Widget> | undefined = undefined
|
||||
</script>
|
||||
|
||||
<WidgetsBar {widgets} {preferences} {selected} roundBorder />
|
||||
<WidgetsBar {widgets} {preferences} {selected} />
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
export let widgets: Widget[] = []
|
||||
export let preferences: WidgetPreference[] = []
|
||||
export let selected: Ref<Widget> | undefined = undefined
|
||||
export let roundBorder = false
|
||||
|
||||
function handleAddWidget (): void {
|
||||
showPopup(AddWidgetsPopup, { widgets })
|
||||
@@ -49,7 +48,7 @@
|
||||
.filter((widget): widget is Widget => widget !== undefined && widget.type === WidgetType.Configurable)
|
||||
</script>
|
||||
|
||||
<div class="root" class:roundBorder>
|
||||
<div class="root">
|
||||
<div class="block">
|
||||
{#each fixedWidgets as widget}
|
||||
<WidgetPresenter
|
||||
@@ -104,10 +103,6 @@
|
||||
max-width: 3.5rem;
|
||||
border-top: 1px solid var(--theme-divider-color);
|
||||
overflow-y: auto;
|
||||
|
||||
&.roundBorder {
|
||||
border-top-left-radius: var(--small-focus-BorderRadius);
|
||||
}
|
||||
}
|
||||
|
||||
.block {
|
||||
|
||||
@@ -855,6 +855,11 @@
|
||||
"projectFolder": "dev/tool",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/import-tool",
|
||||
"projectFolder": "dev/import-tool",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/pod-account",
|
||||
"projectFolder": "pods/account",
|
||||
|
||||
@@ -1017,7 +1017,8 @@ export async function listWorkspacesByAccount (db: Db, email: string): Promise<W
|
||||
export async function countWorkspacesInRegion (
|
||||
db: Db,
|
||||
region: string = '',
|
||||
upToVersion?: Data<Version>
|
||||
upToVersion?: Data<Version>,
|
||||
visitedSince?: number
|
||||
): Promise<number> {
|
||||
const regionQuery = region === '' ? { $or: [{ region: { $exists: false } }, { region: '' }] } : { region }
|
||||
const query: Filter<Workspace>['$and'] = [
|
||||
@@ -1039,6 +1040,10 @@ export async function countWorkspacesInRegion (
|
||||
})
|
||||
}
|
||||
|
||||
if (visitedSince !== undefined) {
|
||||
query.push({ lastVisit: { $gt: visitedSince } })
|
||||
}
|
||||
|
||||
return await db.collection<Workspace>(WORKSPACE_COLLECTION).countDocuments({
|
||||
$and: query
|
||||
})
|
||||
@@ -1256,7 +1261,7 @@ export async function workerHandshake (
|
||||
const workspacesCnt = await ctx.with(
|
||||
'count-workspaces-in-region',
|
||||
{},
|
||||
async (ctx) => await countWorkspacesInRegion(db, region, version)
|
||||
async (ctx) => await countWorkspacesInRegion(db, region, version, Date.now() - 24 * 60 * 60 * 1000)
|
||||
)
|
||||
|
||||
await db.collection<UpgradeStatistic>(UPGRADE_COLLECTION).insertOne({
|
||||
@@ -1494,7 +1499,10 @@ export async function getPendingWorkspace (
|
||||
{
|
||||
$or: [{ mode: 'active' }, { mode: { $exists: false } }]
|
||||
},
|
||||
versionQuery
|
||||
versionQuery,
|
||||
{
|
||||
lastVisit: { $gt: Date.now() - 24 * 60 * 60 * 1000 }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+73
-16
@@ -43,7 +43,7 @@ import { BlobClient, createClient } from '@hcengineering/server-client'
|
||||
import { fullTextPushStagePrefix, type StorageAdapter } from '@hcengineering/server-core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import { connect } from '@hcengineering/server-tool'
|
||||
import { createWriteStream, existsSync, mkdirSync, statSync } from 'node:fs'
|
||||
import { createWriteStream, existsSync, mkdirSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { createGzip } from 'node:zlib'
|
||||
@@ -132,6 +132,7 @@ async function loadDigest (
|
||||
date?: number
|
||||
): Promise<Map<Ref<Doc>, string>> {
|
||||
ctx = ctx.newChild('load digest', { domain, count: snapshots.length })
|
||||
ctx.info('load-digest', { domain, count: snapshots.length })
|
||||
const result = new Map<Ref<Doc>, string>()
|
||||
for (const s of snapshots) {
|
||||
const d = s.domains[domain]
|
||||
@@ -492,9 +493,9 @@ async function cleanDomain (ctx: MeasureContext, connection: CoreClient & Backup
|
||||
}
|
||||
}
|
||||
|
||||
function doTrimHash (s: string | undefined): string {
|
||||
function doTrimHash (s: string | undefined): string | undefined {
|
||||
if (s == null) {
|
||||
return ''
|
||||
return undefined
|
||||
}
|
||||
if (s.startsWith('"') && s.endsWith('"')) {
|
||||
return s.slice(1, s.length - 1)
|
||||
@@ -682,7 +683,7 @@ export async function backup (
|
||||
let downloaded = 0
|
||||
|
||||
const printDownloaded = (msg: string, size?: number | null): void => {
|
||||
if (size == null || Number.isNaN(size)) {
|
||||
if (size == null || Number.isNaN(size) || !Number.isInteger(size)) {
|
||||
return
|
||||
}
|
||||
ops++
|
||||
@@ -710,6 +711,30 @@ export async function backup (
|
||||
let changed: number = 0
|
||||
const needRetrieveChunks: Ref<Doc>[][] = []
|
||||
// Load all digest from collection.
|
||||
ctx.info('processed', {
|
||||
processed,
|
||||
digest: digest.size,
|
||||
time: Date.now() - st,
|
||||
workspace: workspaceId.name
|
||||
})
|
||||
const oldHash = new Map<Ref<Doc>, string>()
|
||||
|
||||
function removeFromNeedRetrieve (needRetrieve: Ref<Doc>[], id: string): void {
|
||||
const pos = needRetrieve.indexOf(id as Ref<Doc>)
|
||||
if (pos !== -1) {
|
||||
needRetrieve.splice(pos, 1)
|
||||
processed--
|
||||
changed--
|
||||
}
|
||||
for (const ch of needRetrieveChunks) {
|
||||
const pos = ch.indexOf(id as Ref<Doc>)
|
||||
if (pos !== -1) {
|
||||
ch.splice(pos, 1)
|
||||
processed--
|
||||
changed--
|
||||
}
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
const currentChunk = await ctx.with('loadChunk', {}, () => connection.loadChunk(domain, idx, options.recheck))
|
||||
@@ -735,17 +760,31 @@ export async function backup (
|
||||
})
|
||||
st = Date.now()
|
||||
}
|
||||
const _hash = doTrimHash(hash)
|
||||
const kHash = doTrimHash(digest.get(id as Ref<Doc>))
|
||||
const _hash = doTrimHash(hash) as string
|
||||
const kHash = doTrimHash(digest.get(id as Ref<Doc>) ?? oldHash.get(id as Ref<Doc>))
|
||||
if (kHash !== undefined) {
|
||||
digest.delete(id as Ref<Doc>)
|
||||
if (digest.delete(id as Ref<Doc>)) {
|
||||
oldHash.set(id as Ref<Doc>, kHash)
|
||||
}
|
||||
if (kHash !== _hash) {
|
||||
if (changes.updated.has(id as Ref<Doc>)) {
|
||||
removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>)
|
||||
}
|
||||
changes.updated.set(id as Ref<Doc>, _hash)
|
||||
needRetrieve.push(id as Ref<Doc>)
|
||||
currentNeedRetrieveSize += size
|
||||
changed++
|
||||
} else if (changes.updated.has(id as Ref<Doc>)) {
|
||||
// We have same
|
||||
changes.updated.delete(id as Ref<Doc>)
|
||||
removeFromNeedRetrieve(needRetrieve, id as Ref<Doc>)
|
||||
processed -= 1
|
||||
}
|
||||
} else {
|
||||
if (domain === DOMAIN_BLOB && changes.added.has(id as Ref<Doc>)) {
|
||||
// We need to clean old need retrieve in case of duplicates.
|
||||
removeFromNeedRetrieve(needRetrieve, id)
|
||||
}
|
||||
changes.added.set(id as Ref<Doc>, _hash)
|
||||
needRetrieve.push(id as Ref<Doc>)
|
||||
changed++
|
||||
@@ -753,7 +792,9 @@ export async function backup (
|
||||
}
|
||||
|
||||
if (currentNeedRetrieveSize > retrieveChunkSize) {
|
||||
needRetrieveChunks.push(needRetrieve)
|
||||
if (needRetrieve.length > 0) {
|
||||
needRetrieveChunks.push(needRetrieve)
|
||||
}
|
||||
currentNeedRetrieveSize = 0
|
||||
needRetrieve = []
|
||||
}
|
||||
@@ -835,12 +876,17 @@ export async function backup (
|
||||
|
||||
const totalChunks = needRetrieveChunks.flatMap((it) => it.length).reduce((p, c) => p + c, 0)
|
||||
let processed = 0
|
||||
let blobs = 0
|
||||
|
||||
while (needRetrieveChunks.length > 0) {
|
||||
if (canceled()) {
|
||||
return
|
||||
}
|
||||
const needRetrieve = needRetrieveChunks.shift() as Ref<Doc>[]
|
||||
|
||||
if (needRetrieve.length === 0) {
|
||||
continue
|
||||
}
|
||||
ctx.info('Retrieve chunk', {
|
||||
needRetrieve: needRetrieveChunks.reduce((v, docs) => v + docs.length, 0),
|
||||
toLoad: needRetrieve.length,
|
||||
@@ -849,6 +895,10 @@ export async function backup (
|
||||
let docs: Doc[] = []
|
||||
try {
|
||||
docs = await ctx.with('load-docs', {}, async (ctx) => await connection.loadDocs(domain, needRetrieve))
|
||||
if (docs.length !== needRetrieve.length) {
|
||||
const nr = new Set(docs.map((it) => it._id))
|
||||
ctx.error('failed to retrieve all documents', { missing: needRetrieve.filter((it) => !nr.has(it)) })
|
||||
}
|
||||
ops++
|
||||
} catch (err: any) {
|
||||
ctx.error('error loading docs', { domain, err, workspace: workspaceId.name })
|
||||
@@ -992,7 +1042,8 @@ export async function backup (
|
||||
ctx.error('error packing file', { err })
|
||||
}
|
||||
})
|
||||
if (blob.size > 1024 * 1024) {
|
||||
blobs++
|
||||
if (blob.size > 1024 * 1024 || blobs >= 10) {
|
||||
ctx.info('download blob', {
|
||||
_id: blob._id,
|
||||
contentType: blob.contentType,
|
||||
@@ -1000,6 +1051,9 @@ export async function backup (
|
||||
provider: blob.provider,
|
||||
pending: docs.length
|
||||
})
|
||||
if (blobs >= 10) {
|
||||
blobs = 0
|
||||
}
|
||||
}
|
||||
|
||||
printDownloaded('', blob.size)
|
||||
@@ -1173,15 +1227,16 @@ export async function backupDownload (storage: BackupStorage, storeIn: string):
|
||||
|
||||
const backupInfo: BackupInfo = JSON.parse(gunzipSync(await storage.loadFile(infoFile)).toString())
|
||||
console.log('workspace:', backupInfo.workspace ?? '', backupInfo.version)
|
||||
const addFileSize = async (file: string | undefined | null): Promise<void> => {
|
||||
if (file != null && (await storage.exists(file))) {
|
||||
const fileSize = await storage.stat(file)
|
||||
|
||||
const addFileSize = async (file: string | undefined | null, force: boolean = false): Promise<void> => {
|
||||
if (file != null) {
|
||||
const target = join(storeIn, file)
|
||||
const dir = dirname(target)
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
if (!existsSync(target) || fileSize !== statSync(target).size) {
|
||||
if (!existsSync(target) || force) {
|
||||
const fileSize = await storage.stat(file)
|
||||
console.log('downloading', file, fileSize)
|
||||
const readStream = await storage.load(file)
|
||||
const outp = createWriteStream(target)
|
||||
@@ -1194,8 +1249,10 @@ export async function backupDownload (storage: BackupStorage, storeIn: string):
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
size += fileSize
|
||||
} else {
|
||||
console.log('file-same', file)
|
||||
}
|
||||
size += fileSize
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1211,7 +1268,7 @@ export async function backupDownload (storage: BackupStorage, storeIn: string):
|
||||
}
|
||||
}
|
||||
}
|
||||
await addFileSize(infoFile)
|
||||
await addFileSize(infoFile, true)
|
||||
|
||||
console.log('Backup size', size / (1024 * 1024), 'Mb')
|
||||
}
|
||||
@@ -1687,7 +1744,7 @@ export async function compactBackup (
|
||||
const oldSnapshots = [...backupInfo.snapshots]
|
||||
|
||||
backupInfo.snapshots = [snapshot]
|
||||
let backupIndex = `${backupInfo.snapshotsIndex ?? oldSnapshots.length}`
|
||||
let backupIndex = `${(backupInfo.snapshotsIndex ?? oldSnapshots.length) + 1}`
|
||||
while (backupIndex.length < 6) {
|
||||
backupIndex = '0' + backupIndex
|
||||
}
|
||||
|
||||
@@ -115,27 +115,37 @@ class BackupWorker {
|
||||
ctx: MeasureContext
|
||||
): Promise<{ failedWorkspaces: BaseWorkspaceInfo[], processed: number, skipped: number }> {
|
||||
const workspacesIgnore = new Set(this.config.SkipWorkspaces.split(';'))
|
||||
ctx.info('skipped workspaces', { workspacesIgnore })
|
||||
let skipped = 0
|
||||
const workspaces = (await listAccountWorkspaces(this.config.Token)).filter((it) => {
|
||||
const lastBackup = it.backupInfo?.lastBackup ?? 0
|
||||
if ((Date.now() - lastBackup) / 1000 < this.config.Interval) {
|
||||
// No backup required, interval not elapsed
|
||||
ctx.info('Skip backup', { workspace: it.workspace, lastBackup: Math.round((Date.now() - lastBackup) / 1000) })
|
||||
skipped++
|
||||
return false
|
||||
}
|
||||
|
||||
if (it.lastVisit == null) {
|
||||
skipped++
|
||||
return false
|
||||
}
|
||||
|
||||
const lastVisitSec = Math.floor((Date.now() - it.lastVisit) / 1000)
|
||||
if (lastVisitSec > this.config.Interval) {
|
||||
// No backup required, interval not elapsed
|
||||
ctx.info('Skip backup, since not visited since last check', {
|
||||
workspace: it.workspace,
|
||||
days: Math.floor(lastVisitSec / 3600 / 24),
|
||||
seconds: lastVisitSec
|
||||
})
|
||||
skipped++
|
||||
return false
|
||||
}
|
||||
return !workspacesIgnore.has(it.workspace)
|
||||
})
|
||||
workspaces.sort((a, b) => b.lastVisit - a.lastVisit)
|
||||
|
||||
ctx.info('Preparing for BACKUP', {
|
||||
total: workspaces.length,
|
||||
skipped,
|
||||
workspaces: workspaces.map((it) => it.workspace)
|
||||
})
|
||||
|
||||
return await this.doBackup(ctx, workspaces)
|
||||
}
|
||||
|
||||
@@ -230,7 +240,7 @@ class BackupWorker {
|
||||
dataSize: Math.round((result.dataSize * 100) / (1024 * 1024)) / 100,
|
||||
blobsSize: Math.round((result.blobsSize * 100) / (1024 * 1024)) / 100
|
||||
}
|
||||
rootCtx.warn('\n\nBACKUP STATS ', {
|
||||
rootCtx.warn('BACKUP STATS', {
|
||||
workspace: ws.workspace,
|
||||
index,
|
||||
...backupInfo,
|
||||
|
||||
@@ -203,8 +203,7 @@ export async function login (user: string, password: string, workspace: string):
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
const { token } = result.result
|
||||
return token
|
||||
return result.result?.token
|
||||
}
|
||||
|
||||
export async function getUserWorkspaces (token: string): Promise<BaseWorkspaceInfo[]> {
|
||||
|
||||
@@ -98,6 +98,7 @@ export class BlobClient {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
readable.on('end', () => {
|
||||
readable.destroy()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,6 +77,16 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
|
||||
async initialize (ctx: MeasureContext, workspaceId: WorkspaceId): Promise<void> {}
|
||||
|
||||
doTrimHash (s: string | undefined): string {
|
||||
if (s == null) {
|
||||
return ''
|
||||
}
|
||||
if (s.startsWith('"') && s.endsWith('"')) {
|
||||
return s.slice(1, s.length - 1)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
async doSyncDocs (ctx: MeasureContext, workspaceId: WorkspaceId, docs: ListBlobResult[]): Promise<void> {
|
||||
const existingBlobs = toIdMap(
|
||||
await this.dbAdapter.find<Blob>(ctx, workspaceId, DOMAIN_BLOB, { _id: { $in: docs.map((it) => it._id) } })
|
||||
@@ -84,10 +94,20 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
const toUpdate: Blob[] = []
|
||||
for (const d of docs) {
|
||||
const blobInfo = existingBlobs.get(d._id)
|
||||
if (blobInfo === undefined || blobInfo.etag !== d.etag || blobInfo.size !== d.size) {
|
||||
const stat = await this.stat(ctx, workspaceId, d._id)
|
||||
if (
|
||||
blobInfo === undefined || // Blob info undefined
|
||||
// Provider are same and etag or size are diffrent.
|
||||
(d.provider === blobInfo.provider &&
|
||||
(this.doTrimHash(blobInfo.etag) !== this.doTrimHash(d.etag) || blobInfo.size !== d.size)) ||
|
||||
// We have replacement in default
|
||||
(d.provider === this.defaultAdapter && blobInfo?.provider !== d.provider)
|
||||
) {
|
||||
const stat = await this.adapters.get(d.provider)?.stat(ctx, workspaceId, d._id)
|
||||
if (stat !== undefined) {
|
||||
stat.provider = d.provider
|
||||
toUpdate.push(stat)
|
||||
} else {
|
||||
ctx.error('blob not found for sync', { provider: d.provider, id: d._id, workspace: workspaceId.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,19 +140,24 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
}
|
||||
|
||||
private makeStorageIterator (ctx: MeasureContext, workspaceId: WorkspaceId): BlobStorageIterator {
|
||||
const adapters = Array.from(this.adapters.values())
|
||||
const adapters = Array.from(this.adapters.entries())
|
||||
let provider: [string, StorageAdapter] | undefined
|
||||
let iterator: BlobStorageIterator | undefined
|
||||
return {
|
||||
next: async () => {
|
||||
while (true) {
|
||||
if (iterator === undefined && adapters.length > 0) {
|
||||
iterator = await (adapters.shift() as StorageAdapter).listStream(ctx, workspaceId)
|
||||
provider = adapters.shift() as [string, StorageAdapter]
|
||||
iterator = await provider[1].listStream(ctx, workspaceId)
|
||||
}
|
||||
if (iterator === undefined) {
|
||||
return []
|
||||
}
|
||||
const docInfos = await iterator.next()
|
||||
if (docInfos.length > 0) {
|
||||
for (const d of docInfos) {
|
||||
d.provider = provider?.[0] as string
|
||||
}
|
||||
// We need to check if our stored version is fine
|
||||
return docInfos
|
||||
} else {
|
||||
@@ -323,7 +348,7 @@ export class AggregatorStorageAdapter implements StorageAdapter, StorageAdapterE
|
||||
|
||||
const result = await adapter.put(ctx, workspaceId, objectName, stream, contentType, size)
|
||||
|
||||
if (size === undefined || size === 0) {
|
||||
if (size === undefined || size === 0 || !Number.isInteger(size)) {
|
||||
const docStats = await adapter.stat(ctx, workspaceId, objectName)
|
||||
if (docStats !== undefined) {
|
||||
if (contentType !== docStats.contentType) {
|
||||
|
||||
@@ -512,8 +512,9 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
if (val.classes !== undefined) {
|
||||
if (val.classes.length === 1) {
|
||||
res.push(`AND ${val.toAlias}._class = '${val.classes[0]}'`)
|
||||
} else {
|
||||
res.push(`AND ${val.toAlias}._class IN (${val.classes.map((c) => `'${c}'`).join(', ')})`)
|
||||
}
|
||||
res.push(`AND ${val.toAlias}._class IN (${val.classes.map((c) => `'${c}'`).join(', ')})`)
|
||||
}
|
||||
}
|
||||
return res.join(' ')
|
||||
@@ -863,8 +864,16 @@ abstract class PostgresAdapterBase implements DbAdapter {
|
||||
private getProjectionsAliases (join: JoinProps): string[] {
|
||||
if (join.table === DOMAIN_MODEL) return []
|
||||
if (join.isReverse) {
|
||||
let classsesQuery = ''
|
||||
if (join.classes !== undefined) {
|
||||
if (join.classes.length === 1) {
|
||||
classsesQuery = ` AND ${join.toAlias}._class = '${join.classes[0]}'`
|
||||
} else {
|
||||
classsesQuery = ` AND ${join.toAlias}._class IN (${join.classes.map((c) => `'${c}'`).join(', ')})`
|
||||
}
|
||||
}
|
||||
return [
|
||||
`(SELECT jsonb_agg(${join.toAlias}.*) FROM ${join.table} AS ${join.toAlias} WHERE ${join.fromAlias}.${join.fromField} = ${join.toAlias}."${join.toField}") AS ${join.toAlias}`
|
||||
`(SELECT jsonb_agg(${join.toAlias}.*) FROM ${join.table} AS ${join.toAlias} WHERE ${join.fromAlias}.${join.fromField} = ${join.toAlias}."${join.toField}" ${classsesQuery}) AS ${join.toAlias}`
|
||||
]
|
||||
}
|
||||
const res: string[] = []
|
||||
|
||||
@@ -20,7 +20,7 @@ import { htmlToMarkup, isEmptyMarkup, jsonToMarkup, MarkupNodeType } from '@hcen
|
||||
import { toHTML } from '@telegraf/entity'
|
||||
import { CallbackQuery, Message, Update } from 'telegraf/typings/core/types/typegram'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { WithId } from 'mongodb'
|
||||
import { ObjectId, WithId } from 'mongodb'
|
||||
|
||||
import config from '../config'
|
||||
import { PlatformWorker } from '../worker'
|
||||
@@ -29,13 +29,13 @@ import { toTelegramFileInfo } from '../utils'
|
||||
import { Command, defineCommands } from './commands'
|
||||
import { ChannelRecord, MessageRecord, TelegramFileInfo, UserRecord, WorkspaceInfo } from '../types'
|
||||
|
||||
function encodeChannelId (workspace: string, channelId: string): string {
|
||||
return `${workspace}_${channelId}`
|
||||
function encodeChannelId (channelId: string): string {
|
||||
return `@${channelId}`
|
||||
}
|
||||
|
||||
function decodeChannelId (id: string): { workspace: string, channelId: string } {
|
||||
const [workspace, channelId] = id.split('_')
|
||||
return { workspace, channelId }
|
||||
function decodeChannelId (id: string): string | undefined {
|
||||
const [, channelId] = id.split('@')
|
||||
return channelId
|
||||
}
|
||||
|
||||
const getNextActionId = (workspace: string, page: number): string => `next_${workspace}_${page}`
|
||||
@@ -114,10 +114,9 @@ async function handleSelectChannel (
|
||||
const userRecord = await worker.getUserRecord(id)
|
||||
if (userRecord === undefined) return ['', false]
|
||||
|
||||
const { workspace, channelId } = decodeChannelId(match)
|
||||
|
||||
const channels = await worker.getChannels(userRecord.email, workspace)
|
||||
const channel = channels.find((it) => it._id.toString() === channelId)
|
||||
const channelId = decodeChannelId(match)
|
||||
if (channelId === undefined || channelId === '') return ['', false]
|
||||
const channel = await worker.getChannel(userRecord.email, new ObjectId(channelId))
|
||||
|
||||
if (channel === undefined) return ['', false]
|
||||
|
||||
@@ -134,6 +133,14 @@ async function handleSelectChannel (
|
||||
return [channel.name, await worker.sendMessage(channel, userMessage.message_id, text, file)]
|
||||
}
|
||||
|
||||
async function showNoChannelsMessage (ctx: Context, worker: PlatformWorker, workspace: string): Promise<void> {
|
||||
const ws = await worker.getWorkspaceInfo(workspace)
|
||||
await ctx.editMessageText(
|
||||
`No channels found in workspace <b>${ws?.name ?? workspace}</b>.\nTo sync channels call /${Command.SyncAllChannels} or /${Command.SyncStarredChannels}`,
|
||||
{ parse_mode: 'HTML' }
|
||||
)
|
||||
}
|
||||
|
||||
async function createSelectChannelKeyboard (
|
||||
ctx: NarrowedContext<TgContext, Update.MessageUpdate>,
|
||||
worker: PlatformWorker,
|
||||
@@ -141,6 +148,12 @@ async function createSelectChannelKeyboard (
|
||||
workspace: string
|
||||
): Promise<void> {
|
||||
const channels = await worker.getChannels(userRecord.email, workspace)
|
||||
|
||||
if (channels.length === 0) {
|
||||
await showNoChannelsMessage(ctx, worker, workspace)
|
||||
return
|
||||
}
|
||||
|
||||
const hasNext = channels.length > channelsPerPage
|
||||
const pageChannels = getPageChannels(channels, 0)
|
||||
|
||||
@@ -148,9 +161,7 @@ async function createSelectChannelKeyboard (
|
||||
reply_parameters: { message_id: ctx.message.message_id },
|
||||
...Markup.inlineKeyboard(
|
||||
[
|
||||
...pageChannels.map((channel) =>
|
||||
Markup.button.callback(channel.name, encodeChannelId(channel.workspace, channel._id.toString()))
|
||||
),
|
||||
...pageChannels.map((channel) => Markup.button.callback(channel.name, encodeChannelId(channel._id.toString()))),
|
||||
...(hasNext ? [Markup.button.callback('Next>', getNextActionId(workspace, 0))] : [])
|
||||
],
|
||||
{ columns: 1 }
|
||||
@@ -283,7 +294,7 @@ export async function setUpBot (worker: PlatformWorker): Promise<Telegraf<TgCont
|
||||
ctx.processingKeyboards.delete(messageId)
|
||||
})
|
||||
|
||||
bot.action(/.+_.+/, async (ctx) => {
|
||||
bot.action(/@.+/, async (ctx) => {
|
||||
const messageId = ctx.callbackQuery.message?.message_id
|
||||
if (messageId === undefined) return
|
||||
if (ctx.processingKeyboards.has(messageId)) return
|
||||
@@ -341,11 +352,7 @@ const editChannelKeyboard = async (
|
||||
const channels = await worker.getChannels(userRecord.email, workspace)
|
||||
|
||||
if (channels.length === 0) {
|
||||
const ws = await worker.getWorkspaceInfo(workspace)
|
||||
await ctx.editMessageText(
|
||||
`No channels found in workspace <b>${ws?.name ?? workspace}</b>.\nTo add channels call /${Command.SyncAllChannels} or /${Command.SyncStarredChannels}`,
|
||||
{ parse_mode: 'HTML' }
|
||||
)
|
||||
await showNoChannelsMessage(ctx, worker, workspace)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -355,9 +362,7 @@ const editChannelKeyboard = async (
|
||||
|
||||
await ctx.editMessageReplyMarkup({
|
||||
inline_keyboard: [
|
||||
...pageChannels.map((channel) => [
|
||||
Markup.button.callback(channel.name, encodeChannelId(channel.workspace, channel._id.toString()))
|
||||
]),
|
||||
...pageChannels.map((channel) => [Markup.button.callback(channel.name, encodeChannelId(channel._id.toString()))]),
|
||||
[
|
||||
...(hasPrev ? [Markup.button.callback('<Prev', getPrevActionId(workspace, page))] : []),
|
||||
...(hasNext ? [Markup.button.callback('Next>', getNextActionId(workspace, page))] : [])
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Collection, WithId } from 'mongodb'
|
||||
import type { Collection, ObjectId, WithId } from 'mongodb'
|
||||
import { MeasureContext, Ref, SortingOrder, systemAccountEmail } from '@hcengineering/core'
|
||||
import { InboxNotification } from '@hcengineering/notification'
|
||||
import { TelegramNotificationRequest } from '@hcengineering/telegram'
|
||||
@@ -44,9 +44,11 @@ const closeWorkspaceTimeout = 10 * 60 * 1000 // 10 minutes
|
||||
export class PlatformWorker {
|
||||
private readonly workspacesClients = new Map<string, WorkspaceClient>()
|
||||
private readonly closeWorkspaceTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>()
|
||||
private readonly intervalId: NodeJS.Timeout | undefined
|
||||
private readonly otpIntervalId: NodeJS.Timeout | undefined
|
||||
private readonly clearIntervalId: NodeJS.Timeout | undefined
|
||||
|
||||
private readonly channelsMap = new Map<string, WithId<ChannelRecord>[]>()
|
||||
private readonly channelsByWorkspace = new Map<string, WithId<ChannelRecord>[]>()
|
||||
private readonly channelById = new Map<ObjectId, WithId<ChannelRecord>>()
|
||||
private readonly workspaceInfoById = new Map<string, WorkspaceInfo>()
|
||||
|
||||
private constructor (
|
||||
@@ -58,12 +60,19 @@ export class PlatformWorker {
|
||||
private readonly repliesStorage: Collection<ReplyRecord>,
|
||||
private readonly channelsStorage: Collection<ChannelRecord>
|
||||
) {
|
||||
this.intervalId = setInterval(
|
||||
this.otpIntervalId = setInterval(
|
||||
() => {
|
||||
void otpStorage.deleteMany({ expires: { $lte: Date.now() } })
|
||||
},
|
||||
3 * 60 * 1000
|
||||
)
|
||||
this.clearIntervalId = setInterval(
|
||||
() => {
|
||||
this.channelsByWorkspace.clear()
|
||||
this.channelById.clear()
|
||||
},
|
||||
60 * 60 * 1000
|
||||
)
|
||||
}
|
||||
|
||||
public async getUsersToDisconnect (): Promise<UserRecord[]> {
|
||||
@@ -75,8 +84,11 @@ export class PlatformWorker {
|
||||
}
|
||||
|
||||
async close (): Promise<void> {
|
||||
if (this.intervalId !== undefined) {
|
||||
clearInterval(this.intervalId)
|
||||
if (this.otpIntervalId !== undefined) {
|
||||
clearInterval(this.otpIntervalId)
|
||||
}
|
||||
if (this.clearIntervalId !== undefined) {
|
||||
clearInterval(this.clearIntervalId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,14 +266,32 @@ export class PlatformWorker {
|
||||
async getChannels (email: string, workspace: string): Promise<WithId<ChannelRecord>[]> {
|
||||
const key = `${email}:${workspace}`
|
||||
|
||||
if (this.channelsMap.has(key)) {
|
||||
return this.channelsMap.get(key) ?? []
|
||||
if (this.channelsByWorkspace.has(key)) {
|
||||
return this.channelsByWorkspace.get(key) ?? []
|
||||
}
|
||||
const res = await this.channelsStorage
|
||||
.find({ workspace, email }, { sort: { name: SortingOrder.Ascending } })
|
||||
.toArray()
|
||||
|
||||
this.channelsMap.set(key, res)
|
||||
this.channelsByWorkspace.set(key, res)
|
||||
for (const channel of res) {
|
||||
this.channelById.set(channel._id, channel)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
async getChannel (email: string, channelId: ObjectId): Promise<WithId<ChannelRecord> | undefined> {
|
||||
if (this.channelById.has(channelId)) {
|
||||
const channel = this.channelById.get(channelId)
|
||||
|
||||
return channel !== undefined && channel.email === email ? channel : undefined
|
||||
}
|
||||
|
||||
const res = (await this.channelsStorage.findOne({ _id: channelId, email })) ?? undefined
|
||||
|
||||
if (res !== undefined) {
|
||||
this.channelById.set(res._id, res)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -317,7 +347,12 @@ export class PlatformWorker {
|
||||
await this.channelsStorage.deleteMany({ _id: { $in: toDelete.map((c) => c._id) } })
|
||||
}
|
||||
|
||||
this.channelsMap.delete(`${email}:${workspace}`)
|
||||
this.channelsByWorkspace.delete(`${email}:${workspace}`)
|
||||
for (const [key, channel] of this.channelById.entries()) {
|
||||
if (channel.email === email) {
|
||||
this.channelById.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getWorkspaceInfo (workspaceId: string): Promise<WorkspaceInfo | undefined> {
|
||||
|
||||
Reference in New Issue
Block a user