Merge branch 'develop' of https://github.com/hcengineering/platform into win-support

This commit is contained in:
Artem Savchenko
2026-02-26 12:30:51 +07:00
430 changed files with 12723 additions and 3127 deletions
@@ -0,0 +1,22 @@
# Reusable action: set all @hcengineering package versions from a v tag ref
# (e.g. refs/tags/v0.7.370 → 0.7.370) using common/scripts/bump.js.
# Call only when github.ref is a v* tag (e.g. if: startsWith(github.ref, 'refs/tags/v')).
name: 'Set package versions'
description: 'Set all @hcengineering package versions from a v tag ref using bump.js'
inputs:
ref:
description: 'Git ref for the tag (e.g. github.ref, e.g. refs/tags/v0.7.370)'
required: true
outputs:
version:
description: 'Semver version derived from the tag (e.g. 0.7.370)'
runs:
using: 'composite'
steps:
- id: bump
run: |
VERSION="${INPUT_REF#refs/tags/v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_ENV
node common/scripts/bump.js "$VERSION"
shell: bash
+65 -5
View File
@@ -2,6 +2,9 @@
name: CI
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
@@ -19,6 +22,16 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
ref:
description: 'Ref to run as (e.g. refs/tags/v0.7.370 to test release flow)'
required: false
default: 'refs/heads/develop'
skip_publish:
description: 'Skip all publishing (npm, Docker, R2) when testing release flow'
required: false
default: false
type: boolean
env:
CacheFolders: |
@@ -57,12 +70,18 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.ref || github.ref }}
fetch-depth: 0
filter: tree:0
submodules: recursive
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Set package versions
if: startsWith(github.event.inputs.ref || github.ref, 'refs/tags/v')
uses: ./.github/actions/set-package-versions
with:
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Cache node modules
uses: actions/cache@v5
env:
@@ -74,6 +93,15 @@ jobs:
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Installing...
run: |
REF="${{ github.event.inputs.ref || github.ref }}"
if [[ "$REF" == refs/tags/v* ]]; then
node common/scripts/install-run-rush.js update
else
node common/scripts/install-run-rush.js install
fi
# - name: Cheking model is updated...
# run: node common/scripts/check_model_version.js
@@ -83,9 +111,6 @@ jobs:
- name: Checking for mis-matching transitive dependencies...
run: node common/scripts/check-versions.js
- name: Installing...
run: node common/scripts/install-run-rush.js install
- name: Model version from git tags
run: node common/scripts/install-run-rush.js model-version
@@ -698,12 +723,12 @@ jobs:
# if: ${{ github.ref == 'refs/heads/main' }}
# run: node common/scripts/install-run-rush.js docker:staging -v
- name: Docker push tag
if: ${{ startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }}
if: ${{ (startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s')) && (github.event_name != 'workflow_dispatch' || github.event.inputs.skip_publish != 'true') }}
run: |
echo Pushing release of tag ${{ github.ref }}
node common/scripts/install-run-rush.js docker:push -v
- name: Docker push love-agent
if: ${{ startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s') }}
if: ${{ (startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/tags/s')) && (github.event_name != 'workflow_dispatch' || github.event.inputs.skip_publish != 'true') }}
run: |
echo Pushing love-agent release of tag ${{ github.ref }}
cd ./services/ai-bot/love-agent
@@ -782,6 +807,7 @@ jobs:
node ../common/scripts/install-run-rushx.js dist-signed --macos --x64 --arm64
./scripts/copy-publish-artifacts.sh ${{ env.PublishTempFolder}}
- name: Publish distribution assets and version
if: github.event_name != 'workflow_dispatch' || github.event.inputs.skip_publish != 'true'
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.R2_ACCOUNT_ID }}
@@ -884,6 +910,7 @@ jobs:
node ../common/scripts/install-run-rushx.js dist-signed --macos --x64 --arm64
./scripts/copy-publish-artifacts.sh ${{ env.PublishTempFolder}}
- name: Publish distribution assets and version
if: github.event_name != 'workflow_dispatch' || github.event.inputs.skip_publish != 'true'
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.R2_ACCOUNT_ID }}
@@ -912,3 +939,36 @@ jobs:
with:
name: TraceX-Linux
path: ./qms-desktop-package/deploy/TraceX-linux-*.zip
publish-npm:
if: startsWith(github.event.inputs.ref || github.ref, 'refs/tags/v') && (github.event_name != 'workflow_dispatch' || github.event.inputs.skip_publish != 'true')
needs: build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.ref || github.ref }}
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Cache node modules
uses: actions/cache@v5
env:
cache-name: cache-node-platform
with:
path: common/temp
key: ${{ runner.os }}-build-cache-node-platform-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-build-cache-node-platform-
- name: Installing...
run: node common/scripts/install-run-rush.js install
- name: Publish to npm
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
REF="${{ github.event.inputs.ref || github.ref }}"
VERSION="${REF#refs/tags/v}"
VERSION="${VERSION#v}"
node common/scripts/bump.js "$VERSION"
node common/scripts/safe-publish.js
+2
View File
@@ -196,6 +196,8 @@ rush docker:up # Will set up all the containers
Be aware `rush docker:build` will automatically execute all required phases like build, bundle, package.
> **Note:** For resource-constrained machines, you can use the minified variants `rush docker:min` and `rush docker:up:min` to build and run only the required services (excludes hulypulse, redis, process, backup, rating, preview, link-preview, elastic, fulltext, payment, stats, print, sign, hulygun, hulykvs).
Alternatively, you can just execute:
```bash
+10
View File
@@ -2,6 +2,16 @@
Changelog.
## [0.7.375] - 2026-02-23
* 🚀 FEATURES: · Add activity section in controlled doc ([#10457](https://github.com/hcengineering/platform/issues/10457)) · Add data converter plugin ([#10460](https://github.com/hcengineering/platform/issues/10460)) · *(ui)* Add Turkish language to settings popup ([#10441](https://github.com/hcengineering/platform/issues/10441)) · Turkish language in dev setup ([#10462](https://github.com/hcengineering/platform/issues/10462)) · Add import notifications ([#10464](https://github.com/hcengineering/platform/issues/10464)) · Guest users to upload attachments and use reactions ([#10469](https://github.com/hcengineering/platform/issues/10469)) · Add activity input for controlled docs ([#10470](https://github.com/hcengineering/platform/issues/10470)) · Ability to select multiple docs and to export to pdf ([#10468](https://github.com/hcengineering/platform/issues/10468)) · Headers legibility ([#10473](https://github.com/hcengineering/platform/issues/10473)) · Brazilian Portuguese translation ([#10478](https://github.com/hcengineering/platform/issues/10478)) · Add drive permissions ([#10489](https://github.com/hcengineering/platform/issues/10489)) · Add unlock card and section functionality with UI updates ([#10492](https://github.com/hcengineering/platform/issues/10492)) · To define different viewlet actions ([#10502](https://github.com/hcengineering/platform/issues/10502)) · Add ability to copy all data from cards/docs tables ([#10505](https://github.com/hcengineering/platform/issues/10505)) · Redesign workspace join ([#10507](https://github.com/hcengineering/platform/issues/10507)) · For guests to update notification settings ([#10517](https://github.com/hcengineering/platform/issues/10517)) · Add project breadcrumb ([#10515](https://github.com/hcengineering/platform/issues/10515)) · Implement process import/export functionality, enhance process … ([#10528](https://github.com/hcengineering/platform/issues/10528)) · Implement CancelSubProcess action, fix process execution flow. ([#10530](https://github.com/hcengineering/platform/issues/10530)) · Add ability to group by issues by project ([#10531](https://github.com/hcengineering/platform/issues/10531))
* 🐛 BUG FIXES: · Okit.graphql is not a function ([#10442](https://github.com/hcengineering/platform/issues/10442)) · Custom attributes in markdown table ([#10445](https://github.com/hcengineering/platform/issues/10445)) · Relationship table diff/refresh ([#10446](https://github.com/hcengineering/platform/issues/10446)) · Handle youtu.be links ([#10458](https://github.com/hcengineering/platform/issues/10458)) · Calendar timezone day-shift bug ([#7048](https://github.com/hcengineering/platform/issues/7048)) ([#10459](https://github.com/hcengineering/platform/issues/10459)) · Mute account unauthorized error ([#10461](https://github.com/hcengineering/platform/issues/10461)) · Do not write analytics event when no token ([#10463](https://github.com/hcengineering/platform/issues/10463)) · Documents space export ([#10465](https://github.com/hcengineering/platform/issues/10465)) · Print layout ([#10472](https://github.com/hcengineering/platform/issues/10472)) · Use the error logging level for expected situations ([#10474](https://github.com/hcengineering/platform/issues/10474)) · Allow to open mermaid diagram in fullscreen ([#10477](https://github.com/hcengineering/platform/issues/10477)) · Rows order in relationsheep table compare ([#10479](https://github.com/hcengineering/platform/issues/10479)) · Custom attributes display in original view ([#10480](https://github.com/hcengineering/platform/issues/10480)) · Card formatting in markdown ([#10475](https://github.com/hcengineering/platform/issues/10475)) · Proper mermaid diagram size ([#10487](https://github.com/hcengineering/platform/issues/10487)) · Embed pdf via direct link ([#10491](https://github.com/hcengineering/platform/issues/10491)) · Secure blobs ([#10490](https://github.com/hcengineering/platform/issues/10490)) · Relationship table refresh ([#10494](https://github.com/hcengineering/platform/issues/10494)) · Issues layout ([#10498](https://github.com/hcengineering/platform/issues/10498)) · Exception during github issue id reading ([#10500](https://github.com/hcengineering/platform/issues/10500)) · Email notifications for export ([#10506](https://github.com/hcengineering/platform/issues/10506)) · Prevent datalake from error flood ([#10521](https://github.com/hcengineering/platform/issues/10521)) · Brazilian Portuguese translation ([#10522](https://github.com/hcengineering/platform/issues/10522)) · Datalake in dev setup does not support secure mode ([#10525](https://github.com/hcengineering/platform/issues/10525)) · Date formatting in md table ([#10523](https://github.com/hcengineering/platform/issues/10523)) · Bump with foundation packages ([#10536](https://github.com/hcengineering/platform/issues/10536)) · Process ([#10543](https://github.com/hcengineering/platform/issues/10543))
* 🚜 REFACTOR: · One metadata interface ([#10444](https://github.com/hcengineering/platform/issues/10444))
* ⚙️ MISCELLANEOUS TASKS: · Changelog · Changelog ([#10439](https://github.com/hcengineering/platform/issues/10439)) · Mail URL from export pod ([#10476](https://github.com/hcengineering/platform/issues/10476)) · Etc/hosts path for Windows ([#10482](https://github.com/hcengineering/platform/issues/10482)) · Space security and permissions ([#10529](https://github.com/hcengineering/platform/issues/10529)) · Kvs and pulse versions ([#10539](https://github.com/hcengineering/platform/issues/10539)) · And publish npm packages for v tags ([#10542](https://github.com/hcengineering/platform/issues/10542))
* FIX: · Images not rendering on GitHub side of GitHub integration ([#10520](https://github.com/hcengineering/platform/issues/10520)) · Show print actions for individual documents ([#10484](https://github.com/hcengineering/platform/issues/10484)) · Password signup crashes with JSON parse error when MAIL_URL is configured ([#10519](https://github.com/hcengineering/platform/issues/10519))
* QFIX: · Allow notifications for guests with collab security ([#10510](https://github.com/hcengineering/platform/issues/10510))
* QFIX: · Remove unused ui dependencies ([#10511](https://github.com/hcengineering/platform/issues/10511))
## [0.7.353] - 2026-01-24
* 🚀 FEATURES: · Link preview service ([#10424](https://github.com/hcengineering/platform/issues/10424)) · Add confirmation modal for table refresh ([#10428](https://github.com/hcengineering/platform/issues/10428)) · Guest update profile (avatar, name etc) ([#10429](https://github.com/hcengineering/platform/issues/10429))
+17 -1
View File
@@ -243,7 +243,7 @@
"commandKind": "global",
"name": "docker",
"summary": "Build docker with platform",
"description": "use to build all docker containers required for platform",
"description": "Build all docker containers required for platform. Use --minified for resource-constrained dev (or rush docker:min).",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "node common/scripts/docker.js"
},
@@ -271,6 +271,22 @@
"safeForSimultaneousRushProcesses": true,
"shellCommand": "docker compose -f dev/docker-compose.yaml up -d --force-recreate transactor_cockroach"
},
{
"commandKind": "global",
"name": "docker:min",
"summary": "Build minified docker images for resource-constrained dev",
"description": "Build docker images excluding optional services (hulypulse, redis, process, backup, rating, preview, link-preview, elastic, fulltext, payment, stats, print, sign, hulygun, hulykvs). Use with rush docker:up:min",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "./common/scripts/docker.sh --minified"
},
{
"commandKind": "global",
"name": "docker:up:min",
"summary": "Up minified development stack",
"description": "Start minified docker compose (docker-compose.yaml + docker-compose.min.yaml) for resource-constrained machines",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "cd ./dev && docker compose -f docker-compose.yaml -f docker-compose.min.yaml up -d --force-recreate"
},
{
"commandKind": "global",
"name": "docker:local",
+1075 -1449
View File
File diff suppressed because it is too large Load Diff
+46 -16
View File
@@ -1,35 +1,51 @@
const fs = require('fs')
const path = require('path')
const execSync = require('child_process').execSync
const repo = '@hcengineering'
const packages = {}
const pathes = {}
const jsons = {}
const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim()
function fillPackages (config) {
for (const package of config.projects) {
if (!package.name.startsWith(repo)) continue
for (const project of config.projects) {
const packageName = project.name ?? project.packageName
if (typeof packageName !== 'string' || !packageName.startsWith(repo)) continue
const projectPath = project.path ?? project.projectFolder ?? path.relative(repoRoot, project.fullPath ?? '')
if (typeof projectPath !== 'string' || projectPath.length === 0) continue
const fullProjectPath = path.resolve(repoRoot, projectPath)
packages[package.name] = {
version: package.version,
path: package.path
packages[packageName] = {
version: project.version,
path: fullProjectPath
}
pathes[package.path] = package.name
pathes[fullProjectPath] = packageName
const file = package.path + '/package.json'
const raw = fs.readFileSync(file)
jsons[package.name] = JSON.parse(raw)
const file = path.join(fullProjectPath, 'package.json')
if (!fs.existsSync(file)) {
console.log('skip, package.json not found:', file)
continue
}
const raw = fs.readFileSync(file)
jsons[packageName] = JSON.parse(raw)
}
}
function bumpPackage (name, newVersion) {
const json = jsons[name]
if (json === undefined) return
json.version = newVersion
if (typeof json.dependencies === 'object') {
for (const [dependency] of Object.entries(json.dependencies)) {
const depTypes = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
for (const depType of depTypes) {
if (typeof json[depType] !== 'object') continue
for (const [dependency, currentVersion] of Object.entries(json[depType])) {
if (packages[dependency] !== undefined) {
json.dependencies[dependency] = `^${newVersion}`
json[depType][dependency] = String(currentVersion).startsWith('workspace:')
? `workspace:^${newVersion}`
: `^${newVersion}`
}
}
}
@@ -44,7 +60,7 @@ function publish (name) {
const package = packages[name]
try {
console.log('publishing', name)
execSync(`cd ${package.path} && npm publish && cd ../..`, { encoding: 'utf-8' })
execSync('npm publish', { encoding: 'utf-8', cwd: package.path })
} catch (err) {
console.log(err)
}
@@ -54,7 +70,7 @@ function fix (name) {
const package = packages[name]
try {
console.log('fixing', name)
execSync(`cd ${package.path} && npm pkg fix && cd ../..`, { encoding: 'utf-8' })
execSync('npm pkg fix', { encoding: 'utf-8', cwd: package.path })
} catch (err) {
console.log(err)
}
@@ -78,7 +94,20 @@ function main () {
console.log('bump version ...', version)
const config = JSON.parse(execSync('rush list -p --json', { encoding: 'utf-8' }))
const output = execSync('node common/scripts/install-run-rush.js list -p --json', { encoding: 'utf-8', cwd: repoRoot })
const lines = output.split('\n')
let jsonStart = -1
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('{')) {
jsonStart = i
break
}
}
if (jsonStart === -1) {
console.error('Could not find JSON output from rush list')
process.exit(1)
}
const config = JSON.parse(lines.slice(jsonStart).join('\n'))
fillPackages(config)
@@ -89,7 +118,8 @@ function main () {
for (const packageName of packageNames) {
const package = packages[packageName]
const file = package.path + '/package.json'
if (jsons[packageName] === undefined) continue
const file = path.join(package.path, 'package.json')
const res = JSON.stringify(jsons[packageName], undefined, 2)
fs.writeFileSync(file, res + '\n')
}
+62 -31
View File
@@ -1,31 +1,62 @@
rush docker:build -p 20 \
--to @hcengineering/pod-server \
--to @hcengineering/pod-front \
--to @hcengineering/prod \
--to @hcengineering/pod-account \
--to @hcengineering/pod-workspace \
--to @hcengineering/pod-collaborator \
--to @hcengineering/tool \
--to @hcengineering/pod-print \
--to @hcengineering/pod-sign \
--to @hcengineering/pod-analytics-collector \
--to @hcengineering/rekoni-service \
--to @hcengineering/pod-ai-bot \
--to @hcengineering/import-tool \
--to @hcengineering/pod-stats \
--to @hcengineering/pod-fulltext \
--to @hcengineering/pod-love \
--to @hcengineering/pod-mail \
--to @hcengineering/pod-datalake \
--to @hcengineering/pod-mail-worker \
--to @hcengineering/pod-export \
--to @hcengineering/pod-media \
--to @hcengineering/pod-preview \
--to @hcengineering/pod-link-preview \
--to @hcengineering/pod-external \
--to @hcengineering/pod-backup \
--to @hcengineering/backup-api-pod \
--to @hcengineering/pod-billing \
--to @hcengineering/pod-process \
--to @hcengineering/pod-rating \
--to @hcengineering/pod-payment
#!/bin/bash
# Supports minified mode for resource-constrained dev machines:
# rush docker --minified or rush docker:min
MINIFIED=false
for arg in "$@"; do
if [ "$arg" = "--minified" ]; then
MINIFIED=true
break
fi
done
if [ "$MINIFIED" = true ]; then
echo "Building minified docker images (excluding optional services)..."
rush docker:build -p 20 \
--to @hcengineering/pod-server \
--to @hcengineering/pod-front \
--to @hcengineering/prod \
--to @hcengineering/pod-account \
--to @hcengineering/pod-workspace \
--to @hcengineering/pod-collaborator \
--to @hcengineering/tool \
--to @hcengineering/pod-analytics-collector \
--to @hcengineering/rekoni-service \
--to @hcengineering/pod-datalake \
--to @hcengineering/pod-export \
--to @hcengineering/pod-media \
--to @hcengineering/pod-external
else
rush docker:build -p 20 \
--to @hcengineering/pod-server \
--to @hcengineering/pod-front \
--to @hcengineering/prod \
--to @hcengineering/pod-account \
--to @hcengineering/pod-workspace \
--to @hcengineering/pod-collaborator \
--to @hcengineering/tool \
--to @hcengineering/pod-print \
--to @hcengineering/pod-sign \
--to @hcengineering/pod-analytics-collector \
--to @hcengineering/rekoni-service \
--to @hcengineering/pod-ai-bot \
--to @hcengineering/import-tool \
--to @hcengineering/pod-stats \
--to @hcengineering/pod-fulltext \
--to @hcengineering/pod-love \
--to @hcengineering/pod-mail \
--to @hcengineering/pod-datalake \
--to @hcengineering/pod-mail-worker \
--to @hcengineering/pod-export \
--to @hcengineering/pod-media \
--to @hcengineering/pod-preview \
--to @hcengineering/pod-link-preview \
--to @hcengineering/pod-external \
--to @hcengineering/pod-backup \
--to @hcengineering/backup-api-pod \
--to @hcengineering/pod-billing \
--to @hcengineering/pod-process \
--to @hcengineering/pod-rating \
--to @hcengineering/pod-payment \
--to @hcengineering/pod-worker
fi
+18 -2
View File
@@ -43,8 +43,24 @@ function checkPackageExists(packageName, version) {
*/
function getPublishablePackages(includePattern) {
try {
const output = execSync('rush list -p --json', { encoding: 'utf-8' })
const config = JSON.parse(output)
const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim()
const output = execSync('node common/scripts/install-run-rush.js list -p --json', {
encoding: 'utf-8',
cwd: repoRoot
})
const lines = output.split('\n')
let jsonStart = -1
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('{')) {
jsonStart = i
break
}
}
if (jsonStart === -1) {
console.error('Could not find JSON output from rush list')
return []
}
const config = JSON.parse(lines.slice(jsonStart).join('\n'))
return config.projects.filter((project) => {
// Check if package should be published according to rush.json
+1 -1
View File
@@ -412,7 +412,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise<void
const languages =
myBranding.languages !== undefined && myBranding.languages !== ''
? myBranding.languages.split(',').map((l) => l.trim())
: ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja', 'tr']
: ['en', 'ru', 'es', 'pt', 'pt-br', 'zh', 'fr', 'cs', 'it', 'de', 'ja', 'tr']
setMetadata(uiPlugin.metadata.Languages, languages)
+47 -304
View File
@@ -1,326 +1,69 @@
# Min override: excludes optional services from min deployment.
# Use with: docker compose -f docker-compose.yaml -f docker-compose.min.yaml up
# (rush docker:up:min)
# Excluded: preview, link-preview, elastic, redis, stats, payment, fulltext_cockroach,
# print, sign, hulykvs, hulygun, hulypulse, process-service, backup-cockroach,
# backup-api, rating_cockroach
# Overrides below remove dependencies on excluded services so the min project validates.
services:
cockroach:
image: cockroachdb/cockroach:latest-v24.3
extra_hosts:
- 'huly.local:host-gateway'
ports:
- '26257:26257'
- '8089:8080'
command: start-single-node --insecure
volumes:
- cockroach_db:/cockroach/cockroach-data
restart: unless-stopped
redpanda:
image: docker.redpanda.com/redpandadata/redpanda:v24.3.6
extra_hosts:
- 'huly.local:host-gateway'
command:
- redpanda
- start
- --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092
- --advertise-kafka-addr internal://redpanda:9092,external://localhost:19092
- --pandaproxy-addr internal://0.0.0.0:8082,external://0.0.0.0:18082
- --advertise-pandaproxy-addr internal://redpanda:8082,external://localhost:18082
- --schema-registry-addr internal://0.0.0.0:8081,external://0.0.0.0:18081
- --rpc-addr redpanda:33145
- --advertise-rpc-addr redpanda:33145
- --mode dev-container
- --smp 1
- --default-log-level=info
container_name: redpanda
volumes:
- redpanda:/var/lib/redpanda/data
ports:
- 18081:18081
- 18082:18082
- 19092:19092
- 19644:9644
healthcheck:
test: ['CMD', 'rpk', 'cluster', 'info', '-X', 'user=superuser', '-X', 'pass=secretpassword']
interval: 10s
timeout: 5s
retries: 10
minio:
image: 'minio/minio'
command: server /data --address ":9000" --console-address ":9001"
extra_hosts:
- 'huly.local:host-gateway'
expose:
- 9000
- 9001
ports:
- 9000:9000
- 9001:9001
volumes:
- files:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
retries: 10
restart: unless-stopped
preview:
profiles: ["full"]
link-preview:
profiles: ["full"]
elastic:
image: 'elasticsearch:7.14.2'
expose:
- 9200
extra_hosts:
- 'huly.local:host-gateway'
volumes:
- elastic:/usr/share/elasticsearch/data
ports:
- 9200:9200
environment:
- ELASTICSEARCH_PORT_NUMBER=9200
- BITNAMI_DEBUG=true
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1024m -Xmx1024m
healthcheck:
interval: 20s
retries: 10
test: curl -s http://localhost:9200/_cluster/health | grep -vq '"status":"red"'
restart: unless-stopped
account:
image: hardcoreeng/account
extra_hosts:
- 'huly.local:host-gateway'
links:
- cockroach
- minio
- stats
ports:
- 3000:3000
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
- ACCOUNT_PORT=3000
- QUEUE_CONFIG=${QUEUE_CONFIG}
- SERVER_SECRET=secret
- ADMIN_EMAILS=admin,${PLATFORM_ADMIN_EMAILS}
- STATS_URL=http://huly.local:4900
- WORKSPACE_LIMIT_PER_USER=10000
- DB_URL=${DB_CR_URL}
# - DB_URL=${MONGO_URL}
# - DB_NS=account-2
# Pass only one region to disallow selection for new workspaces.
- REGION_INFO=cockroach|CockroachDB
- TRANSACTOR_URL=ws://huly.local:3333,ws://huly.local:3332;;cockroach,
- MAIL_URL=
- STORAGE_CONFIG=${STORAGE_CONFIG}
- FRONT_URL=http://huly.local:8087
- MODEL_ENABLED=*
- LAST_NAME_FIRST=true
# - WS_LIVENESS_DAYS=1
- ACCOUNTS_URL=http://huly.local:3000
- BRANDING_PATH=/var/cfg/branding.json
# - DISABLE_SIGNUP=true
restart: unless-stopped
profiles: ["full"]
redis:
profiles: ["full"]
stats:
image: hardcoreeng/stats
extra_hosts:
- 'huly.local:host-gateway'
ports:
- 4900:4900
environment:
- PORT=4900
- SERVER_SECRET=secret
restart: unless-stopped
profiles: ["full"]
payment:
image: hardcoreeng/payment
extra_hosts:
- 'huly.local:host-gateway'
ports:
- 3040:3040
environment:
- SECRET=secret
- PORT=3040
- ACCOUNTS_URL=http://huly.local:3000
- FRONT_URL=http://huly.local:8087
- USE_SANDBOX=true
- POLAR_ACCESS_TOKEN=${POLAR_ACCESS_TOKEN}
- POLAR_WEBHOOK_SECRET=${POLAR_WEBHOOK_SECRET}
- POLAR_SUBSCRIPTION_PLANS=${POLAR_SUBSCRIPTION_PLANS}
restart: unless-stopped
profiles: ["full"]
fulltext_cockroach:
profiles: ["full"]
print:
profiles: ["full"]
sign:
profiles: ["full"]
hulykvs:
profiles: ["full"]
hulygun:
profiles: ["full"]
hulypulse:
profiles: ["full"]
process-service:
profiles: ["full"]
backup-cockroach:
profiles: ["full"]
backup-api:
profiles: ["full"]
rating_cockroach:
profiles: ["full"]
account:
links: !override []
workspace_cockroach:
image: hardcoreeng/workspace
extra_hosts:
- 'huly.local:host-gateway'
links:
links: !override
- cockroach
- minio
- stats
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
- WS_OPERATION=all+backup
- REGION=cockroach
- SERVER_SECRET=secret
- QUEUE_CONFIG=${QUEUE_CONFIG}
- DB_URL=${DB_CR_URL}
- STATS_URL=http://huly.local:4900
- STORAGE_CONFIG=${STORAGE_CONFIG}
- MODEL_ENABLED=*
- ACCOUNTS_URL=http://huly.local:3000
- ACCOUNTS_DB_URL=${DB_CR_URL}
- BRANDING_PATH=/var/cfg/branding.json
- BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG}
- BACKUP_BUCKET=${BACKUP_BUCKET_NAME}
- MAIL_URL=
- INIT_WORKSPACE=staging-dev
restart: unless-stopped
collaborator:
image: hardcoreeng/collaborator
extra_hosts:
- 'huly.local:host-gateway'
links:
- cockroach
links: !override
- minio
- transactor_cockroach
- stats
ports:
- 3078:3078
environment:
- COLLABORATOR_PORT=3078
- SECRET=secret
- ACCOUNTS_URL=http://huly.local:3000
- STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://huly.local:4900
restart: unless-stopped
front:
image: hardcoreeng/front
extra_hosts:
- 'huly.local:host-gateway'
links:
- cockroach
- minio
- elastic
- transactor_cockroach
- collaborator
- stats
ports:
- 8087:8080
- 8088:8080
environment:
- SERVER_PORT=8080
- SERVER_SECRET=secret
- ACCOUNTS_URL=http://huly.local:3000
- STATS_URL=http://huly.local:4900
- UPLOAD_URL=/files
- GMAIL_URL=http://huly.local:8088
- CALENDAR_URL=http://huly.local:8095
- TELEGRAM_URL=http://huly.local:8086
- REKONI_URL=http://huly.local:4004
- COLLABORATOR_URL=ws://huly.local:3078
- STORAGE_CONFIG=${STORAGE_CONFIG}
- GITHUB_URL=http://huly.local:3500
- PRINT_URL=http://huly.local:4005
- SIGN_URL=http://huly.local:4006
# - ANALYTICS_COLLECTOR_URL=http://huly.local:4017
- DESKTOP_UPDATES_URL=https://dist.huly.io
- DESKTOP_UPDATES_CHANNEL=dev
- DESKTOP_UPDATES_CHANNELS=dev;tracex:dev-tracex
- BRANDING_URL=http://huly.local:8087/branding.json
- STREAM_URL=http://huly.local:1080/recording
- PAYMENT_URL=http://huly.local:3040
# - DISABLE_SIGNUP=true
restart: unless-stopped
links: !override []
transactor_cockroach:
image: hardcoreeng/transactor
extra_hosts:
- 'huly.local:host-gateway'
links:
links: !override
- cockroach
- minio
- account
- stats
ports:
- 3332:3332
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
- QUEUE_CONFIG=${QUEUE_CONFIG}
- SERVER_PORT=3332
- REGION=cockroach
- SERVER_SECRET=secret
- ENABLE_COMPRESSION=true
- FULLTEXT_URL=http://huly.local:4702
- STATS_URL=http://huly.local:4900
- DB_URL=${DB_CR_URL}
- METRICS_CONSOLE=false
- METRICS_FILE=metrics.txt
- STORAGE_CONFIG=${STORAGE_CONFIG}
- FRONT_URL=http://huly.local:8087
- MAIL_URL=''
- ACCOUNTS_URL=http://huly.local:3000
- LAST_NAME_FIRST=true
- BRANDING_PATH=/var/cfg/branding.json
- AI_BOT_URL=http://huly.local:4010
- COMMUNICATION_TIME_LOGGING_ENABLED=true
restart: unless-stopped
fulltext_cockroach:
image: hardcoreeng/fulltext
extra_hosts:
- 'huly.local:host-gateway'
restart: unless-stopped
links:
- elastic
- cockroach
ports:
- 4702:4702
environment:
- PORT=4702
- REGION=cockroach
- SERVER_SECRET=secret
- QUEUE_CONFIG=${QUEUE_CONFIG}
- DB_URL=${DB_CR_URL}
- FULLTEXT_DB_URL=http://huly.local:9200
- ELASTIC_INDEX_NAME=local_storage_index # Same index for simplicity
- STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://huly.local:4900
- REKONI_URL=http://huly.local:4004
- ACCOUNTS_URL=http://huly.local:3000
export:
links: !override
- minio
datalake:
image: hardcoreeng/datalake
extra_hosts:
- 'huly.local:host-gateway'
depends_on:
depends_on: !override
minio:
condition: service_healthy
cockroach:
condition: service_started
stats:
condition: service_started
account:
condition: service_started
ports:
- 4030:4030
environment:
- PORT=4030
- SECRET=secret
- ACCOUNTS_URL=http://huly.local:3000
- STATS_URL=http://huly.local:4900
- STREAM_URL=http://huly.local:1080/recording
- DB_URL=${DB_CR_URL}
- BUCKETS=blobs,eu|http://minio:9000?accessKey=minioadmin&secretKey=minioadmin
restart: unless-stopped
redis:
image: redis:8.0.2-alpine3.21
ports:
- 6379:6379
restart: unless-stopped
hulypulse:
image: hardcoreeng/hulypulse
depends_on:
redis:
condition: service_started
ports:
- 8099:8099
environment:
- HULY_REDIS_URLS=redis://redis:6379
- HULY_BIND_PORT=8099
restart: unless-stopped
volumes:
db:
dbpg:
files:
elastic:
cockroach_db:
redpanda:
+17 -1
View File
@@ -79,7 +79,7 @@ services:
- --mode dev-container
- --smp 1
- --default-log-level=info
- --memory 256M
- --memory 512M
container_name: redpanda
volumes:
- redpanda:/var/lib/redpanda/data
@@ -594,6 +594,22 @@ services:
- QUEUE_CONFIG=${QUEUE_CONFIG}
- QUEUE_REGION=cockroach
restart: unless-stopped
time-machine:
image: hardcoreeng/worker
extra_hosts:
- 'huly.local:host-gateway'
depends_on:
redpanda:
condition: service_started
account:
condition: service_started
cockroach:
condition: service_started
environment:
- DB_URL=${DB_CR_URL}
- QUEUE_CONFIG=${QUEUE_CONFIG}
- QUEUE_REGION=cockroach
restart: unless-stopped
# translate:
# image: hardcoreeng/translate
# extra_hosts:
+2 -2
View File
@@ -1,7 +1,7 @@
{
"huly.local:8080": {
"title": "Huly",
"languages": "en,ru,pt,es,zh,fr,de,ja,tr",
"languages": "en,ru,pt,pt-br,es,zh,fr,de,ja,tr",
"defaultLanguage": "en",
"defaultApplication": "tracker",
"defaultSpace": "tracker:project:DefaultProject",
@@ -30,7 +30,7 @@
},
"huly.local:8087": {
"title": "Huly",
"languages": "en,ru,pt,es,zh,fr,de,ja,tr",
"languages": "en,ru,pt,pt-br,es,zh,fr,de,ja,tr",
"defaultLanguage": "en",
"defaultApplication": "tracker",
"defaultSpace": "tracker:project:DefaultProject",
+1 -1
View File
@@ -543,7 +543,7 @@ export async function configurePlatform() {
const languages = myBranding.languages
? myBranding.languages.split(',').map((l) => l.trim())
: ['en', 'ru', 'es', 'pt', 'zh', 'fr', 'cs', 'it', 'de', 'ja', 'tr']
: ['en', 'ru', 'es', 'pt', 'pt-br', 'zh', 'fr', 'cs', 'it', 'de', 'ja', 'tr']
setMetadata(uiPlugin.metadata.Languages, languages)
@@ -42,6 +42,7 @@ import type {
LoginInfo,
LoginInfoByToken,
LoginInfoRequestData,
InviteInfo,
LoginInfoWithWorkspaces,
MailboxInfo,
MailboxOptions,
@@ -124,7 +125,9 @@ export interface AccountClient {
}
) => Promise<string>
checkJoin: (inviteId: string) => Promise<WorkspaceLoginInfo>
joinByToken: (inviteId: string) => Promise<WorkspaceLoginInfo>
checkAutoJoin: (inviteId: string, firstName?: string, lastName?: string) => Promise<WorkspaceLoginInfo>
getInviteInfo: (inviteId: string) => Promise<InviteInfo>
getWorkspaceInfo: (updateLastVisit?: boolean) => Promise<WorkspaceInfoWithStatus>
getWorkspacesInfo: (workspaces: WorkspaceUuid[]) => Promise<WorkspaceInfoWithStatus[]>
updateLastVisit: (workspaces: WorkspaceUuid[]) => Promise<void>
@@ -577,6 +580,15 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async joinByToken (inviteId: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'joinByToken' as const,
params: { inviteId }
}
return await this.rpc(request)
}
async checkAutoJoin (inviteId: string, firstName?: string, lastName?: string): Promise<WorkspaceLoginInfo> {
const request = {
method: 'checkAutoJoin' as const,
@@ -586,6 +598,15 @@ class AccountClientImpl implements AccountClient {
return await this.rpc(request)
}
async getInviteInfo (inviteId: string): Promise<InviteInfo> {
const request = {
method: 'getInviteInfo' as const,
params: { inviteId }
}
return await this.rpc(request)
}
async getWorkspacesInfo (ids: WorkspaceUuid[]): Promise<WorkspaceInfoWithStatus[]> {
const request = {
method: 'getWorkspacesInfo' as const,
@@ -80,6 +80,11 @@ export interface WorkspaceInviteInfo {
name?: string
}
/** Public invite details from getInviteInfo (no auth required). */
export interface InviteInfo {
workspaceName: string | null
}
export interface OtpInfo {
sent: boolean
retryOn: Timestamp
@@ -0,0 +1,68 @@
{
"string": {
"Id": "Id",
"Space": "Espaço",
"Spaces": "Espaços",
"SpacesDescription": "Gerenciamento do tipo de espaço para todos os espaços",
"Modified": "Modificado",
"ModifiedDate": "Data de modificação",
"ModifiedBy": "Modificado por",
"Class": "Classe",
"AttachedTo": "Anexado à",
"AttachedToClass": "Classe anexada",
"Name": "Nome",
"Description": "Descrição",
"Private": "Privado",
"Archived": "Arquivado",
"ClassLabel": "Tipo",
"ClassPropertyLabel": "Legenda",
"String": "Texto",
"Markup": "Marcação",
"Number": "Número",
"Boolean": "Caixa de seleção",
"Timestamp": "Marca de data/hora",
"Date": "Data",
"IntlString": "Texto internacional",
"Ref": "Referência",
"Collection": "Coleção",
"Array": "Seleção múltipla",
"Enum": "Seleção",
"Members": "Membros",
"Hyperlink": "URL",
"MarkupBlobRef": "Colaborativo",
"Object": "Objeto",
"System": "Sistema",
"CreatedBy": "Criado por",
"CreatedDate": "Data de criação",
"Status": "Estado",
"StatusCategory": "Categoria do estado",
"Account": "Conta",
"Rank": "Ranking",
"Owners": "Proprietários",
"Permission": "Permissão",
"CreateObject": "Criar objeto",
"UpdateObject": "Atualizar objeto",
"DeleteObject": "Apagar objeto",
"ForbidDeleteObject": "Proibir apagar objeto",
"UpdateSpace": "Atualizar espaço",
"ArchiveSpace": "Arquivar espaço",
"CreateObjectDescription": "Concede aos usuários a capacidade de criar objetos no espaço",
"UpdateObjectDescription": "Concede aos usuários a capacidade de atualizar objetos no espaço",
"DeleteObjectDescription": "Concede aos usuários a capacidade de apagar objetos no espaço",
"ForbidDeleteObjectDescription": "Proíbe aos usuários a capacidade de apagar objetos no espaço",
"UpdateSpaceDescription": "Concede aos usuários a capacidade de atualizar o espaço",
"ArchiveSpaceDescription": "Concede aos usuários a capacidade de arquivar o espaço",
"AutoJoin": "Auto adesão",
"AutoJoinDescr": "Adesão automática de novos funcionários a este espaço",
"RBAC": "Controle de acesso baseado em funções",
"RBACDescr": "Exigir acesso baseado em funções para executar ações neste espaço",
"BlobSize": "Tamanho",
"BlobContentType": "Tipo de conteúdo",
"Relation": "Relação",
"Relations": "Relações",
"AddRelation": "Adicionar relação",
"PersonId": "ID de pessoa",
"AccountId": "Conta",
"Version": "Versão"
}
}
@@ -947,6 +947,7 @@ export interface ClassCollaborators<T extends Doc> extends Doc {
allFields?: boolean // for all (PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]) attributes
fields: (keyof T)[] // PersonId | Ref<Employee> | PersonId[] | Ref<Employee>[]
provideSecurity?: boolean // If true, will provide security for collaborators
provideAttachedSecurity?: boolean // If true, will provide security for collaborators of attached doc
}
export interface Collaborator extends AttachedDoc {
@@ -249,6 +249,7 @@ export abstract class MemDb extends TxProcessor implements Storage {
// We need to filter instances without mixin was set
result = result.filter((r) => (r as any)[_class] !== undefined)
}
if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy, this)
const total = result.length
result = result.slice(0, options?.limit)
+4 -1
View File
@@ -436,7 +436,10 @@ export abstract class TxProcessor implements WithTx {
}
static txHasUpdate<T extends Doc>(tx: TxUpdateDoc<T>, attribute: string): boolean {
const ops = tx.operations
return TxProcessor.hasUpdate(tx.operations, attribute)
}
static hasUpdate<T extends Doc>(ops: DocumentUpdate<T>, attribute: string): boolean {
if ((ops as any)[attribute] !== undefined) return true
for (const op in ops) {
if (op.startsWith('$')) {
@@ -42,7 +42,7 @@ describe('performance', () => {
// Overhead should be reasonable (typically < 50% for simple operations)
// This is informational rather than a strict assertion
expect(overheadPercentage).toBeLessThan(200)
expect(overheadPercentage).toBeLessThan(400)
})
it('should measure overhead with deep nested contexts', async () => {
@@ -154,7 +154,7 @@ describe('performance', () => {
console.log(` Overhead: ${overhead.toFixed(2)}ms (${overheadPercentage.toFixed(2)}%)`)
// NoMetricsContext should have very low overhead
expect(overheadPercentage).toBeLessThan(50)
expect(overheadPercentage).toBeLessThan(100)
})
})
@@ -195,7 +195,7 @@ describe('performance', () => {
console.log(` Collected operations: ${aggregated.measurements.request?.operations ?? 0}`)
expect(aggregated.measurements.request).toBeDefined()
expect(overheadPercentage).toBeLessThan(100) // Should be less than 100% overhead
expect(overheadPercentage).toBeLessThan(200) // Should be less than 100% overhead
})
})
})
@@ -0,0 +1,31 @@
{
"status": {
"LoadingPlugin": "Carregando plugin {plugin}...",
"UnknownError": "Erro desconhecido: {message}",
"InvalidId": "Id inválido: {id}",
"BadRequest": "Pedido inválido",
"Forbidden": "Proibido",
"Conflict": "Conflito",
"ExpiredLink": "Este link de convite expirou",
"Unauthorized": "Não autorizado",
"UnknownMethod": "Método desconhecido: {method}",
"InternalServerError": "Erro interno do servidor",
"MaintenanceWarning": "Manutenção programada dentro de",
"MaintenanceWarningTime": "{time, plural, =1 {menos de um minuto} other {# minutos}}",
"AccountNotFound": "Conta não encontrada",
"AccountNotConfirmed": "Conta não confirmada",
"WorkspaceNotFound": "Espaço de trabalho não encontrado",
"InvalidPassword": "Senha inválida",
"AccountAlreadyExists": "Conta já existe",
"WorkspaceRateLimit": "O servidor está ocupado. Por favor, espere um pouco e tente novamente",
"AccountAlreadyConfirmed": "Conta já confirmada",
"WorkspaceAlreadyExists": "Espaço de trabalho já existe",
"InvalidOtp": "Código inválido",
"PasswordLoginLocked": "O login com senha está bloqueado devido a muitas tentativas incorretas. Use um método de login OTP para desbloquear sua conta.",
"InviteNotFound": "Convite com e-mail:{email} não encontrado.",
"WorkspaceLimitReached": "Você atingiu o limite de espaços de trabalho. Entre em contato conosco...",
"ReadOnlyAccount": "Demonstração anônima de convidado",
"SystemAccount": "Conta do sistema",
"SocialIdAlreadyExists": "ID social já existe"
}
}
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadMultipart, uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class DatalakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class DatalakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/blob/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
const url = concatLink(this.baseUrl, `/meta/${encodeURIComponent(workspace)}/${encodeURIComponent(file)}`)
try {
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class FrontStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class FrontStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -17,6 +17,11 @@ import { concatLink } from '@hcengineering/core'
import { FileStorage, FileStorageUploadOptions } from '../types'
import { uploadXhr } from '../upload'
const getPathname = (url: string): string => {
const base = window?.location?.href !== undefined ? window.location.href : 'http://localhost'
return new URL(url, base).pathname
}
/** @public */
export class HulylakeStorage implements FileStorage {
constructor (private readonly baseUrl: string) {}
@@ -26,6 +31,11 @@ export class HulylakeStorage implements FileStorage {
return concatLink(this.baseUrl, path)
}
getCookiePath (workspace: string): string {
const url = concatLink(this.baseUrl, `/api/${workspace}`)
return getPathname(url)
}
async getFileMeta (token: string, workspace: string, file: string): Promise<Record<string, any>> {
return {}
}
@@ -29,6 +29,7 @@ export interface FileStorageUploadOptions {
/** @public */
export interface FileStorage {
getFileUrl: (workspace: string, file: string, filename?: string) => string
getCookiePath: (workspace: string) => string
getFileMeta: (token: string, workspace: string, file: string) => Promise<Record<string, any>>
uploadFile: (
token: string,
+1 -1
View File
@@ -1175,7 +1175,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hulypulse"
version = "0.4.0"
version = "0.4.1"
dependencies = [
"actix-cors",
"actix-web",
+3 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "hulypulse"
version = "0.4.0"
version = "0.4.1"
edition = "2024"
[dependencies]
@@ -30,7 +30,7 @@ hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = [ "ac
secrecy = { version = "0.10.3", optional = true }
#redis
redis = { version = "=0.32.5", features = ["aio", "tokio-comp", "sentinel"], optional = true }
redis = { version = "=0.32.5", features = ["aio", "tokio-comp", "sentinel"] }
[[bin]]
name = "hulypulse"
@@ -43,7 +43,6 @@ tokio-tungstenite = { version = "0.21", default-features = false, features = [
] }
[features]
default = ["db-redis","auth"] # lopt
default = ["auth"] # lopt
auth = ["regorus", "uuid", "hulyrs", "secrecy"]
lopt = []
db-redis = ["redis"]
+15 -5
View File
@@ -190,15 +190,14 @@ Size of data is limited to some reasonable size
- `{"message":"Del","key":"00000000-0000-0000-0000-000000000001/foo/bar"}`
## Special options in config/default.toml
- ```memory_mode = true``` Use native memory storage instead Redis
- ```backend = "memory"``` Use native memory storage instead Redis
- ```max_size = 100``` Max value size in bytes
## Special cargo build options
- "db-redis" (default) - use Redis (Memory instead)
- "auth" (default) - use huly-authorization
Disable both:
Disable auth:
cargo build --no-default-features
Enable one:
Enable auth:
cargo build --no-default-features --features "auth"
## Running
@@ -207,7 +206,17 @@ Pre-build docker images is available at: hardcoreeng/service_hulypulse:{tag}.
You can use the following command to run the image locally:
```bash
docker run -p 8095:8095 -it --rm hardcoreeng/service_hulypulse:{tag}"
docker run -p 8095:8095 -it --rm hardcoreeng/service_hulypulse:{tag}
```
Run from source using Redis:
```bash
HULY_REDIS_URLS=redis://huly.local:6379 cargo run
```
Run from source in in-memory mode:
```bash
HULY_BACKEND=memory cargo run
```
If you want to run the service as a part of local huly development environment use the following command:
@@ -228,6 +237,7 @@ The following environment variables are used to configure hulypulse:
- ```HULY_BIND_HOST```: host to bind the server to (default: 0.0.0.0)
- ```HULY_BIND_PORT```: port to bind the server to (default: 8094)
- ```HULY_TOKEN_SECRET```: secret used to sign JWT tokens (default: secret)
- ```HULY_BACKEND```: storage backend "redis" or "memory" (default: "redis")
- ```HULY_REDIS_URLS```: redis connection string (default: redis://huly.local:6379)
- ```HULY_REDIS_PASSWORD```: redis password (default: "&lt;invalid&gt;")
- ```HULY_REDIS_MODE```: redis mode "direct" or "sentinel" (default: "direct")
+7 -8
View File
@@ -19,12 +19,9 @@ use std::{path::Path, sync::LazyLock};
use secrecy::SecretString;
use serde::Deserialize;
#[cfg(feature = "db-redis")]
use serde_with::StringWithSeparator;
#[cfg(feature = "db-redis")]
use serde_with::formats::CommaSeparator;
use serde_with::serde_as;
#[cfg(feature = "db-redis")]
use url::Url;
use config::FileFormat;
@@ -43,6 +40,10 @@ pub enum BackendType {
Redis,
}
fn default_backend() -> BackendType {
BackendType::Redis
}
#[serde_as]
#[derive(Deserialize, Debug)]
pub struct Config {
@@ -52,20 +53,18 @@ pub struct Config {
#[cfg(feature = "auth")]
pub token_secret: SecretString,
#[cfg(feature = "db-redis")]
#[serde(default = "default_backend")]
pub backend: BackendType,
#[serde_as(as = "StringWithSeparator::<CommaSeparator, url::Url>")]
pub redis_urls: Vec<Url>,
#[cfg(feature = "db-redis")]
pub redis_password: String,
#[cfg(feature = "db-redis")]
pub redis_mode: RedisMode,
#[cfg(feature = "db-redis")]
pub redis_service: String,
pub max_ttl: usize,
pub max_size: Option<usize>,
// pub backend: BackendType,
pub heartbeat_timeout: u64,
pub ping_timeout: u64,
@@ -3,6 +3,7 @@ bind_host = "0.0.0.0"
token_secret = "secret"
backend = "redis"
redis_urls = "redis://huly.local:6379"
redis_password = "<invalid>"
redis_mode = "direct"
+161 -125
View File
@@ -1,43 +1,38 @@
#[cfg(not(feature = "db-redis"))]
use std::sync::Arc;
#[cfg(not(feature = "db-redis"))]
use crate::hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event};
#[cfg(not(feature = "db-redis"))]
use crate::memory::{
MemoryBackend, memory_delete, memory_info, memory_list, memory_read, memory_save,
};
#[cfg(feature = "db-redis")]
use crate::redis::{redis_delete, redis_info, redis_list, redis_read, redis_save};
use redis::aio::MultiplexedConnection;
use serde::Serialize;
use tokio::sync::RwLock;
#[cfg(feature = "db-redis")]
use ::redis::aio::MultiplexedConnection;
#[cfg(feature = "db-redis")]
pub type DbError = redis::RedisError;
#[cfg(not(feature = "db-redis"))]
#[derive(Debug)]
pub struct DbError(pub String);
pub enum DbError {
Redis(redis::RedisError),
Message(String),
}
pub type DbResult<T> = Result<T, DbError>;
#[cfg(not(feature = "db-redis"))]
impl std::fmt::Display for DbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
match self {
Self::Redis(err) => write!(f, "{err}"),
Self::Message(msg) => write!(f, "{msg}"),
}
}
}
#[cfg(not(feature = "db-redis"))]
impl std::error::Error for DbError {}
#[cfg(not(feature = "db-redis"))]
use tokio::sync::RwLock;
use serde::Serialize;
impl From<redis::RedisError> for DbError {
fn from(value: redis::RedisError) -> Self {
Self::Redis(value)
}
}
#[derive(Debug, Serialize)]
pub struct DbArray {
@@ -61,33 +56,8 @@ pub enum SaveMode {
Equal(String), // only if md5 matches provided
}
/// return Error
// pub fn error<T>(code: u16, msg: impl Into<String>) -> DbResult<T> {
// let msg = msg.into();
// let full = format!("{}: {}", code, msg);
// Err(redis::RedisError::from((
// redis::ErrorKind::ExtensionError,
// "",
// full,
// )))
// }
pub fn error<T>(code: u16, msg: impl Into<String>) -> DbResult<T> {
let msg = format!("{}: {}", code, msg.into());
#[cfg(feature = "db-redis")]
{
return Err(redis::RedisError::from((
redis::ErrorKind::ExtensionError,
"",
msg,
)));
}
#[cfg(not(feature = "db-redis"))]
{
return Err(DbError(msg));
}
Err(DbError::Message(format!("{}: {}", code, msg.into())))
}
/// Check for redis-deprecated symbols
@@ -108,59 +78,67 @@ pub fn deprecated_symbol_error(s: &str) -> DbResult<()> {
}
}
#[derive(Clone)]
enum DbBackend {
Redis(MultiplexedConnection),
Memory {
db: MemoryBackend,
hub: Arc<RwLock<HubState>>,
},
}
#[derive(Clone)]
pub struct Db {
#[cfg(feature = "db-redis")]
db: MultiplexedConnection,
#[cfg(not(feature = "db-redis"))]
db: MemoryBackend,
#[cfg(not(feature = "db-redis"))]
hub: Arc<RwLock<HubState>>,
backend: DbBackend,
}
impl Db {
pub fn new_db(
#[cfg(not(feature = "db-redis"))] db: MemoryBackend,
#[cfg(feature = "db-redis")] db: MultiplexedConnection,
#[cfg(not(feature = "db-redis"))] hub: Arc<RwLock<HubState>>,
) -> Self {
pub fn new_redis(db: MultiplexedConnection) -> Self {
Self {
db,
#[cfg(not(feature = "db-redis"))]
hub,
backend: DbBackend::Redis(db),
}
}
pub fn new_memory(db: MemoryBackend, hub: Arc<RwLock<HubState>>) -> Self {
Self {
backend: DbBackend::Memory { db, hub },
}
}
pub fn mode(&self) -> &'static str {
match &self.backend {
DbBackend::Redis(_) => "redis",
DbBackend::Memory { .. } => "memory",
}
}
pub async fn info(&self) -> DbResult<String> {
#[cfg(not(feature = "db-redis"))]
return memory_info(&self.db).await;
#[cfg(feature = "db-redis")]
{
let mut c = self.db.clone();
redis_info(&mut c).await
match &self.backend {
DbBackend::Memory { db, .. } => memory_info(db).await,
DbBackend::Redis(conn) => {
let mut c = conn.clone();
redis_info(&mut c).await
}
}
}
pub async fn list(&self, key: &str) -> DbResult<Vec<DbArray>> {
#[cfg(not(feature = "db-redis"))]
return memory_list(&self.db, key).await;
#[cfg(feature = "db-redis")]
{
let mut c = self.db.clone();
redis_list(&mut c, key).await
match &self.backend {
DbBackend::Memory { db, .. } => memory_list(db, key).await,
DbBackend::Redis(conn) => {
let mut c = conn.clone();
redis_list(&mut c, key).await
}
}
}
pub async fn read(&self, key: &str) -> DbResult<Option<DbArray>> {
#[cfg(not(feature = "db-redis"))]
return memory_read(&self.db, key).await;
#[cfg(feature = "db-redis")]
{
let mut c = self.db.clone();
redis_read(&mut c, key).await
match &self.backend {
DbBackend::Memory { db, .. } => memory_read(db, key).await,
DbBackend::Redis(conn) => {
let mut c = conn.clone();
redis_read(&mut c, key).await
}
}
}
@@ -171,54 +149,112 @@ impl Db {
ttl: Option<Ttl>,
mode: Option<SaveMode>,
) -> DbResult<()> {
#[cfg(not(feature = "db-redis"))]
{
memory_save(&self.db, key, value.as_ref(), ttl, mode).await?;
// Send events
let value_str = std::str::from_utf8(value.as_ref())
.ok()
.map(|s| s.to_string());
broadcast_event(
&self.hub,
RedisEvent {
message: RedisEventAction::Set,
key: key.to_string(),
},
value_str,
)
.await;
return Ok(());
}
#[cfg(feature = "db-redis")]
{
let mut c = self.db.clone();
redis_save(&mut c, key, value.as_ref(), ttl, mode).await
match &self.backend {
DbBackend::Memory { db, hub } => {
memory_save(db, key, value.as_ref(), ttl, mode).await?;
let value_str = std::str::from_utf8(value.as_ref())
.ok()
.map(|s| s.to_string());
broadcast_event(
hub,
RedisEvent {
message: RedisEventAction::Set,
key: key.to_string(),
},
value_str,
)
.await;
Ok(())
}
DbBackend::Redis(conn) => {
let mut c = conn.clone();
redis_save(&mut c, key, value.as_ref(), ttl, mode).await
}
}
}
pub async fn delete(&self, key: &str, mode: Option<SaveMode>) -> DbResult<bool> {
#[cfg(not(feature = "db-redis"))]
{
let deleted = memory_delete(&self.db, key, mode).await?;
if deleted {
broadcast_event(
&self.hub,
RedisEvent {
message: RedisEventAction::Del,
key: key.to_string(),
},
None,
)
.await;
match &self.backend {
DbBackend::Memory { db, hub } => {
let deleted = memory_delete(db, key, mode).await?;
if deleted {
broadcast_event(
hub,
RedisEvent {
message: RedisEventAction::Del,
key: key.to_string(),
},
None,
)
.await;
}
Ok(deleted)
}
DbBackend::Redis(conn) => {
let mut c = conn.clone();
redis_delete(&mut c, key, mode).await
}
return Ok(deleted);
}
#[cfg(feature = "db-redis")]
{
let mut c = self.db.clone();
redis_delete(&mut c, key, mode).await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hub_service::HubState;
use crate::memory::MemoryBackend;
use std::sync::Arc;
use tokio::sync::RwLock;
fn memory_db() -> Db {
let hub = Arc::new(RwLock::new(HubState::default()));
let backend = MemoryBackend::new();
Db::new_memory(backend, hub)
}
#[tokio::test]
async fn memory_db_mode_and_crud_work() {
let db = memory_db();
assert_eq!(db.mode(), "memory");
db.save("workspace/tests/key1", b"hello", Some(Ttl::Sec(60)), None)
.await
.expect("save should succeed");
let item = db
.read("workspace/tests/key1")
.await
.expect("read should succeed")
.expect("key should exist");
assert_eq!(item.data, "hello");
let list = db
.list("workspace/tests/")
.await
.expect("list should succeed");
assert_eq!(list.len(), 1);
assert_eq!(list[0].key, "workspace/tests/key1");
let deleted = db
.delete("workspace/tests/key1", None)
.await
.expect("delete should succeed");
assert!(deleted);
assert!(
db.read("workspace/tests/key1")
.await
.expect("read should succeed")
.is_none()
);
}
#[tokio::test]
async fn memory_db_status_reports_memory_backend() {
let hub = Arc::new(RwLock::new(HubState::default()));
let db = Db::new_memory(MemoryBackend::new(), hub.clone());
let info = hub.read().await.info_json(&db).await;
assert_eq!(info["backend"], "memory");
assert_eq!(info["status"], "OK");
}
}
+13 -11
View File
@@ -35,17 +35,19 @@ use crate::workspace_owner::test_rego_http;
pub fn map_redis_error(err: impl std::fmt::Display) -> Error {
let msg = err.to_string();
if let Some(detail) = msg.split(" - ExtensionError: ").nth(1) {
if let Some((code, text)) = detail.split_once(": ") {
let text = format!("{} {}", code, text);
return match code {
"400" => actix_web::error::ErrorBadRequest(text),
"404" => actix_web::error::ErrorNotFound(text),
"412" => actix_web::error::ErrorPreconditionFailed(text),
"500" => actix_web::error::ErrorInternalServerError(text),
_ => actix_web::error::ErrorInternalServerError("unexpected error"),
};
}
let detail = msg
.split(" - ExtensionError: ")
.nth(1)
.unwrap_or(msg.as_str());
if let Some((code, text)) = detail.split_once(": ") {
let text = format!("{} {}", code, text);
return match code {
"400" => actix_web::error::ErrorBadRequest(text),
"404" => actix_web::error::ErrorNotFound(text),
"412" => actix_web::error::ErrorPreconditionFailed(text),
"500" => actix_web::error::ErrorInternalServerError(text),
_ => actix_web::error::ErrorInternalServerError("unexpected error"),
};
}
actix_web::error::ErrorInternalServerError("internal error")
}
+2 -4
View File
@@ -23,7 +23,7 @@ use tokio::sync::RwLock;
use serde_json::{Value, json};
use crate::{BACKEND, db::Db};
use crate::db::Db;
fn subscription_matches(sub_key: &str, key: &str) -> bool {
if sub_key == key {
@@ -54,10 +54,8 @@ pub fn new_session_id() -> SessionId {
pub enum RedisEventAction {
Set,
Del,
#[cfg(feature = "db-redis")]
Unlink,
Expired,
#[cfg(feature = "db-redis")]
Other(String),
}
@@ -171,7 +169,7 @@ impl HubState {
let info = db.info().await.unwrap_or_else(|_| "error".to_string());
json!({
"memory_info": info,
"backend": BACKEND,
"backend": db.mode(),
"websockets": self.sessions.len(),
"subscriptions": self.subs.len(),
"heartbeats": self.heartbeats.len(),
+18 -23
View File
@@ -19,6 +19,8 @@ use actix_web::{
middleware::{self},
web::{self},
};
use std::sync::Arc;
use tokio::sync::RwLock;
#[cfg(feature = "auth")]
use actix_web::{
@@ -45,7 +47,7 @@ mod config;
mod handlers_http;
mod handlers_ws;
#[cfg(feature = "db-redis")]
mod memory;
mod redis;
#[cfg(feature = "auth")]
@@ -57,20 +59,12 @@ use hub_service::HubState;
use config::CONFIG;
mod db;
use crate::config::BackendType;
use crate::db::Db;
#[cfg(not(feature = "db-redis"))]
mod memory;
#[cfg(not(feature = "db-redis"))]
use crate::memory::MemoryBackend;
use crate::hub_service::check_heartbeat;
#[cfg(feature = "db-redis")]
pub const BACKEND: &str = "REDIS";
#[cfg(not(feature = "db-redis"))]
pub const BACKEND: &str = "MEMORY";
fn initialize_tracing() {
use tracing_subscriber::{filter::targets::Targets, prelude::*};
@@ -147,9 +141,8 @@ async fn main() -> anyhow::Result<()> {
// starting heartbeat checker
check_heartbeat(hub_state.clone());
let db_backend = {
#[cfg(feature = "db-redis")]
{
let db_backend = match &CONFIG.backend {
BackendType::Redis => {
let redis_client = redis::client().await?;
let db_connection = redis_client
.get_multiplexed_async_connection()
@@ -166,19 +159,24 @@ async fn main() -> anyhow::Result<()> {
);
e
})?;
tokio::spawn(crate::redis::receiver(redis_client, hub_state.clone()));
Db::new_db(db_connection)
tokio::spawn({
let hub_state = hub_state.clone();
async move {
if let Err(err) = crate::redis::receiver(redis_client, hub_state).await {
tracing::error!("Redis receiver stopped: {err}");
}
}
});
Db::new_redis(db_connection)
}
#[cfg(not(feature = "db-redis"))]
{
BackendType::Memory => {
let db_connection = MemoryBackend::new();
db_connection.spawn_ticker(hub_state.clone());
Db::new_db(db_connection, hub_state.clone())
Db::new_memory(db_connection, hub_state.clone())
}
};
tracing::info!("DB mode: {}", BACKEND);
tracing::info!("DB mode: {}", db_backend.mode());
let socket = std::net::SocketAddr::new(CONFIG.bind_host.as_str().parse()?, CONFIG.bind_port);
@@ -192,9 +190,6 @@ async fn main() -> anyhow::Result<()> {
);
tracing::info!("Status: {}/status", &url);
use std::sync::Arc;
use tokio::sync::RwLock;
let server = HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
+6 -12
View File
@@ -30,7 +30,7 @@ use crate::{
};
use redis::{
Client, ConnectionInfo, ProtocolVersion, RedisConnectionInfo, RedisResult, ToRedisArgs,
Client, ConnectionInfo, ProtocolVersion, RedisConnectionInfo, ToRedisArgs,
aio::MultiplexedConnection,
};
// use serde::Serialize;
@@ -61,7 +61,7 @@ pub async fn push_event(
}
/// redis_info(&connection)
pub async fn redis_info(conn: &mut MultiplexedConnection) -> redis::RedisResult<String> {
pub async fn redis_info(conn: &mut MultiplexedConnection) -> DbResult<String> {
let info: String = redis::cmd("INFO").query_async(conn).await?;
let mut redis_keys: Option<usize> = None;
@@ -91,10 +91,7 @@ pub async fn redis_info(conn: &mut MultiplexedConnection) -> redis::RedisResult<
}
/// redis_list(&connection,prefix)
pub async fn redis_list(
conn: &mut MultiplexedConnection,
key: &str,
) -> redis::RedisResult<Vec<DbArray>> {
pub async fn redis_list(conn: &mut MultiplexedConnection, key: &str) -> DbResult<Vec<DbArray>> {
deprecated_symbol_error(key)?;
if !key.ends_with('/') {
return error(412, "Key must end with slash");
@@ -146,10 +143,7 @@ pub async fn redis_list(
}
/// redis_read(&connection,key)
pub async fn redis_read(
conn: &mut MultiplexedConnection,
key: &str,
) -> redis::RedisResult<Option<DbArray>> {
pub async fn redis_read(conn: &mut MultiplexedConnection, key: &str) -> DbResult<Option<DbArray>> {
deprecated_symbol_error(key)?;
if key.ends_with('/') {
@@ -306,7 +300,7 @@ pub async fn redis_delete(
conn: &mut MultiplexedConnection,
key: &str,
mode: Option<SaveMode>,
) -> RedisResult<bool> {
) -> DbResult<bool> {
deprecated_symbol_error(key)?;
if key.ends_with('/') {
@@ -433,7 +427,7 @@ pub async fn receiver(
while let Some(message) = messages.next().await {
match RedisEvent::try_from(message) {
Ok(ev) => {
push_event(&hub_state, &mut redis, ev); // .await;
push_event(&hub_state, &mut redis, ev).await;
}
Err(e) => {
warn!("invalid redis message: {e}");
+6 -1
View File
@@ -31,7 +31,12 @@ async fn status(base: &str, client: &reqwest::Client) -> () {
let text = resp.text().await.unwrap();
let json: Value = serde_json::from_str(&text).unwrap();
assert_eq!(json["backend"], "memory");
let backend = json["backend"].as_str().unwrap_or_default();
if let Ok(expected_backend) = env::var("TEST_BACKEND") {
assert_eq!(backend, expected_backend);
} else {
assert!(backend == "memory" || backend == "redis");
}
assert_eq!(json["status"], "OK");
assert!(json.get("memory_info").is_some());
assert!(json.get("websockets").is_some());
@@ -19,7 +19,9 @@ export enum QueueTopic {
CalendarEventCUD = 'calendarEventCUD',
// A topic about process events
Process = 'process'
Process = 'process',
TimeMachine = 'timeMachine'
}
export interface ConsumerHandle {
@@ -37,7 +37,7 @@ import core, {
} from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { type Middleware, type TxMiddlewareResult, type PipelineContext } from '@hcengineering/server-core'
import contact from '@hcengineering/contact'
import { BaseMiddleware } from '@hcengineering/server-core'
/**
@@ -446,10 +446,21 @@ export class SpacePermissionsMiddleware extends BaseMiddleware implements Middle
this.checkSpacePermissions(ctx, cudTx, cudTx.objectSpace)
if (isSpace) {
this.checkSpaceTypePermissions(ctx, cudTx as TxCUD<Space>)
this.checkSpacePermissions(ctx, cudTx, cudTx.objectId as Ref<Space>, true)
}
}
private checkSpaceTypePermissions (ctx: MeasureContext, cudTx: TxCUD<Space>): void {
const account = ctx.contextData.account
const h = this.context.hierarchy
if (account.primarySocialId === core.account.System) return
if (h.isDerived(cudTx.objectClass, contact.class.PersonSpace)) {
this.throwForbidden()
}
}
private checkSpacePermissions (
ctx: MeasureContext,
cudTx: TxCUD<Doc>,
@@ -484,10 +484,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
const space = this.spacesMap.get(tx.objectSpace)
if (space === undefined) return undefined
// For all other spaces broadcast to space members + guests that are collaborators for objects with collab security enabled
let collabTargets: AccountUuid[] = []
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass)
if (collabSec?.provideSecurity === true) {
const getCollabTargets = async (_id: Ref<Doc>): Promise<AccountUuid[]> => {
const guests = new Set<AccountUuid>()
for (const val of ctx.contextData.socialStringsToUsers.values()) {
if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(val.role)) {
@@ -495,11 +492,30 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
}
}
const collaboratorObjs = (await this.next?.findAll(ctx, core.class.Collaborator, {
attachedTo: cud.objectId
attachedTo: _id
})) as Collaborator[]
collabTargets = collaboratorObjs.map((it) => it.collaborator).filter((it) => guests.has(it))
return collaboratorObjs.map((it) => it.collaborator).filter((it) => guests.has(it))
}
// For all other spaces broadcast to space members
// + guests that are collaborators for objects with collab security enabled
// + guests that are collaborators for attached objects with collab security enabled
let collabTargets: AccountUuid[] = []
const collabSec = getClassCollaborators(this.context.modelDb, this.context.hierarchy, cud.objectClass)
if (collabSec?.provideSecurity === true) {
collabTargets = await getCollabTargets(cud.objectId)
} else if (cud.attachedTo != null && cud.attachedToClass != null) {
const attachedCollabSec = getClassCollaborators(
this.context.modelDb,
this.context.hierarchy,
cud.attachedToClass
)
if (attachedCollabSec?.provideSecurity === true) {
collabTargets = await getCollabTargets(cud.attachedTo)
}
}
const spaceTargets = space.members.length === 0 ? [] : this.getTargets(space?.members)
const target = [...collabTargets, ...spaceTargets]
@@ -672,7 +688,7 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
findResult.lookupMap
)
}
if (!isOwner(account, ctx) && account.role !== AccountRole.DocGuest) {
if (account.role !== AccountRole.DocGuest) {
if (options?.lookup !== undefined) {
for (const object of findResult) {
if (object.$lookup !== undefined) {
@@ -721,19 +737,29 @@ export class SpaceSecurityMiddleware extends BaseMiddleware implements Middlewar
if (Object.keys(lookup).length === 0) return
const account = ctx.contextData.account
if (isSystem(account, ctx)) return
const owner = isOwner(account, ctx)
const h = this.context.hierarchy
const allowedSpaces = new Set(this.getAllAllowedSpaces(account, true, showArchived))
for (const key in lookup) {
const val = lookup[key]
if (Array.isArray(val)) {
const arr: AttachedDoc[] = []
for (const value of val) {
if (allowedSpaces.has(value.space)) {
const isSpace = '_class' in value && h.isDerived(value._class, core.class.Space)
const availableForOwner = owner && isSpace
const availableSpace = isSpace && allowedSpaces.has(value._id)
const availableDoc = !isSpace && allowedSpaces.has(value.space)
if (availableForOwner || availableSpace || availableDoc) {
arr.push(value)
}
}
lookup[key] = arr as any
} else if (val !== undefined) {
if (!allowedSpaces.has(val.space)) {
const isSpace = '_class' in val && h.isDerived(val._class, core.class.Space)
const availableForOwner = owner && isSpace
const availableSpace = isSpace && allowedSpaces.has(val._id as Ref<Space>)
const availableDoc = !isSpace && allowedSpaces.has(val.space)
if (!availableForOwner && !availableSpace && !availableDoc) {
// allow attached lookups for guests when collaborator security is enabled
// do not check if collaborator of the doc because it's being checked on the storage (DB) level
// as otherwise there will be no doc here at all
@@ -638,11 +638,16 @@ abstract class PostgresAdapterBase implements DbAdapter {
const res = `EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_SPACE)} sec WHERE sec._id = ${domain}.${key} AND sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND ${q})`
const collabSec = getClassCollaborators(this.modelDb, this.hierarchy, _class)
if (collabSec?.provideSecurity === true && [AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(acc.role)) {
const collab = `OR EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_COLLABORATOR)} collab_sec WHERE collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND collab_sec."attachedTo" = ${domain}._id AND collab_sec.collaborator = '${acc.uuid}')`
return `AND (${res} ${collab})`
let collabRes = ''
if ([AccountRole.Guest, AccountRole.ReadOnlyGuest].includes(acc.role)) {
if (collabSec?.provideSecurity === true) {
collabRes += ` OR EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_COLLABORATOR)} collab_sec WHERE collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND collab_sec."attachedTo" = ${domain}._id AND collab_sec.collaborator = '${acc.uuid}')`
}
if (collabSec?.provideAttachedSecurity === true) {
collabRes += ` OR EXISTS (SELECT 1 FROM ${translateDomain(DOMAIN_COLLABORATOR)} collab_sec WHERE collab_sec."workspaceId" = ${vars.add(this.workspaceId, '::uuid')} AND collab_sec."attachedTo" = ${domain}."attachedTo" AND collab_sec.collaborator = '${acc.uuid}')`
}
}
return `AND (${res})`
return `AND (${res}${collabRes})`
}
}
}
+2
View File
@@ -279,6 +279,8 @@ export function createModel (builder: Builder): void {
)
builder.mixin(activity.class.Reaction, core.class.Class, core.mixin.TxAccessLevel, {
createAccessLevel: AccountRole.Guest,
updateAccessLevel: AccountRole.Guest,
removeAccessLevel: AccountRole.Guest
})
+4 -2
View File
@@ -44,12 +44,14 @@ export function buildNotifications (builder: Builder): void {
builder.createDoc<ClassCollaborators<ActivityMessage>>(core.class.ClassCollaborators, core.space.Model, {
attachedTo: activity.class.ActivityMessage,
fields: ['createdBy', 'repliedPersons']
fields: ['createdBy', 'repliedPersons'],
provideAttachedSecurity: true
})
builder.createDoc<ClassCollaborators<DocUpdateMessage>>(core.class.ClassCollaborators, core.space.Model, {
attachedTo: activity.class.DocUpdateMessage,
fields: ['createdBy', 'repliedPersons']
fields: ['createdBy', 'repliedPersons'],
provideAttachedSecurity: true
})
builder.mixin(activity.class.ActivityMessage, core.class.Class, notification.mixin.NotificationContextPresenter, {
+5 -1
View File
@@ -22,7 +22,7 @@ import type {
Photo,
SavedAttachments
} from '@hcengineering/attachment'
import { IndexKind, type Blob, type Class, type Doc, type Domain, type Ref } from '@hcengineering/core'
import { AccountRole, IndexKind, type Blob, type Class, type Doc, type Domain, type Ref } from '@hcengineering/core'
import {
Hidden,
Index,
@@ -124,6 +124,10 @@ export function createModel (builder: Builder): void {
editor: attachment.component.Attachments
})
builder.mixin(attachment.class.Attachment, core.class.Class, core.mixin.TxAccessLevel, {
createAccessLevel: AccountRole.Guest
})
builder.mixin(attachment.class.Photo, core.class.Class, view.mixin.CollectionEditor, {
editor: attachment.component.Photos
})
+2
View File
@@ -40,6 +40,8 @@
"@hcengineering/converter": "workspace:^0.7.0",
"@hcengineering/core": "workspace:^0.7.24",
"@hcengineering/model": "workspace:^0.7.17",
"@hcengineering/notification": "workspace:^0.7.0",
"@hcengineering/model-notification": "workspace:^0.7.0",
"@hcengineering/chunter": "workspace:^0.7.0",
"@hcengineering/model-setting": "workspace:^0.7.0",
"@hcengineering/model-view": "workspace:^0.7.0",
+129
View File
@@ -84,6 +84,7 @@ import { type BuildModelKey } from '@hcengineering/view'
import { createActions } from './actions'
import { definePermissions } from './permissions'
import card from './plugin'
import notification from '@hcengineering/notification'
export { cardId } from '@hcengineering/card'
@@ -149,6 +150,9 @@ export class TCard extends TDoc implements Card {
@Hidden()
@ReadOnly()
peerId?: string
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number
}
@Model(card.class.CardSpace, core.class.TypedSpace, DOMAIN_SPACE)
@@ -445,6 +449,77 @@ export function createModel (builder: Builder): void {
PaletteColorIndexes.Arctic
)
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
{
label: card.string.Card,
icon: card.icon.Card
},
card.ids.CardNotificationGroup
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
label: card.string.CardCreated,
group: card.ids.CardNotificationGroup,
txClasses: [core.class.TxCreateDoc],
objectClass: card.class.Card,
defaultEnabled: true,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p><p>{link}</p>',
subjectTemplate: '{title} created'
}
},
card.ids.CardCreateNotification
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
label: card.string.CardUpdated,
group: card.ids.CardNotificationGroup,
txClasses: [core.class.TxUpdateDoc, core.class.TxMixin],
objectClass: card.class.Card,
defaultEnabled: false,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p><p>{link}</p>',
subjectTemplate: '{title} updated'
}
},
card.ids.CardNotification
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
label: chunter.string.Comments,
group: card.ids.CardNotificationGroup,
txClasses: [core.class.TxCreateDoc],
objectClass: chunter.class.ChatMessage,
attachedToClass: card.class.Card,
defaultEnabled: true,
templates: {
textTemplate: 'New message in {title} ({link}) from {senderName}: {message}',
htmlTemplate: '<p>New message in <b>{title}</b> <b>from {senderName}</b>: {message}<p>{link}</p>',
subjectTemplate: 'New message from {senderName} in {title}'
}
},
card.ids.CardMessageNotification
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: card.class.CardSpace,
descriptor: view.viewlet.Table,
@@ -683,6 +758,60 @@ export function createModel (builder: Builder): void {
card.viewlet.CardRelationshipTable
)
builder.createDoc(
presentation.class.ComponentPointExtension,
core.space.Model,
{
extension: converter.extensions.CopyAsMarkdownAction,
component: converter.component.CopyAsMarkdownButton
},
converter.extensions.CopyAsMarkdownButton
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.RelationshipTable,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CardRelationshipTable
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CardTable
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CopyAsMarkdownTable
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.RelationshipTable,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: card.class.Card
},
card.specialViewAction.CopyAsMarkdownRelationshipTable
)
builder.mixin(card.class.Card, core.class.Class, view.mixin.ObjectPresenter, {
presenter: card.component.CardPresenter
})
+13 -2
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//
import type { ViewletViewAction, Action, ActionCategory, ViewAction } from '@hcengineering/view'
import { type Card, cardId } from '@hcengineering/card'
import card from '@hcengineering/card-resources/src/plugin'
import type { Client, Doc, Ref } from '@hcengineering/core'
@@ -20,8 +21,8 @@ import {} from '@hcengineering/core'
import { mergeIds, type Resource } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import { type Location, type ResolvedLocation } from '@hcengineering/ui/src/types'
import { type Action, type ActionCategory, type ViewAction } from '@hcengineering/view'
import { type LocationData } from '@hcengineering/workbench'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
export default mergeIds(cardId, card, {
app: {
@@ -43,10 +44,20 @@ export default mergeIds(cardId, card, {
Card: '' as Ref<ActionCategory>,
Labels: '' as Ref<TagCategory>
},
specialViewAction: {
CardRelationshipTable: '' as Ref<ViewletViewAction>,
CardTable: '' as Ref<ViewletViewAction>,
CopyAsMarkdownTable: '' as Ref<ViewletViewAction>,
CopyAsMarkdownRelationshipTable: '' as Ref<ViewletViewAction>
},
ids: {
MasterTags: '' as Ref<Doc>,
ManageMasterTags: '' as Ref<Doc>,
TagRelations: '' as Ref<Doc>
TagRelations: '' as Ref<Doc>,
CardNotificationGroup: '' as Ref<NotificationGroup>,
CardCreateNotification: '' as Ref<NotificationType>,
CardNotification: '' as Ref<NotificationType>,
CardMessageNotification: '' as Ref<NotificationType>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>,
+30 -11
View File
@@ -21,6 +21,7 @@ import documentsPlugin, {
DocumentState,
type Document,
type DocumentSpace,
type DocumentTemplate,
type ProjectDocument,
type ChangeControl,
type DocumentRequest
@@ -43,7 +44,7 @@ import setting from '@hcengineering/setting'
import tags from '@hcengineering/tags'
import textEditor from '@hcengineering/text-editor'
import { AccountRole, type ClassCollaborators, type Class, type Doc, type Ref } from '@hcengineering/core'
import { AccountRole, type ClassCollaborators, type Class, type Doc, type Lookup, type Ref } from '@hcengineering/core'
import { type Action } from '@hcengineering/view'
import { definePermissions } from './permissions'
import documents from './plugin'
@@ -255,6 +256,11 @@ export function createModel (builder: Builder): void {
)
// Workflow
const documentTableLookup: Lookup<Document> = {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory,
template: documents.mixin.DocumentTemplate
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -308,16 +314,27 @@ export function createModel (builder: Builder): void {
'modifiedOn'
],
options: {
lookup: {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory,
template: documents.mixin.DocumentTemplate
}
lookup: documentTableLookup
}
},
documents.viewlet.TableDocument
)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: documents.class.Document
},
documents.specialViewAction.TableDocument
)
const documentTemplateTableLookup: Lookup<DocumentTemplate> = {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -367,10 +384,7 @@ export function createModel (builder: Builder): void {
hiddenKeys: ['attachedTo']
},
options: {
lookup: {
owner: contact.mixin.Employee,
category: documents.class.DocumentCategory
}
lookup: documentTemplateTableLookup
}
},
documents.viewlet.TableDocumentTemplate
@@ -1046,7 +1060,7 @@ export function createModel (builder: Builder): void {
label: print.string.PrintToPDF,
icon: print.icon.Print,
category: view.category.General,
input: 'focus', // NOTE: should only work for one doc for now, not bulk
input: 'any',
target,
context: { mode: ['context', 'browser'], group: 'tools' },
visibilityTester: documents.function.CanPrintDocument,
@@ -1069,6 +1083,11 @@ export function createModel (builder: Builder): void {
export function defineNotifications (builder: Builder): void {
builder.mixin(documents.class.ControlledDocument, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: documents.class.ControlledDocument,
components: { input: { component: chunter.component.ChatMessageInput } }
})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: documents.class.DocumentComment,
components: { input: { component: chunter.component.ChatMessageInput } }
+8 -5
View File
@@ -20,7 +20,7 @@ import { type ObjectSearchCategory, type ObjectSearchFactory } from '@hcengineer
import { mergeIds, type Resource } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import { type ActionCategory, type ViewAction } from '@hcengineering/view'
import type { ActionCategory, ViewAction, Viewlet, ViewletViewAction } from '@hcengineering/view'
import { type NotificationType, type NotificationGroup } from '@hcengineering/notification'
import { type TextActionVisibleFunction, type TextActionFunction } from '@hcengineering/text-editor'
@@ -78,11 +78,14 @@ export default mergeIds(documentsId, documents, {
TransferDocument: '' as ViewAction,
EditDocSpace: '' as ViewAction
},
specialViewAction: {
TableDocument: '' as Ref<ViewletViewAction>
},
viewlet: {
TableDocument: '' as Ref<Doc>,
ListDocument: '' as Ref<Doc>,
TableDocumentTemplate: '' as Ref<Doc>,
TableDocumentDomain: '' as Ref<Doc>
TableDocument: '' as Ref<Viewlet>,
ListDocument: '' as Ref<Viewlet>,
TableDocumentTemplate: '' as Ref<Viewlet>,
TableDocumentDomain: '' as Ref<Viewlet>
},
notification: {
DocumentsNotificationGroup: '' as Ref<NotificationGroup>,
+1
View File
@@ -416,6 +416,7 @@ export class TClassCollaborators extends TDoc implements ClassCollaborators<Doc>
allFields?: boolean
fields!: (keyof Doc)[]
provideSecurity?: boolean
provideAttachedSecurity?: boolean
}
@Model(core.class.Collaborator, core.class.Doc, DOMAIN_COLLABORATOR)
+7
View File
@@ -13,6 +13,7 @@
// limitations under the License.
//
import { AccountRole } from '@hcengineering/core'
import { Model, type Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
@@ -33,6 +34,12 @@ export class TDesktopNotificationPreference extends TPreference implements Deskt
export function createModel (builder: Builder): void {
builder.createModel(TDesktopNotificationPreference)
builder.mixin(desktopPreferences.class.DesktopNotificationPreference, core.class.Class, core.mixin.TxAccessLevel, {
createAccessLevel: AccountRole.Guest,
updateAccessLevel: AccountRole.Guest,
removeAccessLevel: AccountRole.Guest
})
builder.createDoc(
notification.class.NotificationPreferencesGroup,
core.space.Model,
+54 -3
View File
@@ -234,6 +234,12 @@ function defineDrive (builder: Builder): void {
icon: drive.icon.Drive,
baseClass: drive.class.Drive,
availablePermissions: [
drive.permission.CreateFolder,
drive.permission.UpdateFolder,
drive.permission.RemoveFolder,
drive.permission.CreateFile,
drive.permission.UpdateFile,
drive.permission.RemoveFile,
core.permission.UpdateSpace,
core.permission.ArchiveSpace,
core.permission.ForbidDeleteObject
@@ -326,7 +332,8 @@ function defineDrive (builder: Builder): void {
mode: ['context', 'browser'],
application: drive.app.Drive,
group: 'create'
}
},
visibilityTester: drive.function.CanCreateFolder
},
drive.action.CreateRootFolder
)
@@ -475,6 +482,7 @@ function defineFolder (builder: Builder): void {
actions: [
view.action.Open,
view.action.OpenInNewTab,
view.action.Delete,
print.action.Print,
tracker.action.EditRelatedTargets,
tracker.action.NewRelatedIssue
@@ -494,7 +502,8 @@ function defineFolder (builder: Builder): void {
mode: ['context', 'browser'],
application: drive.app.Drive,
group: 'create'
}
},
visibilityTester: drive.function.CanCreateFolder
},
drive.action.CreateChildFolder
)
@@ -518,6 +527,25 @@ function defineFolder (builder: Builder): void {
drive.action.RenameFolder
)
createAction(
builder,
{
action: view.actionImpl.Delete,
label: view.string.Delete,
icon: view.icon.Delete,
category: drive.category.Drive,
input: 'none',
target: drive.class.Folder,
context: {
mode: ['context', 'browser'],
application: drive.app.Drive,
group: 'edit'
},
visibilityTester: drive.function.CanDeleteFolder
},
drive.action.DeleteFolder
)
createAction(builder, {
...actionTemplates.move,
action: view.actionImpl.ShowPopup,
@@ -529,6 +557,7 @@ function defineFolder (builder: Builder): void {
}
},
target: drive.class.Folder,
visibilityTester: drive.function.CanUpdateFolder,
context: {
mode: ['browser', 'context'],
group: 'tools'
@@ -569,7 +598,8 @@ function defineFileVersion (builder: Builder): void {
mode: ['context', 'browser'],
application: drive.app.Drive,
group: 'edit'
}
},
visibilityTester: drive.function.CanUpdateFileVersion
},
drive.action.RestoreFileVersion
)
@@ -649,6 +679,7 @@ function defineFile (builder: Builder): void {
actions: [
view.action.Open,
view.action.OpenInNewTab,
view.action.Delete,
print.action.Print,
tracker.action.EditRelatedTargets,
tracker.action.NewRelatedIssue
@@ -692,6 +723,25 @@ function defineFile (builder: Builder): void {
drive.action.RenameFile
)
createAction(
builder,
{
action: view.actionImpl.Delete,
label: view.string.Delete,
icon: view.icon.Delete,
category: drive.category.Drive,
input: 'none',
target: drive.class.File,
context: {
mode: ['context', 'browser'],
application: drive.app.Drive,
group: 'edit'
},
visibilityTester: drive.function.CanDeleteFile
},
drive.action.DeleteFile
)
// createAction(
// builder,
// {
@@ -721,6 +771,7 @@ function defineFile (builder: Builder): void {
}
},
target: drive.class.File,
visibilityTester: drive.function.CanUpdateFile,
context: {
mode: ['browser', 'context'],
group: 'tools'
+2 -2
View File
@@ -14,7 +14,7 @@
//
import core, { type Blob, type Ref, DOMAIN_BLOB, generateId, toIdMap } from '@hcengineering/core'
import type { Drive, File, FileVersion, Resource } from '@hcengineering/drive'
import type { File, FileVersion, Resource } from '@hcengineering/drive'
import {
type MigrateOperation,
type MigrationClient,
@@ -56,7 +56,7 @@ async function migrateFileVersions (client: MigrationClient): Promise<void> {
collection: 'versions',
modifiedOn: file.modifiedOn,
modifiedBy: file.modifiedBy,
space: file.space as Ref<Drive>,
space: file.space,
title: exfile.title,
file: blob._id,
size: blob.size,
+78
View File
@@ -3,6 +3,84 @@ import core from '@hcengineering/core'
import drive from '@hcengineering/drive'
export function definePermissions (builder: Builder): void {
builder.createDoc(
core.class.Permission,
core.space.Model,
{
scope: 'space',
label: drive.string.CreateFilePermission,
description: drive.string.CreateFilePermissionDescription,
txClass: core.class.TxCreateDoc,
objectClass: drive.class.File
},
drive.permission.CreateFile
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
scope: 'space',
label: drive.string.UpdateFilePermission,
description: drive.string.UpdateFilePermissionDescription,
txClass: core.class.TxUpdateDoc,
objectClass: drive.class.File
},
drive.permission.UpdateFile
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
scope: 'space',
label: drive.string.RemoveFilePermission,
description: drive.string.RemoveFilePermissionDescription,
txClass: core.class.TxRemoveDoc,
objectClass: drive.class.File
},
drive.permission.RemoveFile
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
scope: 'space',
label: drive.string.CreateFolderPermission,
description: drive.string.CreateFolderPermissionDescription,
txClass: core.class.TxCreateDoc,
objectClass: drive.class.Folder
},
drive.permission.CreateFolder
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
scope: 'space',
label: drive.string.UpdateFolderPermission,
description: drive.string.UpdateFolderPermissionDescription,
txClass: core.class.TxUpdateDoc,
objectClass: drive.class.Folder
},
drive.permission.UpdateFolder
)
builder.createDoc(
core.class.Permission,
core.space.Model,
{
scope: 'space',
label: drive.string.RemoveFolderPermission,
description: drive.string.RemoveFolderPermissionDescription,
txClass: core.class.TxRemoveDoc,
objectClass: drive.class.Folder
},
drive.permission.RemoveFolder
)
builder.createDoc(
core.class.Permission,
core.space.Model,
+8
View File
@@ -53,8 +53,14 @@ export default mergeIds(driveId, drive, {
DriveLinkProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
FolderLinkProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
FileLinkProvider: '' as Resource<(doc: Doc, props: Record<string, any>) => Promise<Location>>,
CanCreateFolder: '' as Resource<ViewActionAvailabilityFunction>,
CanUpdateFile: '' as Resource<ViewActionAvailabilityFunction>,
CanUpdateFolder: '' as Resource<ViewActionAvailabilityFunction>,
CanDeleteFile: '' as Resource<ViewActionAvailabilityFunction>,
CanDeleteFolder: '' as Resource<ViewActionAvailabilityFunction>,
CanRenameFile: '' as Resource<ViewActionAvailabilityFunction>,
CanRenameFolder: '' as Resource<ViewActionAvailabilityFunction>,
CanUpdateFileVersion: '' as Resource<ViewActionAvailabilityFunction>,
CanDeleteFileVersion: '' as Resource<ViewActionAvailabilityFunction>,
FileTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>,
FolderTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
@@ -79,6 +85,8 @@ export default mergeIds(driveId, drive, {
CreateRootFolder: '' as Ref<Action>,
EditDrive: '' as Ref<Action>,
DownloadFile: '' as Ref<Action>,
DeleteFile: '' as Ref<Action>,
DeleteFolder: '' as Ref<Action>,
RenameFile: '' as Ref<Action>,
RenameFolder: '' as Ref<Action>,
DeleteFileVersion: '' as Ref<Action>,
+6 -1
View File
@@ -89,7 +89,12 @@ export function createModel (builder: Builder): void {
group: exportPlugin.ids.ImportNotificationGroup,
txClasses: [],
objectClass: exportPlugin.class.ExportResultRecord,
defaultEnabled: true
defaultEnabled: true,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p><p>{link}</p>',
subjectTemplate: '{title}'
}
},
exportPlugin.ids.ImportedDocumentsNotification
)
+18
View File
@@ -573,6 +573,24 @@ export function createModel (builder: Builder): void {
removeAccessLevel: AccountRole.Guest
})
builder.mixin(notification.class.NotificationTypeSetting, core.class.Class, core.mixin.TxAccessLevel, {
createAccessLevel: AccountRole.Guest,
updateAccessLevel: AccountRole.Guest,
removeAccessLevel: AccountRole.Guest
})
builder.mixin(notification.class.NotificationProviderSetting, core.class.Class, core.mixin.TxAccessLevel, {
createAccessLevel: AccountRole.Guest,
updateAccessLevel: AccountRole.Guest,
removeAccessLevel: AccountRole.Guest
})
builder.mixin(notification.class.DocNotifyContext, core.class.Class, core.mixin.TxAccessLevel, {
createAccessLevel: AccountRole.Guest,
updateAccessLevel: AccountRole.Guest,
removeAccessLevel: AccountRole.Guest
})
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
+1 -1
View File
@@ -21,7 +21,7 @@ export function createModel (builder: Builder): void {
label: print.string.PrintToPDF,
icon: print.icon.Print,
category: view.category.General,
input: 'focus', // NOTE: should only work for one doc for now, not bulk
input: 'any',
target: core.class.Doc,
context: { mode: ['context', 'browser'], group: 'tools' },
visibilityTester: print.function.CanPrint
+96
View File
@@ -31,6 +31,22 @@ export function defineMethods (builder: Builder): void {
process.method.RunSubProcess
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.RequestApproval,
objectClass: process.class.ApproveRequest,
editor: process.component.ApproveRequestEditor,
presenter: process.component.ToDoPresenter,
createdContext: {
_class: process.class.ApproveRequest
},
requiredParams: ['user']
},
process.method.RequestApproval
)
builder.createDoc(
process.class.Method,
core.space.Model,
@@ -103,4 +119,84 @@ export function defineMethods (builder: Builder): void {
},
process.method.AddTag
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CancelToDo,
editor: process.component.CancelToDoEditor,
presenter: process.component.ToDoValuePresenter,
objectClass: process.class.ProcessToDo,
requiredParams: ['_id'],
createdContext: null
},
process.method.CancelToDo
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.CancelProcess,
editor: process.component.CancelSubProcessEditor,
presenter: process.component.SubProcessPresenter,
objectClass: process.class.Process,
requiredParams: ['_id'],
createdContext: null
},
process.method.CancelSubProcess
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.LockCard,
objectClass: card.class.Card,
requiredParams: [],
createdContext: null
},
process.method.LockCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.LockSection,
objectClass: card.class.Card,
editor: process.component.LockSectionEditor,
presenter: process.component.LockSectionPresenter,
requiredParams: ['_id'],
createdContext: null
},
process.method.LockSection
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.UnlockCard,
objectClass: card.class.Card,
requiredParams: [],
createdContext: null
},
process.method.UnlockCard
)
builder.createDoc(
process.class.Method,
core.space.Model,
{
label: process.string.UnlockSection,
objectClass: card.class.Card,
editor: process.component.LockSectionEditor,
presenter: process.component.UnLockSectionPresenter,
requiredParams: ['_id'],
createdContext: null
},
process.method.UnlockSection
)
}
+122 -2
View File
@@ -43,9 +43,10 @@ import presentation from '@hcengineering/model-presentation'
import { TToDo } from '@hcengineering/model-time'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import notification, { type NotificationGroup } from '@hcengineering/notification'
import { type Asset, type IntlString, type Resource } from '@hcengineering/platform'
import {
type ApproveRequest,
type CheckFunc,
type ContextId,
type CreatedContext,
@@ -102,6 +103,9 @@ export class TProcess extends TDoc implements Process {
@Prop(TypeBoolean(), process.string.StartAutomatically)
autoStart: boolean | undefined
@Prop(TypeBoolean(), process.string.AutomationOnly)
automationOnly: boolean | undefined
context!: Record<ContextId, ProcessContext>
}
@@ -198,12 +202,27 @@ export class TExecution extends TDoc implements Execution {
@Model(process.class.ProcessToDo, time.class.ToDo)
@UX(process.string.ToDo)
export class TProcessToDo extends TToDo implements ProcessToDo {
execution!: Ref<Execution>
@Prop(TypeRef(process.class.Execution), process.string.Execution)
execution!: Ref<Execution>
@Prop(TypeBoolean(), process.string.Rollback)
withRollback!: boolean
}
@Model(process.class.ApproveRequest, process.class.ProcessToDo)
@UX(process.string.ApproveRequest)
export class TApproveRequest extends TProcessToDo implements ApproveRequest {
@Prop(TypeBoolean(), process.string.IsApproved)
approved?: boolean
@Prop(TypeString(), process.string.RejectionReason)
reason?: string
group!: string
card!: Ref<Card>
}
@Model(process.class.Method, core.class.Doc, DOMAIN_MODEL)
export class TMethod extends TDoc implements Method<Doc> {
label!: IntlString
@@ -290,6 +309,7 @@ export function createModel (builder: Builder): void {
TProcess,
TExecution,
TProcessToDo,
TApproveRequest,
TMethod,
TState,
TProcessFunction,
@@ -323,6 +343,28 @@ export function createModel (builder: Builder): void {
process.ids.ProcessToDoCreated
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
allowedForAuthor: true,
label: process.string.ApproveRequest,
group: time.ids.TimeNotificationGroup,
txClasses: [core.class.TxCreateDoc],
objectClass: process.class.ApproveRequest,
onlyOwn: true,
defaultEnabled: true,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
}
},
process.ids.ApproveRequestCreated
)
createAction(builder, {
action: view.actionImpl.Delete,
label: view.string.Delete,
@@ -413,6 +455,46 @@ export function createModel (builder: Builder): void {
presenter: process.component.StatePresenter
})
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
variant: 'cardRequests',
attachTo: process.class.ApproveRequest,
descriptor: view.viewlet.List,
props: {
baseMenuClass: process.class.ApproveRequest
},
viewOptions: {
groupBy: ['user', 'approved', 'execution'],
orderBy: [
['approved', SortingOrder.Descending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: []
},
configOptions: {
strict: true
},
config: [
'user',
{
key: '',
presenter: view.component.GrowPresenter,
displayProps: { grow: true }
},
'reason',
{
key: '',
label: process.string.ApproveRequest,
presenter: process.component.ApproveRequestPresenter
}
]
},
process.viewlet.CardRequests
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -600,6 +682,12 @@ export function createModel (builder: Builder): void {
props: {}
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: card.extensions.EditCardExtension,
component: process.component.RequestsExtension,
props: {}
})
builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
extension: card.extensions.EditCardHeaderExtension,
component: process.component.ProcessesHeaderExtension,
@@ -623,12 +711,44 @@ export function createModel (builder: Builder): void {
process.section.CardProcesses
)
builder.createDoc(
card.class.CardSection,
core.space.Model,
{
label: process.string.ApproveRequest,
component: process.component.RequestsCardSection,
checkVisibility: process.function.CheckRequestsSectionVisibility,
order: 360,
navigation: []
},
process.section.CardApproveRequest
)
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
id: 'processes',
label: process.string.Processes,
component: process.component.ProcessesSettingSection
})
builder.createDoc(notification.class.NotificationType, core.space.Model, {
hidden: false,
generated: false,
allowedForAuthor: true,
label: process.string.NewProcessToDo,
group: time.ids.TimeNotificationGroup as Ref<NotificationGroup>,
txClasses: [core.class.TxCreateDoc],
objectClass: process.class.ProcessToDo,
txMatch: {
objectClass: process.class.ProcessToDo
},
defaultEnabled: true,
templates: {
textTemplate: '{body}',
htmlTemplate: '<p>{body}</p>',
subjectTemplate: '{title}'
}
})
// builder.createDoc(presentation.class.ComponentPointExtension, core.space.Model, {
// extension: workbench.extensions.WorkbenchExtensions,
// component: process.component.NotifierExtension
+4 -2
View File
@@ -24,14 +24,16 @@ export default mergeIds(processId, process, {
Process: '' as Ref<Doc>
},
section: {
CardProcesses: '' as Ref<CardSection>
CardProcesses: '' as Ref<CardSection>,
CardApproveRequest: '' as Ref<CardSection>
},
pipeline: {
ProcessMiddleware: '' as Ref<PresentationMiddlewareFactory>
},
ids: {
ProcessSettings: '' as Ref<Doc>,
ProcessToDoCreated: '' as Ref<Doc>
ProcessToDoCreated: '' as Ref<Doc>,
ApproveRequestCreated: '' as Ref<Doc>
},
actionImpl: {
ContinueExecution: '' as ViewAction
+32
View File
@@ -16,6 +16,38 @@ import { type Builder } from '@hcengineering/model'
import process from './plugin'
export function defineTriggers (builder: Builder): void {
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnApproveRequestApproved,
icon: process.icon.ToDo,
editor: process.component.ApproveRequestTriggerEditor,
presenter: process.component.ApproveRequestTriggerPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ApproveRequestApproved,
init: false,
auto: true
},
process.trigger.OnApproveRequestApproved
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
{
label: process.string.OnApproveRequestRejected,
icon: process.icon.ToDoRemove,
editor: process.component.ApproveRequestTriggerEditor,
presenter: process.component.ApproveRequestTriggerPresenter,
requiredParams: ['_id'],
checkFunction: process.triggerCheck.ApproveRequestRejected,
init: false,
auto: true
},
process.trigger.OnApproveRequestRejected
)
builder.createDoc(
process.class.Trigger,
core.space.Model,
+36 -25
View File
@@ -188,31 +188,7 @@ export function createModel (builder: Builder): void {
createEvent: RecruitEvents.PlusTalentButtonClicked,
createLabel: recruit.string.TalentCreateLabel,
createComponent: recruit.component.CreateCandidate,
createComponentProps: { shouldSaveDraft: false },
actionVisible: true,
actionConfig: {
skipAttributes: ['_class', 'avatarType', 'space'],
attributeKeyMap: { city: 'location' },
attributeTransforms: {
channels: {
operations: [
{
type: 'group_by',
config: {
keyField: 'data.provider',
valueField: 'data.value'
}
},
{
type: 'join',
config: {
delimiter: ', '
}
}
]
}
}
}
createComponentProps: { shouldSaveDraft: false }
},
position: 'vacancy'
},
@@ -323,6 +299,41 @@ export function createModel (builder: Builder): void {
recruit.viewlet.TableCandidate
)
const tableCandidateExportConfig = {
skipAttributes: ['_class', 'avatarType', 'space'],
attributeKeyMap: { city: 'location' },
attributeTransforms: {
channels: {
operations: [
{
type: 'group_by',
config: {
keyField: 'data.provider',
valueField: 'data.value'
}
},
{
type: 'join',
config: {
delimiter: ', '
}
}
]
}
}
}
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.Table,
extension: workbench.extensions.SpecialViewAction,
config: tableCandidateExportConfig,
applicableToClass: recruit.mixin.Candidate
},
recruit.specialViewAction.TableCandidate
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
+5 -1
View File
@@ -29,7 +29,8 @@ import type {
ViewAction,
ViewActionAvailabilityFunction,
ViewQueryAction,
Viewlet
Viewlet,
ViewletViewAction
} from '@hcengineering/view'
export default mergeIds(recruitId, recruit, {
@@ -132,6 +133,9 @@ export default mergeIds(recruitId, recruit, {
template: {
DefaultVacancy: '' as Ref<ProjectType>
},
specialViewAction: {
TableCandidate: '' as Ref<ViewletViewAction>
},
viewlet: {
TableCandidate: '' as Ref<Viewlet>,
TableVacancy: '' as Ref<Viewlet>,
+1
View File
@@ -39,6 +39,7 @@
"@hcengineering/platform": "workspace:^0.7.19",
"@hcengineering/card": "workspace:^0.7.0",
"@hcengineering/communication": "workspace:^0.7.0",
"@hcengineering/server-notification": "workspace:^0.7.0",
"@hcengineering/server-card": "workspace:^0.7.0",
"@hcengineering/server-core": "workspace:^0.7.18"
}
+9
View File
@@ -20,6 +20,7 @@ import serverCore from '@hcengineering/server-core'
import serverCard from '@hcengineering/server-card'
import card from '@hcengineering/card'
import communication from '@hcengineering/communication'
import serverNotification from '@hcengineering/server-notification'
export { serverCardId } from '@hcengineering/server-card'
@@ -129,4 +130,12 @@ export function createModel (builder: Builder): void {
},
title: [['title']]
})
builder.mixin(card.class.Card, core.class.Class, serverNotification.mixin.TextPresenter, {
presenter: serverCard.function.CardTextPresenter
})
builder.mixin(card.class.Card, core.class.Class, serverNotification.mixin.HTMLPresenter, {
presenter: serverCard.function.CardHTMLPresenter
})
}
+38
View File
@@ -70,6 +70,16 @@ export function createModel (builder: Builder): void {
serverCheckFunc: serverProcess.func.FieldChangedCheck
})
builder.mixin(process.trigger.OnApproveRequestApproved, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true,
serverCheckFunc: serverProcess.func.ApproveRequestApproved
})
builder.mixin(process.trigger.OnApproveRequestRejected, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true,
serverCheckFunc: serverProcess.func.ApproveRequestRejected
})
builder.mixin(process.trigger.OnExecutionStart, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
preventRollback: true
})
@@ -97,6 +107,10 @@ export function createModel (builder: Builder): void {
func: serverProcess.func.RunSubProcess
})
builder.mixin(process.method.CancelSubProcess, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.CancelSubProcess
})
builder.mixin(process.method.CreateToDo, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.CreateToDo
})
@@ -117,6 +131,30 @@ export function createModel (builder: Builder): void {
func: serverProcess.func.AddTag
})
builder.mixin(process.method.RequestApproval, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.RequestApproval
})
builder.mixin(process.method.CancelToDo, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.CancelToDo
})
builder.mixin(process.method.LockCard, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.LockCard
})
builder.mixin(process.method.LockSection, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.LockSection
})
builder.mixin(process.method.UnlockCard, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.UnlockCard
})
builder.mixin(process.method.UnlockSection, process.class.Method, serverProcess.mixin.MethodImpl, {
func: serverProcess.func.UnlockSection
})
builder.mixin(process.function.FirstValue, process.class.ProcessFunction, serverProcess.mixin.FuncImpl, {
func: serverProcess.transform.FirstValue
})
+12 -1
View File
@@ -25,7 +25,7 @@ import {
type IntegrationKind
} from '@hcengineering/core'
import exportPlugin from '@hcengineering/export'
import { Mixin, Model, UX, type Builder } from '@hcengineering/model'
import { Mixin, Model, Prop, TypeRecord, UX, type Builder } from '@hcengineering/model'
import core, { TClass, TConfiguration, TDoc } from '@hcengineering/model-core'
import view, { createAction } from '@hcengineering/model-view'
import notification from '@hcengineering/notification'
@@ -39,6 +39,7 @@ import {
type IntegrationType,
type InviteSettings,
type OfficeSettings,
type RoleCapabilitySettings,
type SettingsCategory,
type SpaceTypeCreator,
type SpaceTypeEditor,
@@ -112,6 +113,15 @@ export class TInviteSettings extends TConfiguration implements InviteSettings {
expirationTime!: number
emailMask!: string
limit!: number
defaultInviteRole!: AccountRole
inviteLinkGeneratorRoles!: AccountRole[]
}
@Model(setting.class.RoleCapabilitySettings, core.class.Configuration, DOMAIN_SETTING)
@UX(setting.string.RoleCapabilitySettings)
export class TRoleCapabilitySettings extends TConfiguration implements RoleCapabilitySettings {
@Prop(TypeRecord(), setting.string.RoleCapabilitySettings)
roleByCapability!: Record<string, AccountRole[]>
}
@Model(setting.class.OfficeSettings, core.class.Configuration, DOMAIN_SETTING)
@@ -146,6 +156,7 @@ export function createModel (builder: Builder): void {
TEditable,
TUserMixin,
TInviteSettings,
TRoleCapabilitySettings,
TOfficeSettings,
TWorkspaceSetting,
TSpaceTypeEditor,
+11
View File
@@ -487,6 +487,17 @@ export function createModel (builder: Builder): void {
defineViewlets(builder)
builder.createDoc(
view.class.ViewletViewAction,
core.space.Model,
{
descriptor: view.viewlet.List,
extension: converter.extensions.CopyAsMarkdownAction,
applicableToClass: tracker.class.Issue
},
tracker.specialViewAction.IssueList
)
const issuesId = 'issues'
const componentsId = 'components'
const milestonesId = 'milestones'
+6 -2
View File
@@ -22,7 +22,7 @@ import { type ProjectType } from '@hcengineering/task'
import { trackerId } from '@hcengineering/tracker'
import tracker from '@hcengineering/tracker-resources/src/plugin'
import type { AnyComponent } from '@hcengineering/ui/src/types'
import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view'
import { type Action, type ViewAction, type Viewlet, type ViewletViewAction } from '@hcengineering/view'
import { type Application } from '@hcengineering/workbench'
export default mergeIds(trackerId, tracker, {
@@ -63,13 +63,17 @@ export default mergeIds(trackerId, tracker, {
app: {
Tracker: '' as Ref<Application>
},
specialViewAction: {
IssueList: '' as Ref<ViewletViewAction>
},
viewlet: {
IssueList: '' as Ref<Viewlet>,
IssueTemplateList: '' as Ref<Viewlet>,
IssueKanban: '' as Ref<Viewlet>,
MilestoneList: '' as Ref<Viewlet>,
ComponentList: '' as Ref<Viewlet>,
ProjectList: '' as Ref<Viewlet>
ProjectList: '' as Ref<Viewlet>,
ProjectListGrouped: '' as Ref<Viewlet>
},
ids: {
TrackerNotificationGroup: '' as Ref<NotificationGroup>,
+80 -7
View File
@@ -20,7 +20,7 @@ import core from '@hcengineering/model-core'
import task from '@hcengineering/model-task'
import view, { showColorsViewOption } from '@hcengineering/model-view'
import tags from '@hcengineering/tags'
import { type BuildModelKey, type ViewOptionsModel } from '@hcengineering/view'
import { type ViewOptionModel, type BuildModelKey, type ViewOptionsModel } from '@hcengineering/view'
import tracker from './plugin'
export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
@@ -29,6 +29,7 @@ export const issuesOptions = (kanban: boolean): ViewOptionsModel => ({
'kind',
'assignee',
'priority',
'space',
'component',
'milestone',
'createdBy',
@@ -116,7 +117,9 @@ export function issueConfig (
key: '',
label: tracker.string.Title,
presenter: tracker.component.TitlePresenter,
props: compact ? { shouldUseMargin: true, showParent: false } : {},
props: compact
? { shouldUseMargin: true, showParent: false, grow: true, minWidth: '5rem' }
: { grow: true, minWidth: '5rem' },
displayProps: { key: key + 'title' }
},
{
@@ -150,7 +153,8 @@ export function issueConfig (
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
shouldShowPlaceholder: false,
maxWidth: '30rem'
},
displayProps: {
key: key + 'milestone',
@@ -169,7 +173,8 @@ export function issueConfig (
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
shouldShowPlaceholder: false,
maxWidth: '30rem'
},
displayProps: {
key: key + 'component',
@@ -425,7 +430,8 @@ export function defineViewlets (builder: Builder): void {
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
shouldShowPlaceholder: false,
maxWidth: '30rem'
},
displayProps: { key: 'component', compression: true }
},
@@ -436,7 +442,8 @@ export function defineViewlets (builder: Builder): void {
props: {
kind: 'list',
size: 'small',
shouldShowPlaceholder: false
shouldShowPlaceholder: false,
maxWidth: '30rem'
},
displayProps: { key: 'milestone', compression: true }
},
@@ -536,14 +543,42 @@ export function defineViewlets (builder: Builder): void {
tracker.viewlet.ComponentList
)
const hideArchivedOption: ViewOptionModel = {
key: 'hideArchived',
type: 'toggle',
defaultValue: false,
actionTarget: 'options',
action: view.function.HideArchived,
label: view.string.HideArchived
}
const tableOptions: ViewOptionsModel = {
groupBy: [],
orderBy: [],
other: [hideArchivedOption]
}
const projectListOptions: ViewOptionsModel = {
groupBy: ['createdBy', 'modifiedBy'],
orderBy: [
['name', SortingOrder.Ascending],
['identifier', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [hideArchivedOption]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Project,
descriptor: view.viewlet.Table,
viewOptions: tableOptions,
configOptions: {
hiddenKeys: ['identifier', 'name', 'description']
hiddenKeys: ['identifier', 'name', 'description'],
sortable: true
},
config: [
{
@@ -571,6 +606,44 @@ export function defineViewlets (builder: Builder): void {
tracker.viewlet.ProjectList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Project,
descriptor: view.viewlet.List,
viewOptions: projectListOptions,
configOptions: {
strict: true,
hiddenKeys: ['identifier', 'name', 'description']
},
config: [
{
key: '',
presenter: tracker.component.ProjectPresenter,
props: {
openIssues: true,
shouldUseMargin: true
}
},
'members',
{
key: 'defaultAssignee',
props: { kind: 'list' }
},
{
key: 'modifiedOn',
presenter: tracker.component.ModificationDatePresenter,
displayProps: { fixed: 'right', dividerBefore: true }
}
],
options: {
showArchived: true
}
},
tracker.viewlet.ProjectListGrouped
)
const milestoneOptions: ViewOptionsModel = {
groupBy: ['status', 'createdBy', 'modifiedBy'],
orderBy: [
+33 -4
View File
@@ -29,12 +29,17 @@ import {
type Ref,
type Space
} from '@hcengineering/core'
import { type Builder, Mixin, Model, UX } from '@hcengineering/model'
import { type Builder, Mixin, Model, Prop, TypeRecord, TypeRef, TypeString, UX } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import presentation from '@hcengineering/model-presentation'
import { type Asset, type IntlString, type Resource, type Status } from '@hcengineering/platform'
import { type AnyComponent, type LabelAndProps, type Location } from '@hcengineering/ui/src/types'
import { type Asset, getEmbeddedLabel, type IntlString, type Resource, type Status } from '@hcengineering/platform'
import {
type AnyComponent,
type ComponentExtensionId,
type LabelAndProps,
type Location
} from '@hcengineering/ui/src/types'
import {
type TypeEditor,
type Action,
@@ -99,7 +104,8 @@ import {
type ViewOptionsModel,
type Viewlet,
type ViewletDescriptor,
type ViewletPreference
type ViewletPreference,
type ViewletViewAction
} from '@hcengineering/view'
import view from './plugin'
@@ -318,6 +324,28 @@ export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
label!: IntlString
}
@Model(view.class.ViewletViewAction, core.class.Doc, DOMAIN_MODEL)
@UX(view.string.ViewletViewAction)
export class TViewletViewAction extends TDoc implements ViewletViewAction {
@Prop(TypeRef(view.class.Viewlet), getEmbeddedLabel('Viewlet'))
declare viewlet?: Ref<Viewlet>
@Prop(TypeRef(view.class.ViewletDescriptor), getEmbeddedLabel('Descriptor'))
declare descriptor?: Ref<ViewletDescriptor>
@Prop(TypeString(), getEmbeddedLabel('Extension'))
declare extension: ComponentExtensionId
@Prop(TypeRecord(), getEmbeddedLabel('Config'))
declare config?: Record<string, any>
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('ApplicableToClass'))
declare applicableToClass?: Ref<Class<Doc>>
@Prop(TypeRef(core.class.Class), getEmbeddedLabel('DisabledForClass'))
declare disabledForClass?: Ref<Class<Doc>>
}
@Model(view.class.Viewlet, core.class.Doc, DOMAIN_MODEL)
export class TViewlet extends TDoc implements Viewlet {
attachTo!: Ref<Class<Doc>>
@@ -472,6 +500,7 @@ export function createModel (builder: Builder): void {
TViewletPreference,
TViewletDescriptor,
TViewlet,
TViewletViewAction,
TAction,
TActionCategory,
TObjectValidator,
+1 -1
View File
@@ -17,6 +17,7 @@ import { AccountRole, type AccountUuid, type Class, DOMAIN_MODEL, type Ref, type
import { type Builder, Mixin, Model, Prop, TypeRef, UX } from '@hcengineering/model'
import preference, { TPreference } from '@hcengineering/model-preference'
import { createAction } from '@hcengineering/model-view'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import { type Asset, getEmbeddedLabel, type IntlString, type Resource } from '@hcengineering/platform'
import view, { type KeyBinding } from '@hcengineering/view'
import type {
@@ -32,7 +33,6 @@ import type {
WorkbenchTab
} from '@hcengineering/workbench'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import presentation from '@hcengineering/model-presentation'
import workbench from './plugin'
+64
View File
@@ -0,0 +1,64 @@
{
"string": {
"Create": "Criar",
"Cancel": "Cancelar",
"Ok": "Ok",
"Save": "Salvar",
"Saved": "Salvo...",
"Download": "Fazer download",
"DownloadOriginal": "Fazer download do original",
"Delete": "Excluir",
"Close": "Fechar",
"NotSelected": "Não selecionado",
"Deselect": "Desmarcar",
"Archived": "(arquivado)",
"AddSocialLinks": "Adicionar links sociais",
"EditSocialLinks": "Editar links sociais",
"Change": "Alterar",
"Remove": "Remover",
"Search": "Pesquisar...",
"Spaces": "Espaços",
"NumberSpaces": "{count, plural, =0 {Em} =1 {Em 1 local} other {Em # locais}}",
"InThis": "Neste {space}",
"NoMatchesInThis": "Sem resultados no espaço {space}",
"NoMatchesFound": "Sem resultados encontrados",
"NotInThis": "Não está no espaço {space}",
"Match": "Resultado",
"Add": "Adicionar",
"Edit": "Editar",
"DocumentPreview": "Visualizar",
"MakePrivate": "Tornar privado",
"MakePrivateDescription": "Apenas os membros podem ver",
"Created": "Criado",
"Selected": "Selecionado",
"NoResults": "Sem resultados para mostrar",
"Next": "Próximo",
"FailedToPreview": "Falha ao pré-visualizar",
"ContentType": "Tipo de conteúdo",
"ContentTypeNotSupported": "A visualização não está disponível para este tipo de conteúdo",
"StartDrawing": "Começar a desenhar",
"DrawingHistory": "Histórico de desenhos",
"ColorAdd": "Adicionar predefinição de cor",
"ColorRemove": "Remover predefinição de cor",
"ColorReset": "Resetar predefinições de cores",
"Copy": "Copiar",
"DocumentUrlCopied": "Copiado!",
"CopyLink": "Copiar link",
"AccessDenied": "O objeto não existe ou você não tem permissão para acessá-lo.",
"UnableToFollowMention": "Não foi possível abrir o objeto mencionado",
"Undo": "Desfazer",
"Redo": "Refazer",
"ClearCanvas": "Limpar tela",
"PenTool": "Caneta",
"EraserTool": "Borracha",
"PanTool": "Mover",
"TextTool": "Texto",
"LineTool": "Linha",
"RectangleTool": "Retângulo",
"EllipseTool": "Elipse",
"PaletteManagementMenu": "Gerenciar predefinições de cor"
},
"status": {
"FileTooLarge": "Arquivo muito grande"
}
}
+12 -8
View File
@@ -891,15 +891,19 @@ export function isSpaceClass (_class: Ref<Class<Doc>>): boolean {
}
export function setPresentationCookie (token: string, workspaceUuid: WorkspaceUuid): void {
function setToken (path: string): void {
const res =
encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-')) +
'=' +
encodeURIComponent(token) +
`; path=${path}`
document.cookie = res
const cookieName = encodeURIComponent(plugin.metadata.Token.replaceAll(':', '-'))
const cookieValue = encodeURIComponent(token)
const storage = getMetadata(plugin.metadata.FileStorage)
if (storage !== undefined) {
let path = `/files/${workspaceUuid}`
try {
path = storage.getCookiePath(workspaceUuid)
} catch {}
const normalized = path.startsWith('/') ? path : `/${path}`
document.cookie = `${cookieName}=${cookieValue}; path=${normalized}`
}
setToken('/files/' + workspaceUuid)
}
export const upgradeDownloadProgress = writable(-1)
+2
View File
@@ -886,6 +886,7 @@ a.no-line {
font-size: 1rem;
color: var(--theme-caption-color);
user-select: none;
text-rendering: optimizeLegibility;
}
.trans-title {
text-transform: uppercase;
@@ -893,6 +894,7 @@ a.no-line {
font-size: .75rem;
color: var(--theme-dark-color);
user-select: none;
text-rendering: optimizeLegibility;
}
.text-16px { font-size: 16px; }
.text-xs { font-size: .625rem; }
+1
View File
@@ -65,6 +65,7 @@
min-width: 0;
line-height: 150%;
color: var(--theme-caption-color);
text-rendering: optimizeLegibility;
&.accentHeader { font-size: 1rem; }
}
+1
View File
@@ -55,6 +55,7 @@
font-size: 1.25rem;
line-height: 1.5rem;
color: var(--global-primary-TextColor);
text-rendering: optimizeLegibility;
&:not(.withButton) {
min-height: 4.25rem;
+15 -2
View File
@@ -574,13 +574,26 @@ pre.proseCodeBlock>pre.proseCode {
.mermaidPreviewContainer {
padding: 0.5rem;
cursor: default;
overflow-x: auto;
overflow: auto;
}
&:not(.folded) .mermaidPreviewContainer {
border-top: 1px solid var(--border-color);
min-height: 6rem;
}
.mermaidPreview {
width: 100%;
display: flex;
justify-content: center;
svg {
width: auto;
max-width: 100%;
height: auto;
display: block;
}
}
}
.proseInlineCommentHighlight {
@@ -602,4 +615,4 @@ pre.proseCodeBlock>pre.proseCode {
.theme-light {
@include meta.load-css('./github-light.scss');
}
}
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -50,6 +50,7 @@
"English": "Englisch",
"Russian": "Russisch",
"Portuguese": "Portugiesisch",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Spanisch",
"Chinese": "Chinesisch",
"French": "Französisch",
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -50,6 +50,7 @@
"English": "Inglese",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+114
View File
@@ -0,0 +1,114 @@
{
"string": {
"EditBoxPlaceholder": "Digite o texto...",
"Ok": "Ok",
"Cancel": "Cancelar",
"Save": "Salvar",
"Publish": "Publicar",
"SaveDraft": "Salvar rascunho",
"MinutesAgo": "{minutes, plural, =0 {há menos de um minuto} =1 {há um minuto} other {há # minutos}}",
"HoursAgo": "{hours, plural, =0 {há menos de uma hora} =1 {há uma hora} other {há # horas}}",
"DaysAgo": "{days, plural, =0 {hoje} =1 {ontem} other {há # dias}}",
"MonthsAgo": "{months, plural, =0 {este mês} =1 {há um mês} other {há # meses}}",
"YearsAgo": "{years, plural, =0 {este ano} =1 {há um ano} other {há # anos}}",
"Minutes": "{minutes, plural, =0 {menos de um minuto} =1 {um minuto} other {# minutos}}",
"Hours": "{hours, plural, =0 {menos de uma hora} =1 {uma hora} other {# horas}}",
"Days": "{days, plural, =0 {hoje} =1 {1 dia} other {# dias}}",
"Weeks": "{weeks, plural, =0 {esta semana} =1 {1 semana} other {# semanas}}",
"Months": "{months, plural, =0 {este mês} =1 {1 mês} other {# meses}}",
"Years": "{years, plural, =0 {este ano} =1 {um ano} other {# anos}}",
"Before": "Antes",
"After": "Depois",
"MinutesShort": "{value}m",
"HoursShort": "{value}h",
"DaysShort": "{value}d",
"ShowMore": "Mostrar mais",
"ShowLess": "Mostrar menos",
"Search": "Pesquisar",
"SearchDots": "Pesquisar...",
"SearchResults": "Resultados da pesquisa",
"Suggested": "Sugestões",
"TimeTooltip": "{value}",
"SelectDate": "Selecionar data",
"None": "Nenhum",
"NotSelected": "Não selecionado",
"Today": "Hoje",
"NoDate": "Sem data",
"StartDate": "Data de início",
"TargetDate": "Data alvo",
"Overdue": "Atrasado",
"DueDate": "Data de vencimento",
"AddDueDate": "Adicionar data de vencimento",
"SetDueDate": "Definir data de vencimento",
"EditDueDate": "Editar data de vencimento",
"SaveDueDate": "Salvar data de vencimento",
"NeedsToBeCompletedByThisDate": "Precisa ser concluído até esta data",
"DueDatePopupTitle": "Vence em {value}",
"DueDatePopupOverdueTitle": "Venceu em {value}",
"DueDatePopupDescription": "{value, plural, =0 {Hoje} =1 {Amanhã} other {# dias restantes}}",
"DueDatePopupOverdueDescription": "{value, plural, =1 {1 dia em atraso} other {# dias em atraso}}",
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
"Italian": "Italiano",
"Czech": "Čeština",
"German": "Deutsch",
"Japanese": "日本語",
"Turkish": "Türkçe",
"MinutesBefore": "{minutes, plural, =1 {um minuto antes} other {# minutos antes}}",
"HoursBefore": "{hours, plural, =1 {uma hora antes} other {# horas antes}}",
"DaysBefore": "{days, plural, =1 {um dia antes} other {# dias antes}}",
"WeeksBefore": "{weeks, plural, =1 {uma semana antes} other {# semanas antes}}",
"MonthsBefore": "{months, plural, =1 {um mês antes} other {# meses antes}}",
"MinutesAfter": "{minutes, plural, =1 {em um minuto} other {em # minutos}}",
"HoursAfter": "{hours, plural, =1 {em uma hora} other {em # horas}}",
"DaysAfter": "{days, plural, =1 {em um dia} other {em # dias}}",
"WeeksAfter": "{weeks, plural, =1 {daqui a um dia} other {daqui a # semanas}}",
"MonthsAfter": "{months, plural, =1 {daqui a um mês} other {daqui a # meses}}",
"NoActionsDefined": "Nenhuma ação aplicável",
"Incoming": "Entrada",
"HoursLabel": "Horas",
"Back": "Voltar",
"Next": "Próximo",
"DropdownDefaultLabel": "Selecionar item",
"DD": "DD",
"MM": "MM",
"YYYY": "AAAA",
"HH": "HH",
"Remove": "Remover",
"NoResults": "Sem resultados",
"MoreCount": "{count} mais",
"Spacious": "Espaçoso",
"Compact": "Compacto",
"ThemeLight": "Claro",
"ThemeDark": "Escuro",
"ThemeSystem": "Sistema",
"NoTimeZonesFound": "Nenhum fuso horário encontrado",
"Selected": "Selecionado:",
"Submit": "Enviar",
"NextStep": "Próximo passo",
"TypeHere": "Digite aqui...",
"NormalSize": "Tamanho normal",
"FullSize": "Tamanho completo",
"UseMaxWidth": "Largura máxima",
"Sidebar": "Barra lateral",
"FontSize": "Tamanho da fonte",
"Language": "Idioma",
"DaysWOValue": "{days, plural, =1 {dia} other {dias}}",
"WeeksWOValue": "{weeks, plural, =1 {semana} other {semanas}}",
"MonthsWOValue": "{months, plural, =1 {mês} other {meses}}",
"Filter": "Filtrar",
"Clear": "Limpar",
"Yesterday": "Ontem",
"ThisWeek": "Esta semana",
"ThisMonth": "Este mês",
"ThisYear": "Este ano",
"EmojiStyle": "Estilo de emoji",
"EmojiSystem": "Padrão do sistema",
"EmojiNoto": "Noto Color"
}
}
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -51,6 +51,7 @@
"Russian": "Русский",
"Spanish": "Español",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Chinese": "中文",
"French": "Français",
"Italian": "Italiano",
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
+1
View File
@@ -50,6 +50,7 @@
"English": "English",
"Russian": "Русский",
"Portuguese": "Português",
"PortugueseBrazil": "Português (Brasil)",
"Spanish": "Español",
"Chinese": "中文",
"French": "Français",
-2
View File
@@ -49,8 +49,6 @@
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"dompurify": "^3.1.6",
"emojibase": "^16.0.0",
"emojibase-regex": "^16.0.0",
"fast-equals": "^5.2.2",
"svelte": "^4.2.20",
"plyr": "3.7.8",
@@ -0,0 +1,52 @@
<!--
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { onDestroy } from 'svelte'
import EmbeddedPDF from './EmbeddedPDF.svelte'
import Loading from './Loading.svelte'
export let src: string
export let name: string
export let fit: boolean = false
export let css: string | undefined = undefined
let iframeSrc: string | undefined
async function loadFile (src: string): Promise<void> {
if (iframeSrc !== undefined) {
URL.revokeObjectURL(iframeSrc)
iframeSrc = undefined
}
const response = await fetch(src)
const blob = await response.blob()
iframeSrc = URL.createObjectURL(blob)
}
$: void loadFile(src)
onDestroy(() => {
if (iframeSrc !== undefined) {
URL.revokeObjectURL(iframeSrc)
}
})
</script>
{#if iframeSrc}
<EmbeddedPDF src={iframeSrc} {name} {fit} {css} />
{:else}
<Loading />
{/if}
+1 -29
View File
@@ -14,29 +14,11 @@
-->
<script lang="ts">
import { onDestroy } from 'svelte'
import Loading from './Loading.svelte'
export let src: string
export let name: string
export let fit: boolean = false
export let css: string | undefined = undefined
let iframeSrc: string | undefined = undefined
async function loadFile (src: string): Promise<void> {
if (iframeSrc !== undefined) {
URL.revokeObjectURL(iframeSrc)
iframeSrc = undefined
}
const response = await fetch(src)
const blob = await response.blob()
iframeSrc = URL.createObjectURL(blob)
}
$: void loadFile(src)
let iframe: HTMLIFrameElement | undefined = undefined
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
@@ -58,19 +40,9 @@
}
}
}
onDestroy(() => {
if (iframeSrc !== undefined) {
URL.revokeObjectURL(iframeSrc)
}
})
</script>
{#if iframeSrc}
<iframe bind:this={iframe} class:fit src={iframeSrc + '#view=FitH&navpanes=0'} title={name} on:load />
{:else}
<Loading />
{/if}
<iframe bind:this={iframe} class:fit src={src + '#view=FitH&navpanes=0'} title={name} on:load />
<style lang="scss">
iframe {
@@ -72,6 +72,7 @@
const langs = [
{ id: 'en', label: ui.string.English, logo: '&#x1F1FA;&#x1F1F8;' },
{ id: 'pt', label: ui.string.Portuguese, logo: '&#x1F1F5;&#x1F1F9;' },
{ id: 'pt-br', label: ui.string.PortugueseBrazil, logo: '&#x1F1E7;&#x1F1F7;' },
{ id: 'es', label: ui.string.Spanish, logo: '&#x1F1EA;&#x1F1F8;' },
{ id: 'ru', label: ui.string.Russian, logo: '&#x1F1F7;&#x1F1FA;' },
{ id: 'zh', label: ui.string.Chinese, logo: '&#x1F1E8;&#x1F1F3;' },
+1
View File
@@ -284,6 +284,7 @@ export { default as CodeForm } from './components/CodeForm.svelte'
export { default as CodeInput } from './components/CodeInput.svelte'
export { default as TimeLeft } from './components/TimeLeft.svelte'
export { default as SectionEmpty } from './components/SectionEmpty.svelte'
export { default as EmbeddedHTML } from './components/EmbeddedHTML.svelte'
export { default as EmbeddedPDF } from './components/EmbeddedPDF.svelte'
export { default as NestedMenu } from './components/NestedMenu.svelte'
export { default as NestedDropdown } from './components/NestedDropdown.svelte'
+1
View File
@@ -83,6 +83,7 @@ export const uis = plugin(uiId, {
German: '' as IntlString,
Japanese: '' as IntlString,
Turkish: '' as IntlString,
PortugueseBrazil: '' as IntlString,
MinutesBefore: '' as IntlString,
HoursBefore: '' as IntlString,
DaysBefore: '' as IntlString,
@@ -0,0 +1,8 @@
{
"string": {
"Achievements": "Conquistas",
"EarliestAdopter": "Primeiro usuário",
"Epic": "Épico",
"Legendary": "Legendário"
}
}
+60
View File
@@ -0,0 +1,60 @@
{
"string": {
"Activity": "Atividade",
"Added": "adicionado",
"All": "Tudo",
"AllActivity": "Toda a atividade",
"Attributes": "Atributos",
"Changed": "alterado",
"CollectionUpdated": "Atualizar {collection}",
"Created": "Criado",
"DocAdded": "adicionado {_class}",
"DocCreated": "criado {_class}",
"DocDeleted": "eliminado {_class}",
"Edit": "Editar",
"Edited": "Editado",
"Emoji": "Emoji",
"For": "Para",
"From": "de",
"In": "Em",
"LastReply": "Última resposta",
"New": "Novo",
"NewestFirst": "Mais recente primeiro",
"Pinned": "Fixado",
"Reacted": "Reagiu",
"Reactions": "Reações",
"Reaction": "Reação",
"Removed": "removido",
"Replies": "Respostas",
"RepliesCount": "{replies, plural, =1 {# resposta} other {# respostas}}",
"Set": "definido",
"To": "para",
"Unset": "Não definido",
"Update": "Atualizar",
"Updated": "Atualizado",
"UpdatedCollection": "Atualizado",
"Message": "Mensagem",
"Mentioned": "Mencionado",
"You": "Tu",
"Mentions": "Menções",
"MentionedYouIn": "Você Foi Mencionado em {title}",
"Messages": "Mensagens",
"Thread": "Tópico",
"AddReaction": "Adicionar Reação",
"SaveForLater": "Guardar para mais tarde",
"RemoveFromLater": "Remover dos Guardados",
"ReactionNotificationTitle": "Reação em {title}",
"ReactionNotificationBody": "{senderName}: {reaction}",
"NewObject": "Novo {object}",
"RemovedObject": "Removido {object}",
"ChangedObject": "Alterado {object}",
"UnsetObject": "Não definido {object}",
"UpdatedObject": "Atualizado {object}",
"NewObjectType": "Novo {type}: {title}",
"RemovedObjectType": "Removido {type} : {title}",
"AttributeSetTo": "{name} definido para {value}",
"AddedTag": "Tag adicionado: {title}",
"RemovedTag": "Tag removido: {title}",
"ValueTooLarge": "Valor muito grande para mostrar"
}
}
@@ -0,0 +1,13 @@
{
"string": {
"IntegrationLabel": "Assistente de IA",
"IntegrationDescription": "Seu assistente pessoal alimentado por IA que ajuda a organizar, automatizar e simplificar seu fluxo de trabalho diário na plataforma",
"ConfigLabel": "Assistente de IA",
"ConfigDescription": "Assistente pessoal alimentado por IA",
"Connect": "Conectar",
"Cancel": "Cancelar",
"Status": "Status",
"FailedToConnect": "O serviço do Assistente de IA não está disponível",
"Configure": "Configurar Assistente de IA"
}
}

Some files were not shown because too many files have changed in this diff Show More