Merge remote-tracking branch 'origin/develop' into staging

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2024-11-02 18:56:41 +07:00
155 changed files with 1329 additions and 244 deletions
+110 -14
View File
@@ -272,6 +272,9 @@ dependencies:
'@rush-temp/guest-resources':
specifier: file:./projects/guest-resources.tgz
version: file:projects/guest-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
'@rush-temp/highlight':
specifier: file:./projects/highlight.tgz
version: file:projects/highlight.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
'@rush-temp/hr':
specifier: file:./projects/hr.tgz
version: file:projects/hr.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
@@ -1232,6 +1235,9 @@ dependencies:
'@types/crypto-js':
specifier: ^4.1.1
version: 4.2.2
'@types/csvtojson':
specifier: ^2.0.0
version: 2.0.0
'@types/diff':
specifier: ~5.0.2
version: 5.0.9
@@ -23178,6 +23184,56 @@ packages:
typescript: 5.3.3
dev: false
/svelte-preprocess@5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.6.2):
resolution: {integrity: sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==}
engines: {node: '>= 16.0.0', pnpm: ^8.0.0}
requiresBuild: true
peerDependencies:
'@babel/core': ^7.10.2
coffeescript: ^2.5.1
less: ^3.11.3 || ^4.0.0
postcss: ^7 || ^8
postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
pug: ^3.0.0
sass: ^1.26.8
stylus: ^0.55.0
sugarss: ^2.0.0 || ^3.0.0 || ^4.0.0
svelte: ^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0
typescript: '>=3.9.5 || ^4.0.0 || ^5.0.0'
peerDependenciesMeta:
'@babel/core':
optional: true
coffeescript:
optional: true
less:
optional: true
postcss:
optional: true
postcss-load-config:
optional: true
pug:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
typescript:
optional: true
dependencies:
'@types/pug': 2.0.10
detect-indent: 6.1.0
magic-string: 0.30.7
postcss: 8.4.35
postcss-load-config: 4.0.2(postcss@8.4.35)(ts-node@10.9.2)
sass: 1.71.1
sorcery: 0.11.0
strip-indent: 3.0.0
svelte: 4.2.12
typescript: 5.6.2
dev: false
/svelte@4.2.11:
resolution: {integrity: sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==}
engines: {node: '>=16'}
@@ -27467,7 +27523,7 @@ packages:
dev: false
file:projects/diffview-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-fi0UUvQTrAAt70IEF8CC7O6h5UfknnXO0xHylpL/Wn7h6oOQHlfPl9VhAx0PJW6TDvw3CltEE35pVSLyDml1CA==, tarball: file:projects/diffview-resources.tgz}
resolution: {integrity: sha512-KZ6MRvsPCXYmTb05Jv0YgDwYMREnlmcyVC06L20SivCexX7nymqt6ul7oKdfu2keE9+qrIJjYtqHxAQUKQ4gFA==, tarball: file:projects/diffview-resources.tgz}
id: file:projects/diffview-resources.tgz
name: '@rush-temp/diffview-resources'
version: 0.0.0
@@ -28170,6 +28226,53 @@ packages:
- ts-node
dev: false
file:projects/highlight.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-xj/v9VAzl0qNSFarmoAujWSXP28ZkYEnnOBqx55GxBMULn54HimvBalNi/tymMwJV0LUSQbPyJbCY+HHV1tcuQ==, tarball: file:projects/highlight.tgz}
id: file:projects/highlight.tgz
name: '@rush-temp/highlight'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
highlight.js: 11.8.0
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
lowlight: 3.1.0
prettier: 3.2.5
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
sass: 1.71.1
svelte: 4.2.12
svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)
svelte-eslint-parser: 0.33.1(svelte@4.2.12)
svelte-loader: 3.2.0(svelte@4.2.12)
svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.6.2)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- pug
- stylus
- sugarss
- supports-color
- ts-node
dev: false
file:projects/hr-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-NHRl6EHEnHdUWlcw0CbP7m1ZIKiRytNEO0fXtbXW8aREXmUcplLyHsE6lf0f7Y3iDxtPUm0V7DdfKtmzkuMNtA==, tarball: file:projects/hr-assets.tgz}
id: file:projects/hr-assets.tgz
@@ -28356,7 +28459,7 @@ packages:
dev: false
file:projects/import-tool.tgz:
resolution: {integrity: sha512-QBo/RgAGnie95pESqQNJUQSCpQi1rEpXqZz1G1zZMfhAvisZtW2XutmaGMS1oDu2ZB05pk8WOO8x6bfdc+keZg==, tarball: file:projects/import-tool.tgz}
resolution: {integrity: sha512-KD9EB2XXo43QYnx8xp4j9z62JGValTBx2uTS4Egw11rsdAoENIxNxI+68CIKASfY8DMKMMuTmUZ6N6nK/6XSiw==, tarball: file:projects/import-tool.tgz}
name: '@rush-temp/import-tool'
version: 0.0.0
dependencies:
@@ -30465,7 +30568,7 @@ packages:
dev: false
file:projects/notification-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-xpvJbpFBrKB3RPG8SgT9+cJj2IkysBNcl4jQAV69ACUvnTQMh4RS9P31zpaO7CObYXrm9/ImRF0fXbH+AkiJsg==, tarball: file:projects/notification-resources.tgz}
resolution: {integrity: sha512-7qZrSN1845Nti1pousixaoWtM8ISlPYqgw5pK4UtuhEPdrfSq2NkCSSMnl3pOeSU6bVFzAeJ6YlTZs8Jl2wJ7A==, tarball: file:projects/notification-resources.tgz}
id: file:projects/notification-resources.tgz
name: '@rush-temp/notification-resources'
version: 0.0.0
@@ -31360,7 +31463,7 @@ packages:
dev: false
file:projects/pod-server.tgz(utf-8-validate@6.0.4):
resolution: {integrity: sha512-R0fObVPXSToiK7ioPeEbZ1gZFFjswVFLPCojWYsoNsHg9TL+kTNUdST63PJr4GCr8XVv9VplfYYcEs7qSARvAw==, tarball: file:projects/pod-server.tgz}
resolution: {integrity: sha512-boGyT8HkEJkWqcXXAAZ869HB68/GpHcMTUBZlhkUS9oztHADMfpAbGQ+yVVvypye4e9nsYTF1LhpsDrToOtPYw==, tarball: file:projects/pod-server.tgz}
id: file:projects/pod-server.tgz
name: '@rush-temp/pod-server'
version: 0.0.0
@@ -32804,7 +32907,7 @@ packages:
dev: false
file:projects/server-ai-bot-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-m+ZYZxCuYOJxhC31c3misAQofis4BG32ek1OPxXXdpOOxPQUFI1WjmwVqJlZDTz3Yt9EXTGv3qkvrNwJuIyWZA==, tarball: file:projects/server-ai-bot-resources.tgz}
resolution: {integrity: sha512-UChmRA0uWxgRKIx9jl1+P4wlGdsdLjsMkR3VOFR3Ya/sYwYV78JOCBIvnsTox/vJha8hx4mynqxaTCSe2nqshA==, tarball: file:projects/server-ai-bot-resources.tgz}
id: file:projects/server-ai-bot-resources.tgz
name: '@rush-temp/server-ai-bot-resources'
version: 0.0.0
@@ -32823,20 +32926,13 @@ packages:
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@aws-sdk/credential-providers'
- '@babel/core'
- '@jest/types'
- '@mongodb-js/zstd'
- '@types/node'
- babel-jest
- babel-plugin-macros
- esbuild
- gcp-metadata
- kerberos
- mongodb-client-encryption
- node-notifier
- snappy
- socks
- supports-color
- ts-node
dev: false
@@ -33152,7 +33248,7 @@ packages:
dev: false
file:projects/server-client.tgz(bufferutil@4.0.8)(esbuild@0.20.1)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-L1a1mgOD2Qd1S0E6twotWEVeiFeZfvIK5v93gaPH+A5LkkQoOi4qXoEQv/Udul/cDP/MxPFA9HQ3WbdMIn8XYg==, tarball: file:projects/server-client.tgz}
resolution: {integrity: sha512-ixXx7EdCjt/KMKk6pVedAj/27hWqDwCBnHhmTJc694OR8wTEHyIaohP9t8gcjka/ugJRJJjOmAsF1sd8HphW4g==, tarball: file:projects/server-client.tgz}
id: file:projects/server-client.tgz
name: '@rush-temp/server-client'
version: 0.0.0
@@ -35802,7 +35898,7 @@ packages:
dev: false
file:projects/text-editor-resources.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.20.1)(highlight.js@11.8.0)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-uffVYh80gs1A2XBETq/xa9TBTeZRZxTMb7sD/mjAvolpsXBd/cPCSKqhytZaoEFqIAFnZ6KfEMUqTlNIki4MoQ==, tarball: file:projects/text-editor-resources.tgz}
resolution: {integrity: sha512-0IJEwSBOHUuSL2q4FSBnhVcfzwK0qOc5KcM7cKXbOM8GO/+WgyGRtKULl1b3LDjuBC21wst3eZY3o03qFIBDIQ==, tarball: file:projects/text-editor-resources.tgz}
id: file:projects/text-editor-resources.tgz
name: '@rush-temp/text-editor-resources'
version: 0.0.0
+2 -2
View File
@@ -1,5 +1,5 @@
### Supported Import Options
1. **Notion**: see [Import from Notion Guide](./notion/README.md)
2. **ClickUp**: see [Import from ClickUp Guide](./clickup/README.md)
1. **Notion**: see [Import from Notion Guide](./src/notion/README.md)
2. **ClickUp**: see [Import from ClickUp Guide](./src/clickup/README.md)
+1 -1
View File
@@ -62,7 +62,7 @@ interface ImportIssueEx extends ImportIssue {
}
class ClickupMarkdownPreprocessor implements MarkdownPreprocessor {
private readonly MENTION_REGEX = /@([A-Za-z]+ [A-Za-z]+)/g
private readonly MENTION_REGEX = /@([\p{L}\p{M}]+ [\p{L}\p{M}]+)/gu
constructor (private readonly personsByName: Map<string, Ref<Person>>) {}
process (json: MarkupNode): MarkupNode {
+14 -1
View File
@@ -21,7 +21,7 @@ import { type Resource } from '@hcengineering/platform'
import serverCore, { type TriggerControl } from '@hcengineering/server-core'
import tracker from '@hcengineering/tracker'
import serverTime, { type ToDoFactory, type OnToDo } from '@hcengineering/server-time'
import { type ToDo, type WorkSlot } from '@hcengineering/time'
import time, { type ToDo, type WorkSlot } from '@hcengineering/time'
@Mixin(serverTime.mixin.ToDoFactory, core.class.Class)
export class TToDoFactory extends TClass implements ToDoFactory {
@@ -45,6 +45,7 @@ export function createModel (builder: Builder): void {
trigger: serverTime.trigger.OnToDoUpdate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx.objectClass': time.class.ToDo,
'tx._class': core.class.TxUpdateDoc
}
})
@@ -53,6 +54,7 @@ export function createModel (builder: Builder): void {
trigger: serverTime.trigger.OnToDoRemove,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx.objectClass': time.class.ToDo,
'tx._class': core.class.TxRemoveDoc
}
})
@@ -61,6 +63,7 @@ export function createModel (builder: Builder): void {
trigger: serverTime.trigger.OnToDoCreate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx.objectClass': time.class.ToDo,
'tx._class': core.class.TxCreateDoc
},
isAsync: true
@@ -70,10 +73,20 @@ export function createModel (builder: Builder): void {
trigger: serverTime.trigger.OnWorkSlotCreate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx.objectClass': time.class.WorkSlot,
'tx._class': core.class.TxCreateDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnWorkSlotUpdate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx.objectClass': time.class.WorkSlot,
'tx._class': core.class.TxUpdateDoc
}
})
builder.mixin(tracker.class.Issue, core.class.Class, serverTime.mixin.ToDoFactory, {
factory: serverTime.function.IssueToDoFactory
})
+398
View File
@@ -0,0 +1,398 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Timestamp } from '../classes'
import { getDay, convertToDay } from '..'
const supportedTimezones: string[] = [
'Europe/Andorra',
'Asia/Dubai',
'Asia/Kabul',
'Europe/Tirane',
'Asia/Yerevan',
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Mawson',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
'America/Argentina/Buenos_Aires',
'America/Argentina/Cordoba',
'America/Argentina/Salta',
'America/Argentina/Jujuy',
'America/Argentina/Tucuman',
'America/Argentina/Catamarca',
'America/Argentina/La_Rioja',
'America/Argentina/San_Juan',
'America/Argentina/Mendoza',
'America/Argentina/San_Luis',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Ushuaia',
'Pacific/Pago_Pago',
'Europe/Vienna',
'Australia/Lord_Howe',
'Antarctica/Macquarie',
'Australia/Hobart',
'Australia/Currie',
'Australia/Melbourne',
'Australia/Sydney',
'Australia/Broken_Hill',
'Australia/Brisbane',
'Australia/Lindeman',
'Australia/Adelaide',
'Australia/Darwin',
'Australia/Perth',
'Australia/Eucla',
'Asia/Baku',
'America/Barbados',
'Asia/Dhaka',
'Europe/Brussels',
'Europe/Sofia',
'Atlantic/Bermuda',
'Asia/Brunei',
'America/La_Paz',
'America/Noronha',
'America/Belem',
'America/Fortaleza',
'America/Recife',
'America/Araguaina',
'America/Maceio',
'America/Bahia',
'America/Sao_Paulo',
'America/Campo_Grande',
'America/Cuiaba',
'America/Santarem',
'America/Porto_Velho',
'America/Boa_Vista',
'America/Manaus',
'America/Eirunepe',
'America/Rio_Branco',
'America/Nassau',
'Asia/Thimphu',
'Europe/Minsk',
'America/Belize',
'America/St_Johns',
'America/Halifax',
'America/Glace_Bay',
'America/Moncton',
'America/Goose_Bay',
'America/Blanc-Sablon',
'America/Toronto',
'America/Nipigon',
'America/Thunder_Bay',
'America/Iqaluit',
'America/Pangnirtung',
'America/Atikokan',
'America/Winnipeg',
'America/Rainy_River',
'America/Resolute',
'America/Rankin_Inlet',
'America/Regina',
'America/Swift_Current',
'America/Edmonton',
'America/Cambridge_Bay',
'America/Yellowknife',
'America/Inuvik',
'America/Creston',
'America/Dawson_Creek',
'America/Fort_Nelson',
'America/Vancouver',
'America/Whitehorse',
'America/Dawson',
'Indian/Cocos',
'Europe/Zurich',
'Africa/Abidjan',
'Pacific/Rarotonga',
'America/Santiago',
'America/Punta_Arenas',
'Pacific/Easter',
'Asia/Shanghai',
'Asia/Urumqi',
'America/Bogota',
'America/Costa_Rica',
'America/Havana',
'Atlantic/Cape_Verde',
'America/Curacao',
'Indian/Christmas',
'Asia/Nicosia',
'Asia/Famagusta',
'Europe/Prague',
'Europe/Berlin',
'Europe/Copenhagen',
'America/Santo_Domingo',
'Africa/Algiers',
'America/Guayaquil',
'Pacific/Galapagos',
'Europe/Tallinn',
'Africa/Cairo',
'Africa/El_Aaiun',
'Europe/Madrid',
'Africa/Ceuta',
'Atlantic/Canary',
'Europe/Helsinki',
'Atlantic/Stanley',
'Pacific/Chuuk',
'Pacific/Pohnpei',
'Atlantic/Faroe',
'Europe/Paris',
'Europe/London',
'Asia/Tbilisi',
'America/Cayenne',
'Africa/Accra',
'Europe/Gibraltar',
'America/Godthab',
'America/Danmarkshavn',
'America/Scoresbysund',
'America/Thule',
'Europe/Athens',
'Atlantic/South_Georgia',
'America/Guatemala',
'Pacific/Guam',
'Africa/Bissau',
'America/Guyana',
'Asia/Hong_Kong',
'America/Tegucigalpa',
'America/Port-au-Prince',
'Europe/Budapest',
'Asia/Jakarta',
'Asia/Pontianak',
'Asia/Makassar',
'Asia/Jayapura',
'Europe/Dublin',
'Asia/Jerusalem',
'Asia/Kolkata',
'Indian/Chagos',
'Asia/Baghdad',
'Asia/Tehran',
'Atlantic/Reykjavik',
'Europe/Rome',
'America/Jamaica',
'Asia/Amman',
'Asia/Tokyo',
'Africa/Nairobi',
'Asia/Bishkek',
'Asia/Pyongyang',
'Asia/Seoul',
'Asia/Almaty',
'Asia/Qyzylorda',
'Asia/Qostanay',
'Asia/Aqtobe',
'Asia/Aqtau',
'Asia/Atyrau',
'Asia/Oral',
'Asia/Beirut',
'Asia/Colombo',
'Africa/Monrovia',
'Europe/Vilnius',
'Europe/Luxembourg',
'Europe/Riga',
'Africa/Tripoli',
'Africa/Casablanca',
'Europe/Monaco',
'Europe/Chisinau',
'Asia/Yangon',
'Asia/Ulaanbaatar',
'Asia/Hovd',
'Asia/Choibalsan',
'Asia/Macau',
'America/Martinique',
'Europe/Malta',
'Indian/Mauritius',
'Indian/Maldives',
'America/Mexico_City',
'America/Cancun',
'America/Merida',
'America/Monterrey',
'America/Matamoros',
'America/Mazatlan',
'America/Chihuahua',
'America/Ojinaga',
'America/Hermosillo',
'America/Tijuana',
'America/Bahia_Banderas',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Africa/Maputo',
'Africa/Windhoek',
'Pacific/Noumea',
'Africa/Lagos',
'America/Managua',
'Europe/Amsterdam',
'Europe/Oslo',
'Asia/Kathmandu',
'Pacific/Niue',
'America/Panama',
'America/Lima',
'Pacific/Tahiti',
'Pacific/Marquesas',
'Pacific/Gambier',
'Pacific/Port_Moresby',
'Pacific/Bougainville',
'Asia/Manila',
'Asia/Karachi',
'Europe/Warsaw',
'America/Miquelon',
'Pacific/Pitcairn',
'America/Puerto_Rico',
'Asia/Gaza',
'Asia/Hebron',
'Europe/Lisbon',
'Atlantic/Madeira',
'Atlantic/Azores',
'Pacific/Palau',
'America/Asuncion',
'Asia/Qatar',
'Indian/Reunion',
'Europe/Bucharest',
'Europe/Belgrade',
'Europe/Kaliningrad',
'Europe/Moscow',
'Europe/Simferopol',
'Europe/Kirov',
'Europe/Astrakhan',
'Europe/Volgograd',
'Europe/Saratov',
'Europe/Ulyanovsk',
'Europe/Samara',
'Asia/Yekaterinburg',
'Asia/Omsk',
'Asia/Novosibirsk',
'Asia/Barnaul',
'Asia/Tomsk',
'Asia/Novokuznetsk',
'Asia/Krasnoyarsk',
'Asia/Irkutsk',
'Asia/Chita',
'Asia/Yakutsk',
'Asia/Khandyga',
'Asia/Vladivostok',
'Asia/Ust-Nera',
'Asia/Magadan',
'Asia/Sakhalin',
'Asia/Srednekolymsk',
'Asia/Riyadh',
'Pacific/Guadalcanal',
'Indian/Mahe',
'Africa/Khartoum',
'Europe/Stockholm',
'Asia/Singapore',
'America/Paramaribo',
'Africa/Juba',
'Africa/Sao_Tome',
'America/El_Salvador',
'Asia/Damascus',
'America/Grand_Turk',
'Africa/Ndjamena',
'Indian/Kerguelen',
'Asia/Bangkok',
'Asia/Dushanbe',
'Asia/Dili',
'Asia/Ashgabat',
'Africa/Tunis',
'Europe/Istanbul',
'America/Port_of_Spain',
'Asia/Taipei',
'Europe/Kiev',
'Europe/Uzhgorod',
'Europe/Zaporozhye',
'America/New_York',
'America/Detroit',
'America/Kentucky/Louisville',
'America/Kentucky/Monticello',
'America/Indiana/Indianapolis',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Vevay',
'America/Chicago',
'America/Indiana/Tell_City',
'America/Indiana/Knox',
'America/Menominee',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/North_Dakota/Beulah',
'America/Denver',
'America/Boise',
'America/Phoenix',
'America/Los_Angeles',
'America/Anchorage',
'America/Juneau',
'America/Sitka',
'America/Metlakatla',
'America/Yakutat',
'America/Nome',
'America/Adak',
'Pacific/Honolulu',
'America/Montevideo',
'Asia/Samarkand',
'Asia/Tashkent',
'America/Caracas',
'Asia/Ho_Chi_Minh',
'Pacific/Efate',
'Africa/Johannesburg'
]
function testGetDay (date: Date, timezone: string): void {
const timestamp: Timestamp = getDay(date.getTime())
const convertedDate: Date = new Date(timestamp)
const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' })
const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone })
expect(convertedLocaleDate).toEqual(originalLocaleDate)
}
function testConvertToDay (date: Date, timezone: string): void {
const convertedDate: Date = convertToDay(date)
const originalLocaleDate: string = date.toLocaleDateString('en-US', { timeZone: 'Europe/London' })
const convertedLocaleDate: string = convertedDate.toLocaleDateString('en-US', { timeZone: timezone })
expect(convertedLocaleDate).toEqual(originalLocaleDate)
}
describe('time', () => {
it.each(supportedTimezones)('dates are matched in `getDay` for locale %p', (timezone: string) => {
testGetDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone)
testGetDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone)
testGetDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone)
testGetDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone)
testGetDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone)
})
it.each(supportedTimezones)('dates are matched in `convertToDay` for locale %p', (timezone: string) => {
testConvertToDay(new Date(Date.UTC(1995, 11, 3, 12, 0)), timezone)
testConvertToDay(new Date(Date.UTC(2025, 0, 23, 55, 0)), timezone)
testConvertToDay(new Date(Date.UTC(2020, 4, 12, 30, 50)), timezone)
testConvertToDay(new Date(Date.UTC(2024, 5, 12, 30, 50)), timezone)
testConvertToDay(new Date(Date.UTC(2024, 7, 0, 0, 0)), timezone)
})
it.each([
[0, 0, 0, 0],
[7, 59, 59, 999],
[12, 0, 0, 0],
[23, 59, 59, 999]
])(
'dates are matched for time [h: %p, m: %p, s: %p, ms: %p]',
(hours: number, minutes: number, seconds: number, milliSeconds: number) => {
const date: Date = new Date()
const expectedDay: number = date.getDate()
date.setHours(hours, minutes, seconds, milliSeconds)
const convertedDate: Date = convertToDay(date)
expect(convertedDate.getDate()).toEqual(expectedDay)
}
)
})
+12 -2
View File
@@ -16,8 +16,18 @@ import { Timestamp } from './classes'
export function getDay (time: Timestamp): Timestamp {
const date: Date = new Date(time)
date.setHours(0, 0, 0, 0)
return date.getTime()
return convertToDay(date).getTime()
}
export function convertToDay (date: Date): Date {
const originalDay: number = date.getDate()
const convertedDate: Date = new Date(date)
// Set 12 AM UTC time, since it will be the same day in most timezones
convertedDate.setUTCHours(12, 0, 0, 0)
if (convertedDate.getDate() !== originalDay) {
convertedDate.setDate(originalDay)
}
return convertedDate
}
export function getHour (time: Timestamp): Timestamp {
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}
+46
View File
@@ -0,0 +1,46 @@
{
"name": "@hcengineering/highlight",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Hardcore Engineering Inc.",
"license": "EPL-2.0",
"scripts": {
"build": "compile ui",
"build:docs": "api-extractor run --local",
"format": "format src",
"svelte-check": "do-svelte-check",
"_phase:svelte-check": "do-svelte-check",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"svelte-loader": "^3.2.0",
"sass": "^1.53.0",
"svelte-preprocess": "^5.1.3",
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"prettier-plugin-svelte": "^3.2.2",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"svelte-check": "^3.6.9",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"eslint-plugin-svelte": "^2.35.1",
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"svelte": "^4.2.12",
"@hcengineering/ui": "^0.6.15",
"highlight.js": "~11.8.0",
"lowlight": "^3.1.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
@@ -1,5 +1,5 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
@@ -13,16 +13,29 @@
// limitations under the License.
//
import hljs from 'highlight.js'
import { hljsDefineSvelte } from './languages/svelte-hljs'
import hljs, { type LanguageFn } from 'highlight.js'
import { all } from 'lowlight'
import { svelte } from './languages/svelte'
import { vlang } from './languages/vlang'
hljs.registerLanguage('svelte', hljsDefineSvelte)
/** @public */
export const grammars: Record<string, LanguageFn> = {
...all,
svelte,
vlang
}
Object.entries(grammars).forEach((grammar) => {
hljs.registerLanguage(...grammar)
})
/** @public */
export interface HighlightOptions {
auto?: boolean
language: string | undefined
}
/** @public */
export function highlightText (text: string, options: HighlightOptions): string {
// We should always use highlighter because it sanitizes the input
// We have to always use highlighter to ensure that the input is sanitized
@@ -38,6 +51,7 @@ export function highlightText (text: string, options: HighlightOptions): string
return normalizeHighlightTags(highlighted)
}
/** @public */
export function highlightLines (lines: string[], options: HighlightOptions): string[] {
const highlighted = highlightText(lines.join('\n'), options)
return highlighted.split('\n')
@@ -1,4 +1,19 @@
export function hljsDefineSvelte (hljs: any): any {
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export function svelte (hljs: any): any {
return {
subLanguage: 'xml',
contains: [
+161
View File
@@ -0,0 +1,161 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
const LITERALS = ['true', 'false', 'nil']
const BUILT_INS = [
'close',
'dump',
'eprint',
'eprintln',
'error',
'exit',
'it',
'panic',
'print_backtrace',
'print',
'println'
]
const TYPES = [
'bool',
'string',
'i8',
'i16',
'i32',
'i64',
'i128',
'int',
'u8',
'u16',
'u32',
'u64',
'u128',
'rune',
'f32',
'f64',
'isize',
'usize',
'voidptr',
'charptr',
'byteptr',
'thread'
]
const KWS = [
'as',
'asm',
'assert',
'atomic',
'break',
'const',
'continue',
'defer',
'else',
'enum',
'fn',
'for',
'go',
'goto',
'if',
'import',
'in',
'interface',
'is',
'isreftype',
'lock',
'match',
'module',
'mut',
'nil',
'none',
'or',
'pub',
'return',
'rlock',
'select',
'shared',
'sizeof',
'spawn',
'static',
'struct',
'type',
'typeof',
'union',
'unsafe',
'volatile',
'__global',
'__offsetof'
]
export function vlang (hljs: any): any {
const KEYWORDS = {
keyword: KWS,
type: TYPES,
literal: LITERALS,
built_in: BUILT_INS
}
return {
name: 'V',
aliases: ['vlang'],
keywords: KEYWORDS,
illegal: '</',
contains: [
hljs.C_LINE_COMMENT_MODE,
hljs.C_BLOCK_COMMENT_MODE,
{
className: 'string',
variants: [
hljs.QUOTE_STRING_MODE,
hljs.APOS_STRING_MODE,
{
begin: '`',
end: '`'
}
]
},
{
className: 'number',
variants: [
{
begin: hljs.C_NUMBER_RE + '[i]',
relevance: 1
},
hljs.C_NUMBER_MODE
]
},
{
begin: /:=/ // relevance booster
},
{
className: 'function',
beginKeywords: 'fn',
end: '\\s*(\\{|$)',
excludeEnd: true,
contains: [
hljs.TITLE_MODE,
{
className: 'params',
begin: /\(/,
end: /\)/,
endsParent: true,
keywords: KEYWORDS,
illegal: /['']/
}
]
}
]
}
}
+5
View File
@@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types"
}
}
@@ -58,5 +58,8 @@
"@typescript-eslint/no-floating-promises": "warn"
}
}
]
],
"rules": {
"svelte/no-at-html-tags": "error"
}
}
+1
View File
@@ -1053,6 +1053,7 @@ a.no-line {
.bottom-divider { border-bottom: 1px solid var(--theme-divider-color); }
.left-divider { border-left: 1px solid var(--theme-divider-color); }
.right-divider { border-right: 1px solid var(--theme-divider-color); }
.right-navpanel-border { border-right: 1px solid var(--theme-navpanel-border); }
.bottom-highlight-select { border-bottom: 1px solid var(--highlight-select); }
+3 -2
View File
@@ -13,11 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import dompurify from 'dompurify'
import DOMPurify from 'dompurify'
export let value: string
$: sanitized = dompurify.sanitize(value)
$: sanitized = DOMPurify.sanitize(value)
</script>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html sanitized}
@@ -30,5 +30,6 @@
{#if text}
<!-- "replaceURLs" produces sanitazed string -->
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html replaceURLs(text)}
{/if}
+14 -5
View File
@@ -38,6 +38,8 @@
let sState: SeparatorState
$: sState = typeof float === 'string' ? SeparatorState.FLOAT : float ? SeparatorState.HIDDEN : SeparatorState.NORMAL
const checkFullWidth = (): boolean =>
sState === SeparatorState.FLOAT && $deviceInfo.isMobile && $deviceInfo.isPortrait
const direction: 'horizontal' | 'vertical' = 'horizontal'
let separators: SeparatedItem[] | null = null
@@ -67,6 +69,10 @@
let disabled: boolean = false
let side: 'start' | 'end' | undefined = undefined
$: fs = $deviceInfo.fontSize
const remToPx = (rem: number): number => rem * fs
const pxToRem = (px: number): number => px / fs
const fetchSeparators = (): void => {
const res = getSeparators(name, float)
if (res !== null && !Array.isArray(res)) panel = res
@@ -90,10 +96,6 @@
checkSizes()
}
$: fs = $deviceInfo.fontSize
const remToPx = (rem: number): number => rem * fs
const pxToRem = (px: number): number => px / fs
const convertSize = (prop: TSeparatedItem): string => (typeof prop === 'number' ? `${prop}px` : '')
const setSize = (element: HTMLElement, size: TSeparatedItem, next: boolean = false): void => {
@@ -229,6 +231,12 @@
const checkSizes = (): void => {
if (sState === SeparatorState.FLOAT) {
if (checkFullWidth() && panel != null) {
const s = pxToRem(window.innerWidth)
panel.size = s
panel.maxSize = s
panel.minSize = s
}
if (parentElement != null && panel != null) initSize(parentElement, panel)
} else if (sState === SeparatorState.NORMAL) {
if (prevElement != null && prevElSize != null) initSize(prevElement, prevElSize)
@@ -442,12 +450,13 @@
}
} else if (sState === SeparatorState.FLOAT && parentElement != null) {
parentElement.style.pointerEvents = 'all'
saveSeparator(name, float, panel)
if (!checkFullWidth()) saveSeparator(name, float, panel)
}
document.body.style.cursor = ''
}
function pointerDown (event: PointerEvent): void {
if (checkFullWidth()) return
prepareSeparation(event)
document.addEventListener('pointermove', pointerMove)
document.addEventListener('pointerup', pointerUp)
@@ -13,7 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { DateRangeMode } from '@hcengineering/core'
import { DateRangeMode, convertToDay } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import {
@@ -56,10 +56,10 @@
const saveDate = (withTime: boolean = false): void => {
if (currentDate) {
if (!withTime) {
currentDate.setHours(0)
currentDate.setMinutes(0)
currentDate = convertToDay(currentDate)
} else {
currentDate.setSeconds(0, 0)
}
currentDate.setSeconds(0, 0)
viewDate = currentDate = currentDate
dispatch('update', currentDate)
}
@@ -16,6 +16,7 @@
import { createEventDispatcher } from 'svelte'
import Label from '../Label.svelte'
import IconCheck from '../icons/Check.svelte'
import Html from '../Html.svelte'
export let langs: any
export let selected: string
@@ -34,7 +35,7 @@
dispatch('close', lang.id)
}}
>
<div class="svg-16px flex-no-shrink text-16px mr-2">{@html lang.logo}</div>
<div class="svg-16px flex-no-shrink text-16px mr-2"><Html value={lang.logo} /></div>
<span class="overflow-label flex-grow"><Label label={lang.label} /></span>
<div class="ap-check">
{#if lang.id === selected}
@@ -17,6 +17,7 @@
import { getMetadata } from '@hcengineering/platform'
import ui, { showPopup, deviceOptionsStore as deviceInfo } from '../..'
import LangPopup from './LangPopup.svelte'
import Html from '../Html.svelte'
let pressed: boolean = false
@@ -70,5 +71,7 @@
class:pressed
on:click={selectLanguage}
>
{@html selected?.logo}
{#if selected}
<Html value={selected.logo} />
{/if}
</button>
+1 -1
View File
@@ -148,7 +148,7 @@ export function saveSeparator (
}
export const panelSeparators: DefSeparators = [
{ minSize: 30, size: 'auto', maxSize: 'auto' },
{ minSize: 20, size: 'auto', maxSize: 'auto' },
{ minSize: 17, size: 25, maxSize: 35, float: 'aside' }
]
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import { getMetadata, translate } from '@hcengineering/platform'
import { Button, IconClose, Label, themeStore } from '@hcengineering/ui'
import { Button, Html, IconClose, Label, themeStore } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import calendar from '../plugin'
import { concatLink } from '@hcengineering/core'
@@ -72,7 +72,7 @@
<Label label={calendar.string.RedirectGoogle} />
</div>
<div class="mt-2">
{@html label}
<Html value={label} />
</div>
<img class="image" src={getMetadata(calendar.image.Permissions)} alt="" />
@@ -184,7 +184,7 @@
{getName(client.getHierarchy(), selected)}
{/if}
{:else}
<div class="flex-presenter not-selected">
<div class="flex-presenter not-selected" class:cursor-default={readonly}>
{#if icon}
<div class="icon w-4 flex-no-shrink" class:small-gap={size === 'inline' || size === 'small'}>
<Icon {icon} size={'small'} />
@@ -97,6 +97,7 @@
on:close={() => {
dispatch('close')
}}
withoutInput={readonly}
>
<svelte:fragment slot="title">
<DocNavLink noUnderline {object}>
@@ -106,7 +107,7 @@
<svelte:fragment slot="attributes" let:direction={dir}>
{#if dir === 'column'}
<DocAttributeBar {object} {mixins} {ignoreKeys} />
<DocAttributeBar {object} {mixins} {ignoreKeys} {readonly} />
{/if}
</svelte:fragment>
@@ -117,14 +118,16 @@
</svelte:fragment>
<svelte:fragment slot="utils">
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
{#if !readonly}
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
{/if}
<Button
icon={IconMixin}
kind={'icon'}
@@ -137,7 +140,7 @@
</svelte:fragment>
<div class="flex-col flex-grow flex-no-shrink step-tb-6">
<EditOrganization {object} />
<EditOrganization {object} {readonly} />
<div class="flex-col flex-grow w-full mt-6 relative">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
@@ -145,6 +148,7 @@
key={{ key: 'description', attr: descriptionKey }}
placeholder={core.string.Description}
enableAttachments={false}
{readonly}
on:saved={(evt) => {
saved = evt.detail
}}
@@ -103,6 +103,7 @@
{kind}
{size}
{justify}
disabled={readonly}
showTooltip={label ? { label, direction: labelDirection } : undefined}
on:click={addPerson}
>
@@ -148,7 +148,7 @@
{/key}
</div>
{#if rejectingMessage !== undefined && approver.approved === 'rejected'}
<div class="reject-message">{@html rejectingMessage}</div>
<div class="reject-message">{rejectingMessage}</div>
{/if}
{/each}
</div>
+1 -1
View File
@@ -45,9 +45,9 @@
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/highlight": "^0.6.0",
"@hcengineering/diffview": "^0.6.0",
"fast-equals": "^5.0.1",
"highlight.js": "~11.8.0",
"diff2html": "~3.4.35"
}
}
@@ -13,10 +13,10 @@
// limitations under the License.
-->
<script lang="ts">
import { Loading, themeStore } from '@hcengineering/ui'
import { Html, Loading, themeStore } from '@hcengineering/ui'
import { DiffFile, DiffLine, DiffLineType, DiffViewMode } from '@hcengineering/diffview'
import { DiffLineRenderResult, RenderOptions, renderHunk } from '../highlight'
import { DiffLineRenderResult, RenderOptions, renderHunk } from '../render'
export let file: DiffFile
export let mode: DiffViewMode
@@ -110,7 +110,7 @@
<td class="num-line {lineClass}">{line.oldNumber ?? ''}</td>
<td class="num-line {lineClass}">{line.newNumber ?? ''}</td>
<td class="code-line select-text {lineClass}" data-code-marker={line.prefix}>
{@html line.content}
<Html value={line.content} />
</td>
</tr>
{/each}
@@ -130,11 +130,11 @@
<tr>
<td class="num-line {beforeLineClass}">{before.oldNumber ?? ''}</td>
<td class="code-line select-text {beforeLineClass}" data-code-marker={before.prefix}>
{@html before.content}
<Html value={before.content} />
</td>
<td class="num-line {afterLineClass}">{after.newNumber ?? ''}</td>
<td class="code-line select-text {afterLineClass}" data-code-marker={after.prefix}>
{@html after.content}
<Html value={after.content} />
</td>
</tr>
{/each}
@@ -13,10 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import { highlightText } from '@hcengineering/highlight'
import { Html } from '@hcengineering/ui'
import { highlightText } from '../highlight'
export let value: string
export let language: string | undefined = undefined
@@ -1,17 +0,0 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
export * from './highlight'
export * from './render'
+2
View File
@@ -14,9 +14,11 @@
//
import { type Resources } from '@hcengineering/platform'
import DiffView from './components/DiffView.svelte'
import Highlight from './components/Highlight.svelte'
import InlineDiffView from './components/InlineDiffView.svelte'
export default async (): Promise<Resources> => ({
component: {
DiffView,
@@ -14,7 +14,7 @@
//
import { type DiffHunk, type DiffLine, DiffLineType, EmptyLine } from '@hcengineering/diffview'
import { type HighlightOptions, highlightLines } from './highlight'
import { type HighlightOptions, highlightLines } from '@hcengineering/highlight'
export interface RenderOptions {
syntaxHighlight: {
@@ -15,7 +15,7 @@
-->
<script lang="ts">
import { getMetadata, translate } from '@hcengineering/platform'
import { Button, IconClose, Label, themeStore } from '@hcengineering/ui'
import { Button, Html, IconClose, Label, themeStore } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import gmail from '../plugin'
import { concatLink } from '@hcengineering/core'
@@ -70,7 +70,7 @@
<div class="content">
<Label label={gmail.string.RedirectGoogle} />
<div class="mt-2">
{@html label}
<Html value={label} />
</div>
<div class="footer">
@@ -28,7 +28,8 @@
getCurrentLocation,
showPopup,
type AnySvelteComponent,
type CompAndProps
type CompAndProps,
resizeObserver
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import plugin from '../plugin'
@@ -51,6 +52,7 @@
import CamSettingPopup from './CamSettingPopup.svelte'
import MicSettingPopup from './MicSettingPopup.svelte'
import RoomAccessPopup from './RoomAccessPopup.svelte'
import { afterUpdate } from 'svelte'
export let room: Room
export let fullScreen: boolean = false
@@ -59,6 +61,11 @@
const allowShare: boolean = true
let allowLeave: boolean = false
let popup: CompAndProps | undefined = undefined
let grow: HTMLElement
let leftPanel: HTMLElement
let leftPanelSize: number = 0
let noLabel: boolean = false
let combinePanel: boolean = false
$: allowCam = $currentRoom?.type === RoomType.Video
$: allowLeave = $myInfo?.room !== ($myOffice?._id ?? plugin.ids.Reception)
@@ -149,9 +156,22 @@
const camKeys = client.getModel().findAllSync(view.class.Action, { _id: plugin.action.ToggleVideo })?.[0]?.keyBinding
const micKeys = client.getModel().findAllSync(view.class.Action, { _id: plugin.action.ToggleMic })?.[0]?.keyBinding
const checkBar = (): void => {
if (grow === undefined || leftPanel === undefined) return
if (!noLabel && leftPanel.clientWidth > leftPanelSize) leftPanelSize = leftPanel.clientWidth
if (grow.clientWidth - 16 < leftPanel.clientWidth && !noLabel && !combinePanel) noLabel = true
else if (grow.clientWidth - 16 < leftPanel.clientWidth && noLabel && !combinePanel) combinePanel = true
else if (grow.clientWidth * 2 - 32 > leftPanel.clientWidth && noLabel && combinePanel) combinePanel = false
else if (grow.clientWidth - 32 >= leftPanelSize && noLabel && !combinePanel) noLabel = false
}
afterUpdate(() => {
checkBar()
})
</script>
<div class="bar w-full flex-center flex-gap-2 flex-no-shrink">
<div class="bar w-full flex-center flex-gap-2 flex-no-shrink" class:combinePanel use:resizeObserver={checkBar}>
<div bind:this={grow} class="flex-grow" />
{#if room._id !== plugin.ids.Reception}
<ModernButton
icon={roomAccessIcon[room.access]}
@@ -205,7 +225,7 @@
/>
{/if}
{/if}
<div class="bar__left-panel flex-gap-2 flex-center">
<div bind:this={leftPanel} class="bar__left-panel flex-gap-2 flex-center">
{#if $isConnected}
<ModernButton
icon={$isFullScreen ? love.icon.ExitFullScreen : love.icon.FullScreen}
@@ -232,7 +252,7 @@
{#if allowLeave}
<ModernButton
icon={plugin.icon.LeaveRoom}
label={plugin.string.LeaveRoom}
label={noLabel ? undefined : plugin.string.LeaveRoom}
tooltip={{ label: plugin.string.LeaveRoom, direction: 'top' }}
kind={'negative'}
size={'large'}
@@ -240,6 +260,7 @@
/>
{/if}
</div>
<div class="flex-grow" />
{#if popup && fullScreen}
<PopupInstance
is={popup.is}
@@ -259,6 +280,7 @@
<style lang="scss">
.bar {
overflow-x: auto;
position: relative;
padding: 1rem;
border-top: 1px solid var(--theme-divider-color);
@@ -270,5 +292,9 @@
right: 1rem;
height: 100%;
}
&.combinePanel .bar__left-panel {
position: static;
}
}
</style>
@@ -17,7 +17,7 @@
import { personByIdStore } from '@hcengineering/contact-resources'
import { Room as TypeRoom } from '@hcengineering/love'
import { getMetadata } from '@hcengineering/platform'
import { Label, Loading, resizeObserver } from '@hcengineering/ui'
import { Label, Loading, resizeObserver, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import {
LocalParticipant,
LocalTrackPublication,
@@ -333,7 +333,7 @@
$: if (((document.fullscreenElement && !$isFullScreen) || $isFullScreen) && roomEl) toggleFullscreen()
</script>
<div bind:this={roomEl} class="flex-col-center w-full h-full" class:theme-dark={$isFullScreen}>
<div bind:this={roomEl} class="flex-col-center w-full h-full right-navpanel-border" class:theme-dark={$isFullScreen}>
{#if $isConnected && !$isCurrentInstanceConnected}
<div class="flex justify-center error h-full w-full clear-mins">
<Label label={love.string.AnotherWindowError} />
@@ -345,7 +345,13 @@
{:else if loading}
<Loading />
{/if}
<div class="room-container" class:sharing={$screenSharing} class:many={columns > 3} class:hidden={loading}>
<div
class="room-container"
class:sharing={$screenSharing}
class:many={columns > 3}
class:hidden={loading}
class:mobile={$deviceInfo.isMobile}
>
<div class="screenContainer">
<video class="screen" bind:this={screen}></video>
</div>
@@ -452,6 +458,15 @@
gap: 0.5rem;
}
}
&.mobile {
padding: var(--spacing-0_5);
&:not(.sharing) .videoGrid,
&.sharing {
gap: var(--spacing-0_5);
}
}
}
.hidden {
display: none;
@@ -24,8 +24,8 @@
import SectionEmpty from './SectionEmpty.svelte'
export let objectId: Ref<Doc>
export let applications: number
export let readonly: boolean = false
const createApp = (ev: MouseEvent): void => {
showPopup(CreateApplication, { candidate: objectId, preserveCandidate: true }, ev.target as HTMLElement)
@@ -46,7 +46,9 @@
bind:loading
bind:preference
/>
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={createApp} />
{#if !readonly}
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={createApp} />
{/if}
</div>
</svelte:fragment>
@@ -58,15 +60,18 @@
config={preference?.config ?? viewlet.config}
query={{ attachedTo: objectId, ...(viewlet?.baseQuery ?? {}) }}
loadingProps={{ length: applications }}
{readonly}
/>
</Scroller>
{:else}
<SectionEmpty icon={FileDuo} label={recruit.string.NoApplicationsForTalent}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline content-color" on:click={createApp}>
<Label label={recruit.string.CreateAnApplication} />
</span>
{#if !readonly}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline content-color" on:click={createApp}>
<Label label={recruit.string.CreateAnApplication} />
</span>
{/if}
</SectionEmpty>
{/if}
</svelte:fragment>
@@ -31,6 +31,7 @@
export let _id: Ref<Vacancy>
export let embedded: boolean = false
export let readonly = false
let object: Required<Vacancy>
let rawName: string = ''
@@ -140,6 +141,7 @@
<DocAttributeBar
{object}
{mixins}
{readonly}
ignoreKeys={['name', 'fullDescription', 'private', 'archived', 'type', 'owners']}
/>
{/if}
@@ -151,6 +153,7 @@
kind={'large-style'}
focusable
autoFocus={!embedded}
disabled={readonly}
on:blur={save}
/>
@@ -160,14 +163,16 @@
{/if}
</svelte:fragment>
<svelte:fragment slot="utils">
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
{#if !readonly}
<Button
icon={IconMoreH}
iconProps={{ size: 'medium' }}
kind={'icon'}
on:click={(e) => {
showMenu(e, { object, excludedActions: [view.action.Open] })
}}
/>
{/if}
<Button
icon={IconMixin}
kind={'icon'}
@@ -187,6 +192,7 @@
key={{ key: 'fullDescription', attr: descriptionKey }}
bind:this={descriptionBox}
placeholder={recruit.string.FullDescription}
{readonly}
on:saved={(evt) => {
saved = evt.detail
}}
@@ -194,7 +200,7 @@
</div>
<div class="w-full mt-6">
<VacancyApplications objectId={object._id} />
<VacancyApplications objectId={object._id} {readonly} />
</div>
<div class="w-full mt-6">
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: tracker.string.RelatedIssues }} />
@@ -26,6 +26,7 @@
import SectionEmpty from './SectionEmpty.svelte'
export let objectId: Ref<Vacancy>
export let readonly = false
let applications: number
const query = createQuery()
@@ -60,7 +61,9 @@
bind:preference
bind:loading
/>
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={createApp} />
{#if !readonly}
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={createApp} />
{/if}
</div>
</div>
{#if applications > 0}
@@ -71,6 +74,7 @@
config={preference?.config ?? viewlet.config}
query={{ space: objectId }}
loadingProps={{ length: applications }}
{readonly}
/>
</Scroller>
{:else}
@@ -80,9 +84,11 @@
<SectionEmpty icon={FileDuo} label={recruit.string.NoApplicationsForVacancy}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline content-color" on:click={createApp}>
<Label label={recruit.string.CreateAnApplication} />
</span>
{#if !readonly}
<span class="over-underline content-color" on:click={createApp}>
<Label label={recruit.string.CreateAnApplication} />
</span>
{/if}
</SectionEmpty>
{/if}
</div>
@@ -72,11 +72,13 @@
</Scroller>
{:else}
<SectionEmpty icon={FileDuo} label={recruit.string.NoVacancies}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline content-color" on:click={createApp}>
<Label label={recruit.string.CreateVacancy} />
</span>
{#if !readonly}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline content-color" on:click={createApp}>
<Label label={recruit.string.CreateVacancy} />
</span>
{/if}
</SectionEmpty>
{/if}
</div>
@@ -22,6 +22,7 @@
export let object: Doc
export let _class: Ref<Class<Doc>>
export let key: KeyedAttribute
export let readonly: boolean = false
const client = getClient()
@@ -56,6 +57,7 @@
<TagsEditor
{key}
{readonly}
bind:items
targetClass={_class}
on:open={(evt) => addRef(evt.detail)}
@@ -46,6 +46,7 @@
export let key: KeyedAttribute
export let showTitle = true
export let schema: '0' | '3' | '9' = key.attr.schema ?? '0'
export let readonly = false
let elements: IdMap<TagElement> = new Map()
const elementQuery = createQuery()
@@ -113,13 +114,15 @@
<Label label={key.attr.label} />
</span>
<div class="buttons-group x-small">
<Button
icon={IconAdd}
kind={'ghost'}
showTooltip={{ label: tags.string.AddTagTooltip, props: { word: keyLabel } }}
id={'add-tag'}
on:click={addTag}
/>
{#if !readonly}
<Button
icon={IconAdd}
kind={'ghost'}
showTooltip={{ label: tags.string.AddTagTooltip, props: { word: keyLabel } }}
id={'add-tag'}
on:click={addTag}
/>
{/if}
</div>
</div>
{/if}
@@ -45,6 +45,7 @@
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/core": "^0.6.32",
"@hcengineering/highlight": "^0.6.0",
"@hcengineering/view": "^0.6.13",
"@hcengineering/text": "^0.6.5",
"@hcengineering/text-editor": "^0.6.0",
@@ -13,6 +13,7 @@
// limitations under the License.
//
import { grammars } from '@hcengineering/highlight'
import { backtickInputRegex, codeBlockOptions, tildeInputRegex } from '@hcengineering/text'
import { DropdownLabelsPopup, getEventPositionElement, showPopup } from '@hcengineering/ui'
import { isActive, textblockTypeInputRule } from '@tiptap/core'
@@ -20,7 +21,7 @@ import { type CodeBlockLowlightOptions, CodeBlockLowlight } from '@tiptap/extens
import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
import { Plugin, PluginKey } from '@tiptap/pm/state'
import { Decoration, DecorationSet, type EditorView } from '@tiptap/pm/view'
import { common, createLowlight } from 'lowlight'
import { createLowlight } from 'lowlight'
import { isChangeEditable } from './editable'
type Lowlight = ReturnType<typeof createLowlight>
@@ -31,7 +32,7 @@ const chevronSvg = `<svg width="16" height="16" viewBox="0 0 32 32" fill="curren
export const codeBlockHighlightOptions: CodeBlockLowlightOptions = {
...codeBlockOptions,
lowlight: createLowlight(common)
lowlight: createLowlight(grammars)
}
export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowlightOptions>({
@@ -16,8 +16,8 @@
import { createQuery } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../plugin'
import ToDoPresenter from './ToDoPresenter.svelte'
import calendar from '@hcengineering/calendar'
export let event: WorkSlot
export let oneRow: boolean = false
@@ -26,13 +26,15 @@
let todo: ToDo
const query = createQuery()
$: query.query(event.attachedToClass, { _id: event.attachedTo }, (res) => {
todo = res[0]
})
$: !hideDetails
? query.query(event.attachedToClass, { _id: event.attachedTo }, (res) => {
todo = res[0]
})
: query.unsubscribe()
</script>
{#if hideDetails}
<Label label={time.string.WorkSlot} />
<Label label={calendar.string.Busy} />
{:else if todo}
<ToDoPresenter value={todo} withoutSpace={oneRow} />
{/if}
@@ -106,14 +106,15 @@
&:not(.readonly) {
cursor: pointer;
}
.add-action {
visibility: hidden;
}
&:hover {
.add-action {
visibility: visible;
visibility: hidden;
}
&:hover {
.add-action {
visibility: visible;
}
}
}
}
@@ -14,7 +14,7 @@
-->
<script lang="ts">
import Play from '../icons/Play.svelte'
import { getPlatformColor, themeStore } from '@hcengineering/ui'
import { Html, getPlatformColor, themeStore } from '@hcengineering/ui'
export let href: string
const maxWidth = 400
@@ -75,7 +75,7 @@
</div>
{:else}
<div bind:this={emb}>
{@html data.html}
<Html value={data.html} />
</div>
{/if}
</div>
@@ -647,6 +647,18 @@
const checkOnHide = (): void => {
if ($deviceInfo.navigator.visible && $deviceInfo.docWidth <= 1024) $deviceInfo.navigator.visible = false
}
let oldNavVisible: boolean = $deviceInfo.navigator.visible
let oldASideVisible: boolean = $deviceInfo.aside.visible
$: if (oldNavVisible !== $deviceInfo.navigator.visible || oldASideVisible !== $deviceInfo.aside.visible) {
if ($deviceInfo.isMobile && $deviceInfo.isPortrait && $deviceInfo.navigator.float) {
if ($deviceInfo.navigator.visible && $deviceInfo.aside.visible) {
if (oldNavVisible) $deviceInfo.navigator.visible = false
else $deviceInfo.aside.visible = false
}
}
oldNavVisible = $deviceInfo.navigator.visible
oldASideVisible = $deviceInfo.aside.visible
}
$: $deviceInfo.navigator.direction = $deviceInfo.isMobile && $deviceInfo.isPortrait ? 'horizontal' : 'vertical'
let appsMini: boolean
$: appsMini =
@@ -57,23 +57,25 @@
</div>
</div>
{:else if !mini}
<ScrollerBar bind:scroller padding={'.25rem 0'}>
<div class="flex-row-center gap-1 py-1 overflow-x-auto">
{#each $tabsStore as tab (tab._id)}
<WorkbenchTabPresenter {tab} />
{/each}
</ScrollerBar>
</div>
{:else if selectedTab !== undefined}
<WorkbenchTabPresenter tab={selectedTab} />
<ButtonIcon
bind:element
icon={IconMoreH}
iconProps={{ fill: 'var(--theme-dark-color)' }}
size={'extra-small'}
kind={'tertiary'}
hasMenu
{pressed}
on:click={showTabs}
/>
{#if $tabsStore.length > 1}
<ButtonIcon
bind:element
icon={IconMoreH}
iconProps={{ fill: 'var(--theme-dark-color)' }}
size={'extra-small'}
kind={'tertiary'}
hasMenu
{pressed}
on:click={showTabs}
/>
{/if}
{/if}
{#if !popup}
<ButtonIcon
+5
View File
@@ -445,6 +445,11 @@
"projectFolder": "packages/storage",
"shouldPublish": false
},
{
"packageName": "@hcengineering/highlight",
"projectFolder": "packages/highlight",
"shouldPublish": false
},
{
"packageName": "@hcengineering/collaboration",
"projectFolder": "server/collaboration",
+8 -8
View File
@@ -49,7 +49,7 @@ import {
} from '@hcengineering/server-notification-resources'
async function getOldDepartment (
currentTx: TxMixin<Employee, Staff> | TxUpdateDoc<Employee>,
currentTx: TxMixin<Employee, Staff>,
control: TriggerControl
): Promise<Ref<Department> | undefined> {
const txes = await control.findAll<TxMixin<Employee, Staff>>(
@@ -73,7 +73,7 @@ async function getOldDepartment (
async function buildHierarchy (_id: Ref<Department>, control: TriggerControl): Promise<Department[]> {
const res: Department[] = []
const ancestors = new Map<Ref<Department>, Ref<Department>>()
const departments = await control.findAll(control.ctx, hr.class.Department, {})
const departments = await control.queryFind(control.ctx, hr.class.Department, {})
for (const department of departments) {
if (department._id === hr.ids.Head || department.parent === undefined) continue
ancestors.set(department._id, department.parent)
@@ -240,20 +240,20 @@ export async function OnEmployee (tx: Tx, control: TriggerControl): Promise<Tx[]
*/
export async function OnEmployeeDeactivate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
if (core.class.TxUpdateDoc !== actualTx._class) {
if (core.class.TxMixin !== actualTx._class) {
return []
}
const ctx = actualTx as TxUpdateDoc<Employee>
if (ctx.objectClass !== contact.mixin.Employee || ctx.operations.active !== false) {
const ctx = actualTx as TxMixin<Person, Employee>
if (ctx.mixin !== contact.mixin.Employee || ctx.attributes.active !== false) {
return []
}
const targetAccount = control.modelDb.getAccountByPersonId(ctx.objectId) as PersonAccount[]
if (targetAccount.length === 0) return []
const lastDepartment = await getOldDepartment(ctx, control)
if (lastDepartment === undefined) return []
const set = new Set(targetAccount.map((it) => it._id))
const removed = await buildHierarchy(lastDepartment, control)
const departments = await control.queryFind(control.ctx, hr.class.Department, {})
const removed = departments.filter((dep) => dep.members.some((p) => set.has(p)))
return getTxes(
control.txFactory,
targetAccount.map((it) => it._id),
@@ -607,6 +607,8 @@ export async function createPushNotification (
}
}
const errorMessages = ['expired', 'Unregistered', 'No such subscription']
async function sendPushToSubscription (
control: TriggerControl,
targetUser: Ref<Account>,
@@ -617,9 +619,11 @@ async function sendPushToSubscription (
await webpush.sendNotification(subscription, JSON.stringify(data))
} catch (err) {
control.ctx.info('Cannot send push notification to', { user: targetUser, err })
if (err instanceof WebPushError && err.body.includes('expired')) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply(control.ctx, [tx])
if (err instanceof WebPushError) {
if (errorMessages.some((p) => (err as WebPushError).body.includes(p))) {
const tx = control.txFactory.createTxRemoveDoc(subscription._class, subscription.space, subscription._id)
await control.apply(control.ctx, [tx])
}
}
}
}
+13 -17
View File
@@ -85,20 +85,19 @@ export async function applicationTextPresenter (doc: Doc, control: TriggerContro
* @public
*/
export async function OnRecruitUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx)
const cud = actualTx as TxCUD<Doc>
if (!control.hierarchy.isDerived(cud.objectClass, recruit.class.Vacancy)) {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
if (!control.hierarchy.isDerived(actualTx.objectClass, recruit.class.Vacancy)) {
return []
}
const res: Tx[] = []
if (actualTx._class === core.class.TxCreateDoc) {
handleVacancyCreate(control, cud, actualTx, res)
handleVacancyCreate(control, actualTx, res)
} else if (actualTx._class === core.class.TxUpdateDoc) {
await handleVacancyUpdate(control, cud, res)
await handleVacancyUpdate(control, actualTx, res)
} else if (actualTx._class === core.class.TxRemoveDoc) {
await handleVacancyRemove(control, cud, actualTx)
handleVacancyRemove(control, actualTx, res)
}
return res
}
@@ -156,16 +155,13 @@ async function handleVacancyUpdate (control: TriggerControl, cud: TxCUD<Doc>, re
}
}
async function handleVacancyRemove (control: TriggerControl, cud: TxCUD<Doc>, actualTx: Tx): Promise<void> {
const removeTx = actualTx as TxRemoveDoc<Vacancy>
function handleVacancyRemove (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[]): void {
const removeTx = cud as TxRemoveDoc<Vacancy>
// It could be null or new value
const txes = (
await control.findAll(control.ctx, core.class.TxCUD, {
objectId: removeTx.objectId
})
).filter((it) => it._id !== removeTx._id)
const vacancy = TxProcessor.buildDoc2Doc(txes) as Vacancy
const res: Tx[] = []
const vacancy = control.removedMap.get(removeTx.objectId) as Vacancy
if (vacancy === undefined) {
return
}
if (vacancy.company != null) {
// We have old value
res.push(
@@ -182,8 +178,8 @@ async function handleVacancyRemove (control: TriggerControl, cud: TxCUD<Doc>, ac
}
}
function handleVacancyCreate (control: TriggerControl, cud: TxCUD<Doc>, actualTx: Tx, res: Tx[]): void {
const createTx = actualTx as TxCreateDoc<Vacancy>
function handleVacancyCreate (control: TriggerControl, cud: TxCUD<Doc>, res: Tx[]): void {
const createTx = cud as TxCreateDoc<Vacancy>
const vacancy = TxProcessor.createDoc2Doc(createTx)
if (vacancy.company !== undefined) {
res.push(
+33 -1
View File
@@ -69,6 +69,21 @@ export async function OnTask (tx: Tx, control: TriggerControl): Promise<Tx[]> {
return []
}
export async function OnWorkSlotUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<WorkSlot>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
const updTx = actualTx as TxUpdateDoc<WorkSlot>
const visibility = updTx.operations.visibility
if (visibility !== undefined) {
const workslot = (await control.findAll(control.ctx, time.class.WorkSlot, { _id: updTx.objectId }, { limit: 1 }))[0]
if (workslot === undefined) return []
const todo = (await control.findAll(control.ctx, time.class.ToDo, { _id: workslot.attachedTo }))[0]
return [control.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, { visibility })]
}
return []
}
export async function OnWorkSlotCreate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<WorkSlot>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.WorkSlot)) return []
@@ -578,6 +593,22 @@ async function changeIssueStatusHandler (
}
}
}
} else if (status.category === task.statusCategory.Won || status.category === task.statusCategory.Lost) {
const issue = (await control.findAll(control.ctx, tracker.class.Issue, { _id: issueId }))[0]
if (issue !== undefined) {
const todos = await control.findAll(control.ctx, time.class.ToDo, {
attachedTo: issue._id,
doneOn: null
})
const res: Tx[] = []
const now = Date.now()
for (const todo of todos) {
if (todo.doneOn == null) {
res.push(control.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, { doneOn: now }))
}
}
return res
}
}
return []
}
@@ -648,6 +679,7 @@ export default async () => ({
OnToDoUpdate,
OnToDoRemove,
OnToDoCreate,
OnWorkSlotCreate
OnWorkSlotCreate,
OnWorkSlotUpdate
}
})
+2 -1
View File
@@ -56,6 +56,7 @@ export default plugin(serverTimeId, {
OnToDoUpdate: '' as Resource<TriggerFunc>,
OnToDoRemove: '' as Resource<TriggerFunc>,
OnToDoCreate: '' as Resource<TriggerFunc>,
OnWorkSlotCreate: '' as Resource<TriggerFunc>
OnWorkSlotCreate: '' as Resource<TriggerFunc>,
OnWorkSlotUpdate: '' as Resource<TriggerFunc>
}
})
+2 -3
View File
@@ -13,7 +13,7 @@
// limitations under the License.
//
import core, { AnyAttribute, Hierarchy, Tx, TxCUD, TxProcessor, TxRemoveDoc } from '@hcengineering/core'
import core, { AnyAttribute, Hierarchy, Tx, TxRemoveDoc } from '@hcengineering/core'
import type { TriggerControl } from '@hcengineering/server-core'
import view from '@hcengineering/view'
@@ -24,8 +24,7 @@ export async function OnCustomAttributeRemove (tx: Tx, control: TriggerControl):
const hierarchy = control.hierarchy
const ptx = tx as TxRemoveDoc<AnyAttribute>
if (!checkTx(ptx, hierarchy)) return []
const txes = await control.findAll<TxCUD<AnyAttribute>>(control.ctx, core.class.TxCUD, { objectId: ptx.objectId })
const attribute = TxProcessor.buildDoc2Doc<AnyAttribute>(txes)
const attribute = control.removedMap.get(ptx.objectId) as AnyAttribute
if (attribute === undefined) return []
const preferences = await control.findAll(control.ctx, view.class.ViewletPreference, {
config: attribute.name,
+8 -21
View File
@@ -35,7 +35,7 @@ import fs, { createReadStream, mkdtempSync } from 'fs'
import { rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
const cacheControlValue = 'public, max-age=365d'
const cacheControlValue = 'public, no-cache, must-revalidate, max-age=365d'
const cacheControlNoCache = 'public, no-store, no-cache, must-revalidate, max-age=0'
async function storageUpload (
@@ -385,14 +385,14 @@ export function start (
'handle-file',
{},
async (ctx) => {
let payload: Token = { email: 'guest', workspace: { name: req.query.workspace as string } }
try {
const cookies = ((req?.headers?.cookie as string) ?? '').split(';').map((it) => it.trim().split('='))
const token =
cookies.find((it) => it[0] === 'presentation-metadata-Token')?.[1] ??
(req.query.token as string | undefined)
payload = token !== undefined ? decodeToken(token) : payload
(req.query.token as string | undefined) ??
''
const payload = decodeToken(token)
const uuid = req.params.file ?? req.query.file
if (uuid === undefined) {
@@ -412,20 +412,6 @@ export function start (
return
}
// try image and octet streams
const isImage =
blobInfo.contentType.includes('image/') || blobInfo.contentType.includes('application/octet-stream')
if (token === undefined) {
if (blobInfo !== undefined && !isImage) {
// Do not allow to return non images with no token.
if (token === undefined) {
res.status(403).send()
return
}
}
}
if (req.method === 'HEAD') {
res.writeHead(200, {
'accept-ranges': 'bytes',
@@ -438,6 +424,9 @@ export function start (
res.end()
return
}
// try image and octet streams
const isImage =
blobInfo.contentType.includes('image/') || blobInfo.contentType.includes('application/octet-stream')
const size = req.query.size !== undefined ? parseInt(req.query.size as string) : undefined
const accept = req.headers.accept
@@ -475,9 +464,7 @@ export function start (
error?.Code === 'NoSuchKey'
) {
ctx.error('No such storage key', {
file: req.query.file,
workspace: payload?.workspace,
email: payload?.email
file: req.query.file
})
res.status(404).send()
return
+37
View File
@@ -0,0 +1,37 @@
DO $$
DECLARE
tbl_name text;
hash_col_not_exists boolean;
data_col_exists boolean;
BEGIN
FOR tbl_name IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_type = 'BASE TABLE'
LOOP
EXECUTE format('
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = %L
AND column_name = ''data''
AND data_type = ''jsonb''
);', tbl_name) INTO data_col_exists;
EXECUTE format('
SELECT NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = %L
AND column_name = ''"%%hash%%"''
);', tbl_name) INTO hash_col_not_exists;
IF data_col_exists AND hash_col_not_exists THEN
EXECUTE format('
ALTER TABLE %I ADD COLUMN "%%hash%%" text;', tbl_name);
EXECUTE format('
UPDATE %I SET "%%hash%%" = data->>''%%data%%'';', tbl_name);
END IF;
END LOOP;
END $$;
+25
View File
@@ -0,0 +1,25 @@
ALTER TABLE notification_dnc
ADD "objectId" text,
ADD "objectClass" text,
ADD "user" text;
ALTER TABLE notification_dnc
DROP COLUMN "attachedTo";
UPDATE notification_dnc
SET "objectId" = (data->>'objectId');
UPDATE notification_dnc
SET "objectClass" = (data->>'objectClass');
UPDATE notification_dnc
SET "user" = (data->>'user');
ALTER TABLE notification_dnc
ALTER COLUMN "objectId" SET NOT NULL;
ALTER TABLE notification_dnc
ALTER COLUMN "objectClass" SET NOT NULL;
ALTER TABLE notification_dnc
ALTER COLUMN "user" SET NOT NULL;
@@ -0,0 +1,11 @@
ALTER TABLE doc_index_state
ADD "needIndex" bool;
ALTER TABLE doc_index_state
DROP COLUMN "attachedTo";
UPDATE doc_index_state
SET "needIndex" = (data->>'needIndex')::boolean;
ALTER TABLE doc_index_state
ALTER COLUMN "needIndex" SET NOT NULL;
@@ -0,0 +1,25 @@
ALTER TABLE notification
ADD "isViewed" bool,
ADD archived bool,
ADD "user" text;
ALTER TABLE notification
DROP COLUMN "attachedTo";
UPDATE notification
SET "isViewed" = (data->>'isViewed')::boolean;
UPDATE notification
SET "archived" = (data->>'archived')::boolean;
UPDATE notification
SET "user" = (data->>'user');
ALTER TABLE notification
ALTER COLUMN "isViewed" SET NOT NULL;
ALTER TABLE notification
ALTER COLUMN archived SET NOT NULL;
ALTER TABLE notification
ALTER COLUMN "user" SET NOT NULL;
+15
View File
@@ -0,0 +1,15 @@
ALTER TABLE tx
ADD "objectSpace" text,
ADD "objectId" text;
ALTER TABLE tx
DROP COLUMN "attachedTo";
UPDATE tx
SET "objectId" = (data->>'objectId');
UPDATE tx
SET "objectSpace" = (data->>'objectSpace');
ALTER TABLE tx
ALTER COLUMN "objectSpace" SET NOT NULL;
+11
View File
@@ -0,0 +1,11 @@
ALTER TABLE notification_user
ADD "user" text;
ALTER TABLE notification_user
DROP COLUMN "attachedTo";
UPDATE notification_user
SET "user" = (data->>'user');
ALTER TABLE notification_user
ALTER COLUMN "user" SET NOT NULL;
+2 -1
View File
@@ -14,4 +14,5 @@
//
export * from './storage'
export { getDBClient, convertDoc, createTable, retryTxn, translateDomain, getDocFieldsByDomains } from './utils'
export { getDBClient, convertDoc, createTable, retryTxn } from './utils'
export { getDocFieldsByDomains, translateDomain } from './schemas'
+55 -12
View File
@@ -1,10 +1,10 @@
import { DOMAIN_SPACE } from '@hcengineering/core'
import { DOMAIN_DOC_INDEX_STATE, DOMAIN_SPACE, DOMAIN_TX } from '@hcengineering/core'
type DataType = 'bigint' | 'bool' | 'text' | 'text[]'
type Schema = Record<string, [DataType, boolean]>
export const defaultSchema: Schema = {
const baseSchema: Schema = {
_id: ['text', true],
_class: ['text', true],
space: ['text', true],
@@ -12,25 +12,68 @@ export const defaultSchema: Schema = {
createdBy: ['text', false],
modifiedOn: ['bigint', true],
createdOn: ['bigint', false],
'%hash%': ['text', false]
}
const defaultSchema: Schema = {
...baseSchema,
attachedTo: ['text', false]
}
export const spaceSchema: Schema = {
_id: ['text', true],
_class: ['text', true],
space: ['text', true],
modifiedBy: ['text', true],
createdBy: ['text', false],
modifiedOn: ['bigint', true],
createdOn: ['bigint', false],
const spaceSchema: Schema = {
...baseSchema,
private: ['bool', true],
members: ['text[]', true]
}
const txSchema: Schema = {
...baseSchema,
objectSpace: ['text', true],
objectId: ['text', false]
}
const notificationSchema: Schema = {
...baseSchema,
isViewed: ['bool', true],
archived: ['bool', true],
user: ['text', true]
}
const dncSchema: Schema = {
...baseSchema,
objectId: ['text', true],
objectClass: ['text', true],
user: ['text', true]
}
const userNotificationSchema: Schema = {
...baseSchema,
user: ['text', true]
}
const docIndexStateSchema: Schema = {
...baseSchema,
needIndex: ['bool', true]
}
export function translateDomain (domain: string): string {
return domain.replaceAll('-', '_')
}
export const domainSchemas: Record<string, Schema> = {
[DOMAIN_SPACE]: spaceSchema
[DOMAIN_SPACE]: spaceSchema,
[DOMAIN_TX]: txSchema,
[translateDomain(DOMAIN_DOC_INDEX_STATE)]: docIndexStateSchema,
notification: notificationSchema,
[translateDomain('notification-dnc')]: dncSchema,
[translateDomain('notification-user')]: userNotificationSchema
}
export function getSchema (domain: string): Schema {
return domainSchemas[domain] ?? defaultSchema
return domainSchemas[translateDomain(domain)] ?? defaultSchema
}
export function getDocFieldsByDomains (domain: string): string[] {
const schema = domainSchemas[translateDomain(domain)] ?? defaultSchema
return Object.keys(schema)
}
+3 -4
View File
@@ -74,7 +74,6 @@ import {
type DBDoc,
escapeBackticks,
getDBClient,
getDocFieldsByDomains,
inferType,
isDataField,
isOwner,
@@ -82,9 +81,9 @@ import {
parseDoc,
parseDocWithProjection,
parseUpdate,
type PostgresClientReference,
translateDomain
type PostgresClientReference
} from './utils'
import { getDocFieldsByDomains, translateDomain } from './schemas'
abstract class PostgresAdapterBase implements DbAdapter {
protected readonly _helper: DBCollectionHelper
@@ -1550,7 +1549,7 @@ class PostgresTxAdapter extends PostgresAdapterBase implements TxAdapter {
async getModel (ctx: MeasureContext): Promise<Tx[]> {
const res = await this
.client`SELECT * FROM ${this.client(translateDomain(DOMAIN_TX))} WHERE "workspaceId" = ${this.workspaceId.name} AND data->>'objectSpace' = ${core.space.Model} ORDER BY _id ASC, "modifiedOn" ASC`
.client`SELECT * FROM ${this.client(translateDomain(DOMAIN_TX))} WHERE "workspaceId" = ${this.workspaceId.name} AND "objectSpace" = ${core.space.Model} ORDER BY _id ASC, "modifiedOn" ASC`
const model = res.map((p) => parseDoc<Tx>(p as any))
// We need to put all core.account.System transactions first
+1 -10
View File
@@ -30,7 +30,7 @@ import core, {
import { PlatformError, unknownStatus } from '@hcengineering/platform'
import { type DomainHelperOperations } from '@hcengineering/server-core'
import postgres from 'postgres'
import { defaultSchema, domainSchemas, getSchema } from './schemas'
import { getDocFieldsByDomains, getSchema, translateDomain } from './schemas'
const connections = new Map<string, PostgresClientReferenceImpl>()
@@ -329,10 +329,6 @@ export class DBCollectionHelper implements DomainHelperOperations {
}
}
export function translateDomain (domain: string): string {
return domain.replaceAll('-', '_')
}
export function parseDocWithProjection<T extends Doc> (doc: DBDoc, projection: Projection<T> | undefined): T {
const { workspaceId, data, ...rest } = doc
for (const key in rest) {
@@ -397,11 +393,6 @@ export function isDataField (domain: string, field: string): boolean {
return !getDocFieldsByDomains(domain).includes(field)
}
export function getDocFieldsByDomains (domain: string): string[] {
const schema = domainSchemas[domain] ?? defaultSchema
return Object.keys(schema)
}
export interface JoinProps {
table: string // table to join
path: string // _id.roles, attachedTo.attachedTo, space...
+10 -10
View File
@@ -33,7 +33,6 @@ import core, {
WorkspaceId,
WorkspaceIdWithUrl,
type Client,
type Doc,
type Ref,
type WithLookup
} from '@hcengineering/core'
@@ -112,15 +111,16 @@ export async function initModel (
try {
logger.log('creating database...', workspaceId)
await adapter.upload(ctx, DOMAIN_TX, [
{
_class: core.class.Tx,
_id: 'first-tx' as Ref<Doc>,
modifiedBy: core.account.System,
modifiedOn: Date.now(),
space: core.space.DerivedTx
}
])
const firstTx: Tx = {
_class: core.class.Tx,
_id: 'first-tx' as Ref<Tx>,
modifiedBy: core.account.System,
modifiedOn: Date.now(),
space: core.space.DerivedTx,
objectSpace: core.space.DerivedTx
}
await adapter.upload(ctx, DOMAIN_TX, [firstTx])
await progress(30)
+1 -1
View File
@@ -3,7 +3,7 @@
# Restore workspace contents in mongo/elastic
./tool.sh backup-restore ./sanity-ws sanity-ws
./tool.sh upgrade-workspace sanity-ws --indexes
./tool.sh upgrade-workspace sanity-ws --indexes --force
# Re-assign user to workspace.
./tool.sh assign-workspace user1 sanity-ws
Binary file not shown.
Binary file not shown.

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