From 57ee462ca6347bdd2faad1724467c6b65c9a6e84 Mon Sep 17 00:00:00 2001 From: Anna Khismatullina Date: Thu, 12 Dec 2024 18:11:09 +0700 Subject: [PATCH 1/5] Init scripts with unified import format (#7242) * Integrate import to workspace initialization Signed-off-by: Anna Khismatullina * Support importing drawing along with attachments Signed-off-by: Anna Khismatullina * Put init scripts into workspace container Signed-off-by: Anna Khismatullina --------- Signed-off-by: Anna Khismatullina --- .github/workflows/main.yml | 9 + .vscode/launch.json | 2 +- .vscode/settings.json | 2 +- common/config/rush/pnpm-lock.yaml | 118 ++++++++++- dev/.env | 2 +- dev/docker-compose.yaml | 3 - dev/import-tool/README.md | 8 +- .../{src => docs}/clickup/README.md | 0 dev/import-tool/{src => docs}/huly/README.md | 0 .../huly/example-workspace/Documentation.yaml | 1 + .../Documentation/Getting Started.md | 0 .../Documentation/User Guide.md | 0 .../Documentation/User Guide/Installation.md | 0 .../Documentation/files/architecture.png | Bin .../huly/example-workspace/Project Alpha.yaml | 1 + .../Project Alpha/1.Project Setup.md | 2 + .../1.Project Setup/2.Configure CI.md | 0 .../Project Alpha/4.Update Docs.md | 0 .../Project Alpha/files/config.yaml | 0 .../Project Alpha/files/screenshot.png | Bin 0 -> 39710 bytes .../files/screenshot/drawing1.json | 55 +++++ .../{src => docs}/notion/README.md | 0 dev/import-tool/package.json | 31 +-- dev/import-tool/src/index.ts | 30 ++- dev/tool/src/index.ts | 7 +- packages/importer/.eslintrc.js | 7 + packages/importer/.npmignore | 4 + packages/importer/jest.config.js | 7 + packages/importer/package.json | 69 ++++++ .../importer}/src/clickup/clickup.ts | 23 +- .../importer}/src/huly/unified.ts | 188 ++++++++++++----- .../importer}/src/importer/builder.ts | 33 +++ .../importer}/src/importer/dowloader.ts | 0 .../importer/src/importer/frontUploader.ts | 54 +++-- .../importer}/src/importer/importer.ts | 197 ++++++++++++++---- packages/importer/src/importer/logger.ts | 18 ++ .../importer}/src/importer/preprocessor.ts | 0 .../importer/src/importer/storageUploader.ts | 59 ++++++ packages/importer/src/importer/uploader.ts | 33 +++ packages/importer/src/index.ts | 23 ++ .../importer}/src/notion/notion.ts | 8 +- packages/importer/tsconfig.json | 10 + pods/workspace/Dockerfile | 3 + rush.json | 5 + server/account-service/package.json | 1 - server/account-service/src/index.ts | 6 - server/tool/package.json | 1 + server/tool/src/index.ts | 20 +- server/tool/src/initializer.ts | 26 ++- server/tool/src/plugin.ts | 2 +- server/workspace-service/src/index.ts | 9 +- 51 files changed, 883 insertions(+), 194 deletions(-) rename dev/import-tool/{src => docs}/clickup/README.md (100%) rename dev/import-tool/{src => docs}/huly/README.md (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Documentation.yaml (94%) rename dev/import-tool/{src => docs}/huly/example-workspace/Documentation/Getting Started.md (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Documentation/User Guide.md (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Documentation/User Guide/Installation.md (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Documentation/files/architecture.png (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Project Alpha.yaml (95%) rename dev/import-tool/{src => docs}/huly/example-workspace/Project Alpha/1.Project Setup.md (94%) rename dev/import-tool/{src => docs}/huly/example-workspace/Project Alpha/1.Project Setup/2.Configure CI.md (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Project Alpha/4.Update Docs.md (100%) rename dev/import-tool/{src => docs}/huly/example-workspace/Project Alpha/files/config.yaml (100%) create mode 100644 dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot.png create mode 100644 dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot/drawing1.json rename dev/import-tool/{src => docs}/notion/README.md (100%) create mode 100644 packages/importer/.eslintrc.js create mode 100644 packages/importer/.npmignore create mode 100644 packages/importer/jest.config.js create mode 100644 packages/importer/package.json rename {dev/import-tool => packages/importer}/src/clickup/clickup.ts (93%) rename {dev/import-tool => packages/importer}/src/huly/unified.ts (76%) rename {dev/import-tool => packages/importer}/src/importer/builder.ts (94%) rename {dev/import-tool => packages/importer}/src/importer/dowloader.ts (100%) rename dev/import-tool/src/importer/uploader.ts => packages/importer/src/importer/frontUploader.ts (53%) rename {dev/import-tool => packages/importer}/src/importer/importer.ts (73%) create mode 100644 packages/importer/src/importer/logger.ts rename {dev/import-tool => packages/importer}/src/importer/preprocessor.ts (100%) create mode 100644 packages/importer/src/importer/storageUploader.ts create mode 100644 packages/importer/src/importer/uploader.ts create mode 100644 packages/importer/src/index.ts rename {dev/import-tool => packages/importer}/src/notion/notion.ts (98%) create mode 100644 packages/importer/tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f051325abe..3333d250d3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,6 +42,7 @@ env: tools PublishTempFolder: publish_artifacts MODEL_VERSION_MODE: ${{ startsWith(github.ref, 'refs/tags/s') && 'tagTime' || 'file' }} + INIT_SCRIPTS_BRANCH: 'unified-init-scripts' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -493,6 +494,14 @@ jobs: with: fetch-depth: 0 filter: tree:0 + + - name: Checkout init repository + run: | + wget https://github.com/hcengineering/init/archive/refs/heads/${{env.INIT_SCRIPTS_BRANCH}}.zip + unzip ${{env.INIT_SCRIPTS_BRANCH}}.zip -d pods/workspace + mv pods/workspace/init-${{env.INIT_SCRIPTS_BRANCH}} pods/workspace/init + rm -rf ${{env.INIT_SCRIPTS_BRANCH}}.zip + - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' diff --git a/.vscode/launch.json b/.vscode/launch.json index 646aab7971..6823fd564f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -624,7 +624,7 @@ "name": "Debug Huly import", "type": "node", "request": "launch", - "args": ["src/__start.ts", "import", "/home/anna/xored/huly/platform/dev/import-tool/src/huly/example-workspace", "-u", "user1", "-pw", "1234", "-ws", "ws12"], + "args": ["src/__start.ts", "import", "/home/anna/huly/platform/dev/import-tool/docs/huly/example-workspace", "-u", "user1", "-pw", "1234", "-ws", "ws1"], "env": { "FRONT_URL": "http://localhost:8087" }, diff --git a/.vscode/settings.json b/.vscode/settings.json index a7c6b18b4e..bb197af43f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, "[json]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 83ee914bb6..8126b9d196 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -296,6 +296,9 @@ dependencies: '@rush-temp/import-tool': specifier: file:./projects/import-tool.tgz version: file:projects/import-tool.tgz + '@rush-temp/importer': + specifier: file:./projects/importer.tgz + version: file:projects/importer.tgz(esbuild@0.20.1)(ts-node@10.9.2) '@rush-temp/inventory': specifier: file:./projects/inventory.tgz version: file:projects/inventory.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2) @@ -1640,6 +1643,9 @@ dependencies: htmlparser2: specifier: ^9.0.0 version: 9.1.0 + image-size: + specifier: ^1.1.1 + version: 1.1.1 intl-messageformat: specifier: ^9.7.1 version: 9.13.0 @@ -5242,6 +5248,10 @@ packages: resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} dev: false + /@sec-ant/readable-stream@0.4.1: + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + dev: false + /@selderee/plugin-htmlparser2@0.11.0: resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} dependencies: @@ -12476,6 +12486,16 @@ packages: token-types: 4.2.1 dev: false + /file-type@19.6.0: + resolution: {integrity: sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ==} + engines: {node: '>=18'} + dependencies: + get-stream: 9.0.1 + strtok3: 9.1.1 + token-types: 6.0.0 + uint8array-extras: 1.4.0 + dev: false + /filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: @@ -12897,6 +12917,14 @@ packages: engines: {node: '>=10'} dev: false + /get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + dev: false + /get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} engines: {node: '>= 0.4'} @@ -13654,6 +13682,14 @@ packages: '@types/node': 16.9.1 dev: false + /image-size@1.1.1: + resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==} + engines: {node: '>=16.x'} + hasBin: true + dependencies: + queue: 6.0.2 + dev: false + /immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} dev: false @@ -14070,6 +14106,11 @@ packages: engines: {node: '>=8'} dev: false + /is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + dev: false + /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -17235,6 +17276,11 @@ packages: engines: {node: '>=8'} dev: false + /peek-readable@5.3.1: + resolution: {integrity: sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw==} + engines: {node: '>=14.16'} + dev: false + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: false @@ -18106,6 +18152,12 @@ packages: requiresBuild: true dev: false + /queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + dependencies: + inherits: 2.0.4 + dev: false + /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: false @@ -19540,6 +19592,14 @@ packages: peek-readable: 4.1.0 dev: false + /strtok3@9.1.1: + resolution: {integrity: sha512-FhwotcEqjr241ZbjFzjlIYg6c5/L/s4yBGWSMvJ9UoExiSqL+FnFA/CaeZx17WGaZMS/4SOZp8wH18jSS4R4lw==} + engines: {node: '>=16'} + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.3.1 + dev: false + /style-loader@3.3.4(webpack@5.97.1): resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} engines: {node: '>= 12.13.0'} @@ -20092,6 +20152,14 @@ packages: ieee754: 1.2.1 dev: false + /token-types@6.0.0: + resolution: {integrity: sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==} + engines: {node: '>=14.16'} + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + dev: false + /toposort@2.0.2: resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} dev: false @@ -20619,6 +20687,11 @@ packages: resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} dev: false + /uint8array-extras@1.4.0: + resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} + engines: {node: '>=18'} + dev: false + /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} dependencies: @@ -21807,7 +21880,7 @@ packages: dev: false file:projects/account-service.tgz: - resolution: {integrity: sha512-Cyt9smf3yBf80n9zd8D77piZd9wnJIYLjzSJzSfcZLA+w4WPacwDMZEU9hN3Pf9aAIpeaaVpuZqXSvnOoBvv4A==, tarball: file:projects/account-service.tgz} + resolution: {integrity: sha512-Kxz4jt8+j+W4r7hgcpPq5D1wVL5G4AmpEWF6yxyUTR6+vxQERCTILfJo6wRbecPBvEPcFvA7Snogo8exY2YJQg==, tarball: file:projects/account-service.tgz} name: '@rush-temp/account-service' version: 0.0.0 dependencies: @@ -24718,7 +24791,7 @@ packages: dev: false file:projects/import-tool.tgz: - resolution: {integrity: sha512-0Q1/hHZxEdYFPr2qqfovlVJRA8JyvWsKtY3ubHrffnaAMbbN6BNWt6Jf+GzwyGeIwImLI2Oud2x/WqOFb/USdg==, tarball: file:projects/import-tool.tgz} + resolution: {integrity: sha512-Pkbjr/0XN1X7G8Gi6vwh0DjhRr8f0un2HSW2ef6ZMsIv2MyH8BgE2erUpXmzHETxSKsyJllAm6T1DmfSa26XIg==, tarball: file:projects/import-tool.tgz} name: '@rush-temp/import-tool' version: 0.0.0 dependencies: @@ -24765,6 +24838,45 @@ packages: - supports-color dev: false + file:projects/importer.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-H8zT+HFFD2VV3Fo3ALiiDtd0VtXQZpWEfm10a5udm4D07Gv4DgxJOLl8uF4wNId//6UNKOp6/z517p8D/8velg==, tarball: file:projects/importer.tgz} + id: file:projects/importer.tgz + name: '@rush-temp/importer' + version: 0.0.0 + dependencies: + '@types/csvtojson': 2.0.0 + '@types/jest': 29.5.12 + '@types/js-yaml': 4.0.9 + '@types/mime-types': 2.1.4 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2) + commander: 8.3.0 + csvtojson: 2.0.10 + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + file-type: 19.6.0 + image-size: 1.1.1 + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + js-yaml: 4.1.0 + mime-types: 2.1.35 + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2) + typescript: 5.6.2 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + dev: false + file:projects/inventory-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): resolution: {integrity: sha512-Uayr70kuiNfwBgNoFcu1rkWhdHhnbg7aEDcbozbhn5Eyel/B6he2uUYxPZc2gl1VUiEA8KBGszSAEXMs0YER0A==, tarball: file:projects/inventory-assets.tgz} id: file:projects/inventory-assets.tgz @@ -31166,7 +31278,7 @@ packages: dev: false file:projects/server-tool.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2): - resolution: {integrity: sha512-Pd5QWUOAPkgU3vgG4J4yXrQJuskV46yXxzATYcWPzY+4LQRJ4jshHlld/rI2EGJKJNr5fy6trH1p8LJ+xvLdeA==, tarball: file:projects/server-tool.tgz} + resolution: {integrity: sha512-FwqM3BepKqMKvnizEtaWgyv6YTuhwVDaJ4yPF+DEDLqHmxdeGC5u2Akj+sIdP0qtmSRQy9ts5NMwJw2SVNIetQ==, tarball: file:projects/server-tool.tgz} id: file:projects/server-tool.tgz name: '@rush-temp/server-tool' version: 0.0.0 diff --git a/dev/.env b/dev/.env index 6650314f86..7f2e5141e7 100644 --- a/dev/.env +++ b/dev/.env @@ -1,3 +1,3 @@ STORAGE_CONFIG="minio|minio?accessKey=minioadmin&secretKey=minioadmin" MONGO_URL=mongodb://mongodb:27017?compressors=snappy -DB_URL_PG=postgresql://postgres:example@postgres:5432 \ No newline at end of file +DB_URL_PG=postgresql://postgres:example@postgres:5432 diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml index 01adc293e9..bed9eceb30 100644 --- a/dev/docker-compose.yaml +++ b/dev/docker-compose.yaml @@ -138,7 +138,6 @@ services: - ACCOUNTS_URL=http://host.docker.internal:3000 - BRANDING_PATH=/var/cfg/branding.json # - PARALLEL=2 - - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml - INIT_WORKSPACE=test restart: unless-stopped workspacepg: @@ -165,7 +164,6 @@ services: - ACCOUNTS_URL=http://host.docker.internal:3000 - BRANDING_PATH=/var/cfg/branding.json # - PARALLEL=2 - - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml # - INIT_WORKSPACE=onboarding restart: unless-stopped workspace_cockroach: @@ -191,7 +189,6 @@ services: - ACCOUNTS_URL=http://host.docker.internal:3000 - BRANDING_PATH=/var/cfg/branding.json # - PARALLEL=2 - - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml # - INIT_WORKSPACE=onboarding restart: unless-stopped collaborator: diff --git a/dev/import-tool/README.md b/dev/import-tool/README.md index 4e1c0d56b8..59de5713b5 100644 --- a/dev/import-tool/README.md +++ b/dev/import-tool/README.md @@ -5,9 +5,9 @@ Tool for importing data into Huly workspace. ## Recommended Import Method ### Unified Format Import -The recommended way to import data into Huly is using our [Unified Import Format](./src/huly/README.md). This format provides a straightforward way to migrate data from any system by converting it into an intermediate, human-readable structure. +The recommended way to import data into Huly is using our [Unified Import Format](./docs/huly/README.md). This format provides a straightforward way to migrate data from any system by converting it into an intermediate, human-readable structure. -See the [complete guide](./src/huly/README.md) and [example workspace](./src/huly/example-workspace) to get started. +See the [complete guide](./docs/huly/README.md) and [example workspace](./docs/huly/example-workspace) to get started. ### Why Use Unified Format? - Simple, human-readable format using YAML and Markdown @@ -19,7 +19,7 @@ See the [complete guide](./src/huly/README.md) and [example workspace](./src/hul We also support direct import from some platforms: -1. **Notion**: see [Import from Notion Guide](./src/notion/README.md) -2. **ClickUp**: see [Import from ClickUp Guide](./src/clickup/README.md) +1. **Notion**: see [Import from Notion Guide](./docs/notion/README.md) +2. **ClickUp**: see [Import from ClickUp Guide](./docs/clickup/README.md) These direct imports are suitable for simple migrations, but for complex cases or systems not listed above, please use the Unified Format. \ No newline at end of file diff --git a/dev/import-tool/src/clickup/README.md b/dev/import-tool/docs/clickup/README.md similarity index 100% rename from dev/import-tool/src/clickup/README.md rename to dev/import-tool/docs/clickup/README.md diff --git a/dev/import-tool/src/huly/README.md b/dev/import-tool/docs/huly/README.md similarity index 100% rename from dev/import-tool/src/huly/README.md rename to dev/import-tool/docs/huly/README.md diff --git a/dev/import-tool/src/huly/example-workspace/Documentation.yaml b/dev/import-tool/docs/huly/example-workspace/Documentation.yaml similarity index 94% rename from dev/import-tool/src/huly/example-workspace/Documentation.yaml rename to dev/import-tool/docs/huly/example-workspace/Documentation.yaml index 3279e182fb..781c2b6f12 100644 --- a/dev/import-tool/src/huly/example-workspace/Documentation.yaml +++ b/dev/import-tool/docs/huly/example-workspace/Documentation.yaml @@ -1,5 +1,6 @@ class: document:class:Teamspace title: Documentation +emoji: 📖 private: false autoJoin: true owners: diff --git a/dev/import-tool/src/huly/example-workspace/Documentation/Getting Started.md b/dev/import-tool/docs/huly/example-workspace/Documentation/Getting Started.md similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Documentation/Getting Started.md rename to dev/import-tool/docs/huly/example-workspace/Documentation/Getting Started.md diff --git a/dev/import-tool/src/huly/example-workspace/Documentation/User Guide.md b/dev/import-tool/docs/huly/example-workspace/Documentation/User Guide.md similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Documentation/User Guide.md rename to dev/import-tool/docs/huly/example-workspace/Documentation/User Guide.md diff --git a/dev/import-tool/src/huly/example-workspace/Documentation/User Guide/Installation.md b/dev/import-tool/docs/huly/example-workspace/Documentation/User Guide/Installation.md similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Documentation/User Guide/Installation.md rename to dev/import-tool/docs/huly/example-workspace/Documentation/User Guide/Installation.md diff --git a/dev/import-tool/src/huly/example-workspace/Documentation/files/architecture.png b/dev/import-tool/docs/huly/example-workspace/Documentation/files/architecture.png similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Documentation/files/architecture.png rename to dev/import-tool/docs/huly/example-workspace/Documentation/files/architecture.png diff --git a/dev/import-tool/src/huly/example-workspace/Project Alpha.yaml b/dev/import-tool/docs/huly/example-workspace/Project Alpha.yaml similarity index 95% rename from dev/import-tool/src/huly/example-workspace/Project Alpha.yaml rename to dev/import-tool/docs/huly/example-workspace/Project Alpha.yaml index 0e13f3656c..ee5389ad33 100644 --- a/dev/import-tool/src/huly/example-workspace/Project Alpha.yaml +++ b/dev/import-tool/docs/huly/example-workspace/Project Alpha.yaml @@ -1,6 +1,7 @@ class: tracker:class:Project title: Project Alpha identifier: ALPHA +emoji: 🦄 private: false autoJoin: true owners: diff --git a/dev/import-tool/src/huly/example-workspace/Project Alpha/1.Project Setup.md b/dev/import-tool/docs/huly/example-workspace/Project Alpha/1.Project Setup.md similarity index 94% rename from dev/import-tool/src/huly/example-workspace/Project Alpha/1.Project Setup.md rename to dev/import-tool/docs/huly/example-workspace/Project Alpha/1.Project Setup.md index 833418a26d..53a4813312 100644 --- a/dev/import-tool/src/huly/example-workspace/Project Alpha/1.Project Setup.md +++ b/dev/import-tool/docs/huly/example-workspace/Project Alpha/1.Project Setup.md @@ -16,6 +16,8 @@ comments: - author: john.doe@example.com text: | Perfect, don't forget to update [documentation](../Documentation/User%20Guide/Installation.md) when you're done. + attachments: + - ./files/screenshot.png --- **Initial project infrastructure setup.** diff --git a/dev/import-tool/src/huly/example-workspace/Project Alpha/1.Project Setup/2.Configure CI.md b/dev/import-tool/docs/huly/example-workspace/Project Alpha/1.Project Setup/2.Configure CI.md similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Project Alpha/1.Project Setup/2.Configure CI.md rename to dev/import-tool/docs/huly/example-workspace/Project Alpha/1.Project Setup/2.Configure CI.md diff --git a/dev/import-tool/src/huly/example-workspace/Project Alpha/4.Update Docs.md b/dev/import-tool/docs/huly/example-workspace/Project Alpha/4.Update Docs.md similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Project Alpha/4.Update Docs.md rename to dev/import-tool/docs/huly/example-workspace/Project Alpha/4.Update Docs.md diff --git a/dev/import-tool/src/huly/example-workspace/Project Alpha/files/config.yaml b/dev/import-tool/docs/huly/example-workspace/Project Alpha/files/config.yaml similarity index 100% rename from dev/import-tool/src/huly/example-workspace/Project Alpha/files/config.yaml rename to dev/import-tool/docs/huly/example-workspace/Project Alpha/files/config.yaml diff --git a/dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot.png b/dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d727c76daef0ad966703df85686414441dbeb67b GIT binary patch literal 39710 zcmeFZcR1Jo`#$`VsAy0`A+pNIs*IA6mJvcl$WF`NWtGTQR+L>vi%=pn*%YOW2pLJp z%*eXWN1yNaKJI^h$M1I^$36b}IgaXvdt`MX zE!+0O^Vb#Cgx6JNiaYad-4_@Vv%5`b`)A(h3|-B@4T?i<^A$)BgKeMTPgj zAC&#?KmYHpLq?m&>^B!pEv<0(PtDB(zc{3A6*9XD90u#SOe>4r=Ql6?{o_+oBDa3S z266W}Tlcx~pvz6r~-PhKJdbdyi+9B)e zz5hx}lKyM!4E@(4)6>&Q$;meaPd`aAEOd@JbX{`Qs#SISjvYUK9G}i3Ah7P^$B(-C zwmU@(-{_=?Gcul!(jCQP1 zWaw|HuB_ZCBC_#p*7?f&`>!;;x^nhvdk)Rkty@!fT(RyxT^-7ydT1**_g{B0@!d3C zU0u34<^hMD;^X3a`ulat89mx1tvmh4$8GJ}tE&706gT42g$hJEUS5i@G&R0@Rru_g zRF$aB0ndvH9EUdlogE94vgz7g^nQM7@MFqRdUiQ?*5Ye_1mFJb-^RwqBPgh%HS@~- z?2#inu4?O5-iGt3Ha$5lbhW$WQ1zc1H*Uo2yR`Y-tIISlE-sYa+5HNJh8$L3UozhF z@$unXs)&k=G`6zZBf`eY8hL_!dVYQ=J<;|@!#1+t6wTsmPSOVt-m}x;YpkwbGcz-D z;^fIa<9W);%0FAPI6uT6yuFd*K%|1BjDC*!I)04=;i}HZ+PkKQn$theXdF3`xQm~b zPgqQhfzs2{bH~PT*REZwX=$S`+8Y}idk?o}1=UMRO6J@3`Q<;$%oNdEkx@}s7a3-d zqo+Bfu}43zB4U?@?aH#pZXuy}pFVBAfB!xoli@rACubNV&#@Xl8hnFQ#-8`u%t!#X z;V^&1ckENX>jby+@6Lcnk9f1K&z;-uw~-^TsYz4knQpdDZ-SV~s=qU%K`&m2={(U{ zw_*D}@9=P~$0HB?{pF0Xcmax7^g+tdx7<{R`I#)1vFM>d7-%1_i6IvimN7 zUdt?W=GK!Zf`U33-ux%;8-IOyNk)AzmN~-Dj}~DhDkb*PvN0jtHBrvveqRX>H}?zM z?}yvk+uzpIti62sa#Kr-Uu^8QH!fqLMqW$I+1c4N2TdxUWr$xmnR?mO^ybuHqiV~y zd^;ukgsM;uAw8p*q0=;q-uLdY91GjA!NI{nCqt9!>({Rtdb#{$l}9^Xt9lwXDk~c~ z6eyoKQO4=DxKm)Sq-2%+$|BD_CUZ-?Cq0Wu+2_wpf`WoE*qie5a(gv8Irlj(m#c10 zjrNX?kxvu198GU{A08gQN=iz~@ptF8(@)R%rls-!`ttO4wpn#=SMl`%>vaFJGKGd1 zaq9Q?_bUz5MI|REQ*GI@rLC>a7)NF;J-v|D0uD& zoyXs4#;u~~g>hmG+e$nNo6%M>gUXZu2ymUR;Yh}5>I&rDFz`^qSTc0&uU6k!U*JgMg{IqrYn{IZX7T zqN2VtPT$LEu-ZL!Q9{0*&vl}gM#QK@IQEdTswzFc(c5iS6--63qg-_U_Qpx+)#Wd|R+ba9 zv(+}GOZ<S@e{2Vz!z+ZO`M#t{8H0>$7kt%UT+pelD&r08}X#$^l2fL8@ z)Sy7(+U)N=BCoHsW@=89cr0x7S+gnn!Pn-dCWe-l7R|F~k85f&?2z{q&WxxvkCCvT zL$>3OWfqb;TKt2_?$58&wK@0iZ~xVrWz^$bmn-A&qvqrlI?AIbCFP@`obu;0>9y*k_k8c^@m7fv{Ei?UpPVem@vi){vN*LVT3GLG~Nsi4b^;jY{O1rVd3hzjhu4AuNzBOmJc?XeMLwnJ$@`eVK^4b zE`Wlgl_Mu2qW-fj`=LSML6_eR*~Q^MRdhPV>27i%xt-8@ee!{%Tf(lReTMnA$p(iI zW8ZNV;n%Luj4*!w@`dlu$jC_lSD}C_>Ws){;m*Oq!E|5L&)4M$oiZ?pOM3F;zM0hX zk?zx`rYSF+N6!_E(G@uI9dP&&Y*3u4dav%w!uWR|V;dXM$LYALipolXYk#o-G1)?l z%zAxQA$HS0Pkqib)E934{JA2O<6u&H`mV>ykulK8tj8c<;afU#pYlr$@@VCGG|Ng`Zkls)D!eec?Ey<@@y;@{oCfRYxA6 z64sA{gX3yP-btUb?$R9swtpkdX}$A@e7 z?c3L1_v+QFfj7Qbo!T$WpFV{PFZ})c;~MpsN6K41Khw)y`|(FZRTZt!*~|u~;?&gC zPVtg!f1b4U)zl9%JS;6O?R1g4{5iv5*z3R)ixNz;-f#roUd1bw@9f$0n8x=ZR6$qS`A3by8$dT0BX6sdA z0|M4<-E;01-ok)@y(dgOf^Xuin{wR$di@Ou2(Wj0_WU_t->Q_96k}`aP&tpkNp4d= zP^~v^m%QqK?a!})`KMOOT4&DqrKP2rkK4Gh?bh32Iz2TNAymKobUFRW>8G?#PENJ- z6*;B0qU7LBK79C4yT?jSDsdx!OH-3~L4jN5pD&+3Pg?Djme&34o~4_;3gGgC(*uq4 zryg_TCM63yk(b_9Bt^#Fzc09$lPh{7FfhtEkdu11*X7GkIv+fo+RVvm z)Gv)Fw{45^uQLeQz8@)ngPB>M>~OK$6fHi+FDmME_6Gy|-<>is4<4A!jekce5xsfy z=J=2I5A2=ZEy+}@Rj@vjRKELHP;HU5{{y^+1r9+!W8e$231aZ>N)#Z$_+^>~ph zgl8m!`W99kudZQajM^b5zS{!0UD|EZJi!UYrsPRF9i<7?Q`U8yLQ+I1yUfk9vND6g z#Cx^oa|8E`G|?Wc=aeftu~6(hx*fPrz{SU~z}`Y9>pTaupbkTQra^($Pnn6JUI%}1 z)frv7r>2#Gl?=)sUtMaZtq$X4xrdNt*uFiGcEk4FFFoA6%9EZyHypA0F}+my=8fs! z>7mLJQEqNQPe4IEV`Hw)Z{Mb#q4ZCF;iIJ_Wo3ow8{%n+c;pE*dK6sI`bw18X_Vroo>ETulYgy7Zb(MJNKU!k08e}kwMmW_) z4b0Myr+-tETS_f?_R^;Nd^jtm?Bm1Z*~V{I`Q|ms9rv=1jEwBBV{!{Hw9C+W9BwW% zytMV`-R4=NgbSwJ+$lxbLZ<|Vo6|R;m`*Md(EaS4u*3H4+ez9-0=bRWtsJoZ;qkEO z&-lCg-8Y|#P+dw&O2TtY-uaoJ;8{r4%xfcj+Oa~%GDlDe3R=Pf1H)C>uU@?xcqp@+ zRl%)YO%I|1{4>navzh2g{rUa-cW+1ucTU*Rxle`6ZfW%;Tb2Mn{9^0QyI}1D~kyqMsEyEr)l$Rg2u6j_Emc95nz4Yp;SSbq`KDGyvR$8tX zn2&nZo_%y)xpFPjh=G90>C?>GX=-;Pmu9=YVq_fBI zQ9g@<&Ld^OUM40cH&CMca7gdenrEEN+JWSulVLU=nVh^6721d6p!12kv(L}@5*Ula zc|u*i3hnRsMBj02^zP}<8$cYUO-YK<_Wl04GeB65BW;Yx*M}b-3%L^=?T0X`S(>e` ztzCzpZ*=jKKpxw zhWf;>%NH-!e9_A`J$l1*6_?}BCdCt~su5R&czBdD&~s&We91C0(&~Qy{$%o!-nnyh zEFuOqcHe+5c&!p-9Qa$E5huJYswYk`ynOl6{#h%Uv&|u=7*>A|lV7gMEe85l{e^;M zl=%TG$#Ue;fjTUBV&8IiksHs; ziyu?URx-bJb_UKI1wC|EOnj~`vbsV6S2tF=@?pwPZozV-E&DKCqL(M^5_|8rA~e+yvdWbsgRt@+{C*5B-}3SBqlT6^;aC?NJ>3yt+E z+{Y9dZXqs`K`0gE=|z%H*$GT=u|3D4q&pp1qthwc(Cx(f)jOx&*GX-D5<@$jB@dDxPGYj22++x^-`HP{hn@81`LlW599IJbl_1@ptaE zbqE5p4?IO1%5Q?hi`ida=+9;vD1wqRF*lF2H1g!*;{#aSfcEbg`R3)zbbuPHtgK1M zhoHCCkwbss{e4Ac<#p&(S;b9nby7MsI;Ll4s#1;yHN7x-C%Z6-=Av&!`$6aRJjX+ifaywl`jj3wOsf3G zjT_tEI0@nK@H%G+HUhZ$UZ4H_9g9NR!tMJmQV>xYPoAXM57hnq{CJCjp&{e;?Q}vy zPglMAR^;Z;S?IE9ujPj$KC~N>Qd4R0uKTPzSwTWngs@3XF3Hi?cXqNP(O0IOe9)9_ zcARzFHbo^RYJ_BETeg|q;0KQN+aw;9jsE)etNh(NYM0-gZ%5j5D{l-TKZr?69&1iJ zDek%G>U#Y;&+gr8L74H`Is#QQsN5`*>HSB@mN>hcD*U6W)z<&}hJOGRBl)`VzyJLI zeH|_|CTtKA5<>IywyUe8Y*e#mBm3He2M?+x$Zo^gwb)9J%k*&5sXsswG}O<# z`Zm#PIo2{BWhn)Oa4Hj{Kpi3jC5M_aH$VR!KYa_@uwGgaBE?@VF&xHG5a9knLDZC& zRvlYO*!TIX_;FXqMn^{vO5n{;odO2&{`vFgsxMOe_wPwQL8k%8QdbvysK#6(*4*%n zTgyuH8sORL;_I^%%9}TDj2unxblZ6@jJ_V9no>gUd~)V_Ajt4Y=NO`w(3qKyN5z5% z1}V6jLe2#!HVOp^i4PFBUg9PDx|^CUJ0t0ihURLgt4n(=%aTUK%*-q!BO~&#IeCv) z#-^H-fr)ix_eQ`&nFY@xLzy+<@yEm7;BOoZ~9oRdx%0U2zYj2XXOGr)}o1b092Z4Sm6r}Yk zl;(l2woXpIPoD}=deRd;%W>ZMhQY_KwXw0eM&C1OkgZRdaYkw|hcirVVuKDx~8g`>n&k&D=neSg;UGWbMJ%;vD@NDLKqzTzZ zrPfTt2}lwYZ(xrznFayK7X(3=_L?FyP4rbiL^hz2SHz;|WQvO?tQM>#Kc=bqwEXsx9gx+DVunNUjfA%T@lI0 z&AkeXIF5t~`as;g=FY0uMqRUs-%!*D+$(V4U}pA3HAilF_wgg6y1M#&o>Na5)icAw zGg?2!#=_9G1G?D;Q-e}wNm~q-NNp^~R#md~Xsi*~#mj39<}W;_zuz28gogdYb13v6 zsZM`7E{9aLW+OWS;?kwJ4Grs2?4w?$W@T;KyLWF)Y^)+0QsI?9twwxO1IT+wbAAXp zUH*vL7r2jGw{9`bG&!7ZTVk1(o^k+P-w-FGZJ!BRF&PLy;N7=x9Ka?GxzAZd&zHS_ zzrlNPRyd-zr?Xb zXzF;P!?y3I15I}mG#Nl(5O8TPf-2vBfG#dRzVg~lt{zm*N>6pVLWGN@XHNICD_5?V zp?l7U&VqBa8cU%3`XOorH!yi?9t0bDeSLjWqok}mN!NC&)YHSx!`)qWadD9;Gz2$R zC(lo-!-W&-7ZI^#`p+LwkE`&mwW@jM65DV}DhJxybZch-H@B>0Y9_BDC?EWpDR6o( ztIy3RhJ}){hYn33Itc9R75!6JR|mP{QNQ0D0HyEO<8EE@6s$fMpoM|6qvH`YLF6+~ zsnLL$0&MjDjf`Z9lXa75ogw@M3k%EVj8oKrR)nBWQ;!#XtgTgj7;$Xj6F|j$0zk6A zPL8AD@9%F1Cv23Ilxp7qhjYuyvTxhA%~a2tI-35lzCIfOYJ*)of_cuRW%^m=^JlZ1 z3=CX)dD+=_pEkcRQGy7dXz*c}WyNm;Tb3pZ*trSAAqcx}K;*cGXN*=a?SrHo|c< zKcS)`sM%p>XBR%8(Y=nM;QqiydH3*=Wp++ZxG=J%VW#?t6aKsb*?D;)6L=Hq{gcu~kEBqT-rj`74a*V)9Zv|He_^ z8u(k`g^payP3S_Y%)5Avid-47gpj?eAVu@pG-4Gq%q7{Wsi}Vs4Sffo#;2|GVr_b4USW_lF;=a0rj(H%B3 zw?udDq$1LJY;5e@kM5!tmg;3@8TF$L*fubniqI%QnvG92%9}x|0wj+L9;A44dd?|| zJl=_@%VI0s>|ueh=8CB0RaT6CHW}B#+yae4+;!Zzw!VJz=FJQo9D9`MAR>gU+rnRC zB@bb{)){0Clu0HV$EP}vS3xPSm3(c}T?NJ<2%ObE$dy37OzB_MG&GKzn?HI~uLX)F z`N{oQo_ceMBgc+$^Yb4|{!^Llbu?6)5jT#*6W_lMKk$3B0sWdg`es&H*B9lZn^A6B zZ)mK~IP&<(6VnfIGPbt1We_(QSXup%xOeW~&kW7v;@6h~B}+58Ckg=|Md!$l~y;NbN{Jn43y+JMfO8YgG*?{JoG@wNSk;{MHvP(Mt8 zvq>L-(>IVd(O~0oYBuYqonU!O%T{zBgrLkkUmOTfVQy}ogjV21PEL7KvJ$iBDpIzM zSoo43b>-u4_)6U1jI&L?{$~!7Ar*RFK$vR#62@4>n!lXOeU~#iT{TnJeB62 zVT#-?kfSou5ANTOw&(qKsg*$*|7jW4s@&vSYr(Oapf1L!J#zos0z>xW|7!W60EBFT zAQJ;#4@al&06R8Vkot)Q2PG=x&K(B!1CDEv87jcgqto|8c`X0%VdFc$4L;e~c6zsU ztw+0x*-8S!!90RqAlzvF)1{CcWx=r#Eki)h7N4vJ zRK{%2rE6#tDBixlG{0J&2ZeFU_X38xF3jv9X=Y)p^dYWQ^VBJH9rdr#5R`Rx8W^an zsN4hth*vNmn*$k}EY3|>u$nA?{`}b%AhWa7ixWv8u~K^@Sx6A%B@*BQwnp3pq%Lo7 z*E6^WCJ^|j7aWPLgM(1iqaL6lVi%cMii`w=G6wbtC(4rA{e9~wu$aW!C$dt!K|cV4 zpKURTRE4%ng!*AI?4=4jxgAu6ic;hWzF|;t;toI50GAcej#H5ELLii`q9CW=7^uIe zN-QagU}FehHa*h51w1O+uemv6BsJH`{&k=|O)g$s3tcvtO==?$2E`i}+K1|Hs!#R` z8ax1iH=w@%z@#c?~4>rY~QZtJ&PodCrXJTtuGZ@Gg+zHT zImjT?^78VJ($nA7)zM+$ptcKm$5lrViv~r^rb~7`yEKEs59yHjgD&L&_2iP8niRn2 z5QBxZw6p^D?DusfUqPD~R(Fe>zv0w`Gc~cWps}zR(u^39vIok!_&I|SGML@`q{XAO zw8KD;240Z7Cw?qHuqmOUjC8!-1O_}@5`41VNSinqCV7JMz-c|#EUK-oB^kiFyJU|* zsl(vL-<<_MxL09=e6eaPw71zF3(ma!{DIbyf;hM>uI8a`u?Z%YLti}3IH)`+)oX}sb0-@ikFthg(6SwS*aVb7^3 zK(8$I^w-q%Rx6fi$S)Deh@xO>$_*ht6eTJcy~&3UA9|agqcwx@2ligceS5^|^KHXQ z469+k>O*jv#^Q7)$aw_fgbOWJjDX`M+&BDfl{vtk@KJ0b>CBg&<)5;B`pc*EXE60iJ^3MJXczgbvRbflcv-A1ujhWjVaU z981!BGBG}$gP4P2)o5XA>U`rwze_z%0n!N3*F$Graoq^1A1FeQBc4u_d@DVG$ARiwM@oM2Uh+m%D0wg?cbI^KOa(Cw@r*zCtmxs`ZajQ{Of>Z&|l) z9jQeKWaPh1#6m(E&j%0KfK{j{5XNtT=8?{;Xl~Y4qa%pO@z-bk&N`y(LRD>gc}bg} z27HU2F?J7Vy9x>A`_Rxj3K*kzkG0b)*hGO(Nftx3+Tu08{NsVmc!y1?BE#yqgD&c7 z>!GCE4b(AH6mX@awxN6QH=`wDsdLBAretdTZ5>XLouCdnSmn){&(G~A>Z8H(+yoCa z23oGE+DfG4kX6?*((Ev?OH zqs4(b|8XWHCnF}I;wC8!Hg|U$NgU%A7e6I?PvOGsQ(>4jsP4=&nE(-+-uw*Pz%4&N zZfR*&Ty6O#`9%sYjoim#Wf9~iBKlV3fp1rF8aWK041O*Ir$Mz2$Ekg2co^h@q5G<+ zxHw_wfXh%l4Gat@=AJGwGm zi67eN4q{sc0Pw?^3hH=e)p15bVF5yDZ#S|dV^})aV3>Fq3f&3Ta#`FqEHwRYlRRNE z&@Onx-C}NtwjU*S*U(wRR2AlZmp-jl_#s-MCsmH~WGWT^%=(C4-x^9!e{F=cUGM50 za_;^(=}N(lxIv|xZ0c+;@Z5%yi2F$$!U6uE!Lbpod!0|$L8&yK4~o$o_ct! zw;R|a<9zV}N)NmOG~3sEAV(9rV-ZQC7fFU9VPkzfm=$eL)d4U%6ypmQ_`4J=FSR_| z=G|cQBlnkuhE~olR!YU#!H#4GDn~(2FIE|_*W=7wMURk$nHe>TD}V=q zJD?2E9=}DeLC=+2Pa+t_mBKZ3_7CwbAkjdXIgZL#VSNOSMW!{cehm#JYb44M@(X~f z@@>tNCr^m-$*&%lG5%hwWdqlp_EGdYLen#*9$+8dMM^JX#dQEY9^rqg+(Pk zMJ*Pixf!juxfe7mys4;r1_pewJ+;kJ5U<-hI?N#R$%JOZWd5&A4G=hZ;Q}qFF(SZW zqkO;u0Aeyg4pld4f37c;|K<(xqM}j1*qRyN7LuWQPbB=teRu~I5cGDv73%;Ol=vfd z9y-JU$Wo5DAfQe8-p-={c}a(T=?XGgRwpwYH*!SJ`%v0&2mtYg*NP`4R_dY5d$HB# zUFHyHWLhP2S0*tFZ7O7^rbjIFvFpwb?FX=Q{yBAVc;XR76qe z1QpCow{D#xvKGt_dtPTkTJ7!DTk5#cu-uBks>Xke3=G}?$rH4@pR%3W3MLO$!SzTt zN;v5W@?N3Xqj!)2W$afI0EKJ@k0?EA870Abdc@WG<-9jO9u*{fG(Fc6%d7NCVJSYj1iZ70Y`3 zWkO4T98^3KB+ka6>?_QGyl$1^v_{*2L8N75-qzMC$64^H!aVKn?p`rKAa#r7hMr)5 ze+J7+aWSzw!-sppk%C}bMS1z^6`|{(T++gZOOfzWejk4JUJP2$fZCv}tgOzE-S?g z^|e-rA+_r6pL#bnwe=vud7>@f7wH(#IjB`W-j@|tl-UgX7Gk6q)tHdhfK68caj9SB zx?CsNI*XjA@Vj@ZumHTfcJb`n$AqQ#NloR0bLs|&r81<{qJqmEdC$$CoTU8x`by|b z)_GJ|{^bLmsj4i>-40$}Uce1JD03))O5g=gB_F1N7!e?KynRjdg_YB?(9*$z9fQrh zDc{Zv*}xb=D`am4Tr&Zy@Hv+Q(4u*$f`gfa({2HQ0MYw`SAY?HU%!6s8cHm{q0PjA zjer9;AqF2$Fst|~QnwKxcCen30)b=AY^n7NS1D=J1t$_MD0_?!P=VU3Lic40e%8Q@ zI99-i-^9ug+bA5liiZ!cQje29HBznLWG1Md>gHiyst$c1IW_g4&Up4L3tX~gASm$; zmq>gf0}?-HWo#(`7_U!y04P`?ep(|&W@ctU7!xoP`Ccn>s}&VUAo=+4{XvxZ0HD`f z89mnGBbrZRqlWN;{5-8(MXqkKuaPlQXr~KLSoDG-YgUX0}jh1Pf!jI`^ zfMD++yE4L419PKt-sv-EqKo#oULFJ8>#ra?IDI66DB zg+89Gg7JFCjvcy1uHta}7@M1~#!>(I>sJcy2C_vJXt&CeiNv~uwdNrQYcj#3v=v{M zLXY8x7FOZ-@ij#BgbfF349PP@On8nYZ>!I{1I0@nE216D})5E0qpE5JR@zEYo#!hqj&I^Wlf!FX)G3)3 zGQEPN>WfbMAv$T@Y*QNK#qI~C;?=7?nd9XC<4jByQjhUR z5PWFC;m3|}31*ks3=Azeo(DfS8UE?*?G?M)_JVyF>W4AFxMq>yR< zQSHb+B0Pw0!b0x{R4YRtYK!xhv+?L*xD=pv+)Pe3x$)W%@*UE&4-!%eYA-GuvJmn3 zh?`ZF;UL1p{O*x*ge@>N*hHrNi-)!Q^Q&Rm0YMOTUK6{lb0LP6fx-pU12hOi=jOwQ zJV5(F9-Pq0h%W&ygq;z!_Rk4Z4r%HgXiBufEbbrj>NC+fYIGlyTmvg40^$ zHq`{AO15ezq3>6h1=8x9Nr&a z^B{{6JG2GFI`BLtnM!H2!hooN$K{+pJ)jIltC~FXEy%g?;jcpAmkPdIIT;l za7lnGKx}JdWo7LTSyQAPeysOfzwIU(J5nQvbcFrf)&2uX62mN_K-ah60a5O)tEnl2 ze|h}9RCgJ;G>}Y{=)>rlr3h{o;ip|q(S^GUt@s8?zQ=;(8DtCYg>5)eh>6<>sDb=t zFgX~$Is#CH;>##(_=X7}F}yPl32!^;I$xMj69|v;p=duPfRclB5p(}O5ZV@L+a3y- za5B*1YubtS^mljZCVVYIw?r3HjXdx0tOLz2AXb0-dFZ(q>j?ouLXV9xnvKvu%RuoY z!L@$Yh+(x_`~ha95t8pbSC(8Vs;Y=C7k&-p7rU{!CDSkneaK+`82SfA)FN~>H*$0L zqQnIQeE!g#g-AtgF`mowmIiMe*Aej&mYmxtaM|imo5?a@oCUd#n~RGARJ^s-rtJk4 z##X=@u327Quptv`#N5Pd5mM^^h6 zG!hxa14FQef>kla$sj{(ew#Q$aaWLyvB(?*Ra1;X?qOep+x&oCVOZy-wt?D4fBpJ( z+-M2_p<5MDPY|ls+lq?SlyUSsOhYjtAtB#?{R$*f5BN-gJj7t*t8d7dMBGO zH!Ngj7<+j8mIPXwt_`-9Q~~(h#B6&M0AWn<9oLoRLxdWGUPxFsikNk$bld?aI&>vR zFcZQQig7M~70xZxt0?l@rkoyw9@aWf9_8J>tNFY-X;sO*3eD`of9t~#&?CLKZ8yTdbU{b+8 ze#2vvcmIY>$S@+*r9PZ9*FV3Q;ZWb*f*Bs)`}fa&VAHC z%0jGQSaM=>^;IV%hy%OoomaoLfBg8-R_Y~>iPp+=S}=~LEa~BszrF|&?H*LF@BQS* z_%#4=Lc@N{IK`9Q+1<^FVj%KC6A&_E^|<9B!M&srGrvlqkTEEq#6(VP&26+Mm}WFZ zeITQ20G;TkHmq5*=9xjkW{`15%*}a`Lou!>nrC8Zc^fg_d(X)8EuwZ}aoz#qB`bUm zRCnC=%7IHM*mf;PSoZ}*HCk9vM~4Y!@gU^kS|`K0@R%5X(1hr#`*4L|)S2(Wi%3O5 zwIHH65ze9Ml2Z7++?ROJU{LP=N~#5PgBR}e_OjP!)}vJ^zez*igwZ3|Ntu2b2=#*E zAff=k>^Lq5Z?AfRy)F#2AMK_c!EiGX)fJqR&+Xf5h;g=8)B&V*N=hHf>0a=%#25~L zLZt*)Pr?-2+S$Funa~834&HI-+~nj7P+(?Q93V-*=;*B_E6dkmCJ7jhii*lcse)C7 zNCM<^gTBtjJPsw+@H}{WhvFerY7r1Bx8X{KcXHpAmi4%&y6oQ&3VVM25)>Fl-SKXy zu*C0^{sVI-*@d)*8_42tnXVS?`8RMpz?~jut`JhiB>|>Zb=x&TS{Zm(@&O%}v znc#6_WEAU|xZOMIegi!%Nj!*bO6(v%yy!UHWHhwzC&5FHfGnemN>^yLv?wh18V2U+ zy=zYN{Qf2(T6%p}^V#{REvV8S$#WInD%ElA-mmt9uW5VvzNme< zv#wf$vfPkdNKorX76ps!l;#iy=A*BzW53x)MtXO9F38sRonQ6*+}0%n22$&lMpSU$ zx7o$?{@V+Hf5t`OZtw-9MBa$;fAr_v4r)O@Ah>tf>k12yfiJDUZHL!~@D+hp^2oMI zu$p7n+DxxZX{Xu=6Pp2?Z9p(*gh$Cc58Q?p{TYdizU#hm9u0(o75#M?SM{^L3U?lYFa;qDb88@gOnGG`4XE0-91B(QD;6HY$Q}>~ zM4~+KDUiHLvPH@wstnXP=>rE~l_}DhIf&B?asQYb1vI}%-nWru9-=qUEwJZfQvDz~-5{z7%f<$7UZO+2XOoIy{d5;uI zAgeo|bwTnaqb)Km#z=H9r~D3F6UnCV<8xiI#05jkdk5vn#KtDTzpIOHI2DUh&>F`lSF&9OKkknYTB+_HG*8-@Y9MZ6nWuuFa1Gi*n>7w`?HK$T%eVY z0P$U(3sz_+s&P3~ppavwb;EJ zS#^Zl{B$oK^wJYwpZ-{A=IECZfh5 z2mofCi21v@xjCf^(+rqPFTv`OyslW2cp z@j^?tIP}4jvyWbHUPl2Rkh_)||GzFWiYm0qWpIQ{vjY(z0?bZTRaW|~>p!Ta5kRW5;Z}Jo)atwOu_bAl?fq^M# zMuIsGMxzX`a#6Yk+abElUI?PQz#;1EcihfPynA=z$dbB-Mzy)bp^pXsmNohDkyV{b z7i6TRV_;U?%+4M+q`zP}0HZ)Ul0(e<3o7?SLNElF=G%xR+E=11s-3nTYE6#xjxAf> z;qZryx^%hS-oF2G&)8Taz%%KeDNse>?ui4F{_rXqsnC-T{!Zzt@oT5;r$(bzfiGH10}tKaA<_wj#G&yG}e11H?k_@GyqJ4q(%~%oi|jL;da%3TL zN)p>-eTtf+<30=~HO#oedC@<1^sPd|12Dx~*w_dW3^LFX1s{Dj1lUh+ql?zk!1Bn|>IJ;Xw0tA=ZOWK(Shs1D)YpR94NJ(a0q`ChdCv1u zK$u~>CTe#j65j0Lm#jBvX@-P)oeK%C-w*od4V#jAKU|09!ghSKraLHYbFD+JS@p)gQ7Ht~L*R4h z-FG(gsjylAxyMS`>;Mb?1Ne$$H!Q$*`m4C-53oWKi#W0#?8SFMZftw|-IG5e0MgUM zhl69|i7Sh@(Ut7I(jp95<^-B_P(#UBSseKJ*kB8oRAFhm7bf@DAA6t;97xnU?3eBA z^CJMgyP!$IMqgA^wBv8kdxSJ*6K{hJ$u+OtJo)2&5DME^@$3%#fP43Df9|7!VsJg< zlWM@zyUq{X8v;*|_Fx&oodeY#qH+lQ*`PC05vA=VwEtq+w9fTYKrQ#Ae05`E7?P0O zKJql6njlr}0GvU5z5UPV#l9#E4&$q73(V5^Qv9#)Yl{5`oiMHc_v6(x|DzxG%e+C9 zCwk0mLk|HzCCp`O&^r~Zr0?VA;USV6{5}P3+Ia7~EMYfq$g)5wP67#rJ~a@7Q8^gM zM*ghLv8YEtiq!$C2Li%Oeo__Pwy<5wx&g|!Lvkx_id%H!aOZFDB35K=1@Kg{@Lqt2 zqac$Jtc?%FQwh-b-v@UEzpPYW5yA&(!TxE-zu#VeNSuL!lcR`=W9Q)T5OQXmv>o^H zVLX*$3xKxd;kSqQR3eQr*sH7G{XPRR3@V5N>$Vo-Gz#|i5;)?6&tG@deUwAp_pezw zg-QgCpbP{E6=|)u@T=8izd;&Ph7Jlvp{~UZh8Z&ucobp19M~P2*oPzVoBq20XAQg5 zsej@s8i$df(9o)H-@eVg25MIu)YI3GkoWSWpxV61&8^S=_ksR=G}Kf{c+^H%r|0%r z>z~?MTEQMT^??16I2@=c8sLgUU+p5d+n#RBMS%)Y{;x(5MNsC93tB<}tw(DiNs7<3 zfBmZUL`MLn;3!E;ul{-eG>+NuZKfcFE7{tP?|s_c(SZ{`?}rC~=n5-@1_r|P{XO=T z5cvSThr4Je4W@s6Adw!G_pOiT?HF7d1jF@>F% zro(T+^FsJ3^4SUsltb61xsYx^6v^A4vct~Wxws4#&vx}a43oPSdT1*`$1OVs%A$mQ z;TH1I+~I}O75|Cf|BBE54(I>nu>GGz_5c3y|9UQyoUWZ8K7=A+d_t8;zf95^o`7`<>Mss{;*-p{Jm|v6>IFDI)L{?Q_#u2& zVcclX%t}->FJfkL-@d4yG!Ae^1!-#H*X~Q%r~|+XXeLo8NKp-m@`+&X`Y)VK1YBc6 zn|$fkP2^r;L%A<$RrfTjgihhuFOqc4$d3mrQ95$+k7()QVdMr1?As==U_q8*t!{m;4-$PZ>{(A3 znR>Dg@sm`Uo@zXPv%qyCWuhHfp*IEswUXcZoiZ}9vQQTfU@Y+|c#{|$tSwN?(C&bN zO9kq|FirdJLBJ0XUFKIg$ykPz6a^IX(Z^b;F{pU(CQ%#XNnjX!Y}(Cjsgn>DE5WEMjT?#1o)*4*Vmvj!uVIv@aNpB|IWR556~WDP~Y@s$OQSDOlOQh?{d9ZVAEb^dsGx_PfS`3C73_$=!Qq zIHhg9Ve5GT?G8rkDV@FoyFOZymLw%xq@}Rvz%1DSDtXr5>Hxs@E}bNSy5soU9-6*^ zfl2^uD&opsSsL?teER8DL~S~pig>Wr&<&|5=qtV80%H)8&Z8>OT`=ush7^k@z3s&# ziinmCh)XkzoAeE#L~_v;kl| ze-=PzAgJIPvI(dttjS3bVG$yCz-be=n;lG+PjnuqXo5Q7);NOu{@1j)jD5hs zzIS2Phyhj{Ehcpt5EFFrPne!HQ&9m{Lrl_?0pEBnPbE^YA_0hBz+(-RQ*vWoE6aEw z*~gUk*ck0n&n0$_<5;gbm={B=pc^Bm$VeI!klZiPd^Sil39^&~PB5wdCGPXWp@x90ApONW{yLf& zVN_tE{oT8FDeeGj!Hw{%dK^3SVWhECT(iX8jrgy~R2A$gZvFG{*umWODeWX19kZah zoiHLQO6cq)4xQX|RHoNfX6B>NQYcQpK4R30(;j5 zv;do)?G+LET`;|5f-X0t(Q9$ycpBTXywK?Z0Zh+~4vke?@^T|pLWz(t_0Cjt3} zSuHGIc?n$^7K^1ad$0vib8367j<;`XM6387XIxoDCCBxXcD}6%9+l$;v3T$Edjv(+ z($V;^#I;{d!}FtkYPdg|0ENY#xgMGqhLDY5)GAT2E$CSs}V+})s^ zp}Di~Y#0YSJ01|hPXQjQ(-O-^oX_uY+mp~QMKczX4t}0MDlk5?nkuEZDeXrilGqWLRqe^6HY98h;4%ejus;L()IgqA>GzzdS+L<3u(iqun=sDIK+mrc`edK3u0AkV)A@o^CnT2&pPO-zgAcfy(zbd&^ z^z0Lz+TF8=IAe*}-RFy>AxqUbMl5n++lZY71q=@zYd~;@*vvlIe`Th|VXz_AZ1Ltq z&s!h2KVQzejlOCR>U_KxuB` zAQNFV?{v|CTN@Bu#ff+7x;DHAEZ;HgFg^wI1jd$@$I&L^Iga$m7F8$)y|N&;@C24x zWCX$ng7&SFL6NdRCW$@oaSk#b7_WSo8;HGJjlaul#iK7DgB@^*4M0MGljzi|%ctd5 z79HR@o`(QU1!JRR3l*r>hPmfdFhG&k567ke?+!^3k06M_(+I0eLTb>*!7*_P&)o!0 zQi4ewcMG_r391B>&2KT&YKI5eIR1WEWo}{N3r{#mH?agkyks0JF$pj5W(Ef1?6mL7 zRp4PPFx_1MtsVAaILRVJjpPXR1eyC{cgYe@wFtq7V%JQ$R`&j0@$&%nh`hpg@A|ak zvUMLe{qBm@Id@?KQn=|ze+~V>m{dY;?u#Kr4?&9D^`GzGy!l`M{;!|!X8rf$wg2^N z|8MEBGEa!0eWC{^13vW@hO-AR76Y;4(1a%>{3AusaAy_IY)3!a2d-7- zYfCe+@gh(PzJ(Y}pkx1t0TwiM79$IIq|Q8)ABQY$EYf7B?x6FU{;WPUlO_l;P_DNT ziv1VA!3tW|-X4phinCAIeKwOFkH-NcHU9NV*1*N<8ozMr3%>apm`TLK+wveg9#;Z; zn?v?-^0u-=0XJAN+EY(X86?EeRt_*2AZU zFtI~4#N#l0f-fCx-4SGDO!^lcjRhr4js@OQ6$IJb{9qzyTT7&X_8f%&0vO;x&ywS~ zwOZ-$VKR;(CnpDBughMDItKZL%o4(BL0~1A)Ex&z;Pi(sG6Wd`rM*^*V>WHydytxd z>ev^mr^-_ymD@O7`zI$MH5sp zXca=X<63|qsco13I^m!k#l#H;8%!ZzLkiyJgnbsr>!8iomjQ?tF+6~l zJXRJd2QmC|_m&+y)X`LNUh}}a_RnjH-Ld?Ql1@RNuuFe%k5Ne+uDcm_)VTO~&`xG> z9ze3!kDQvA*on%B9QhHCSAlB_O*ZPQEn?#GJ zHxs#g>k=LqCXIc32lpi0mmt?Igg>1;al#lc#Y99XPtryXQ+for8~YcmEu1gaS0(X$rX8q3;<0KYodpqiHF=-fa6Y7HJ}xeAlni)` ztm3*cm9lRXw786iUMB~nuUJ)!UQEOV%CW(>-%F1U3}v4l;7u5nC*G2 zo1Icw0+0mX+6D|Q6Bdio(y}s}mF2SZns4O;VtuW7=JYPELZO9?y||`MYP?Orx)B9%le> zq3`jpYX%1|_?E zfDw6OpV?}mo*?^wbax&=S)Ol~e-(wOQDbai0ZSC@2nvDYs0V|Mo2@Bg2j*`3|lStm|1Zz%8kJoj_m z=Q`(H=N2-71X$1yz=t15nGtPn%yJlF-xmEl+w?PnW^N5D(3m{=ng7`aCA!U0;PTvg z^F+2NPuYpmU0kS=?3Kzt=xo_C7OW+T;*F}9NP*4^s*AGtp{5lV2A;uI4V*ZF78Op1 zPQ7`E$R_rlqn7pQ=Blji$pfCm{u>S9^PkJ*+OM%Y2z~;;zb)H7Me$3Sc1OUN@wv7& zIn9L0VH^to>kmkFA%!7Q3L)jo7rQxg=E!O}+W8u_oP4Q?dt}X>ZF0=^39SO(uuwdO z6*n^SBBht1E-RuhXJx8-|KkPVJDANW;({ke)(ev0#d7UYC>dDBBf$R)CQefiOb4xq z2Be^Rt^45t6LeuqA#V#oKcWk!JN^Z;_yP}-68HUeXMV;LrDfdMp;Lb#$D zC1A{@Y(f`COqtJruuXcGR)}1pOwTdJn-y)`lWoI@6&G9WWD}^!(nIs+&3hK->@4!? z31ha{y?|$``rPHYBkIM?o2P1 z@~8a9V7ys~S$p3%7K@rpqf_RX6y!>*09#{Y?wgAIIBlSwqeT@B{=y8T3@}Y+1;>w_ zu* z(Srr7=+f`|$5tQy68b^{U~vr44wmWyqig~lkc+1nnq{gY+!RbC5`HfRj8p<_&Syq7 zmCRPDZ)|)=yO3FB0WMAM3og}xl@z0F+uGwg#vAT!GQ+Kj2W9W*?ahBf9YR+Ah>TQc zQ#7I7O{|5Lq=7xiOYmL5A#0|8CCKc;{e_(_3EmkbDJ8~-g?=`+?^-k*o!T26A5c1LT3btSkAWGO%Zgkx<(gDL6ONSjl0 zb_&W7^Gy)wY%<;|l&B*rG%=3tO=UO|B$k#nGRuZT2@V=L|Hv5#K0AB+T0oMQzyGEo z&bj$gLY#%N>ly5 z=F&+3=)x&28e(jzVi$v1i&p%w7FijjDp)ln*#ntu$!Cj+qh6- zsWgHX(`uj@^#+y zx+W`O ze~RftHfo3>)cI~=ep6g{%l_2$<=x%j3E>wd+`ASVmQ^WTO}K?Q)tslxkw? zGgO_WGVc#=c3?anc}7|Sv0voYTT2M@#KQbcmzgxtBJcLrfq?fSaGF>Qu(UWf+U7b< zr=+o9@4WU?w>11pcF`MbWS?czlx?Y@{YOhZvOOYu+vItv@B-|Zwm1&pqwD7G%ymAU z5v)z}&u)B|_M43UdrOPS-!c7TJ=QpMjO`(z>Yd1Z%V@`4a1@sHiAW&IB({ zmn6t?HnM(WSG{-rB$h`+i1Dxh{Gcx={9O11;Ii%QNvwBM`Vvu9;Yn0j>fn{fB4;PQ z4(upq19;AL5X(iQFsMqa>jOUSRVV>yD0HmDwm7 zPKDnXVJ@ZtIY8MToF7UY+YIX#E@IJF_Xw8z?T%`J>2!Polenv}2Ey0Kww86x7408M z#Iz6QQ63A%&}1Oz1ayAd*24)0qQ>6VyYsYh6lZfmeBVfvzWHBX5A565#CmD?3q!~x zAw*G}2;e;pe7?y}KmN1WENwjYxyn*q5(SE8ZNdbNMqc^i=jA2I?b(asE3)PELzy;p z6PRn|%0)|;jzVt^%YU4&%0Q)7Tf<5q%iXL5#OZTROTg6}NX@Y`&nVDe3wgjRU`D0{s>+Km`@e9ndbSnsB1A;dZo^X{+YF z&qPY1r?EjSL}2t6h?OwV$ngQ~s>lbV(hTTfKXb+mD;t|4@8w#B zz)$A3J#fnsadjd@O`-{($!d?6hXukr|)|8y1+*fI1;x#Srhv~{K1ZO-6-qg z=FtMlvh{S9k`f&-bvEEm&1yPG?1B#f`S|}X;4~KtkEdXe89$LQ@b!cS;FuGJCNA8e zz`(#2i9E45{!|HD#in?W`G4wU_LX95!4#9aBAjueuLXLzCY%@&k)1|*zCXqcw%JFN zKk>cxbtU5?SH##^W28xE1c`~MK_C(W0}f$LFdf@+xABSJq|5^fdxb~lAVP>j#}^rf z&a($a@ZgjTSvg-X?q2_6iZ<-_oYZl3457dy>ijN%-^8DP*u0avLMa|G;{BojO%|^; zg$2lANs3g?Vu4|y!wCo}TbPIQ#VpxtxR_KgFHhlFh{xq+bH2KT_{(lwZxOIC3+{@Q zy51vlr00Jj=>|Y47BU+H_3Op85vCz<`(vowH8FOzE3<~82Q$oqK$8|UGmvSGdbqS_ z?2g1jG&h{?=s+Ls)}zNrI6l^XCz4VIkS{)Q12my~F z0Xcc7I4ONzMdfvBsvv!`)TpC*AlOMJ9*-e5NgUI&nKlW!QcWL06P?Dw!_6guY3PO5 zk!RKphCb+ChlGa_-?0~E9%5d~B{K5!$jks@NyG*Qm2{5vsPVjaC+`$7`2>*6{0o`P zCb|c=L}UK)?;jhh1w!q4`uOyJ^=f&r0v*mKhJX8XuPz78Lg=a8QQ!VXgz8>qYJn}m z9IMZOQUvsDT4D5DA!PA*9>7jFcsqEjlNbZY&CNaH49-#hAf^b=VX=pLv#frQlS|1m za$bsqG#iHI!mu!`N<4Zd+gkn;xpw5T3gbjP1oGb~Mhpo&bICg<+}V~r2SB7+Wtn$x zDwLUwm_Zc}95;;pA%VvT%}nJ90mSCb7vALO2cu)@ue%Bj$)ok@sp4n>aj@=)oDvR~ ziTzU41LuV5(VdJtF~;-)F#~R)Ydm0O^UEtf39?v%Hod+%ocE*JY-M@xHEYWXGY#_L zJB&+b5d$PFLo#ZHAko!+{1}go0)z0m45q@0%gU0##v%*#8?SkUREg7qT|;_A=bi^o zN@DhUa0}sOfAH)o@BFaJJ@##s?SV6AhT)Pp42mZ3yMX>t-y#zFkUYEX!g9HMN5{LvKp`$n%gs3yV zzlpc5w2;IgPy?JFlTSUz>0K*;bE7sTp3N1x==j^|L4?UkaErNw*3h;iHH}ZK&F^UU z17_lEiIQnl1ieE4DC3HpPoPC^PYrg?|7`J|(D?}!xxTFd# zrtC%~H%I|F*PZQL^bG+P zTZjTp4pg56jvyxD7^WCtwFhvS$|NJkBaxz@4xx@zGMKA_8XPAE^wa{aZEVi+11ov5 z*P3j>Ar2=JA||i1vjujg2lxZH3>kW2l2H&_d%Y;Lnpv-2`EqCb$je()=J3I(st?#i zs0BYFwTebmKl~>wxgfNgB-27IM!d{)Sgto#@0x%4Yb0q%hlI?`-pJIMMZPyXq$QV9<**Pfin>S$_WVx8{!B&Nr;SbN$BbS-&oZi{IsOT1k&`mB(NU~ zsV$s(P{spS%JNOk@KV`-ipGupDv1_+eBudT-$X9=_=nwcd#b}r3OlSs^!$MDK|6hv zRy^ErO1N9q(&-Mmj$;ta?~EW@uskRK0lyw1&5`$EK!d^6~yx)<$~L_4%nr< z>FF>!X)znh8h6?h$hs0Ps+@1k?pMav*4jGkyV;%}9=SciX=r-y9Y17U^se-?dH)c< zS0+!LaJ32tSF3@N?84Cr+qdiDNxLCIBSSi6G&I73!ZA)C*`JwN z@f+Kq$QxpwtmW~}nA*g_s5aJs@o)4Q?NElEf{hNp?{5NlMy<2(z&fipYoC>+G&~f} zxT@v4x+5k`sJi?iboGs(gulETKx>eza(s{A$_1}mj8*;d%a@-nziE#^^H;%5#{mc# zti)NA!=v+_@y-iGg4WoF90pF%L{$LyG zE*tsmiDOhk6m@XJwB6N1;7lX^2Nf~bOJi^3^r=5DY!q63;68b7$3*c23i~*unMj$c zWv->3$po#2ArOedjwUwYw=q#+`D~gbrXfGavob{TvgplW=@QSrc^suU`suJ82@fzGE&Ug|@S$C}pAFk9+uwNJb zFZt;^7rJv31zomqVRD#4=YD{>2_W4+fc+d@@{lzvBz#Poe^SJddk1~x)|M=BEZO5K zBcvd}kuel$VoWnn{b_E>pdKa!Hed%}@=9EgpTkWTkw?)QzdvI66Y##cMNW4wvW<0- zmi`4B4@_J#xLu44WA06&YQo8ChrfkW{$a?FwBNh+t6&`P+AQIqRWs;LHjm zV8!3W^52lRM^yyd3*javMX-du<=EB(vEAOUPcv7Aau8yuAkfhAAXhT!eItHd4nN2V zjAyVSc{a|}|%z}Wl_7!co_JW5%glmXb{maJNJ!n*#$yLX9T5BW9K46aQPaJ5Y1v&u$t zAO4bk#965dYu@qNd^sbd{Ic9f3l@WANzmcY1YaZ|`p88fbJRW)i`^n1q5Y3*w$W0@ zquC6o_`S>{REmsJSP!Bfh5S%$47qSYXk~2KjDh}yRFjq<&t90Ef3#ZK$O<8_fSD(_ z_vBzl80q;+B148|n(%SBR&*XTm>C>Zgs}D<1VadFoP!q~&S%$6mm1JY$5BD1*9MMQ{W(Yd)p~DuJiT4N4 zp-LuEK}9eu^egZ71yYhskZ3o7RglWVaa!X*`-j2{B4pdgh#^Nf)Do^&Olw`LXjA}) z+Ar;bAERgKlkh_$rl_SQ50k^$5W38RLkZgWetzh6K8|F9nOI4X8GCPN(>-H}0D|#^NjJ<*|xuZ@8no^rV~Yr$~5en4a`w9W^L9 z#ezL9(V$`q#upI0wo;o}JWPEemsRuyMXy1P-Fo)C{dzasw%|GV_ySpglhr-CcI_I* zz=X;4i4#4*^289vWAzIt^*{RZ#lo{g{9Jd&Hp;>gS)WF>` zUx`>OW87GxZKCm#5{DkZHXg0ximK0oWWGo;Ud112|H%fHj-xebKBLQs|5~O~N)N?ZS{D9if=?bfGt~rKjR_ z5p}UPp@UO`wY7;k!NHUFJ_kSghFdj-g}6BQs5n3(OvnHPn@9j%HI zVE>5aK{Gh#$QzF>mlFEI*|%Z>_7grx{Hs?3Tq57@ai>(+kfJVPS9sb@)}3<@<_Sl~ zn6RpXf_R4XbOAsSxx%`YT>SpfgB}?`XIRP3UdN6-U`@&YrZ#r$VSMfq2p*H)JG;=` zi;*7*ks(W9YS~ys`m>8L``jzMufBb>hDNs1XP*7RNpx-FpKRQ@%y}8!p($FV;j9_L zlY6q(p7ol2B@##Vd?g99NspMNeI4uE>imxP#1(;BI)|8*z2)oHX3d}}eI;nhVnt!v<@{hk@-V|1`V{1)& z@z&55Z4d1X&+QxIZZ}O<9h~_4A2+5nEiHW>@;q}eZ*Cbnhy~4GIlNxz|{qwkso}sXZ*U@b%#*Hih+OR-m$lhXUr>`( z*0v{3J9FmeA^5R6ixZdqT93fSf!E^fa?5wzyWjZnqsU>8ThpE}N;^d&8MI*0+}W!h zW*<2GloP$EnZLekLoqlpSazL~lG3Hz)7~ydRc`s`VhR-1)O=f89YcDtn=jn|3*%iF z<(?AoAQ%qTW9rbyM$?q5a--@;1;=cS%t4GifA_9GlVMo4olJN1-3C|LKb6`)8;-ZLa1Xa?^g&3k#%}keV_o1BXbOD4DK%PpbX|csmY0~`MmB` zjZLB3qbGqY=&#$-D|(1ESb3*v%lh@hS%#rm0}VSgc=ANac5V(`!hN$bd3MD0E(vLY z2jur01nBuV`0Bg6Q1uXstM%GJoZ=8AC4($-R?exo-xb$9v`bg%B6G|RQ25745;U68 ze#5!VonE?*5-zB=$YLZ@sIVx*$Sp4qD+bx^<97z4@J6MDE|`Dr*>=BRC|mgLiaqiA zy1Lz=TP09BSygf}{i{O!42v#b3=+q^2oIx~d9N$0Z&WwVx(knYligM#x>*{H)OD74 zoah=&f&3OV4s3_y*s5$#PEECSI2{<+av)UL#7q2?d=};k^9E0QTah(hQ+!!+oYTu$ z^6?wOa5FAt;}hOTc0IE@{f9DHMEV_)ptQ5IGm~M$lpW+yUC4L|!K$yWw_?W?(k;g| zum}HwaX4tthcQ`2@3j}R-J`%?1R|3+(3f9@bK{VQhdIoKz)0Jd#vAO;n@44G-g)fy zvrS^=IJ){?4~F^0U?P+y^t;9KnObGOYb`{R1NF29;~D4&E2?^d6=?%5a{DNVXe z_O7mH;1-ZE3`z$G6~=#?e!G7gioHJh?;+VKCV#)K^v}@jKc?S*hG)Nd zgFAm4qILbIF2w&m!}#;JANxOkyK(HBoB`1xvuzx?oU}gw;t1({ovPv z9lnkC#fv%pWRrJ3(mQ*pV9SbpcV1`_FX%*cUG3Ry$gO>IF(FC)3@IV?*r@9ev86b+ zP6yI@?v5P|zgp-gj8U=)Zw}Ur)m%0At-fNf1Ku}>_1&Gj;m?&OmPh`mH_Kl%VOaUq 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 literal 0 HcmV?d00001 diff --git a/dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot/drawing1.json b/dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot/drawing1.json new file mode 100644 index 0000000000..4848657f09 --- /dev/null +++ b/dev/import-tool/docs/huly/example-workspace/Project Alpha/files/screenshot/drawing1.json @@ -0,0 +1,55 @@ +{ + "class": "drawing:class:Drawing", + "content": [ + { + "lineWidth": 4, + "erasing": false, + "penColor": "#0000ff", + "points": [ + { "x": 213, "y": 181.5 }, + { "x": 203, "y": 181.5 }, + { "x": 193, "y": 181.5 }, + { "x": 168, "y": 181.5 }, + { "x": 136, "y": 181.5 }, + { "x": 108, "y": 181.5 }, + { "x": 84, "y": 188.5 }, + { "x": 59, "y": 196.5 }, + { "x": 42, "y": 205.5 }, + { "x": 35, "y": 220.5 }, + { "x": 32, "y": 227.5 }, + { "x": 31, "y": 233.5 }, + { "x": 31, "y": 241.5 }, + { "x": 31, "y": 248.5 }, + { "x": 34, "y": 257.5 }, + { "x": 41, "y": 269.5 }, + { "x": 53, "y": 280.5 }, + { "x": 69, "y": 290.5 }, + { "x": 84, "y": 297.5 }, + { "x": 112, "y": 305.5 }, + { "x": 190, "y": 319.5 }, + { "x": 250, "y": 323.5 }, + { "x": 314, "y": 323.5 }, + { "x": 372, "y": 317.5 }, + { "x": 410, "y": 303.5 }, + { "x": 424, "y": 292.5 }, + { "x": 427, "y": 279.5 }, + { "x": 427, "y": 265.5 }, + { "x": 425, "y": 250.5 }, + { "x": 413, "y": 225.5 }, + { "x": 407, "y": 214.5 }, + { "x": 398, "y": 203.5 }, + { "x": 380, "y": 189.5 }, + { "x": 361, "y": 181.5 }, + { "x": 317, "y": 168.5 }, + { "x": 261, "y": 154.5 }, + { "x": 207, "y": 148.5 }, + { "x": 168, "y": 148.5 }, + { "x": 153, "y": 148.5 }, + { "x": 147, "y": 151.5 }, + { "x": 143, "y": 157.5 }, + { "x": 142, "y": 165.5 }, + { "x": 142, "y": 165.5 } + ] + } + ] +} diff --git a/dev/import-tool/src/notion/README.md b/dev/import-tool/docs/notion/README.md similarity index 100% rename from dev/import-tool/src/notion/README.md rename to dev/import-tool/docs/notion/README.md diff --git a/dev/import-tool/package.json b/dev/import-tool/package.json index 29af15302d..a2db246214 100644 --- a/dev/import-tool/package.json +++ b/dev/import-tool/package.json @@ -34,43 +34,26 @@ "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", - "@types/csvtojson": "^2.0.0", - "@types/js-yaml": "^4.0.9" + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "prettier": "^3.1.0" }, "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", - "@hcengineering/rank": "^0.6.4", - "@hcengineering/tracker": "^0.6.24", - "commander": "^8.1.0", - "mime-types": "~2.1.34", - "csvtojson": "^2.0.10", - "@hcengineering/task": "^0.6.20", - "@hcengineering/contact": "^0.6.24", - "@hcengineering/chunter": "^0.6.20", - "js-yaml": "^4.1.0" + "@hcengineering/importer": "^0.6.1", + "commander": "^8.1.0" } } diff --git a/dev/import-tool/src/index.ts b/dev/import-tool/src/index.ts index 3ca141dd71..ff40a87eaa 100644 --- a/dev/import-tool/src/index.ts +++ b/dev/import-tool/src/index.ts @@ -20,11 +20,29 @@ import serverClientPlugin, { selectWorkspace } from '@hcengineering/server-client' import { program } from 'commander' -import { importNotion } from './notion/notion' import { setMetadata } from '@hcengineering/platform' -import { FrontFileUploader, type FileUploader } from './importer/uploader' -import { ClickupImporter } from './clickup/clickup' -import { UnifiedFormatImporter } from './huly/unified' +import { + UnifiedFormatImporter, + ClickupImporter, + importNotion, + FrontFileUploader, + type FileUploader, + type Logger +} from '@hcengineering/importer' + +class ConsoleLogger implements Logger { + log (msg: string, data?: any): void { + console.log(msg, data) + } + + warn (msg: string, data?: any): void { + console.warn(msg, data) + } + + error (msg: string, data?: any): void { + console.error(msg, data) + } +} /** * @public @@ -127,7 +145,7 @@ export function importTool (): void { .action(async (file: string, cmd) => { const { workspace, user, password } = cmd await authorize(user, password, workspace, async (client, uploader) => { - const importer = new ClickupImporter(client, uploader) + const importer = new ClickupImporter(client, uploader, new ConsoleLogger()) await importer.importClickUpTasks(file) }) }) @@ -142,7 +160,7 @@ export function importTool (): void { .action(async (dir: string, cmd) => { const { workspace, user, password } = cmd await authorize(user, password, workspace, async (client, uploader) => { - const importer = new UnifiedFormatImporter(client, uploader) + const importer = new UnifiedFormatImporter(client, uploader, new ConsoleLogger()) await importer.importFolder(dir) }) }) diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 4d99f62fa7..66594fabab 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -60,7 +60,7 @@ import serverClientPlugin, { } from '@hcengineering/server-client' import { createBackupPipeline, getConfig } from '@hcengineering/server-pipeline' import serverToken, { decodeToken, generateToken } from '@hcengineering/server-token' -import toolPlugin, { FileModelLogger } from '@hcengineering/server-tool' +import { FileModelLogger } from '@hcengineering/server-tool' import { createWorkspace, upgradeWorkspace } from '@hcengineering/workspace-service' import path from 'path' @@ -167,11 +167,6 @@ export function devTool ( console.error('please provide transactor url.') } - const initScriptUrl = process.env.INIT_SCRIPT_URL - if (initScriptUrl !== undefined) { - setMetadata(toolPlugin.metadata.InitScriptURL, initScriptUrl) - } - setMetadata(accountPlugin.metadata.Transactors, transactorUrl) setMetadata(serverClientPlugin.metadata.Endpoint, accountsUrl) setMetadata(serverToken.metadata.Secret, serverSecret) diff --git a/packages/importer/.eslintrc.js b/packages/importer/.eslintrc.js new file mode 100644 index 0000000000..72235dc283 --- /dev/null +++ b/packages/importer/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/packages/importer/.npmignore b/packages/importer/.npmignore new file mode 100644 index 0000000000..e3ec093c38 --- /dev/null +++ b/packages/importer/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/packages/importer/jest.config.js b/packages/importer/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/packages/importer/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/packages/importer/package.json b/packages/importer/package.json new file mode 100644 index 0000000000..35424c2174 --- /dev/null +++ b/packages/importer/package.json @@ -0,0 +1,69 @@ +{ + "name": "@hcengineering/importer", + "version": "0.6.1", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "files": [ + "lib/**/*", + "types/**/*", + "tsconfig.json" + ], + "author": "Anticrm Platform Contributors", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint": "^8.54.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.4.0", + "eslint-plugin-promise": "^6.1.1", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "~20.11.16", + "@types/mime-types": "~2.1.1", + "@types/csvtojson": "^2.0.0", + "@types/js-yaml": "^4.0.9" + }, + "dependencies": { + "@hcengineering/attachment": "^0.6.14", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/collaboration": "^0.6.0", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/core": "^0.6.32", + "@hcengineering/document": "^0.6.0", + "@hcengineering/model-attachment": "^0.6.0", + "@hcengineering/model-core": "^0.6.0", + "@hcengineering/platform": "^0.6.11", + "@hcengineering/rank": "^0.6.4", + "@hcengineering/server-core": "^0.6.1", + "@hcengineering/task": "^0.6.20", + "@hcengineering/text": "^0.6.5", + "@hcengineering/tracker": "^0.6.24", + "@hcengineering/view": "^0.6.13", + "commander": "^8.1.0", + "mime-types": "~2.1.34", + "csvtojson": "^2.0.10", + "js-yaml": "^4.1.0", + "image-size": "^1.1.1" + }, + "repository": "https://github.com/hcengineering/platform", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } +} diff --git a/dev/import-tool/src/clickup/clickup.ts b/packages/importer/src/clickup/clickup.ts similarity index 93% rename from dev/import-tool/src/clickup/clickup.ts rename to packages/importer/src/clickup/clickup.ts index b39d00a67b..431c759bfd 100644 --- a/dev/import-tool/src/clickup/clickup.ts +++ b/packages/importer/src/clickup/clickup.ts @@ -25,9 +25,9 @@ import { type ImportProject, type ImportProjectType } from '../importer/importer' -import { type FileUploader } from '../importer/uploader' +import { type Logger } from '../importer/logger' import { BaseMarkdownPreprocessor } from '../importer/preprocessor' - +import { type FileUploader } from '../importer/uploader' interface ClickupTask { 'Task ID': string 'Task Name': string @@ -86,7 +86,8 @@ class ClickupImporter { constructor ( private readonly client: TxOperations, - private readonly fileUploader: FileUploader + private readonly fileUploader: FileUploader, + private readonly logger: Logger ) {} async importClickUpTasks (file: string): Promise { @@ -102,13 +103,13 @@ class ClickupImporter { spaces } - console.log('========================================') - console.log('IMPORT DATA STRUCTURE: ', JSON.stringify(importData, null, 4)) - console.log('========================================') + this.logger.log('========================================') + this.logger.log('IMPORT DATA STRUCTURE: ', JSON.stringify(importData, null, 4)) + this.logger.log('========================================') const postprocessor = new ClickupMarkdownPreprocessor(this.personsByName) - await new WorkspaceImporter(this.client, this.fileUploader, importData, postprocessor).performImport() - console.log('========================================') - console.log('IMPORT SUCCESS ') + await new WorkspaceImporter(this.client, this.logger, this.fileUploader, importData, postprocessor).performImport() + this.logger.log('========================================') + this.logger.log('IMPORT SUCCESS ') } private async processTasksCsv (file: string, process: (json: ClickupTask) => Promise | void): Promise { @@ -136,8 +137,8 @@ class ClickupImporter { statuses.add(clickupTask.Status) }) - console.log(projects) - console.log(statuses) + this.logger.log('Projects: ' + JSON.stringify(projects)) + this.logger.log('Statuses: ' + JSON.stringify(statuses)) const importProjectType = this.createClickupProjectType(Array.from(statuses)) diff --git a/dev/import-tool/src/huly/unified.ts b/packages/importer/src/huly/unified.ts similarity index 76% rename from dev/import-tool/src/huly/unified.ts rename to packages/importer/src/huly/unified.ts index 67713179a4..264ed772fd 100644 --- a/dev/import-tool/src/huly/unified.ts +++ b/packages/importer/src/huly/unified.ts @@ -18,8 +18,9 @@ import contact, { type Person, type PersonAccount } from '@hcengineering/contact import { type Class, type Doc, generateId, type Ref, type Space, type TxOperations } from '@hcengineering/core' import document, { type Document } from '@hcengineering/document' import { MarkupMarkType, type MarkupNode, MarkupNodeType, traverseNode, traverseNodeMarks } from '@hcengineering/text' -import tracker, { type Issue } from '@hcengineering/tracker' +import tracker, { type Issue, Project } from '@hcengineering/tracker' import * as fs from 'fs' +import sizeOf from 'image-size' import * as yaml from 'js-yaml' import { contentType } from 'mime-types' import * as path from 'path' @@ -28,6 +29,7 @@ import { type ImportAttachment, type ImportComment, type ImportDocument, + ImportDrawing, type ImportIssue, type ImportProject, type ImportProjectType, @@ -35,12 +37,14 @@ import { type ImportWorkspace, WorkspaceImporter } from '../importer/importer' +import { type Logger } from '../importer/logger' import { BaseMarkdownPreprocessor } from '../importer/preprocessor' import { type FileUploader } from '../importer/uploader' interface UnifiedComment { author: string text: string + attachments?: string[] } interface UnifiedIssueHeader { @@ -59,14 +63,17 @@ interface UnifiedSpaceSettings { title: string private?: boolean autoJoin?: boolean + archived?: boolean owners?: string[] members?: string[] description?: string + emoji?: string } interface UnifiedProjectSettings extends UnifiedSpaceSettings { class: 'tracker:class:Project' identifier: string + id?: 'tracker:project:DefaultProject' projectType?: string defaultIssueStatus?: string } @@ -97,6 +104,7 @@ interface UnifiedWorkspaceSettings { class HulyMarkdownPreprocessor extends BaseMarkdownPreprocessor { constructor ( private readonly urlProvider: (id: string) => string, + private readonly logger: Logger, private readonly metadataByFilePath: Map, private readonly metadataById: Map, DocMetadata>, private readonly attachMetadataByPath: Map, @@ -130,7 +138,7 @@ class HulyMarkdownPreprocessor extends BaseMarkdownPreprocessor { const attachmentMeta = this.attachMetadataByPath.get(fullPath) if (attachmentMeta === undefined) { - console.warn(`Attachment image not found for ${fullPath}`) + this.logger.error(`Attachment image not found for ${fullPath}`) return } @@ -160,7 +168,7 @@ class HulyMarkdownPreprocessor extends BaseMarkdownPreprocessor { this.updateAttachmentMetadata(fullPath, attachmentMeta, id, spaceId, sourceMeta) } } else { - console.log('Unknown link type, leave it as is:', href) + this.logger.log('Unknown link type, leave it as is: ' + href) } }) } @@ -218,7 +226,7 @@ class HulyMarkdownPreprocessor extends BaseMarkdownPreprocessor { private getSourceMetadata (id: Ref): DocMetadata | null { const sourceMeta = this.metadataById.get(id) if (sourceMeta == null) { - console.warn(`Source metadata not found for ${id}`) + this.logger.error(`Source metadata not found for ${id}`) return null } return sourceMeta @@ -259,56 +267,118 @@ interface AttachmentMetadata { export class UnifiedFormatImporter { private readonly metadataById = new Map, DocMetadata>() private readonly metadataByFilePath = new Map() - private readonly attachMetadataByPath = new Map() + private readonly fileMetadataByPath = new Map() private personsByName = new Map>() private accountsByEmail = new Map>() constructor ( private readonly client: TxOperations, - private readonly fileUploader: FileUploader + private readonly fileUploader: FileUploader, + private readonly logger: Logger ) {} async importFolder (folderPath: string): Promise { await this.cachePersonsByNames() await this.cacheAccountsByEmails() + await this.collectFileMetadata(folderPath) + const workspaceData = await this.processImportFolder(folderPath) - console.log('========================================') - console.log('IMPORT DATA STRUCTURE: ', JSON.stringify(workspaceData, null, 4)) - console.log('========================================') + this.logger.log('========================================') + this.logger.log('IMPORT DATA STRUCTURE: ' + JSON.stringify(workspaceData)) + this.logger.log('========================================') - console.log('Importing documents...') + this.logger.log('Importing documents...') const preprocessor = new HulyMarkdownPreprocessor( this.fileUploader.getFileUrl, + this.logger, this.metadataByFilePath, this.metadataById, - this.attachMetadataByPath, + this.fileMetadataByPath, this.personsByName ) - await new WorkspaceImporter(this.client, this.fileUploader, workspaceData, preprocessor).performImport() + await new WorkspaceImporter( + this.client, + this.logger, + this.fileUploader, + workspaceData, + preprocessor + ).performImport() - console.log('Importing attachments...') - const attachments: ImportAttachment[] = Array.from(this.attachMetadataByPath.values()) - .filter((attachment) => attachment.parentId !== undefined) - .map((attachment) => { - return { - id: attachment.id, - title: path.basename(attachment.path), - blobProvider: async () => { - const data = fs.readFileSync(attachment.path) - return new Blob([data]) - }, - parentId: attachment.parentId, - parentClass: attachment.parentClass, - spaceId: attachment.spaceId + this.logger.log('Importing attachments...') + + const attachments: ImportAttachment[] = await Promise.all( + Array.from(this.fileMetadataByPath.values()) + .filter((attachMeta) => attachMeta.parentId !== undefined) + .map(async (attachMeta: AttachmentMetadata) => await this.processAttachment(attachMeta)) + ) + await new WorkspaceImporter(this.client, this.logger, this.fileUploader, { attachments }).performImport() + + this.logger.log('========================================') + this.logger.log('IMPORT SUCCESS') + } + + private async processAttachment (attachMeta: AttachmentMetadata): Promise { + const fileType = contentType(attachMeta.name) + + const attachment: ImportAttachment = { + id: attachMeta.id, + title: path.basename(attachMeta.path), + blobProvider: async () => { + const data = fs.readFileSync(attachMeta.path) + const props = fileType !== false ? { type: fileType } : undefined + return new Blob([data], props) + }, + parentId: attachMeta.parentId, + parentClass: attachMeta.parentClass, + spaceId: attachMeta.spaceId + } + + if (fileType !== false && fileType?.startsWith('image/')) { + try { + const imageDimensions = sizeOf(attachMeta.path) + attachment.metadata = { + originalWidth: imageDimensions.width ?? 0, + originalHeight: imageDimensions.height ?? 0 } - }) - await new WorkspaceImporter(this.client, this.fileUploader, { attachments }).performImport() + } catch (error) { + this.logger.error(`Failed to get image dimensions: ${attachMeta.path}`, error) + } - console.log('========================================') - console.log('IMPORT SUCCESS') + const pathDetails = path.parse(attachMeta.path) + const childrenDir = path.join(pathDetails.dir, pathDetails.name.replace(pathDetails.ext, '')) + if (fs.existsSync(childrenDir) && fs.statSync(childrenDir).isDirectory()) { + attachment.drawings = await this.processDrawings(childrenDir) + } + } + + return attachment + } + + private async processDrawings (folderPath: string): Promise { + this.logger.log(`Processing drawings in ${folderPath}...`) + const entries = fs.readdirSync(folderPath, { withFileTypes: true }) + const drawings: ImportDrawing[] = [] + for (const entry of entries) { + const fullPath = path.join(folderPath, entry.name) + if (entry.isFile() && entry.name.endsWith('.json')) { + const content = fs.readFileSync(fullPath, 'utf8') + const json = JSON.parse(content) + if (json.class !== 'drawing:class:Drawing') { + this.logger.log(`Skipping ${fullPath}: not a drawing`) + continue + } + + drawings.push({ + contentProvider: async () => { + return JSON.stringify(json.content) + } + }) + } + } + return drawings } private async processImportFolder (folderPath: string): Promise { @@ -336,11 +406,11 @@ export class UnifiedFormatImporter { const spacePath = path.join(folderPath, spaceName) try { - console.log(`Processing ${spaceName}...`) + this.logger.log(`Processing ${spaceName}...`) const spaceConfig = yaml.load(fs.readFileSync(yamlPath, 'utf8')) as UnifiedSpaceSettings - if (spaceConfig.class === undefined) { - console.warn(`Skipping ${spaceName}: not a space - no class specified`) + if (spaceConfig?.class === undefined) { + this.logger.error(`Skipping ${spaceName}: not a space - no class specified`) continue } @@ -373,8 +443,6 @@ export class UnifiedFormatImporter { } } - await this.processAttachments(folderPath) - return builder.build() } @@ -392,7 +460,7 @@ export class UnifiedFormatImporter { const issueHeader = (await this.readYamlHeader(issuePath)) as UnifiedIssueHeader if (issueHeader.class === undefined) { - console.warn(`Skipping ${issueFile}: not an issue`) + this.logger.error(`Skipping ${issueFile}: not an issue`) continue } @@ -420,7 +488,7 @@ export class UnifiedFormatImporter { priority: issueHeader.priority, estimation: issueHeader.estimation, remainingTime: issueHeader.remainingTime, - comments: this.processComments(issueHeader.comments), + comments: await this.processComments(currentPath, issueHeader.comments), subdocs: [], // Will be added via builder assignee: this.findPersonByName(issueHeader.assignee) } @@ -470,7 +538,7 @@ export class UnifiedFormatImporter { const docHeader = (await this.readYamlHeader(docPath)) as UnifiedDocumentHeader if (docHeader.class === undefined) { - console.warn(`Skipping ${docFile}: not a document`) + this.logger.error(`Skipping ${docFile}: not a document`) continue } @@ -506,13 +574,27 @@ export class UnifiedFormatImporter { } } - private processComments (comments: UnifiedComment[] = []): ImportComment[] { - return comments.map((comment) => { - return { - text: comment.text, - author: this.findAccountByEmail(comment.author) - } - }) + private processComments (currentPath: string, comments: UnifiedComment[] = []): Promise { + return Promise.all( + comments.map(async (comment) => { + const attachments: ImportAttachment[] = [] + if (comment.attachments !== undefined) { + for (const attachmentPath of comment.attachments) { + const fullPath = path.resolve(currentPath, attachmentPath) + const attachmentMeta = this.fileMetadataByPath.get(fullPath) + if (attachmentMeta !== undefined) { + const importAttachment = await this.processAttachment(attachmentMeta) + attachments.push(importAttachment) + } + } + } + return { + text: comment.text, + author: this.findAccountByEmail(comment.author), + attachments + } + }) + ) } private processProjectTypes (wsHeader: UnifiedWorkspaceSettings): ImportProjectType[] { @@ -534,11 +616,14 @@ export class UnifiedFormatImporter { private async processProject (projectHeader: UnifiedProjectSettings): Promise { return { class: tracker.class.Project, + id: projectHeader.id as Ref, title: projectHeader.title, identifier: projectHeader.identifier, private: projectHeader.private ?? false, autoJoin: projectHeader.autoJoin ?? true, + archived: projectHeader.archived ?? false, description: projectHeader.description, + emoji: projectHeader.emoji, defaultIssueStatus: projectHeader.defaultIssueStatus !== undefined ? { name: projectHeader.defaultIssueStatus } : undefined, owners: @@ -555,7 +640,9 @@ export class UnifiedFormatImporter { title: spaceHeader.title, private: spaceHeader.private ?? false, autoJoin: spaceHeader.autoJoin ?? true, + archived: spaceHeader.archived ?? false, description: spaceHeader.description, + emoji: spaceHeader.emoji, owners: spaceHeader.owners !== undefined ? spaceHeader.owners.map((email) => this.findAccountByEmail(email)) : [], members: spaceHeader.members !== undefined ? spaceHeader.members.map((email) => this.findAccountByEmail(email)) : [], @@ -564,7 +651,7 @@ export class UnifiedFormatImporter { } private async readYamlHeader (filePath: string): Promise { - console.log('Read YAML header from: ', filePath) + this.logger.log('Read YAML header from: ' + filePath) const content = fs.readFileSync(filePath, 'utf8') const match = content.match(/^---\n([\s\S]*?)\n---/) if (match != null) { @@ -601,7 +688,7 @@ export class UnifiedFormatImporter { }, new Map()) } - private async processAttachments (folderPath: string): Promise { + private async collectFileMetadata (folderPath: string): Promise { const processDir = async (dir: string): Promise => { const entries = fs.readdirSync(dir, { withFileTypes: true }) @@ -611,11 +698,8 @@ export class UnifiedFormatImporter { if (entry.isDirectory()) { await processDir(fullPath) } else if (entry.isFile()) { - // Skip files that are already processed as documents or issues - if (!this.metadataByFilePath.has(fullPath)) { - const attachmentId = generateId() - this.attachMetadataByPath.set(fullPath, { id: attachmentId, name: entry.name, path: fullPath }) - } + const attachmentId = generateId() + this.fileMetadataByPath.set(fullPath, { id: attachmentId, name: entry.name, path: fullPath }) } } } diff --git a/dev/import-tool/src/importer/builder.ts b/packages/importer/src/importer/builder.ts similarity index 94% rename from dev/import-tool/src/importer/builder.ts rename to packages/importer/src/importer/builder.ts index d47a84a5f6..c9f0ccbc48 100644 --- a/dev/import-tool/src/importer/builder.ts +++ b/packages/importer/src/importer/builder.ts @@ -256,6 +256,18 @@ export class ImportWorkspaceBuilder { errors.push('defaultIssueStatus not found: ' + project.defaultIssueStatus.name) } + if (project.id !== undefined && project.id !== tracker.project.DefaultProject) { + errors.push('update operation is only allowed for tracker:project:DefaultProject') + } + + if (project.archived !== undefined) { + errors.push(...this.validateType(project.archived, 'boolean', 'archived')) + } + + if (project.emoji !== undefined) { + errors.push(...this.validateEmoji(project.emoji)) + } + errors.push(...this.validateProjectIdentifier(project.identifier)) return errors } @@ -303,9 +315,22 @@ export class ImportWorkspaceBuilder { errors.push(...this.validateType(teamspace.description, 'string', 'description')) } + if (teamspace.archived !== undefined) { + errors.push(...this.validateType(teamspace.archived, 'boolean', 'archived')) + } + + if (teamspace.emoji !== undefined) { + errors.push(...this.validateType(teamspace.emoji, 'string', 'emoji')) + } + + if (teamspace.emoji !== undefined) { + errors.push(...this.validateEmoji(teamspace.emoji)) + } + if (!this.validateStringDefined(teamspace.title)) { errors.push('title is required') } + if (teamspace.class !== document.class.Teamspace) { errors.push('invalid class: ' + teamspace.class) } @@ -446,6 +471,14 @@ export class ImportWorkspaceBuilder { issue.subdocs = childIssues } + private validateEmoji (emoji: string): string[] { + const errors: string[] = [] + if (typeof emoji === 'string' && emoji.codePointAt(0) == null) { + errors.push('Invalid emoji: ' + emoji) + } + return errors + } + private validateType (value: unknown, type: 'string' | 'number' | 'boolean', fieldName: string): string[] { const errors: string[] = [] switch (type) { diff --git a/dev/import-tool/src/importer/dowloader.ts b/packages/importer/src/importer/dowloader.ts similarity index 100% rename from dev/import-tool/src/importer/dowloader.ts rename to packages/importer/src/importer/dowloader.ts diff --git a/dev/import-tool/src/importer/uploader.ts b/packages/importer/src/importer/frontUploader.ts similarity index 53% rename from dev/import-tool/src/importer/uploader.ts rename to packages/importer/src/importer/frontUploader.ts index d2ae4fb7d9..95fe9b09b7 100644 --- a/dev/import-tool/src/importer/uploader.ts +++ b/packages/importer/src/importer/frontUploader.ts @@ -13,19 +13,27 @@ // limitations under the License. // import { - type Ref, - type Blob as PlatformBlob, type CollaborativeDoc, concatLink, - makeCollabJsonId + makeCollabJsonId, + Markup, + type Blob as PlatformBlob, + type Ref } from '@hcengineering/core' +import { FileUploader, UploadResult } from './uploader' -export interface FileUploader { - uploadFile: (name: string, file: Blob) => Promise> - uploadCollaborativeDoc: (collabId: CollaborativeDoc, data: Buffer) => Promise> - getFileUrl: (id: string) => string +interface FileUploadError { + key: string + error: string } +interface FileUploadSuccess { + key: string + id: string +} + +type FileUploadResult = FileUploadSuccess | FileUploadError + export class FrontFileUploader implements FileUploader { constructor ( private readonly frontUrl: string, @@ -35,11 +43,11 @@ export class FrontFileUploader implements FileUploader { this.getFileUrl = this.getFileUrl.bind(this) } - public async uploadFile (name: string, file: Blob): Promise> { + public async uploadFile (name: string, blob: Blob): Promise { const form = new FormData() - form.append('file', file, name) + form.append('file', blob, name) - const res = await fetch(concatLink(this.frontUrl, '/files'), { + const response = await fetch(concatLink(this.frontUrl, '/files'), { method: 'POST', headers: { Authorization: 'Bearer ' + this.token @@ -47,20 +55,36 @@ export class FrontFileUploader implements FileUploader { body: form }) - if (res.ok && res.status === 200) { - return name as Ref + if (response.status !== 200) { + return { success: false, error: response.statusText } } - throw new Error('Failed to upload file') + const responseText = await response.text() + if (responseText === undefined) { + return { success: false, error: response.statusText } + } + + const uploadResult = JSON.parse(responseText) as FileUploadResult[] + if (!Array.isArray(uploadResult) || uploadResult.length === 0) { + return { success: false, error: response.statusText } + } + + const result = uploadResult[0] + if ('error' in result) { + return { success: false, error: result.error } + } + + return { success: true, id: result.id as Ref } } public getFileUrl (id: string): string { return concatLink(this.frontUrl, `/files/${this.workspaceId}/${id}?file=${id}&workspace=${this.workspaceId}`) } - public async uploadCollaborativeDoc (collabId: CollaborativeDoc, data: Buffer): Promise> { + public async uploadCollaborativeDoc (collabId: CollaborativeDoc, content: Markup): Promise { + const buffer = Buffer.from(content) const blobId = makeCollabJsonId(collabId) - const blob = new Blob([data], { type: 'application/json' }) + const blob = new Blob([buffer], { type: 'application/json' }) return await this.uploadFile(blobId, blob) } } diff --git a/dev/import-tool/src/importer/importer.ts b/packages/importer/src/importer/importer.ts similarity index 73% rename from dev/import-tool/src/importer/importer.ts rename to packages/importer/src/importer/importer.ts index 93f285ee70..571e6fc786 100644 --- a/dev/import-tool/src/importer/importer.ts +++ b/packages/importer/src/importer/importer.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import attachment, { type Attachment } from '@hcengineering/attachment' +import attachment, { Drawing, type Attachment } from '@hcengineering/attachment' import chunter, { type ChatMessage } from '@hcengineering/chunter' import { type Person } from '@hcengineering/contact' import core, { @@ -51,8 +51,10 @@ import tracker, { type Project, TimeReportDayType } from '@hcengineering/tracker' +import view from '@hcengineering/view' import { type MarkdownPreprocessor, NoopMarkdownPreprocessor } from './preprocessor' import { type FileUploader } from './uploader' +import { Logger } from './logger' export interface ImportWorkspace { projectTypes?: ImportProjectType[] @@ -82,7 +84,9 @@ export interface ImportSpace { title: string private: boolean autoJoin?: boolean + archived?: boolean description?: string + emoji?: string owners?: Ref[] members?: Ref[] docs: T[] @@ -107,6 +111,7 @@ export interface ImportDocument extends ImportDoc { export interface ImportProject extends ImportSpace { class: Ref> + id?: Ref identifier: string projectType?: ImportProjectType defaultIssueStatus?: ImportStatus @@ -139,6 +144,17 @@ export interface ImportAttachment { parentId?: Ref parentClass?: Ref>> spaceId?: Ref + metadata?: ImportImageMetadata + drawings?: ImportDrawing[] +} + +export interface ImportImageMetadata { + originalWidth: number + originalHeight: number +} + +export interface ImportDrawing { + contentProvider: () => Promise } export class WorkspaceImporter { @@ -147,6 +163,7 @@ export class WorkspaceImporter { constructor ( private readonly client: TxOperations, + private readonly logger: Logger, private readonly fileUploader: FileUploader, private readonly workspaceData: ImportWorkspace, private readonly preprocessor: MarkdownPreprocessor = new NoopMarkdownPreprocessor() @@ -234,9 +251,9 @@ export class WorkspaceImporter { } async importTeamspace (space: ImportTeamspace): Promise> { - console.log('Creating teamspace: ', space.title) + this.logger.log('Creating teamspace: ' + space.title) const teamspaceId = await this.createTeamspace(space) - console.log('Teamspace created: ', teamspaceId) + this.logger.log('Teamspace created: ' + teamspaceId) for (const doc of space.docs) { await this.createDocumentWithSubdocs(doc, document.ids.NoParent, teamspaceId) } @@ -248,9 +265,9 @@ export class WorkspaceImporter { parentId: Ref, teamspaceId: Ref ): Promise> { - console.log('Creating document: ', doc.title) + this.logger.log('Creating document: ' + doc.title) const documentId = await this.createDocument(doc, parentId, teamspaceId) - console.log('Document created: ', documentId) + this.logger.log('Document created: ' + documentId) for (const child of doc.subdocs) { await this.createDocumentWithSubdocs(child, documentId, teamspaceId) } @@ -259,16 +276,19 @@ export class WorkspaceImporter { async createTeamspace (space: ImportTeamspace): Promise> { const teamspaceId = generateId() + const codePoint = space.emoji?.codePointAt(0) const data = { type: document.spaceType.DefaultTeamspaceType, description: space.description ?? '', title: space.title, name: space.title, private: space.private, + color: codePoint, + icon: codePoint === undefined ? undefined : view.ids.IconWithEmoji, owners: space.owners ?? [], members: space.members ?? [], autoJoin: space.autoJoin, - archived: false + archived: space.archived ?? false } await this.client.createDoc(document.class.Teamspace, core.space.Space, data, teamspaceId) return teamspaceId @@ -287,7 +307,7 @@ export class WorkspaceImporter { const lastRank = await getFirstRank(this.client, teamspaceId, parentId) const rank = makeRank(lastRank, undefined) - const attachedData: Data = { + const data: Data = { title: doc.title, content: contentId, parent: parentId, @@ -299,14 +319,22 @@ export class WorkspaceImporter { rank } - await this.client.createDoc(document.class.Document, teamspaceId, attachedData, id) + await this.client.createDoc(document.class.Document, teamspaceId, data, id) return id } async importProject (project: ImportProject): Promise> { - console.log('Creating project: ', project.title) - const projectId = await this.createProject(project) - console.log('Project created: ' + projectId) + let projectId: Ref + if (project.id === tracker.project.DefaultProject) { + this.logger.log('Setting up default project: ' + project.title) + projectId = tracker.project.DefaultProject + await this.updateProject(projectId, project) + this.logger.log('Default project updated: ' + projectId) + } else { + this.logger.log('Creating project: ', project.title) + projectId = await this.createProject(project) + this.logger.log('Project created: ' + projectId) + } const projectDoc = await this.client.findOne(tracker.class.Project, { _id: projectId }) if (projectDoc === undefined) { @@ -314,7 +342,7 @@ export class WorkspaceImporter { } for (const issue of project.docs) { - await this.createIssueWithSubissues(issue, tracker.ids.NoParent, projectDoc, []) + await this.createIssueWithSubissues(issue, tracker.ids.NoParent, projectDoc, projectId, []) } return projectId } @@ -323,31 +351,69 @@ export class WorkspaceImporter { issue: ImportIssue, parentId: Ref, project: Project, + spaceId: Ref, parentsInfo: IssueParentInfo[] ): Promise<{ id: Ref, identifier: string }> { - console.log('Creating issue: ', issue.title) - const issueResult = await this.createIssue(issue, project, parentId, parentsInfo) - console.log('Issue created: ', issueResult) + this.logger.log('Creating issue: ' + issue.title) + const issueResult = await this.createIssue(issue, project, parentId, spaceId, parentsInfo) + this.logger.log('Issue created: ' + JSON.stringify(issueResult)) if (issue.subdocs.length > 0) { const parentsInfoEx = [ { parentId: issueResult.id, parentTitle: issue.title, - space: project._id, + space: spaceId, identifier: issueResult.identifier }, ...parentsInfo ] for (const child of issue.subdocs) { - await this.createIssueWithSubissues(child as ImportIssue, issueResult.id, project, parentsInfoEx) + await this.createIssueWithSubissues(child as ImportIssue, issueResult.id, project, spaceId, parentsInfoEx) } } return issueResult } + async updateProject (projectId: Ref, project: ImportProject): Promise> { + const oldProject = await this.client.findOne(tracker.class.Project, { _id: projectId }) + if (oldProject === undefined) { + throw new Error('Project not found: ' + projectId) + } + const maxIssueNumber = this.getMaxImportIssueNumber(project) + const codePoint = project.emoji?.codePointAt(0) + const projectData = { + name: project.title, + private: project.private, + description: project.description ?? oldProject.description, + members: project.members ?? oldProject.members, + owners: project.owners ?? oldProject.owners, + archived: project.archived ?? oldProject.archived, + autoJoin: project.autoJoin ?? oldProject.autoJoin, + identifier: + project.identifier !== undefined + ? await this.uniqueProjectIdentifier(project.identifier) + : oldProject.identifier, + sequence: Math.max(oldProject.sequence, maxIssueNumber), + color: codePoint ?? oldProject.color, + icon: codePoint === undefined ? undefined : view.ids.IconWithEmoji, + defaultIssueStatus: + project.defaultIssueStatus !== undefined + ? this.issueStatusByName.get(project.defaultIssueStatus.name) + : oldProject.defaultIssueStatus, + defaultTimeReportDay: oldProject.defaultTimeReportDay, + type: + project.projectType !== undefined + ? this.projectTypeByName.get(project.projectType.name) ?? tracker.ids.ClassingProjectType + : oldProject.type + } + + await this.client.updateDoc(tracker.class.Project, core.space.Space, projectId, projectData) + return projectId + } + async createProject (project: ImportProject): Promise> { const projectId = generateId() @@ -362,6 +428,7 @@ export class WorkspaceImporter { : tracker.status.Backlog const identifier = await this.uniqueProjectIdentifier(project.identifier) + const codePoint = project.emoji?.codePointAt(0) const projectData = { name: project.title, description: project.description ?? '', @@ -371,7 +438,9 @@ export class WorkspaceImporter { archived: false, autoJoin: project.autoJoin, identifier, - sequence: 0, + sequence: this.getMaxImportIssueNumber(project), + color: codePoint, + icon: codePoint != null ? view.ids.IconWithEmoji : undefined, defaultIssueStatus: defaultIssueStatus ?? tracker.status.Backlog, defaultTimeReportDay: TimeReportDayType.PreviousWorkDay, type: projectType as Ref @@ -384,24 +453,30 @@ export class WorkspaceImporter { return projectId } + private getMaxImportIssueNumber (project: ImportProject): number { + const maxIssueNumber = Math.max(...project.docs.map((doc) => doc.number ?? 0)) + return maxIssueNumber + } + async createIssue ( issue: ImportIssue, project: Project, parentId: Ref, + spaceId: Ref, parentsInfo: IssueParentInfo[] ): Promise<{ id: Ref, identifier: string }> { const issueId = issue.id ?? generateId() const content = await issue.descrProvider() const collabId = makeCollabId(tracker.class.Issue, issueId, 'description') - const contentId = await this.createCollaborativeContent(issueId, collabId, content, project._id) + const contentId = await this.createCollaborativeContent(issueId, collabId, content, spaceId) const { number, identifier } = issue.number !== undefined ? { number: issue.number, identifier: `${project.identifier}-${issue.number}` } - : await this.getNextIssueIdentifier(project) + : await this.getNextIssueIdentifier(project, spaceId) const kind = await this.getIssueKind(project) - const rank = await this.getIssueRank(project) + const rank = await this.getIssueRank(project, spaceId) const status = await this.findIssueStatusByName(issue.status.name) const priority = issue.priority !== undefined @@ -436,7 +511,7 @@ export class WorkspaceImporter { await this.client.addCollection( tracker.class.Issue, - project._id, + spaceId, parentId, tracker.class.Issue, 'subIssues', @@ -445,16 +520,19 @@ export class WorkspaceImporter { ) if (issue.comments !== undefined) { - await this.importComments(issueId, issue.comments, project._id) + await this.importComments(issueId, issue.comments, spaceId) } return { id: issueId, identifier } } - private async getNextIssueIdentifier (project: Project): Promise<{ number: number, identifier: string }> { + private async getNextIssueIdentifier ( + project: Project, + spaceId: Ref + ): Promise<{ number: number, identifier: string }> { const incResult = await this.client.updateDoc( tracker.class.Project, core.space.Space, - project._id, + spaceId, { $inc: { sequence: 1 } }, true ) @@ -467,15 +545,15 @@ export class WorkspaceImporter { const taskKind = project?.type !== undefined ? { parent: project.type } : {} const kind = await this.client.findOne(task.class.TaskType, taskKind) if (kind === undefined) { - throw new Error(`Task type not found for project: ${project._id}`) + throw new Error(`Task type not found for project: ${project.name}`) } return kind } - private async getIssueRank (project: Project): Promise { + private async getIssueRank (project: Project, spaceId: Ref): Promise { const lastIssue = await this.client.findOne( tracker.class.Issue, - { space: project._id }, + { space: spaceId }, { sort: { rank: SortingOrder.Descending } } ) return makeRank(lastIssue?.rank, undefined) @@ -529,28 +607,44 @@ export class WorkspaceImporter { ): Promise { const blob = await attachment.blobProvider() if (blob === null) { - console.warn('Failed to read attachment file: ', attachment.title) + this.logger.error('Failed to read attachment file: ' + attachment.title) return } - const file = new File([blob], attachment.title) + const file = new File([blob], attachment.title, { type: blob.type }) try { - await this.createAttachment(attachment.id ?? generateId(), file, spaceId, parentId, parentClass) + const blobId = await this.createAttachment( + attachment.id ?? generateId(), + spaceId, + parentId, + parentClass, + file, + attachment.metadata + ) + + if (attachment.drawings !== undefined) { + for (const drawing of attachment.drawings) { + await this.createDrawing(blobId, drawing, spaceId) + } + } } catch { - console.warn('Failed to upload attachment file: ', attachment.title) + this.logger.error('Failed to upload attachment file: ', attachment.title) } } private async createAttachment ( id: Ref, - file: File, spaceId: Ref, parentId: Ref, - parentClass: Ref>> - ): Promise> { - const attachmentId = generateId() - const blobId = await this.fileUploader.uploadFile(id, file) + parentClass: Ref>>, + file: File, + metadata?: ImportImageMetadata + ): Promise> { + const uploadResult = await this.fileUploader.uploadFile(id, file) + if (!uploadResult.success) { + throw new Error('Failed to upload attachment file: ' + file.name) + } await this.client.addCollection( attachment.class.Attachment, spaceId, @@ -558,15 +652,31 @@ export class WorkspaceImporter { parentClass, 'attachments', { - file: blobId, + file: uploadResult.id, lastModified: Date.now(), name: file.name, size: file.size, - type: file.type + type: file.type, + metadata }, id ) - return attachmentId + return uploadResult.id + } + + private async createDrawing ( + blobId: Ref, + drawing: ImportDrawing, + spaceId: Ref + ): Promise> { + const id = generateId() + const data: Data = { + parent: blobId, + parentClass: core.class.Blob, + content: await drawing.contentProvider() + } + await this.client.createDoc(attachment.class.Drawing, spaceId, data, id) + return id } // Collaborative content handling @@ -580,9 +690,12 @@ export class WorkspaceImporter { const processedJson = this.preprocessor.process(json, id, spaceId) const markup = jsonToMarkup(processedJson) - const buffer = Buffer.from(markup) - return await this.fileUploader.uploadCollaborativeDoc(collabId, buffer) + const result = await this.fileUploader.uploadCollaborativeDoc(collabId, markup) + if (result.success) { + return result.id + } + throw new Error('Failed to upload collaborative document: ' + id) } async findIssueStatusByName (name: string): Promise> { diff --git a/packages/importer/src/importer/logger.ts b/packages/importer/src/importer/logger.ts new file mode 100644 index 0000000000..5d734933af --- /dev/null +++ b/packages/importer/src/importer/logger.ts @@ -0,0 +1,18 @@ +// +// 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. +// +export interface Logger { + log: (msg: string, data?: any) => void + error: (msg: string, data?: any) => void +} diff --git a/dev/import-tool/src/importer/preprocessor.ts b/packages/importer/src/importer/preprocessor.ts similarity index 100% rename from dev/import-tool/src/importer/preprocessor.ts rename to packages/importer/src/importer/preprocessor.ts diff --git a/packages/importer/src/importer/storageUploader.ts b/packages/importer/src/importer/storageUploader.ts new file mode 100644 index 0000000000..3668d55985 --- /dev/null +++ b/packages/importer/src/importer/storageUploader.ts @@ -0,0 +1,59 @@ +// +// 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 { saveCollabJson } from '@hcengineering/collaboration' +import { + CollaborativeDoc, + Markup, + MeasureContext, + Blob as PlatformBlob, + Ref, + WorkspaceIdWithUrl +} from '@hcengineering/core' +import type { StorageAdapter } from '@hcengineering/server-core' +import { FileUploader, UploadResult } from './uploader' + +export class StorageFileUploader implements FileUploader { + constructor ( + private readonly ctx: MeasureContext, + private readonly storageAdapter: StorageAdapter, + private readonly wsUrl: WorkspaceIdWithUrl + ) { + this.uploadFile = this.uploadFile.bind(this) + } + + public async uploadFile (id: string, blob: Blob): Promise { + try { + const arrayBuffer = await blob.arrayBuffer() + const buffer = Buffer.from(arrayBuffer) + await this.storageAdapter.put(this.ctx, this.wsUrl, id, buffer, blob.type, buffer.byteLength) + return { success: true, id: id as Ref } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + } + + public async uploadCollaborativeDoc (collabId: CollaborativeDoc, content: Markup): Promise { + try { + const blobId = await saveCollabJson(this.ctx, this.storageAdapter, this.wsUrl, collabId, content) + return { success: true, id: blobId } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } + } + + public getFileUrl (id: string): string { + return '' + } +} diff --git a/packages/importer/src/importer/uploader.ts b/packages/importer/src/importer/uploader.ts new file mode 100644 index 0000000000..7f2a4cc0c4 --- /dev/null +++ b/packages/importer/src/importer/uploader.ts @@ -0,0 +1,33 @@ +// +// 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 { type CollaborativeDoc, type Blob as PlatformBlob, type Markup, type Ref } from '@hcengineering/core' + +export interface SuccessUploadResult { + success: true + id: Ref +} + +export interface FailureUploadResult { + success: false + error: string +} + +export type UploadResult = SuccessUploadResult | FailureUploadResult + +export interface FileUploader { + uploadFile: (name: string, blob: Blob) => Promise + uploadCollaborativeDoc: (collabId: CollaborativeDoc, content: Markup) => Promise + getFileUrl: (id: string) => string +} diff --git a/packages/importer/src/index.ts b/packages/importer/src/index.ts new file mode 100644 index 0000000000..83ab51cf82 --- /dev/null +++ b/packages/importer/src/index.ts @@ -0,0 +1,23 @@ +// +// 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. +// + +export * from './huly/unified' +export * from './clickup/clickup' +export * from './notion/notion' + +export * from './importer/uploader' +export * from './importer/storageUploader' +export * from './importer/frontUploader' +export * from './importer/logger' diff --git a/dev/import-tool/src/notion/notion.ts b/packages/importer/src/notion/notion.ts similarity index 98% rename from dev/import-tool/src/notion/notion.ts rename to packages/importer/src/notion/notion.ts index aff260f759..c61b56f207 100644 --- a/dev/import-tool/src/notion/notion.ts +++ b/packages/importer/src/notion/notion.ts @@ -441,11 +441,13 @@ async function importPageDocument ( preProcessMarkdown(json, documentMetaMap, fileUploader) } const markup = jsonToMarkup(json) - const buffer = Buffer.from(markup) const id = docMeta.id as Ref const collabId = makeCollabId(document.class.Document, id, 'content') - const blobId = await fileUploader.uploadCollaborativeDoc(collabId, buffer) + const uploadResult = await fileUploader.uploadCollaborativeDoc(collabId, markup) + if (!uploadResult.success) { + throw new Error('Failed to upload collaborative document: ' + docMeta.id) + } const parent = (parentMeta?.id as Ref) ?? document.ids.NoParent @@ -454,7 +456,7 @@ async function importPageDocument ( const attachedData: Data = { title: docMeta.name, - content: blobId, + content: uploadResult.id, parent, attachments: 0, embeddings: 0, diff --git a/packages/importer/tsconfig.json b/packages/importer/tsconfig.json new file mode 100644 index 0000000000..59e4fd4297 --- /dev/null +++ b/packages/importer/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/pods/workspace/Dockerfile b/pods/workspace/Dockerfile index e648d52bde..85a3276463 100644 --- a/pods/workspace/Dockerfile +++ b/pods/workspace/Dockerfile @@ -14,4 +14,7 @@ ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true COPY bundle/bundle.js ./ COPY bundle/bundle.js.map ./ +COPY ini[t]/ ./init-scripts/ + CMD [ "node", "bundle.js" ] + diff --git a/rush.json b/rush.json index a2f8e5ed0e..fb79acc469 100644 --- a/rush.json +++ b/rush.json @@ -455,6 +455,11 @@ "projectFolder": "packages/api-client", "shouldPublish": false }, + { + "packageName": "@hcengineering/importer", + "projectFolder": "packages/importer", + "shouldPublish": false + }, { "packageName": "@hcengineering/collaboration", "projectFolder": "server/collaboration", diff --git a/server/account-service/package.json b/server/account-service/package.json index 937390de69..cb7c4c5048 100644 --- a/server/account-service/package.json +++ b/server/account-service/package.json @@ -53,7 +53,6 @@ "koa-router": "^12.0.1", "koa-bodyparser": "^4.4.1", "@koa/cors": "^5.0.0", - "@hcengineering/server-tool": "^0.6.0", "@hcengineering/server-token": "^0.6.11", "@hcengineering/analytics": "^0.6.0" } diff --git a/server/account-service/src/index.ts b/server/account-service/src/index.ts index 3bfd42de8c..884289f5e9 100644 --- a/server/account-service/src/index.ts +++ b/server/account-service/src/index.ts @@ -17,7 +17,6 @@ import { registerProviders } from '@hcengineering/auth-providers' import { metricsAggregate, type BrandingMap, type MeasureContext } from '@hcengineering/core' import platform, { Severity, Status, addStringsLoader, setMetadata } from '@hcengineering/platform' import serverToken, { decodeToken } from '@hcengineering/server-token' -import toolPlugin from '@hcengineering/server-tool' import cors from '@koa/cors' import { type IncomingHttpHeaders } from 'http' import Koa from 'koa' @@ -87,11 +86,6 @@ export function serveAccount (measureCtx: MeasureContext, brandings: BrandingMap setMetadata(serverToken.metadata.Secret, serverSecret) - const initScriptUrl = process.env.INIT_SCRIPT_URL - if (initScriptUrl !== undefined) { - setMetadata(toolPlugin.metadata.InitScriptURL, initScriptUrl) - } - const hasSignUp = process.env.DISABLE_SIGNUP !== 'true' const methods = getMethods(hasSignUp) diff --git a/server/tool/package.json b/server/tool/package.json index 387660b94d..51c9436fe2 100644 --- a/server/tool/package.json +++ b/server/tool/package.json @@ -46,6 +46,7 @@ "@hcengineering/contact": "^0.6.24", "@hcengineering/client-resources": "^0.6.27", "@hcengineering/client": "^0.6.18", + "@hcengineering/importer": "^0.6.1", "@hcengineering/model": "^0.6.11", "@hcengineering/rank": "^0.6.4", "uuid": "^8.3.2", diff --git a/server/tool/src/index.ts b/server/tool/src/index.ts index 2fb7408b86..b7a945988a 100644 --- a/server/tool/src/index.ts +++ b/server/tool/src/index.ts @@ -192,14 +192,20 @@ export async function initializeWorkspace ( progress: (value: number) => Promise ): Promise { const initWS = branding?.initWorkspace ?? getMetadata(toolPlugin.metadata.InitWorkspace) - const scriptUrl = getMetadata(toolPlugin.metadata.InitScriptURL) - ctx.info('Init script details', { scriptUrl, initWS }) - if (initWS === undefined || scriptUrl === undefined) return + const initRepoDir = getMetadata(toolPlugin.metadata.InitRepoDir) + ctx.info('Init script details', { initWS, initRepoDir }) + if (initWS === undefined || initRepoDir === undefined) return + + const initScriptFile = path.resolve(initRepoDir, 'script.yaml') + if (!fs.existsSync(initScriptFile)) { + ctx.warn('Init script file not found in init directory', { initScriptFile }) + return + } + try { - // `https://raw.githubusercontent.com/hcengineering/init/main/script.yaml` - const req = await fetch(scriptUrl) - const text = await req.text() + const text = fs.readFileSync(initScriptFile, 'utf8') const scripts = yaml.load(text) as any as InitScript[] + let script: InitScript | undefined if (initWS !== undefined) { script = scripts.find((it) => it.name === initWS) @@ -211,7 +217,7 @@ export async function initializeWorkspace ( return } - const initializer = new WorkspaceInitializer(ctx, storageAdapter, wsUrl, client) + const initializer = new WorkspaceInitializer(ctx, storageAdapter, wsUrl, client, initRepoDir) await initializer.processScript(script, logger, progress) } catch (err: any) { ctx.error('Failed to initialize workspace', { error: err }) diff --git a/server/tool/src/initializer.ts b/server/tool/src/initializer.ts index 250115c633..1ee620a67d 100644 --- a/server/tool/src/initializer.ts +++ b/server/tool/src/initializer.ts @@ -15,9 +15,11 @@ import core, { } from '@hcengineering/core' import { ModelLogger } from '@hcengineering/model' import { makeRank } from '@hcengineering/rank' +import { StorageFileUploader, UnifiedFormatImporter } from '@hcengineering/importer' import type { StorageAdapter } from '@hcengineering/server-core' import { jsonToMarkup, parseMessageMarkdown } from '@hcengineering/text' import { v4 as uuid } from 'uuid' +import path from 'path' const fieldRegexp = /\${\S+?}/ @@ -35,7 +37,7 @@ export type InitStep = | UpdateStep | FindStep | UploadStep - + | ImportStep export interface CreateStep { type: 'create' _class: Ref> @@ -82,6 +84,11 @@ export interface UploadStep { resultVariable?: string } +export interface ImportStep { + type: 'import' + path: string +} + export type Props = Data & Partial & { space: Ref } export class WorkspaceInitializer { @@ -93,7 +100,8 @@ export class WorkspaceInitializer { private readonly ctx: MeasureContext, private readonly storageAdapter: StorageAdapter, private readonly wsUrl: WorkspaceIdWithUrl, - private readonly client: TxOperations + private readonly client: TxOperations, + private readonly initRepoDir: string ) {} async processScript ( @@ -118,6 +126,8 @@ export class WorkspaceInitializer { await this.processFind(step, vars) } else if (step.type === 'upload') { await this.processUpload(step, vars, logger) + } else if (step.type === 'import') { + await this.processImport(step, vars, logger) } await progress(Math.round(((index + 1) * 100) / script.steps.length)) @@ -152,6 +162,18 @@ export class WorkspaceInitializer { } } + private async processImport (step: ImportStep, vars: Record, logger: ModelLogger): Promise { + try { + const uploader = new StorageFileUploader(this.ctx, this.storageAdapter, this.wsUrl) + const initPath = path.resolve(this.initRepoDir, step.path) + const importer = new UnifiedFormatImporter(this.client, uploader, logger) + await importer.importFolder(initPath) + } catch (error) { + logger.error('Import failed', error) + throw error + } + } + private async processFind(step: FindStep, vars: Record): Promise { const query = this.fillProps(step.query, vars) const res = await this.client.findOne(step._class, { ...(query as any) }) diff --git a/server/tool/src/plugin.ts b/server/tool/src/plugin.ts index 880560884d..3a3136caec 100644 --- a/server/tool/src/plugin.ts +++ b/server/tool/src/plugin.ts @@ -11,7 +11,7 @@ export const toolId = 'tool' as Plugin const toolPlugin = plugin(toolId, { metadata: { InitWorkspace: '' as Metadata, - InitScriptURL: '' as Metadata + InitRepoDir: '' as Metadata } }) diff --git a/server/workspace-service/src/index.ts b/server/workspace-service/src/index.ts index 438642aafb..92f35bfd46 100644 --- a/server/workspace-service/src/index.ts +++ b/server/workspace-service/src/index.ts @@ -82,12 +82,11 @@ export function serveWorkspaceAccount ( if (initWS !== undefined) { setMetadata(toolPlugin.metadata.InitWorkspace, initWS) } - const initScriptUrl = process.env.INIT_SCRIPT_URL - if (initScriptUrl !== undefined) { - setMetadata(toolPlugin.metadata.InitScriptURL, initScriptUrl) - } - setMetadata(serverClientPlugin.metadata.UserAgent, 'WorkspaceService') + const initRepoDir = process.env.INIT_REPO_DIR ?? './init-scripts' + setMetadata(toolPlugin.metadata.InitRepoDir, initRepoDir) + + setMetadata(serverClientPlugin.metadata.UserAgent, 'WorkspaceService') setMetadata(serverNotification.metadata.InboxOnlyNotifications, true) let canceled = false From f265242c6fc8576950f1be2faf0dcdbce2eb2268 Mon Sep 17 00:00:00 2001 From: Kristina Date: Thu, 12 Dec 2024 15:31:26 +0400 Subject: [PATCH 2/5] Fix chat red dots (#7440) Signed-off-by: Kristina Fefelova --- .../chunter-resources/src/channelDataProvider.ts | 3 ++- .../components/ReverseChannelScrollView.svelte | 16 +++------------- plugins/chunter-resources/src/scroll.ts | 6 +++--- plugins/chunter-resources/src/utils.ts | 8 +++++--- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts index 31fc5c95fc..47ae48ca5f 100644 --- a/plugins/chunter-resources/src/channelDataProvider.ts +++ b/plugins/chunter-resources/src/channelDataProvider.ts @@ -216,7 +216,8 @@ export class ChannelDataProvider implements IChannelDataProvider { } async updateNewTimestamp (context?: DocNotifyContext): Promise { - this.context = context ?? this.context + if (context === undefined) return + this.context = context const firstNewMsgIndex = await this.getFirstNewMsgIndex() const metadata = get(this.metadataStore) this.newTimestampStore.set(firstNewMsgIndex !== undefined ? metadata[firstNewMsgIndex]?.createdOn : undefined) diff --git a/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte b/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte index 9863ceee4d..89908ba69e 100644 --- a/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte +++ b/plugins/chunter-resources/src/components/ReverseChannelScrollView.svelte @@ -13,17 +13,7 @@ // limitations under the License. -->