Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2024-02-29 00:53:09 +06:00 committed by GitHub
parent f6826dd5f9
commit 287652f279
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
108 changed files with 7816 additions and 1 deletions

View File

@ -338,6 +338,9 @@ dependencies:
'@rush-temp/model-server-templates': '@rush-temp/model-server-templates':
specifier: file:./projects/model-server-templates.tgz specifier: file:./projects/model-server-templates.tgz
version: file:projects/model-server-templates.tgz(svelte@4.2.11) version: file:projects/model-server-templates.tgz(svelte@4.2.11)
'@rush-temp/model-server-time':
specifier: file:./projects/model-server-time.tgz
version: file:projects/model-server-time.tgz(svelte@4.2.11)
'@rush-temp/model-server-tracker': '@rush-temp/model-server-tracker':
specifier: file:./projects/model-server-tracker.tgz specifier: file:./projects/model-server-tracker.tgz
version: file:projects/model-server-tracker.tgz(svelte@4.2.11) version: file:projects/model-server-tracker.tgz(svelte@4.2.11)
@ -368,6 +371,9 @@ dependencies:
'@rush-temp/model-text-editor': '@rush-temp/model-text-editor':
specifier: file:./projects/model-text-editor.tgz specifier: file:./projects/model-text-editor.tgz
version: file:projects/model-text-editor.tgz(svelte@4.2.11) version: file:projects/model-text-editor.tgz(svelte@4.2.11)
'@rush-temp/model-time':
specifier: file:./projects/model-time.tgz
version: file:projects/model-time.tgz(svelte@4.2.11)
'@rush-temp/model-tracker': '@rush-temp/model-tracker':
specifier: file:./projects/model-tracker.tgz specifier: file:./projects/model-tracker.tgz
version: file:projects/model-tracker.tgz(svelte@4.2.11) version: file:projects/model-tracker.tgz(svelte@4.2.11)
@ -578,6 +584,12 @@ dependencies:
'@rush-temp/server-templates': '@rush-temp/server-templates':
specifier: file:./projects/server-templates.tgz specifier: file:./projects/server-templates.tgz
version: file:projects/server-templates.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2) version: file:projects/server-templates.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2)
'@rush-temp/server-time':
specifier: file:./projects/server-time.tgz
version: file:projects/server-time.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2)
'@rush-temp/server-time-resources':
specifier: file:./projects/server-time-resources.tgz
version: file:projects/server-time-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2)
'@rush-temp/server-token': '@rush-temp/server-token':
specifier: file:./projects/server-token.tgz specifier: file:./projects/server-token.tgz
version: file:projects/server-token.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2) version: file:projects/server-token.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2)
@ -668,6 +680,15 @@ dependencies:
'@rush-temp/theme': '@rush-temp/theme':
specifier: file:./projects/theme.tgz specifier: file:./projects/theme.tgz
version: file:projects/theme.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) version: file:projects/theme.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/time':
specifier: file:./projects/time.tgz
version: file:projects/time.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2)
'@rush-temp/time-assets':
specifier: file:./projects/time-assets.tgz
version: file:projects/time-assets.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2)
'@rush-temp/time-resources':
specifier: file:./projects/time-resources.tgz
version: file:projects/time-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/tool': '@rush-temp/tool':
specifier: file:./projects/tool.tgz specifier: file:./projects/tool.tgz
version: file:projects/tool.tgz(bufferutil@4.0.8)(svelte@4.2.11) version: file:projects/tool.tgz(bufferutil@4.0.8)(svelte@4.2.11)
@ -19942,6 +19963,27 @@ packages:
- svelte - svelte
dev: false dev: false
file:projects/model-server-time.tgz(svelte@4.2.11):
resolution: {integrity: sha512-u27Dbh9sMM9RQznVDnMB5Tzj+FKN98E16dNoWVTz9G1YzhnPXxi2ZDD1hYtlYH//G/Cc9qfls+ggB8OKrm4cMQ==, tarball: file:projects/model-server-time.tgz}
id: file:projects/model-server-time.tgz
name: '@rush-temp/model-server-time'
version: 0.0.0
dependencies:
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
- svelte
dev: false
file:projects/model-server-tracker.tgz(svelte@4.2.11): file:projects/model-server-tracker.tgz(svelte@4.2.11):
resolution: {integrity: sha512-8TqauIOhrL/TrZIwsKyaixz5G2XlQjulPp9aMFm7gMxRHm7b3bjUiPI7SZ0bC4+kP99o5mxepmyiLsU/XrYSig==, tarball: file:projects/model-server-tracker.tgz} resolution: {integrity: sha512-8TqauIOhrL/TrZIwsKyaixz5G2XlQjulPp9aMFm7gMxRHm7b3bjUiPI7SZ0bC4+kP99o5mxepmyiLsU/XrYSig==, tarball: file:projects/model-server-tracker.tgz}
id: file:projects/model-server-tracker.tgz id: file:projects/model-server-tracker.tgz
@ -20152,6 +20194,27 @@ packages:
- svelte - svelte
dev: false dev: false
file:projects/model-time.tgz(svelte@4.2.11):
resolution: {integrity: sha512-XCIFtZCPReL043R2aEMcHhdSvq2w7h+GsMsXAb1mKMfjMPLDbRoF5631UKaNve3s4hnFHajVkKqZmP+ERL0LPQ==, tarball: file:projects/model-time.tgz}
id: file:projects/model-time.tgz
name: '@rush-temp/model-time'
version: 0.0.0
dependencies:
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
- svelte
dev: false
file:projects/model-tracker.tgz(svelte@4.2.11): file:projects/model-tracker.tgz(svelte@4.2.11):
resolution: {integrity: sha512-JekAX6mPwRvA0RvyDDELrdcyCDDGYtSeqseUfklkZZKSj0t3zrAT1KQlmIFTe05x3uqOMCcQXgPRVBvtgsPqUA==, tarball: file:projects/model-tracker.tgz} resolution: {integrity: sha512-JekAX6mPwRvA0RvyDDELrdcyCDDGYtSeqseUfklkZZKSj0t3zrAT1KQlmIFTe05x3uqOMCcQXgPRVBvtgsPqUA==, tarball: file:projects/model-tracker.tgz}
id: file:projects/model-tracker.tgz id: file:projects/model-tracker.tgz
@ -22537,6 +22600,70 @@ packages:
- ts-node - ts-node
dev: false dev: false
file:projects/server-time-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-KLlFROUBWJhmETbztTgHNZYtWRTcuHOndKlim9XA1mSduJH3cHdlboqUEttVIA8ed1X9QeJ6X4Ct/HM4gigPUw==, tarball: file:projects/server-time-resources.tgz}
id: file:projects/server-time-resources.tgz
name: '@rush-temp/server-time-resources'
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.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- svelte
- ts-node
dev: false
file:projects/server-time.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-bq1MSajRmqHhjS/q5aSewETO75Yy9w/fi8MHU5ZpWcV2MNTyR3Do4oi7ei1KRyEq6NRncwRAH6R5inlvkaGeIQ==, tarball: file:projects/server-time.tgz}
id: file:projects/server-time.tgz
name: '@rush-temp/server-time'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- svelte
- ts-node
dev: false
file:projects/server-token.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2): file:projects/server-token.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-njkCF8ZsS1x6rIwZPjyN368EONN8FrN9R+6f3yYoMR+2luiJt/GknPNNopZwuvEIcorJ43fUC79ECsJFMzHfMg==, tarball: file:projects/server-token.tgz} resolution: {integrity: sha512-njkCF8ZsS1x6rIwZPjyN368EONN8FrN9R+6f3yYoMR+2luiJt/GknPNNopZwuvEIcorJ43fUC79ECsJFMzHfMg==, tarball: file:projects/server-token.tgz}
id: file:projects/server-token.tgz id: file:projects/server-token.tgz
@ -23746,6 +23873,116 @@ packages:
- ts-node - ts-node
dev: false dev: false
file:projects/time-assets.tgz(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-GdLQ03f2nfzZmXBSyNztwmlmUv3yJe4fDz93b8MuOJIX4Ajd+4ylmwz454yb6Pc8w1PnWRLexFg0/mmjqWlc3w==, tarball: file:projects/time-assets.tgz}
id: file:projects/time-assets.tgz
name: '@rush-temp/time-assets'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- svelte
- ts-node
dev: false
file:projects/time-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-Qeyao7d/LxS2q9N19l1zoSB76fYbicJwz6zhE3tQDOUfR061XG9n3YlTxgoaBA4GNtKDJc9EI7smbVybGYH1Yw==, tarball: file:projects/time-resources.tgz}
id: file:projects/time-resources.tgz
name: '@rush-temp/time-resources'
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.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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.11)(ts-node@10.9.2)
fast-equals: 2.0.4
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
sass: 1.71.1
svelte: 4.2.11
svelte-check: 3.6.4(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.11)
svelte-eslint-parser: 0.33.1(svelte@4.2.11)
svelte-loader: 3.1.9(svelte@4.2.11)
svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.11)(typescript@5.3.3)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
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/time.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.11)(ts-node@10.9.2):
resolution: {integrity: sha512-yneBtaqzPc7z4BLY6cwk+dinYNBWnH6+hsN82ELJW3PQLH7zlZxXEIvUOVeTlz0CyAj9WFHIk/x/mDmyduITvQ==, tarball: file:projects/time.tgz}
id: file:projects/time.tgz
name: '@rush-temp/time'
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.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
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.3.3)
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)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- svelte
- ts-node
dev: false
file:projects/tool.tgz(bufferutil@4.0.8)(svelte@4.2.11): file:projects/tool.tgz(bufferutil@4.0.8)(svelte@4.2.11):
resolution: {integrity: sha512-u/v+y38hfzb8fBpMNT1IkihE0UA06lngEqh/bQWl6rMb4EMKOVc6V5N6YPq41mLIQvDnqMz4mzDsVRlq6c4HDQ==, tarball: file:projects/tool.tgz} resolution: {integrity: sha512-u/v+y38hfzb8fBpMNT1IkihE0UA06lngEqh/bQWl6rMb4EMKOVc6V5N6YPq41mLIQvDnqMz4mzDsVRlq6c4HDQ==, tarball: file:projects/tool.tgz}
id: file:projects/tool.tgz id: file:projects/tool.tgz

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "model"
}

View File

@ -0,0 +1,41 @@
{
"name": "@hcengineering/model-server-time",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/model-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-time": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/model-core": "^0.6.0"
}
}

View File

@ -0,0 +1,84 @@
//
// 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.
//
import { Mixin, type Builder } from '@hcengineering/model'
import core, { type Tx } from '@hcengineering/core'
import { TClass } from '@hcengineering/model-core'
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'
@Mixin(serverTime.mixin.ToDoFactory, core.class.Class)
export class TToDoFactory extends TClass implements ToDoFactory {
factory!: Resource<(tx: Tx, control: TriggerControl) => Promise<Tx[]>>
}
@Mixin(serverTime.mixin.OnToDo, core.class.Class)
export class TOnToDo extends TClass implements OnToDo {
onDone!: Resource<(control: TriggerControl, workslots: WorkSlot[], todo: ToDo) => Promise<Tx[]>>
}
export function createModel (builder: Builder): void {
builder.createModel(TToDoFactory, TOnToDo)
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnTask
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnToDoUpdate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxUpdateDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnToDoRemove,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxRemoveDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnToDoCreate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc
}
})
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTime.trigger.OnWorkSlotCreate,
txMatch: {
_class: core.class.TxCollectionCUD,
'tx._class': core.class.TxCreateDoc
}
})
builder.mixin(tracker.class.Issue, core.class.Class, serverTime.mixin.ToDoFactory, {
factory: serverTime.function.IssueToDoFactory
})
builder.mixin(tracker.class.Issue, core.class.Class, serverTime.mixin.OnToDo, {
onDone: serverTime.function.IssueToDoDone
})
}
export * from '@hcengineering/server-time'

View File

@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

7
models/time/.eslintrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

4
models/time/.npmignore Normal file
View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "model"
}

55
models/time/package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "@hcengineering/model-time",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Uberflow Contributors",
"template": "@hcengineering/model-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.1.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/core": "^0.6.28",
"@hcengineering/model": "^0.6.7",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/view": "^0.6.9",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/task": "^0.6.13",
"@hcengineering/lead": "^0.6.0",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/board": "^0.6.12",
"@hcengineering/calendar": "^0.6.17",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-calendar": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"@hcengineering/time-resources": "^0.6.0"
}
}

369
models/time/src/index.ts Normal file
View File

@ -0,0 +1,369 @@
//
// 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.
//
import activity from '@hcengineering/activity'
import board from '@hcengineering/board'
import calendarPlugin, { type Visibility } from '@hcengineering/calendar'
import contactPlugin, { type Person } from '@hcengineering/contact'
import {
DOMAIN_MODEL,
type Class,
type Domain,
type Markup,
type Ref,
type Space,
type Timestamp,
type Type,
DateRangeMode
} from '@hcengineering/core'
import lead from '@hcengineering/lead'
import { Collection, Mixin, Model, Prop, TypeRef, TypeString, UX, type Builder, TypeDate } from '@hcengineering/model'
import { TEvent } from '@hcengineering/model-calendar'
import core, { TAttachedDoc, TClass, TDoc, TType } from '@hcengineering/model-core'
import tracker from '@hcengineering/model-tracker'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
import notification from '@hcengineering/notification'
import recruit from '@hcengineering/recruit'
import tags from '@hcengineering/tags'
import { type AnyComponent } from '@hcengineering/ui'
import {
type TodoDoneTester,
timeId,
type ItemPresenter,
type ProjectToDo,
type ToDo,
type ToDoPriority,
type TodoAutomationHelper,
type WorkSlot
} from '@hcengineering/time'
import { type Resource } from '@hcengineering/platform'
import time from './plugin'
import task from '@hcengineering/task'
export { timeId } from '@hcengineering/time'
export { default } from './plugin'
export const DOMAIN_TIME = 'time' as Domain
export function TypeToDoPriority (): Type<ToDoPriority> {
return { _class: time.class.TypeToDoPriority, label: time.string.Priority }
}
@Mixin(time.mixin.ItemPresenter, core.class.Class)
export class TItemPresenter extends TClass implements ItemPresenter {
presenter!: AnyComponent
}
@Model(time.class.WorkSlot, calendarPlugin.class.Event)
@UX(time.string.WorkSlot)
export class TWorkSlot extends TEvent implements WorkSlot {
declare attachedTo: Ref<ToDo>
declare attachedToClass: Ref<Class<ToDo>>
}
@Model(time.class.TypeToDoPriority, core.class.Type, DOMAIN_MODEL)
export class TTypeToDoPriority extends TType {}
@Model(time.class.ToDo, core.class.AttachedDoc, DOMAIN_TIME)
@UX(time.string.ToDo, time.icon.Planned)
export class TToDO extends TAttachedDoc implements ToDo {
@Prop(TypeDate(DateRangeMode.DATE), task.string.DueDate)
dueDate?: number | null | undefined
@Prop(TypeToDoPriority(), time.string.Priority)
priority!: ToDoPriority
visibility!: Visibility
attachedSpace?: Ref<Space> | undefined
@Prop(TypeString(), calendarPlugin.string.Title)
title!: string
@Prop(TypeString(), calendarPlugin.string.Description)
description!: Markup
doneOn?: Timestamp | null
@Prop(TypeRef(contactPlugin.class.Person), contactPlugin.string.For)
user!: Ref<Person>
@Prop(Collection(time.class.WorkSlot, time.string.WorkSlot), time.string.WorkSlot)
workslots!: number
@Prop(Collection(tags.class.TagReference, tags.string.TagLabel), tags.string.Tags)
labels?: number | undefined
}
@Model(time.class.ProjectToDo, time.class.ToDo)
@UX(time.string.ToDo, time.icon.Planned)
export class TProjectToDo extends TToDO implements ProjectToDo {
declare attachedSpace: Ref<Space>
}
@Model(time.class.TodoAutomationHelper, core.class.Doc, DOMAIN_MODEL)
@UX(time.string.ToDo, time.icon.Planned)
export class TTodoAutomationHelper extends TDoc implements TodoAutomationHelper {
onDoneTester!: Resource<TodoDoneTester>
}
export function createModel (builder: Builder): void {
builder.createModel(TWorkSlot, TItemPresenter, TToDO, TProjectToDo, TTypeToDoPriority, TTodoAutomationHelper)
builder.mixin(time.class.ToDo, core.class.Class, activity.mixin.IgnoreActivity, {})
builder.mixin(time.class.ProjectToDo, core.class.Class, activity.mixin.IgnoreActivity, {})
builder.mixin(time.class.TypeToDoPriority, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: time.component.PriorityEditor
})
builder.mixin(time.class.WorkSlot, core.class.Class, calendarPlugin.mixin.CalendarEventPresenter, {
presenter: time.component.WorkSlotElement
})
builder.mixin(tracker.class.Issue, core.class.Class, time.mixin.ItemPresenter, {
presenter: time.component.IssuePresenter
})
builder.mixin(lead.class.Lead, core.class.Class, time.mixin.ItemPresenter, {
presenter: time.component.LeadPresenter
})
builder.mixin(recruit.class.Applicant, core.class.Class, time.mixin.ItemPresenter, {
presenter: time.component.ApplicantPresenter
})
builder.mixin(board.class.Card, core.class.Class, time.mixin.ItemPresenter, {
presenter: time.component.CardPresenter
})
builder.mixin(time.class.WorkSlot, core.class.Class, view.mixin.ObjectEditor, {
editor: time.component.EditWorkSlot
})
builder.mixin(time.class.ToDo, core.class.Class, view.mixin.ObjectTitle, {
titleProvider: time.function.ToDoTitleProvider
})
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: time.string.Planner,
icon: calendarPlugin.icon.Calendar,
alias: timeId,
hidden: false,
position: 'top',
component: time.component.Me
},
time.app.Me
)
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: time.string.Team,
icon: time.icon.Team,
alias: 'team',
hidden: false,
position: 'top',
component: time.component.Team
},
time.app.Team
)
builder.mixin(time.class.ToDo, core.class.Class, view.mixin.IgnoreActions, {
actions: [view.action.Open, tracker.action.NewRelatedIssue, view.action.Delete]
})
createAction(
builder,
{
action: view.actionImpl.Delete,
actionProps: {
skipCheck: true
},
label: view.string.Delete,
icon: view.icon.Delete,
keyBinding: ['Meta + Backspace'],
category: view.category.General,
input: 'any',
override: [view.action.Delete],
target: time.class.ToDo,
context: { mode: ['context', 'browser'], group: 'remove' }
},
time.action.DeleteToDo
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,
{ label: time.string.Planner, visible: true },
time.category.Time
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: time.component.CreateToDoPopup,
element: 'top',
fillProps: {
_object: 'object'
}
},
label: time.string.CreateToDo,
icon: time.icon.Target,
keyBinding: [],
input: 'none',
category: time.category.Time,
target: core.class.Doc,
context: {
mode: [],
group: 'associate'
},
override: [time.action.CreateToDoGlobal]
},
time.action.CreateToDo
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: time.component.CreateToDoPopup,
element: 'top',
fillProps: {
_object: 'object'
}
},
label: time.string.CreateToDo,
icon: time.icon.Target,
keyBinding: [],
input: 'none',
category: time.category.Time,
target: core.class.Doc,
context: {
mode: [],
group: 'create'
}
},
time.action.CreateToDoGlobal
)
createAction(
builder,
{
action: view.actionImpl.ShowPopup,
actionProps: {
component: time.component.EditToDo,
element: 'top',
fillProps: {
_object: 'object',
space: 'space'
}
},
label: time.string.EditToDo,
icon: view.icon.Edit,
keyBinding: [],
input: 'focus',
category: time.category.Time,
target: time.class.ToDo,
context: {
mode: ['context', 'browser'],
group: 'edit'
}
},
time.action.EditToDo
)
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: {
mode: 'app',
application: 'time'
},
label: time.string.GotoTimePlaning,
icon: view.icon.ArrowRight,
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
createAction(builder, {
action: workbench.actionImpl.Navigate,
actionProps: {
mode: 'app',
application: 'team'
},
label: time.string.GotoTimeTeamPlaning,
icon: view.icon.ArrowRight,
input: 'none',
category: view.category.Navigation,
target: core.class.Doc,
context: {
mode: ['workbench', 'browser', 'editor', 'panel', 'popup']
}
})
builder.createDoc(
notification.class.NotificationGroup,
core.space.Model,
{
label: time.string.ToDos,
icon: time.icon.Team,
objectClass: time.class.ToDo
},
time.ids.TimeNotificationGroup
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
generated: false,
allowedForAuthor: true,
label: time.string.NewToDo,
group: time.ids.TimeNotificationGroup,
txClasses: [core.class.TxCreateDoc],
objectClass: time.class.ProjectToDo,
onlyOwn: true,
providers: {
[notification.providers.PlatformNotification]: true
}
},
time.ids.ToDoCreated
)
builder.mixin(time.class.ToDo, core.class.Class, notification.mixin.ClassCollaborators, {
fields: ['user']
})
builder.mixin(time.class.ToDo, core.class.Class, notification.mixin.NotificationObjectPresenter, {
presenter: time.component.NotificationToDoPresenter
})
builder.mixin(time.class.ProjectToDo, core.class.Class, view.mixin.ObjectPanel, {
component: view.component.AttachedDocPanel
})
}
export * from './migration'

View File

@ -0,0 +1,165 @@
//
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { type PersonAccount } from '@hcengineering/contact'
import { type Account, type Doc, type Ref, TxOperations } from '@hcengineering/core'
import {
type MigrateOperation,
type MigrationClient,
type MigrationUpgradeClient,
createOrUpdate
} from '@hcengineering/model'
import core from '@hcengineering/model-core'
import task from '@hcengineering/task'
import tags from '@hcengineering/tags'
import { type ToDo, ToDoPriority } from '@hcengineering/time'
import { DOMAIN_TIME } from '.'
import time from './plugin'
export async function migrateWorkSlots (client: TxOperations): Promise<void> {
const h = client.getHierarchy()
const desc = h.getDescendants(task.class.Task)
const oldWorkSlots = await client.findAll(time.class.WorkSlot, {
attachedToClass: { $in: desc }
})
const now = Date.now()
const todos = new Map<Ref<Doc>, Ref<ToDo>>()
const count = new Map<Ref<ToDo>, number>()
for (const oldWorkSlot of oldWorkSlots) {
const todo = todos.get(oldWorkSlot.attachedTo)
if (todo === undefined) {
const acc = oldWorkSlot.space.replace('_calendar', '') as Ref<Account>
const account = (await client.findOne(core.class.Account, { _id: acc })) as PersonAccount
if (account.person !== undefined) {
const todo = await client.addCollection(
time.class.ProjectToDo,
time.space.ToDos,
oldWorkSlot.attachedTo,
oldWorkSlot.attachedToClass,
'todos',
{
attachedSpace: (oldWorkSlot as any).attachedSpace,
title: oldWorkSlot.title,
description: '',
doneOn: oldWorkSlot.dueDate > now ? null : oldWorkSlot.dueDate,
workslots: 0,
priority: ToDoPriority.NoPriority,
user: account.person,
visibility: 'public'
}
)
await client.update(oldWorkSlot, {
attachedTo: todo,
attachedToClass: time.class.ProjectToDo,
collection: 'workslots'
})
todos.set(oldWorkSlot.attachedTo, todo)
count.set(todo, 1)
}
} else {
await client.update(oldWorkSlot, {
attachedTo: todo,
attachedToClass: time.class.ProjectToDo,
collection: 'workslots'
})
const c = count.get(todo) ?? 1
count.set(todo, c + 1)
}
}
for (const [todoId, c] of count.entries()) {
const todo = await client.findOne(time.class.ToDo, { _id: todoId })
if (todo === undefined) continue
const tx = client.txFactory.createTxUpdateDoc(time.class.ToDo, todo.space, todo._id, {
workslots: c
})
tx.space = core.space.DerivedTx
await client.tx(tx)
}
}
async function migrateTodosSpace (client: TxOperations): Promise<void> {
const oldTodos = await client.findAll(time.class.ToDo, {
space: { $ne: time.space.ToDos }
})
for (const oldTodo of oldTodos) {
const account = (await client.findOne(core.class.Account, {
_id: oldTodo.space as string as Ref<Account>
})) as PersonAccount
if (account.person === undefined) continue
await client.update(oldTodo, {
user: account.person,
space: time.space.ToDos
})
}
}
async function createDefaultSpace (tx: TxOperations): Promise<void> {
const current = await tx.findOne(core.class.Space, {
_id: time.space.ToDos
})
if (current === undefined) {
await tx.createDoc(
core.class.Space,
core.space.Space,
{
name: 'Todos',
description: 'Space for all todos',
private: false,
archived: false,
members: []
},
time.space.ToDos
)
}
}
async function fillProps (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_TIME,
{ _class: time.class.ProjectToDo, visibility: { $exists: false } },
{ visibility: 'public' }
)
await client.update(
DOMAIN_TIME,
{ _class: time.class.ToDo, visibility: { $exists: false } },
{ visibility: 'private' }
)
await client.update(DOMAIN_TIME, { priority: { $exists: false } }, { priority: ToDoPriority.NoPriority })
}
export const timeOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await fillProps(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createDefaultSpace(tx)
await createOrUpdate(
tx,
tags.class.TagCategory,
tags.space.Tags,
{
icon: tags.icon.Tags,
label: 'Other',
targetClass: time.class.ToDo,
tags: [],
default: true
},
time.category.Other
)
await migrateWorkSlots(tx)
await migrateTodosSpace(tx)
}
}

68
models/time/src/plugin.ts Normal file
View File

@ -0,0 +1,68 @@
//
// 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.
//
import { type Client, type Doc, type Ref } from '@hcengineering/core'
import { type Application } from '@hcengineering/model-workbench'
import { type IntlString, mergeIds, type Resource } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import { type Action, type ActionCategory } from '@hcengineering/view'
import { timeId } from '@hcengineering/time'
import time from '@hcengineering/time-resources/src/plugin'
import { type NotificationGroup, type NotificationType } from '@hcengineering/notification'
import { type TxViewlet } from '@hcengineering/activity'
export default mergeIds(timeId, time, {
action: {
EditToDo: '' as Ref<Action<Doc, any>>,
CreateToDo: '' as Ref<Action<Doc, any>>,
DeleteToDo: '' as Ref<Action<Doc, any>>,
CreateToDoGlobal: '' as Ref<Action<Doc, any>>
},
string: {
EditToDo: '' as IntlString,
GotoTimePlaning: '' as IntlString,
GotoTimeTeamPlaning: '' as IntlString,
NewToDo: '' as IntlString,
ToDo: '' as IntlString,
Priority: '' as IntlString,
MarkedAsDone: '' as IntlString
},
category: {
Time: '' as Ref<ActionCategory>
},
component: {
IssuePresenter: '' as AnyComponent,
ApplicantPresenter: '' as AnyComponent,
CardPresenter: '' as AnyComponent,
LeadPresenter: '' as AnyComponent,
WorkSlotElement: '' as AnyComponent,
EditWorkSlot: '' as AnyComponent,
CreateToDoPopup: '' as AnyComponent,
NotificationToDoPresenter: '' as AnyComponent,
PriorityEditor: '' as AnyComponent
},
app: {
Me: '' as Ref<Application>,
Team: '' as Ref<Application>
},
ids: {
TimeNotificationGroup: '' as Ref<NotificationGroup>,
ToDoCreated: '' as Ref<NotificationType>,
TxToDoCreated: '' as Ref<TxViewlet>
},
function: {
ToDoTitleProvider: '' as Resource<(client: Client, ref: Ref<Doc>, doc?: Doc) => Promise<string>>
}
})

10
models/time/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="team2" viewBox="0 0 32 32">
<path d="M11,18c3.9,0,7-3.1,7-7c0-3.9-3.1-7-7-7c-3.9,0-7,3.1-7,7C4,14.9,7.1,18,11,18z M11,6c2.8,0,5,2.2,5,5s-2.2,5-5,5s-5-2.2-5-5S8.2,6,11,6z" />
<path d="M13,20H9c-3.9,0-7,3.1-7,7c0,0.6,0.4,1,1,1s1-0.4,1-1c0-2.8,2.2-5,5-5h4c2.8,0,5,2.2,5,5c0,0.6,0.4,1,1,1s1-0.4,1-1C20,23.1,16.9,20,13,20z" />
<path d="M29.1,14.2c-0.8-1.5-2.1-2.7-3.6-3.5c-0.5-0.2-1.1,0-1.3,0.5c-0.2,0.5,0,1.1,0.5,1.3c1.2,0.6,2.1,1.5,2.7,2.6c0.6,1.1,0.8,2.4,0.6,3.7c-0.2,1.3-0.8,2.5-1.7,3.4c-0.9,0.9-2.1,1.5-3.4,1.7c-0.5,0.1-0.9,0.6-0.9,1.1c0.1,0.5,0.6,0.9,1.1,0.9c1.7-0.2,3.3-1,4.5-2.2s2-2.8,2.3-4.5C30.2,17.5,29.9,15.8,29.1,14.2z" />
<path d="M26,19c0-0.6-0.4-1-1-1h-3v-5c0-0.6-0.4-1-1-1s-1,0.4-1,1v6c0,0.6,0.4,1,1,1h4C25.6,20,26,19.6,26,19z" />
</symbol>
<symbol id="team" viewBox="0 0 24 24">
<g>
<path d="M12.3,7.6c-2.6,0-4.6,2.1-4.6,4.6s2.1,4.6,4.6,4.6s4.6-2.1,4.6-4.6S14.8,7.6,12.3,7.6z M12.3,15.6c-1.8,0-3.3-1.5-3.3-3.3s1.5-3.3,3.3-3.3s3.3,1.5,3.3,3.3S14.1,15.6,12.3,15.6z" />
<path d="M12.2,5.7c0.4,0,0.6-0.3,0.6-0.6V3.5c0-0.4-0.3-0.6-0.6-0.6s-0.6,0.3-0.6,0.6v1.6C11.6,5.4,11.9,5.7,12.2,5.7z" />
<path d="M12.2,18.8c-0.4,0-0.6,0.3-0.6,0.7V21c0,0.4,0.3,0.7,0.6,0.7s0.6-0.3,0.6-0.7v-1.6C12.9,19,12.6,18.8,12.2,18.8z" />
<path d="M6.7,7.7C6.9,7.8,7,7.8,7.2,7.8s0.3-0.1,0.5-0.2c0.3-0.3,0.3-0.7,0-0.9L6.5,5.6c-0.3-0.3-0.7-0.3-0.9,0s-0.3,0.7,0,0.9L6.7,7.7z" />
<path d="M17.8,16.8c-0.3-0.3-0.7-0.3-0.9,0s-0.3,0.7,0,0.9l1.1,1.1c0.1,0.1,0.3,0.2,0.5,0.2s0.3-0.1,0.5-0.2c0.3-0.3,0.3-0.7,0-0.9L17.8,16.8z" />
<path d="M5.7,12.2c0-0.4-0.3-0.6-0.6-0.6H3.5c-0.4,0-0.6,0.3-0.6,0.6s0.3,0.6,0.6,0.6h1.6C5.4,12.9,5.7,12.6,5.7,12.2z" />
<path d="M21,11.6h-1.6c-0.4,0-0.7,0.3-0.7,0.6s0.3,0.6,0.7,0.6H21c0.4,0,0.7-0.3,0.7-0.6S21.4,11.6,21,11.6z" />
<path d="M6.7,16.8L5.6,18c-0.3,0.3-0.3,0.7,0,0.9c0.1,0.1,0.3,0.2,0.5,0.2s0.3-0.1,0.5-0.2l1.1-1.1c0.3-0.3,0.3-0.7,0-0.9S7,16.6,6.7,16.8z" />
<path d="M17.3,7.8c0.2,0,0.3-0.1,0.5-0.2l1.1-1.1c0.3-0.3,0.3-0.7,0-0.9s-0.7-0.3-0.9,0l-1.1,1.1c-0.3,0.3-0.3,0.7,0,0.9C17,7.8,17.1,7.8,17.3,7.8z" />
</g>
</symbol>
<symbol id="inbox" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.2 8.39976C9.38885 8.14795 9.68524 7.99976 10 7.99976H22C22.3148 7.99976 22.6111 8.14795 22.8 8.39976L27 13.9998H19C18.4477 13.9998 18 14.4475 18 14.9998C18 15.2624 17.9483 15.5225 17.8478 15.7651C17.7472 16.0078 17.5999 16.2283 17.4142 16.414C17.2285 16.5997 17.008 16.747 16.7654 16.8475C16.5227 16.948 16.2626 16.9998 16 16.9998C15.7374 16.9998 15.4773 16.948 15.2346 16.8475C14.992 16.747 14.7715 16.5997 14.5858 16.414C14.4001 16.2283 14.2528 16.0078 14.1522 15.7651C14.0517 15.5225 14 15.2624 14 14.9998C14 14.4475 13.5523 13.9998 13 13.9998H5L9.2 8.39976ZM12.127 15.9998H4V21.9998C4 23.1043 4.89543 23.9998 6 23.9998H26C27.1046 23.9998 28 23.1043 28 21.9998V15.9998H19.873C19.8264 16.1802 19.7672 16.3576 19.6955 16.5305C19.4945 17.0158 19.1999 17.4567 18.8284 17.8282C18.457 18.1996 18.016 18.4943 17.5307 18.6953C17.0454 18.8963 16.5253 18.9998 16 18.9998C15.4747 18.9998 14.9546 18.8963 14.4693 18.6953C13.984 18.4943 13.543 18.1996 13.1716 17.8282C12.8001 17.4567 12.5055 17.0158 12.3045 16.5305C12.2329 16.3576 12.1736 16.1802 12.127 15.9998ZM10 5.99976C9.05573 5.99976 8.16656 6.44434 7.6 7.19976L2.2 14.3998C2.07018 14.5729 2 14.7834 2 14.9998V21.9998C2 24.2089 3.79086 25.9998 6 25.9998H26C28.2091 25.9998 30 24.2089 30 21.9998V14.9998C30 14.7834 29.9298 14.5729 29.8 14.3998L24.4 7.19976C23.8334 6.44434 22.9443 5.99976 22 5.99976H10Z" />
</symbol>
<symbol id="hashtag" viewBox="0 0 32 32">
<path d="M13.9839 5.18321C14.0828 4.63985 13.7224 4.11922 13.1791 4.02035C12.6357 3.92149 12.1151 4.28183 12.0162 4.82519L11.0744 10.0011L6.99205 10.0007C6.43976 10.0006 5.992 10.4483 5.99194 11.0006C5.99189 11.5529 6.43955 12.0006 6.99184 12.0007L10.7105 12.0011L9.25503 20.0005L4.99996 20.0007C4.44767 20.0007 3.99998 20.4484 4 21.0007C4.00002 21.553 4.44776 22.0007 5.00004 22.0007L8.89112 22.0005L8.01389 26.8218C7.91502 27.3651 8.27536 27.8858 8.81872 27.9846C9.36208 28.0835 9.88271 27.7232 9.98158 27.1798L10.924 22.0004L18.8885 22.0001L18.0106 26.8181C17.9116 27.3614 18.2718 27.8821 18.8152 27.9811C19.3585 28.0801 19.8792 27.7199 19.9782 27.1766L20.9214 22L25 21.9998C25.5523 21.9998 26 21.5521 26 20.9998C26 20.4475 25.5522 19.9998 25 19.9998L21.2858 20L22.743 12.0023L27.0001 12.0028C27.5524 12.0028 28.0001 11.5552 28.0002 11.0029C28.0003 10.4506 27.5526 10.0028 27.0003 10.0028L23.1074 10.0024L23.9852 5.1848C24.0842 4.64146 23.724 4.12074 23.1806 4.02174C22.6373 3.92274 22.1166 4.28295 22.0176 4.82629L21.0745 10.0021L13.1072 10.0013L13.9839 5.18321ZM12.7433 12.0013L20.7101 12.0021L19.2529 20.0001L11.2879 20.0004L12.7433 12.0013Z" />
</symbol>
<symbol id="target" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6.5" stroke="currentColor"/>
<circle cx="8" cy="8" r="3.5" stroke="currentColor"/>
<circle cx="8" cy="8" r="0.5" stroke="currentColor"/>
</symbol>
<symbol id="flag" viewBox="0 0 14 14" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.75 1.3125C1.75 1.07088 1.94588 0.875 2.1875 0.875H11.8125C11.9715 0.875 12.1181 0.961309 12.1952 1.10041C12.2723 1.23952 12.2678 1.40951 12.1835 1.54437L10.1409 4.8125L12.1835 8.08063C12.2678 8.21549 12.2723 8.38548 12.1952 8.52459C12.1181 8.66369 11.9715 8.75 11.8125 8.75H2.625V12.6875C2.625 12.9291 2.42912 13.125 2.1875 13.125C1.94588 13.125 1.75 12.9291 1.75 12.6875V1.3125ZM2.625 7.875H11.0231L9.254 5.04437C9.16533 4.90251 9.16533 4.72249 9.254 4.58063L11.0231 1.75H2.625V7.875Z" fill="currentColor"/>
</symbol>
<symbol id="filledFlag" viewBox="0 0 14 14">
<path d="M10.1,4.8l2-3.3c0.1-0.1,0.1-0.3,0-0.4S12,0.9,11.8,0.9H2.2c-0.2,0-0.4,0.2-0.4,0.4v11.4c0,0.2,0.2,0.4,0.4,0.4s0.4-0.2,0.4-0.4V8.8h9.2c0.2,0,0.3-0.1,0.4-0.2s0.1-0.3,0-0.4L10.1,4.8z"/>
</symbol>
<symbol id="planned" viewBox="0 0 20 20" fill="none">
<path d="M10 1.24988C8.26942 1.24988 6.57769 1.76306 5.13876 2.72452C3.69983 3.68598 2.57832 5.05254 1.91606 6.6514C1.25379 8.25025 1.08051 10.0096 1.41813 11.7069C1.75575 13.4043 2.58911 14.9634 3.81282 16.1871C5.03653 17.4108 6.59563 18.2441 8.29296 18.5817C9.9903 18.9194 11.7496 18.7461 13.3485 18.0838C14.9473 17.4216 16.3139 16.3 17.2754 14.8611C18.2368 13.4222 18.75 11.7305 18.75 9.99988C18.75 7.67923 17.8281 5.45364 16.1872 3.81269C14.5462 2.17175 12.3206 1.24988 10 1.24988ZM10 17.4999C8.51664 17.4999 7.0666 17.06 5.83323 16.2359C4.59986 15.4118 3.63856 14.2404 3.07091 12.87C2.50325 11.4996 2.35473 9.99156 2.64411 8.5367C2.9335 7.08184 3.64781 5.74547 4.6967 4.69658C5.7456 3.64768 7.08197 2.93338 8.53683 2.64399C9.99168 2.3546 11.4997 2.50312 12.8701 3.07078C14.2406 3.63844 15.4119 4.59973 16.236 5.8331C17.0601 7.06647 17.5 8.51652 17.5 9.99988C17.5 11.989 16.7098 13.8967 15.3033 15.3032C13.8968 16.7097 11.9891 17.4999 10 17.4999Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1919 7.05809C14.436 7.30217 14.436 7.6979 14.1919 7.94197L9.19194 12.942C8.94786 13.1861 8.55214 13.1861 8.30806 12.942L5.80806 10.442C5.56398 10.1979 5.56398 9.80217 5.80806 9.55809C6.05214 9.31401 6.44786 9.31401 6.69194 9.55809L8.75 11.6161L13.3081 7.05809C13.5521 6.81401 13.9479 6.81401 14.1919 7.05809Z" fill="currentColor"/>
</symbol>
<symbol id="all" viewBox="0 0 20 20" fill="none">
<path d="M9.99998 15.0001C9.89651 15 9.79468 14.9743 9.70367 14.9251L2.12886 10.8463C1.82502 10.6827 1.71137 10.3037 1.87505 9.99992C2.03867 9.69621 2.41749 9.58261 2.72124 9.74616L9.99998 13.6652L17.2785 9.7463C17.5823 9.58268 17.9613 9.69637 18.125 10.0002C18.2886 10.3041 18.1749 10.6832 17.871 10.8468L10.2963 14.9255C10.2052 14.9746 10.1034 15.0002 9.99998 15.0001Z" fill="currentColor"/>
<path d="M9.99998 18.7501C9.89651 18.75 9.79468 18.7243 9.70367 18.6751L2.12886 14.5963C1.82502 14.4327 1.71137 14.0537 1.87505 13.7499C2.03867 13.4462 2.41749 13.3326 2.72124 13.4962L9.99998 17.4152L17.2785 13.4963C17.5823 13.3327 17.9613 13.4464 18.125 13.7502C18.2886 14.0541 18.1749 14.4332 17.871 14.5968L10.2963 18.6755C10.2052 18.7246 10.1034 18.7502 9.99998 18.7501Z" fill="currentColor"/>
<path d="M9.99998 11.2501C9.89651 11.25 9.79468 11.2243 9.70367 11.1751L1.57867 6.80005C1.47934 6.74654 1.39635 6.66713 1.33851 6.57026C1.28066 6.47338 1.25012 6.36266 1.25012 6.24983C1.25012 6.13701 1.28066 6.02628 1.33851 5.92941C1.39635 5.83254 1.47934 5.75313 1.57867 5.69961L9.70367 1.32461C9.7947 1.27548 9.89653 1.24976 9.99998 1.24976C10.1034 1.24976 10.2053 1.27548 10.2963 1.32461L18.4213 5.69961C18.5206 5.75313 18.6036 5.83254 18.6615 5.92941C18.7193 6.02628 18.7498 6.13701 18.7498 6.24983C18.7498 6.36266 18.7193 6.47338 18.6615 6.57026C18.6036 6.66713 18.5206 6.74654 18.4213 6.80005L10.2963 11.1751C10.2053 11.2243 10.1034 11.25 9.99998 11.2501ZM3.19336 6.25005L9.99998 9.91524L16.8066 6.25005L9.99998 2.58493L3.19336 6.25005Z" fill="currentColor"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "assets"
}

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"]
}

View File

@ -0,0 +1,54 @@
{
"string": {
"Planner": "Planner",
"Calendar": "Calendar",
"Agenda": "Agenda",
"Me": "Me",
"Team": "Team",
"Today": "Today",
"TodayColon": "Today:",
"Tomorrow": "Tomorrow",
"Yesterday": "Yesterday",
"Completed": "Completed",
"Now": "Now",
"Scheduled": "Scheduled",
"Schedule": "Schedule",
"WithoutProject": "Without project",
"TotalGroupTime": "{days, plural, =0 {} other {#d}} {hours, plural, =0 {} other {#h}} {minutes, plural, =0 {} other {#m}}",
"Tasks": "Tasks",
"WorkSlot": "Work Slot",
"WorkItem": "Work Item",
"Inbox": "Inbox",
"All": "All",
"Days": "{days}d",
"Hours": "{hours}h",
"Minutes": "{minutes}m",
"AddToDo": "Add ToDo",
"CreateToDo": "Add todo, press Enter to save",
"ToDos": "Todo's",
"Done": "Done",
"EditToDo": "Edit Todo",
"Unplanned": "Unplanned",
"Planned": "Planned",
"AddSlot": "Add Slot",
"NoPriority": "No Priority",
"LowPriority": "Low Priority",
"MediumPriority": "Medium Priority",
"HighPriority": "High Priority",
"AddTo": "Add to",
"AddTitle": "Add Title",
"MyWork": "My work",
"WeekCalendar": "Week",
"DayCalendar": "Day",
"GotoTimePlaning": "Planing",
"GotoTimeTeamPlaning": "Team Planing",
"NewToDo": "New Todo",
"ToDo": "Todo",
"ToDoColon": "Todo:",
"Priority": "Priority",
"CreatedToDo": "Created Todo",
"NewToDoDetails": "New Todo: {details}",
"MarkedAsDone": "Completed",
"WorkSchedule": "Work schedule"
}
}

View File

@ -0,0 +1,54 @@
{
"string": {
"Planner": "Планировщик",
"Calendar": "Календарь",
"Agenda": "Agenda",
"Me": "Me",
"Team": "Команда",
"Today": "Сегодня",
"TodayColon": "Сегодня:",
"Tomorrow": "Завтра",
"Yesterday": "Вчера",
"Completed": "Выполнено",
"Now": "Сейчас",
"Scheduled": "Запланировано",
"Schedule": "График",
"WithoutProject": "Без проекта",
"TotalGroupTime": "{days, plural, =0 {} other {#д}} {hours, plural, =0 {} other {#ч}} {minutes, plural, =0 {} other {#м}}",
"Tasks": "Задачи",
"WorkSlot": "Work Slot",
"WorkItem": "Задача",
"Inbox": "Входящие",
"All": "Все",
"Days": "{days}д",
"Hours": "{hours}ч",
"Minutes": "{minutes}м",
"AddToDo": "Добавить ToDo",
"CreateToDo": "Добавьте Todo, нажмите Ввод, чтобы сохранить",
"ToDos": "Todo's",
"Done": "Завершено",
"EditToDo": "Редактировать Todo",
"Unplanned": "Не запланировано",
"Planned": "Запланировано",
"AddSlot": "Добавить слот",
"NoPriority": "Без приоритета",
"HighPriority": "Высокий приоритет",
"MediumPriority": "Средний приоритет",
"LowPriority": "Низкий приоритет",
"AddTo": "Добавить в",
"AddTitle": "Добавить заголовок",
"MyWork": "Моя работа",
"WeekCalendar": "Неделя",
"DayCalendar": "Дни",
"GotoTimePlaning": "Планирование",
"GotoTimeTeamPlaning": "Командное планирование",
"NewToDo": "Новое Todo",
"ToDo": "Todo",
"ToDoColon": "Todo:",
"Priority": "Приоритет",
"CreatedToDo": "Создал(а) Todo",
"NewToDoDetails": "Новое Todo: {details}",
"MarkedAsDone": "Выполнено",
"WorkSchedule": "Расписание работы"
}
}

View File

@ -0,0 +1,40 @@
{
"name": "@hcengineering/time-assets",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Uberflow Contributors",
"template": "@hcengineering/assets-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"test": "jest --passWithNoTests --silent",
"build:docs": "",
"format": "format src",
"build:watch": "compile",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-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",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"@types/node": "~20.11.16",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"prettier-plugin-svelte": "^3.1.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/platform": "^0.6.9",
"@hcengineering/time": "^0.6.0"
}
}

View File

@ -0,0 +1,6 @@
import { makeLocalesTest } from '@hcengineering/platform'
it(
'Locales are equale',
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
)

View File

@ -0,0 +1,29 @@
//
// Copyright © 2022 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 { loadMetadata } from '@hcengineering/platform'
import time from '@hcengineering/time'
const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(time.icon, {
Team: `${icons}#team`,
Hashtag: `${icons}#hashtag`,
Inbox: `${icons}#inbox`,
Target: `${icons}#target`,
Flag: `${icons}#flag`,
FilledFlag: `${icons}#filledFlag`,
Planned: `${icons}#planned`,
All: `${icons}#all`
})

View File

@ -0,0 +1,11 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"types": ["node", "jest"],
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}

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"]
}

View File

@ -0,0 +1,67 @@
{
"name": "@hcengineering/time-resources",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Uberflow Contributors",
"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.1.9",
"sass": "^1.53.0",
"svelte-preprocess": "^5.1.0",
"@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.1.0",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"svelte-check": "^3.6.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"eslint-plugin-svelte": "^2.34.0",
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"svelte": "^4.2.5",
"@hcengineering/ui": "^0.6.11",
"@hcengineering/calendar": "^0.6.17",
"@hcengineering/calendar-resources": "^0.6.0",
"@hcengineering/presentation": "^0.6.2",
"@hcengineering/core": "^0.6.28",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/tracker-resources": "^0.6.0",
"@hcengineering/task": "^0.6.13",
"@hcengineering/task-resources": "^0.6.0",
"@hcengineering/tags": "^0.6.12",
"@hcengineering/tags-resources": "^0.6.0",
"@hcengineering/lead": "^0.6.0",
"@hcengineering/recruit": "^0.6.21",
"@hcengineering/board": "^0.6.12",
"@hcengineering/view": "^0.6.9",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/text-editor": "^0.6.0",
"@hcengineering/time": "^0.6.0",
"fast-equals": "^2.0.3",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1"
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}

View File

@ -0,0 +1,6 @@
<script lang="ts">
</script>
<svg width="3" viewBox="0 0 2 868" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.12" d="M1 0V1900" stroke="black" stroke-width="1.4" stroke-dasharray="5 3" />
</svg>

View File

@ -0,0 +1,65 @@
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ActionIcon, EditBox, IconAdd, showPopup, ModernEditbox } from '@hcengineering/ui'
import { ToDoPriority } from '@hcengineering/time'
import time from '../plugin'
import CreateToDoPopup from './CreateToDoPopup.svelte'
export let fullSize: boolean = false
let value: string = ''
async function save () {
let [name, description] = value.split('//')
name = name.trim()
if (name.length === 0) return
description = description?.trim() ?? ''
const client = getClient()
const acc = getCurrentAccount() as PersonAccount
await client.addCollection(time.class.ToDo, time.space.ToDos, time.ids.NotAttached, time.class.ToDo, 'todos', {
title: name,
description,
user: acc.person,
workslots: 0,
priority: ToDoPriority.NoPriority,
visibility: 'private'
})
clear()
}
function clear () {
value = ''
}
function openPopup () {
showPopup(CreateToDoPopup, {}, 'top')
}
</script>
<div class="flex-row-center flex-gap-1 container" on:blur={clear}>
<ModernEditbox
label={time.string.CreateToDo}
width={fullSize ? '100%' : ''}
size={'medium'}
autoAction={false}
bind:value
on:keydown={(e) => {
if (e.key === 'Enter') {
save()
e.preventDefault()
e.stopPropagation()
}
}}
>
<ActionIcon icon={IconAdd} action={openPopup} size={'small'} />
</ModernEditbox>
</div>
<style lang="scss">
.container {
padding: var(--spacing-2) var(--spacing-2) var(--spacing-2_5);
min-height: calc(4.75rem + 0.5px);
border-bottom: 1px solid var(--theme-divider-color);
}
</style>

View File

@ -0,0 +1,295 @@
<!--
// 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.
-->
<script lang="ts">
import { Calendar, generateEventId } from '@hcengineering/calendar'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import { PersonAccount } from '@hcengineering/contact'
import core, { AttachedData, Doc, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
import { SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
import tagsPlugin, { TagReference } from '@hcengineering/tags'
import task from '@hcengineering/task'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Button, Component, EditBox, IconClose, Label } from '@hcengineering/ui'
import { ToDo, ToDoPriority, WorkSlot } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
import DueDateEditor from './DueDateEditor.svelte'
import PriorityEditor from './PriorityEditor.svelte'
import Workslots from './Workslots.svelte'
import { VisibilityEditor } from '@hcengineering/calendar-resources'
export let object: Doc | undefined
const acc = getCurrentAccount() as PersonAccount
const todo: AttachedData<ToDo> = {
workslots: 0,
title: '',
description: '',
priority: ToDoPriority.NoPriority,
attachedSpace: object?.space,
visibility: 'private',
user: acc.person
}
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return true
}
let loading = false
async function saveToDo (): Promise<void> {
loading = true
const id = await client.addCollection(
time.class.ToDo,
time.space.ToDos,
object?._id ?? time.ids.NotAttached,
object?._class ?? time.class.ToDo,
'todos',
{
workslots: 0,
title: todo.title,
description: todo.description,
priority: todo.priority,
visibility: todo.visibility,
user: acc.person,
dueDate: todo.dueDate,
attachedSpace: todo.attachedSpace
}
)
const space = `${acc._id}_calendar` as Ref<Calendar>
for (const slot of slots) {
await client.addCollection(time.class.WorkSlot, space, id, time.class.ToDo, 'workslots', {
eventId: generateEventId(),
date: slot.date,
dueDate: slot.dueDate,
description: todo.description,
participants: [acc.person],
title: todo.title,
allDay: false,
access: 'owner',
visibility: todo.visibility === 'public' ? 'public' : 'freeBusy',
reminders: []
})
}
for (const tag of tags) {
await client.addCollection(tagsPlugin.class.TagReference, time.space.ToDos, id, time.class.ToDo, 'labels', tag)
}
dispatch('close', true)
}
const currentUser = getCurrentAccount() as PersonAccount
let space: Ref<Calendar> = `${currentUser._id}_calendar` as Ref<Calendar>
const q = createQuery()
q.query(calendar.class.ExternalCalendar, { default: true, members: currentUser._id }, (res) => {
if (res.length > 0) {
space = res[0]._id
}
})
let slots: WorkSlot[] = []
function removeSlot (e: CustomEvent<{ _id: Ref<WorkSlot> }>): void {
const index = slots.findIndex((p) => p._id === e.detail._id)
if (index !== -1) {
slots.splice(index, 1)
slots = slots
}
}
function createSlot (): void {
const defaultDuration = 30 * 60 * 1000
const now = Date.now()
const date = Math.ceil(now / (30 * 60 * 1000)) * (30 * 60 * 1000)
const dueDate = date + defaultDuration
slots.push({
eventId: generateEventId(),
date,
dueDate,
description: todo.description,
participants: [acc.person],
title: todo.title,
allDay: false,
access: 'owner',
visibility: todo.visibility,
reminders: [],
space,
_id: generateId(),
_class: time.class.WorkSlot,
attachedTo: generateId(),
attachedToClass: time.class.ToDo,
collection: 'workslots',
modifiedOn: Date.now(),
modifiedBy: acc._id
})
slots = slots
}
function changeSlot (e: CustomEvent<{ startDate: number, dueDate: number, slot: Ref<WorkSlot> }>): void {
const { startDate, dueDate, slot } = e.detail
const workslot = slots.find((s) => s._id === slot)
if (workslot !== undefined) {
workslot.dueDate = dueDate
workslot.date = startDate
slots = slots
}
}
function changeDueSlot (e: CustomEvent<{ dueDate: number, slot: Ref<WorkSlot> }>): void {
const { dueDate, slot } = e.detail
const workslot = slots.find((s) => s._id === slot)
if (workslot !== undefined) {
workslot.dueDate = dueDate
slots = slots
}
}
let tags: AttachedData<TagReference>[] = []
</script>
<div class="eventPopup-container">
<div class="header flex-between">
<EditBox
bind:value={todo.title}
kind={'ghost-large'}
placeholder={time.string.AddTitle}
fullSize
focusable
autoFocus
focusIndex={10001}
/>
<div class="flex-row-center gap-1 flex-no-shrink ml-3">
<Button
id="card-close"
icon={IconClose}
kind={'ghost'}
size={'small'}
on:click={() => {
dispatch('close')
}}
/>
</div>
</div>
<div class="block flex-no-shrink">
<div class="pb-4">
<StyledTextBox
alwaysEdit={true}
maxHeight="limited"
showButtons={false}
placeholder={calendar.string.Description}
bind:content={todo.description}
/>
</div>
<div class="flex-row-center gap-1-5 mb-1">
<DueDateEditor bind:value={todo.dueDate} />
<PriorityEditor bind:value={todo.priority} />
</div>
</div>
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<div>
<Label label={time.string.AddTo} />
</div>
<SpaceSelector
_class={task.class.Project}
query={{ archived: false, members: getCurrentAccount()._id }}
label={core.string.Space}
autoSelect={false}
allowDeselect
kind={'regular'}
size={'medium'}
focus={false}
bind:space={todo.attachedSpace}
/>
<VisibilityEditor bind:value={todo.visibility} />
</div>
</div>
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<Component
is={tagsPlugin.component.DraftTagsEditor}
props={{ tags, targetClass: time.class.ToDo }}
on:change={(e) => {
tags = e.detail
}}
/>
</div>
</div>
<div class="block flex-no-shrink end">
<div class="flex-row-center gap-1-5">
<Workslots
bind:slots
on:remove={removeSlot}
on:create={createSlot}
on:change={changeSlot}
on:dueChange={changeDueSlot}
/>
</div>
</div>
<div class="flex-row-reverse btn flex-no-shrink">
<Button
kind="primary"
{loading}
label={time.string.AddToDo}
on:click={saveToDo}
disabled={todo?.title === undefined || todo?.title === ''}
/>
</div>
</div>
<style lang="scss">
.eventPopup-container {
display: flex;
flex-direction: column;
padding-top: 2rem;
padding-bottom: 2rem;
max-width: 40rem;
min-width: 40rem;
min-height: 0;
background: var(--theme-popup-color);
border-radius: 1rem;
box-shadow: var(--theme-popup-shadow);
.header {
flex-shrink: 0;
padding-right: 2rem;
padding-left: 2rem;
}
.btn {
padding-top: 1rem;
padding-right: 1rem;
padding-left: 1rem;
}
.block {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding-top: 1rem;
padding-bottom: 1rem;
padding-right: 2rem;
padding-left: 2rem;
&:not(.end) {
border-bottom: 1px solid var(--theme-divider-color);
}
}
}
</style>

View File

@ -0,0 +1,55 @@
<!--
// 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.
-->
<script lang="ts">
import ui, { Button, DatePopup, Icon, Label, showPopup } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
export let value: number | null | undefined
const dispatch = createEventDispatcher()
let opened: boolean = false
</script>
<Button
kind={'regular'}
on:click={(e) => {
if (!opened) {
opened = true
showPopup(
DatePopup,
{ noShift: true, currentDate: value ? new Date(value) : null, label: ui.string.SetDueDate },
'top',
(result) => {
if (result != null && result.value !== undefined) {
dispatch('change', result.value)
value = result.value ? result.value.getTime() : null
}
opened = false
}
)
}
}}
>
<div slot="content" class="flex-row-center flex-gap-1">
<Icon icon={time.icon.Target} size="medium" />
{#if value}
{new Date(value).toLocaleDateString()}
{:else}
<Label label={ui.string.DueDate} />
{/if}
</div>
</Button>

View File

@ -0,0 +1,271 @@
<!--
// 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.
-->
<script lang="ts">
import { Visibility } from '@hcengineering/calendar'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import core, { Class, Ref, Space, getCurrentAccount, Markup } from '@hcengineering/core'
import { SpaceSelector, getClient, createQuery } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import task from '@hcengineering/task'
import { StyledTextBox } from '@hcengineering/text-editor'
import {
ModernEditbox,
CheckBox,
Component,
EditBox,
IconClose,
Label,
Modal,
Spinner,
ButtonIcon
} from '@hcengineering/ui'
import { ToDo, ToDoPriority } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
import DueDateEditor from './DueDateEditor.svelte'
import PriorityEditor from './PriorityEditor.svelte'
import TodoWorkslots from './TodoWorkslots.svelte'
import { VisibilityEditor } from '@hcengineering/calendar-resources'
// export let object: ToDo
export let _id: Ref<ToDo>
export let _class: Ref<Class<ToDo>>
export let embedded: boolean = false
export let kind: 'default' | 'modern' = 'default'
let object: ToDo
let title: string
let description: Markup
let countTag: number = 0
const dispatch = createEventDispatcher()
const queryClient = createQuery()
const client = getClient()
$: _id !== undefined &&
_class !== undefined &&
queryClient.query<ToDo>(_class, { _id }, async (result) => {
;[object] = result
if (object !== undefined) {
title = object.title
description = object.description
}
})
export function canClose (): boolean {
return true
}
async function updateName () {
if (object.title !== title) {
await client.update(object, { title })
}
}
async function updateDescription () {
if (object.description !== description) {
await client.update(object, { description })
}
}
async function markDone () {
object.doneOn = object.doneOn == null ? Date.now() : null
await client.update(object, { doneOn: object.doneOn })
}
async function dueDateChange (value: Date | null) {
const dueDate = value != null ? value.getTime() : null
await client.update(object, { dueDate })
object.dueDate = dueDate
}
async function priorityChange (priority: ToDoPriority) {
await client.update(object, { priority })
object.priority = priority
}
async function visibilityChange (visibility: Visibility) {
await client.update(object, { visibility })
object.visibility = visibility
}
async function spaceChange (space: Ref<Space>) {
await client.update(object, { attachedSpace: space })
object.attachedSpace = space
}
</script>
<Modal type={'type-component'} noResize padding={'0'}>
<svelte:fragment slot="beforeTitle">
<div class="flex-center flex-no-shrink min-w-6 min-h-6">
{#if object}
<CheckBox on:value={markDone} checked={object.doneOn != null} kind={'todo'} />
{:else}
<Spinner size={'medium'} />
{/if}
</div>
</svelte:fragment>
<svelte:fragment slot="title">
{#if object}
<VisibilityEditor
value={object.visibility}
size={'small'}
disabled={object._class === time.class.ProjectToDo}
on:change={(e) => visibilityChange(e.detail)}
/>
<Component
is={tags.component.DocTagsEditor}
props={{ object, targetClass: time.class.ToDo, type: 'type-button-only' }}
on:change={(event) => {
if (event.detail !== undefined) countTag = event.detail
}}
/>
{/if}
</svelte:fragment>
<svelte:fragment slot="actions">
<ButtonIcon icon={IconClose} kind={'tertiary'} size={'small'} on:click={() => dispatch('close')} />
</svelte:fragment>
{#if object}
<div class="top-content">
<ModernEditbox
bind:value={title}
label={time.string.AddTitle}
kind={'ghost'}
size={'large'}
focusIndex={10001}
on:change={updateName}
/>
<div class="min-h-16 px-4">
<StyledTextBox
alwaysEdit={true}
isScrollable={false}
showButtons={false}
placeholder={calendar.string.Description}
bind:content={description}
on:value={updateDescription}
/>
</div>
<div class="emphasized">
<div class="flex-row-center flex-gap-4">
<span class="font-medium"><Label label={time.string.AddTo} /></span>
<SpaceSelector
_class={task.class.Project}
query={{ archived: false, members: getCurrentAccount()._id }}
label={core.string.Space}
kind={'regular'}
size={'medium'}
allowDeselect
autoSelect={false}
readonly={object._class === time.class.ProjectToDo}
space={object.attachedSpace}
on:change={(e) => spaceChange(e.detail)}
/>
</div>
</div>
</div>
{#if countTag}
<div class="labels-content">
<Component
is={tags.component.DocTagsEditor}
props={{ object, targetClass: time.class.ToDo, type: 'type-content-only' }}
/>
</div>
{/if}
<div class="slots-content">
<div class="flex-row-top justify-between flex-gap-2">
<span class="font-medium-14 secondary-textColor">
<Label label={time.string.WorkSchedule} />
</span>
<div class="flex-row-center gap-2">
<DueDateEditor value={object.dueDate} on:change={(e) => dueDateChange(e.detail)} />
<PriorityEditor value={object.priority} on:change={(e) => priorityChange(e.detail)} />
</div>
</div>
<TodoWorkslots todo={object} />
</div>
{/if}
</Modal>
<style lang="scss">
.labels-content,
.slots-content,
.top-content,
.emphasized {
display: flex;
flex-direction: column;
flex-shrink: 0;
min-width: 0;
min-height: 0;
}
.top-content {
padding: var(--spacing-3) var(--spacing-2) var(--spacing-4);
}
.emphasized {
gap: var(--spacing-1_5);
margin: var(--spacing-2_5) var(--spacing-2) 0;
padding: var(--spacing-2);
color: var(--global-primary-TextColor);
background-color: var(--theme-bg-accent-color);
border-radius: var(--medium-BorderRadius);
}
.labels-content {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
padding: var(--spacing-1_5) var(--spacing-4);
width: 100%;
border-top: 1px solid var(--theme-divider-color);
}
.slots-content {
gap: var(--spacing-2);
padding: var(--spacing-3) var(--spacing-4);
border-top: 1px solid var(--theme-divider-color);
border-bottom: 1px solid var(--theme-divider-color);
}
.eventPopup-container {
display: flex;
flex-direction: column;
padding-top: 2rem;
padding-bottom: 2rem;
max-width: 40rem;
min-width: 40rem;
min-height: 0;
background: var(--theme-popup-color);
border-radius: 1rem;
box-shadow: var(--theme-popup-shadow);
.header {
flex-shrink: 0;
padding-right: 2rem;
padding-left: 2rem;
}
.block {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding-top: 1rem;
padding-bottom: 1rem;
padding-right: 2rem;
padding-left: 2rem;
&:not(.end) {
border-bottom: 1px solid var(--theme-divider-color);
}
}
}
</style>

View File

@ -0,0 +1,194 @@
<!--
// Copyright © 2022 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 { Event } from '@hcengineering/calendar'
import {
CalendarSelector,
EventReminders,
EventTimeEditor,
VisibilityEditor,
isReadOnly
} from '@hcengineering/calendar-resources'
import calendar from '@hcengineering/calendar-resources/src/plugin'
import { DocumentUpdate } from '@hcengineering/core'
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Button, EditBox, Icon, IconClose, createFocusManager } from '@hcengineering/ui'
import FocusHandler from '@hcengineering/ui/src/components/FocusHandler.svelte'
import { ToDo, WorkSlot } from '@hcengineering/time'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
import TaskSelector from './TaskSelector.svelte'
export let object: WorkSlot
$: readOnly = isReadOnly(object)
let title = object.title
let startDate = object.date
const duration = object.dueDate - object.date
let dueDate = startDate + duration
const allDay = object.allDay
let reminders = [...(object.reminders ?? [])]
let description = object.description
let visibility = object.visibility ?? 'public'
let space = object.space
let _doc: ToDo | undefined
const q = createQuery()
q.query(
object.attachedToClass,
{
_id: object.attachedTo
},
(res) => {
_doc = res[0]
}
)
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return true
}
async function saveEvent () {
if (readOnly) {
return
}
const update: DocumentUpdate<Event> = {}
if (object.description !== description) {
update.description = description.trim()
}
if (object.date !== startDate) {
update.date = startDate
}
if (object.dueDate !== dueDate) {
update.dueDate = dueDate
}
if (object.visibility !== visibility) {
update.visibility = visibility
}
if (object.space !== space) {
update.space = space
}
if (!deepEqual(object.reminders, reminders)) {
update.reminders = reminders
}
if (Object.keys(update).length > 0) {
await client.update(object, update)
}
dispatch('close')
}
const manager = createFocusManager()
</script>
<FocusHandler {manager} />
<div class="eventPopup-container">
<div class="header flex-between">
<EditBox bind:value={title} disabled kind={'ghost-large'} fullSize focusable focusIndex={10001} />
<div class="flex-row-center gap-1 flex-no-shrink ml-3">
<Button
id="card-close"
focusIndex={10003}
icon={IconClose}
kind={'ghost'}
size={'small'}
on:click={() => {
dispatch('close')
}}
/>
</div>
</div>
<div class="block first flex-no-shrink">
<EventTimeEditor {allDay} bind:startDate bind:dueDate disabled={readOnly} focusIndex={10004} />
</div>
<div class="block flex-no-shrink">
<div class="flex-row-center gap-1-5 mb-1">
<Icon icon={calendar.icon.Description} size={'small'} />
<StyledTextBox
alwaysEdit={true}
kind={'indented'}
maxHeight={'limited'}
focusIndex={10005}
showButtons={false}
placeholder={calendar.string.Description}
bind:content={description}
/>
</div>
<TaskSelector bind:value={_doc} focusIndex={10006} />
</div>
<div class="block rightCropPadding">
<CalendarSelector bind:value={space} disabled={readOnly} focusIndex={10007} />
<div class="flex-row-center flex-gap-1">
<Icon icon={calendar.icon.Hidden} size={'small'} />
<VisibilityEditor bind:value={visibility} kind={'tertiary'} withoutIcon disabled={readOnly} focusIndex={10008} />
</div>
<EventReminders bind:reminders disabled={readOnly} focusIndex={10009} />
</div>
<div class="flex-between p-5 flex-no-shrink">
<div />
<Button
kind="primary"
label={presentation.string.Save}
disabled={readOnly}
on:click={saveEvent}
focusIndex={10010}
/>
</div>
</div>
<style lang="scss">
.eventPopup-container {
display: flex;
flex-direction: column;
max-width: 25rem;
min-width: 25rem;
min-height: 0;
background: var(--theme-popup-color);
border-radius: 1rem;
box-shadow: var(--theme-popup-shadow);
.header {
flex-shrink: 0;
padding: 0.75rem 0.75rem 0.5rem;
}
.block {
display: flex;
flex-direction: column;
padding: 0.75rem 1.25rem;
min-width: 0;
min-height: 0;
border-bottom: 1px solid var(--theme-divider-color);
&.first {
padding-top: 0;
}
&:not(.rightCropPadding) {
padding: 0.75rem 1.25rem;
}
&.rightCropPadding {
padding: 0.75rem 1rem 0.75rem 1.25rem;
}
}
}
</style>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import { Employee, PersonAccount, getName } from '@hcengineering/contact'
import { Avatar, employeeByIdStore, personAccountByIdStore } from '@hcengineering/contact-resources'
import { Account, IdMap, Ref } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import task, { Project } from '@hcengineering/task'
import { Button, Scroller } from '@hcengineering/ui'
export let value: Ref<Project>
export let selected: Ref<Employee>
let space: Project | undefined = undefined
const client = getClient()
const query = createQuery()
$: query.query(task.class.Project, { _id: value }, (res) => {
space = res[0]
})
let employees: Employee[] = []
function getEmployee (
_id: Ref<Account>,
personAccountByIdStore: IdMap<PersonAccount>,
employeeByIdStore: IdMap<Employee>
): Employee | undefined {
const employee = personAccountByIdStore.get(_id as Ref<PersonAccount>)
return employee ? employeeByIdStore.get(employee.person as Ref<Employee>) : undefined
}
function getEmployees (
space: Project | undefined,
personAccountByIdStore: IdMap<PersonAccount>,
employeeByIdStore: IdMap<Employee>
): void {
employees = []
if (space === undefined) return
for (const member of space.members) {
const emp = getEmployee(member, personAccountByIdStore, employeeByIdStore)
if (emp) employees.push(emp)
}
employees.sort((a, b) => getName(client.getHierarchy(), a).localeCompare(getName(client.getHierarchy(), b)))
employees = employees
}
$: getEmployees(space, $personAccountByIdStore, $employeeByIdStore)
</script>
{#if space}
<Scroller padding={'.25rem'} gap={'gap-2'} contentDirection={'horizontal'} noFade={false}>
{#each employees as employee}
<Button size={'x-large'} selected={employee._id === selected} on:click={() => (selected = employee._id)}>
<svelte:fragment slot="content">
<Avatar avatar={employee.avatar} name={employee.name} size={'smaller'} />
<span class="ml-2">{getName(client.getHierarchy(), employee)}</span>
</svelte:fragment>
</Button>
{/each}
</Scroller>
{/if}

View File

@ -0,0 +1,81 @@
<script lang="ts">
import calendar from '@hcengineering/calendar'
import { Timestamp } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { Button, IconBack, IconForward, Label, areDatesEqual, ticker } from '@hcengineering/ui'
export let currentDate: Date = new Date()
function inc (val: number): void {
if (val === 0) {
currentDate = new Date()
return
}
currentDate.setDate(currentDate.getDate() + val)
currentDate = currentDate
}
function getTitle (day: Date, now: Timestamp): IntlString {
// const today = new Date(now)
// const tomorrow = new Date(new Date(now).setDate(new Date(now).getDate() + 1))
// const yesterday = new Date(new Date(now).setDate(new Date(now).getDate() - 1))
// if (areDatesEqual(day, today)) return time.string.Today
// if (areDatesEqual(day, yesterday)) return time.string.Yesterday
// if (areDatesEqual(day, tomorrow)) return time.string.Tomorrow
const isCurrentYear = day.getFullYear() === new Date().getFullYear()
return getEmbeddedLabel(
day.toLocaleDateString('default', {
month: 'long',
day: 'numeric',
year: isCurrentYear ? undefined : 'numeric'
})
)
}
$: isToday = areDatesEqual(currentDate, new Date($ticker))
</script>
<div class="flex-between header-container bottom-divider flex-reverse">
<div class="my-1 flex-row-center">
<slot />
<div class="date">
<Label label={getTitle(currentDate, $ticker)} />
</div>
<Button
icon={IconBack}
kind={'ghost'}
on:click={() => {
inc(-1)
}}
/>
<div class="antiHSpacer x2" />
<Button
icon={IconForward}
kind={'ghost'}
on:click={() => {
inc(1)
}}
/>
<div class="antiHSpacer x4" />
<Button
label={calendar.string.Today}
disabled={isToday}
kind={isToday ? 'primary' : 'regular'}
on:click={() => {
inc(0)
}}
/>
</div>
</div>
<style lang="scss">
.header-container {
flex-shrink: 0;
padding: 0.5rem 2rem;
}
.date {
color: var(--theme-caption-color);
font-size: 1.25rem;
margin-right: 1rem;
}
</style>

View File

@ -0,0 +1,11 @@
<script lang="ts">
import PlanView from './PlanView.svelte'
export let visibleNav: boolean = true
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
</script>
<div class="hulyPanels-container">
<PlanView {visibleNav} {navFloat} {appsDirection} on:change />
</div>

View File

@ -0,0 +1,30 @@
<!--
// 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.
-->
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { ToDo } from '@hcengineering/time'
import time from '../plugin'
export let value: ToDo
</script>
<div class="flex-col flex-gap-2">
<div>
<Label label={time.string.CreateToDo} />
</div>
<div>
{value.title}
</div>
</div>

View File

@ -0,0 +1,98 @@
<script lang="ts">
import { createEventDispatcher, afterUpdate } from 'svelte'
import calendar, { Calendar, generateEventId } from '@hcengineering/calendar'
import { PersonAccount } from '@hcengineering/contact'
import { Ref, getCurrentAccount } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { TagElement } from '@hcengineering/tags'
import { Separator, defineSeparators } from '@hcengineering/ui'
import { ToDo } from '@hcengineering/time'
import { ToDosMode } from '..'
import time from '../plugin'
import { timeSeparators } from '../utils'
import PlanningCalendar from './PlanningCalendar.svelte'
import ToDos from './ToDos.svelte'
import ToDosNavigator from './ToDosNavigator.svelte'
export let visibleNav: boolean = true
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
const dispatch = createEventDispatcher()
const defaultDuration = 30 * 60 * 1000
let replacedPanel: HTMLElement
let currentDate: Date = new Date()
let dragItem: ToDo | undefined = undefined
const client = getClient()
async function drop (e: CustomEvent<any>) {
if (dragItem === undefined) return
const doc = dragItem
const date = e.detail.date.getTime()
const currentUser = getCurrentAccount() as PersonAccount
const extCalendar = await client.findOne(calendar.class.ExternalCalendar, {
members: currentUser._id,
archived: false,
default: true
})
const space = extCalendar ? extCalendar._id : (`${currentUser._id}_calendar` as Ref<Calendar>)
const dueDate = date + defaultDuration
await client.addCollection(time.class.WorkSlot, space, doc._id, doc._class, 'workslots', {
eventId: generateEventId(),
date,
dueDate,
description: doc.description,
participants: [currentUser.person],
title: doc.title,
allDay: false,
access: 'owner',
visibility: doc.visibility === 'public' ? 'public' : 'freeBusy',
reminders: []
})
}
defineSeparators('time', timeSeparators)
let mode: ToDosMode = (localStorage.getItem('todos_last_mode') as ToDosMode) ?? 'unplanned'
let tag: Ref<TagElement> | undefined = (localStorage.getItem('todos_last_tag') as Ref<TagElement>) ?? undefined
dispatch('change', true)
afterUpdate(() => {
dispatch('change', { type: 'replacedPanel', replacedPanel })
})
</script>
{#if visibleNav}
<ToDosNavigator bind:mode bind:tag bind:currentDate {navFloat} {appsDirection} />
<Separator
name={'time'}
float={navFloat}
index={0}
disabledWhen={['panel-aside']}
color={'var(--theme-navpanel-border)'}
/>
<div class="flex-col clear-mins">
<ToDos
{mode}
{tag}
bind:currentDate
on:dragstart={(e) => (dragItem = e.detail)}
on:dragend={() => (dragItem = undefined)}
/>
</div>
<Separator name={'time'} float={navFloat} index={1} color={'transparent'} separatorSize={0} short />
{/if}
<div class="w-full clear-mins" bind:this={replacedPanel}>
<PlanningCalendar
{dragItem}
{visibleNav}
bind:currentDate
displayedDaysCount={3}
on:dragDrop={drop}
on:change={(event) => (visibleNav = event.detail)}
/>
</div>

View File

@ -0,0 +1,243 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import calendar, { Calendar, Event, generateEventId, getAllEvents } from '@hcengineering/calendar'
import { DayCalendar, calendarStore, hidePrivateEvents } from '@hcengineering/calendar-resources'
import { PersonAccount } from '@hcengineering/contact'
import { Ref, SortingOrder, Timestamp, getCurrentAccount } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import {
AnyComponent,
ButtonBase,
ButtonIcon,
IconChevronLeft,
IconChevronRight,
Label,
areDatesEqual,
showPopup,
ticker,
Header,
getFormattedDate,
resizeObserver,
deviceOptionsStore as deviceInfo
} from '@hcengineering/ui'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../plugin'
import IconSun from './icons/Sun.svelte'
export let dragItem: ToDo | undefined = undefined
export let currentDate: Date = new Date()
export let displayedDaysCount = 1
export let createComponent: AnyComponent | undefined = calendar.component.CreateEvent
export let visibleNav: boolean = true
const dispatch = createEventDispatcher()
const q = createQuery()
function getFrom (date: Date): Timestamp {
return new Date(date).setHours(0, 0, 0, 0)
}
function getTo (date: Date): Timestamp {
return new Date(date).setDate(date.getDate() + 3)
}
let dayCalendar: DayCalendar
let raw: Event[] = []
let objects: Event[] = []
let showLabel: boolean = true
const rem = (n: number): number => n * $deviceInfo.fontSize
const acc = getCurrentAccount()._id
const calendarsQ = createQuery()
let calendars: Calendar[] = []
let todayDate = new Date()
$: calendarsQ.query(calendar.class.Calendar, { members: acc, archived: false }, (res) => {
calendars = res
})
$: from = getFrom(currentDate)
$: to = getTo(currentDate)
function update (calendars: Calendar[]) {
q.query<Event>(
calendar.class.Event,
{ space: { $in: calendars.map((p) => p._id) } },
(result) => {
raw = result
},
{ sort: { date: SortingOrder.Ascending } }
)
}
$: update(calendars)
$: all = getAllEvents(raw, from, to)
$: objects = hidePrivateEvents(all, $calendarStore)
function inc (val: number): void {
if (val === 0) {
currentDate = new Date()
dayCalendar.scrollToTime(currentDate)
return
}
currentDate.setDate(currentDate.getDate() + val)
currentDate = currentDate
}
function getTitle (day: Date, now: Timestamp): IntlString {
const today = new Date(now)
const tomorrow = new Date(new Date(now).setDate(new Date(now).getDate() + 1))
const yesterday = new Date(new Date(now).setDate(new Date(now).getDate() - 1))
if (areDatesEqual(day, today)) return time.string.Today
if (areDatesEqual(day, yesterday)) return time.string.Yesterday
if (areDatesEqual(day, tomorrow)) return time.string.Tomorrow
const isCurrentYear = day.getFullYear() === new Date().getFullYear()
return getEmbeddedLabel(
day.toLocaleDateString('default', {
month: 'long',
day: 'numeric',
year: isCurrentYear ? undefined : 'numeric'
})
)
}
const dragItemId = 'drag_item' as Ref<WorkSlot>
function dragEnter (e: CustomEvent<any>) {
if (dragItem != null) {
const current = raw.find((p) => p._id === dragItemId)
if (current !== undefined) {
current.attachedTo = dragItem._id
current.attachedToClass = dragItem._class
current.date = e.detail.date.getTime()
current.dueDate = new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30)
} else {
const me = getCurrentAccount() as PersonAccount
const space = `${me._id}_calendar` as Ref<Calendar>
const ev: WorkSlot = {
_id: dragItemId,
allDay: false,
eventId: generateEventId(),
title: '',
description: '',
access: 'owner',
attachedTo: dragItem._id,
attachedToClass: dragItem._class,
_class: time.class.WorkSlot,
collection: 'events',
visibility: 'public',
space,
modifiedBy: me._id,
participants: [me.person],
modifiedOn: Date.now(),
date: e.detail.date.getTime(),
dueDate: new Date(e.detail.date).setMinutes(new Date(e.detail.date).getMinutes() + 30)
}
raw.push(ev)
}
raw = raw
all = getAllEvents(raw, from, to)
objects = hidePrivateEvents(all, $calendarStore)
}
}
function dragLeave (event: DragEvent) {
const rect = dayCalendar.getCalendarRect()
if (!rect) return
if (event.x < rect.left || event.x > rect.right || event.y < rect.top || event.y > rect.bottom) {
raw = raw.filter((r) => r._id !== dragItemId)
}
}
function clear (dragItem: ToDo | undefined) {
if (dragItem === undefined) {
raw = raw.filter((p) => p._id !== dragItemId)
all = getAllEvents(raw, from, to)
objects = hidePrivateEvents(all, $calendarStore)
}
}
$: clear(dragItem)
function showCreateDialog (date: Date, withTime: boolean) {
if (createComponent === undefined) {
return
}
showPopup(createComponent, { date, withTime }, 'top')
}
</script>
<div
class="hulyComponent modal"
use:resizeObserver={(element) => {
showLabel = showLabel ? element.clientWidth > rem(3.5) + 399 : element.clientWidth > rem(3.5) + 400
}}
>
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
<span class="heading-medium-20 overflow-label">
<Label label={time.string.Schedule} />: <Label label={getTitle(currentDate, $ticker)} />
</span>
<svelte:fragment slot="actions">
<ButtonIcon
icon={IconChevronLeft}
kind={'secondary'}
size={'small'}
on:click={() => {
inc(-1)
}}
/>
<ButtonBase
icon={IconSun}
label={showLabel ? time.string.TodayColon : undefined}
title={showLabel ? getFormattedDate(todayDate.getTime(), { weekday: 'short', day: 'numeric' }) : undefined}
type={showLabel ? 'type-button' : 'type-button-icon'}
kind={'secondary'}
size={'small'}
inheritFont
hasMenu
on:click={() => {
inc(0)
}}
/>
<ButtonIcon
icon={IconChevronRight}
kind={'secondary'}
size={'small'}
on:click={() => {
inc(1)
}}
/>
</svelte:fragment>
</Header>
<div class="hulyComponent-content__container">
<DayCalendar
bind:this={dayCalendar}
events={objects}
{displayedDaysCount}
startFromWeekStart={false}
clearCells={dragItem !== undefined}
{dragItemId}
on:dragEnter={dragEnter}
on:dragleave={dragLeave}
on:create={(e) => {
showCreateDialog(e.detail.date, e.detail.withTime)
}}
on:dragDrop
bind:currentDate
bind:todayDate
/>
</div>
</div>
<style lang="scss">
.title {
padding: 1.75rem 2rem;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
.tools {
padding: 0 2rem 0.75rem;
}
</style>

View File

@ -0,0 +1,89 @@
<!--
// 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.
-->
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { ButtonKind, Dropdown, ListItem, themeStore } from '@hcengineering/ui'
import { ToDoPriority } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
export let value: ToDoPriority = ToDoPriority.NoPriority
export let kind: ButtonKind | undefined = 'regular'
export let onChange: (value: ToDoPriority) => void = () => {}
let items: ListItem[] = []
$: fill($themeStore.language)
async function fill (lang: string) {
items = [
{
_id: ToDoPriority.NoPriority.toString(),
label: await translate(time.string.NoPriority, {}, lang),
icon: time.icon.Flag
},
{
_id: ToDoPriority.High.toString(),
label: await translate(time.string.HighPriority, {}, lang),
icon: time.icon.FilledFlag,
iconProps: {
fill: '#F96E50'
}
},
{
_id: ToDoPriority.Medium.toString(),
label: await translate(time.string.MediumPriority, {}, lang),
icon: time.icon.FilledFlag,
iconProps: {
fill: '#FFCD6B'
}
},
{
_id: ToDoPriority.Low.toString(),
label: await translate(time.string.LowPriority, {}, lang),
icon: time.icon.FilledFlag,
iconProps: {
fill: '#0084FF'
}
}
]
}
$: selected = items.find((item) => item._id === value.toString())
const dispatch = createEventDispatcher()
function change (val: string) {
const priority = parseInt(val)
if (priority !== value) {
dispatch('change', priority)
value = priority
onChange(priority)
}
}
</script>
<Dropdown
icon={time.icon.Flag}
{kind}
size={'medium'}
placeholder={time.string.NoPriority}
{items}
{selected}
withSearch={false}
on:selected={(e) => {
change(e.detail._id)
}}
/>

View File

@ -0,0 +1,38 @@
<!--
// 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.
-->
<script lang="ts">
import { Icon } from '@hcengineering/ui'
import { ToDoPriority } from '@hcengineering/time'
import time from '../plugin'
export let value: ToDoPriority
function getIconProps (value: ToDoPriority): string {
switch (value) {
case ToDoPriority.High:
return '#F96E50'
case ToDoPriority.Medium:
return '#FFCD6B'
case ToDoPriority.Low:
return '#0084FF'
case ToDoPriority.NoPriority:
return '#FFFFFF'
}
}
</script>
{#if value !== ToDoPriority.NoPriority}
<Icon icon={time.icon.FilledFlag} size={'medium'} iconProps={{ fill: getIconProps(value) }} />
{/if}

View File

@ -0,0 +1,42 @@
<script lang="ts">
import { Class } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Task } from '@hcengineering/task'
import { Button, Icon, Label, showPanel } from '@hcengineering/ui'
import view, { ObjectPanel } from '@hcengineering/view'
import { ToDo } from '@hcengineering/time'
import time from '../plugin'
import WorkItemPresenter from './WorkItemPresenter.svelte'
export let value: ToDo | undefined
export let focusIndex = -1
const client = getClient()
const hierarchy = client.getHierarchy()
function click (e: MouseEvent) {
if (value && value.attachedTo !== time.ids.NotAttached) {
const panelComponent = hierarchy.classHierarchyMixin<Class<Task>, ObjectPanel>(
value.attachedToClass,
view.mixin.ObjectPanel
)
const component = panelComponent?.component ?? view.component.EditDoc
showPanel(component, value.attachedTo, value.attachedToClass, 'content')
}
}
</script>
{#if value && value.attachedTo !== time.ids.NotAttached}
<div class="flex-row-center gap-1-5">
<Icon icon={time.icon.Hashtag} size={'small'} />
<Button kind={'ghost'} padding={'0 .5rem'} {focusIndex} shrink={1} on:click={click}>
<svelte:fragment slot="content">
{#if value}
<WorkItemPresenter todo={value} withoutSpace />
{:else}
<Label label={time.string.WorkItem} />
{/if}
</svelte:fragment>
</Button>
</div>
{/if}

View File

@ -0,0 +1,107 @@
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { areDatesEqual } from '@hcengineering/ui'
import { ToDo, WorkSlot } from '@hcengineering/time'
import timePlugin from '../plugin'
import { getNearest } from '../utils'
export let events: WorkSlot[]
export let todo: ToDo
$: overdue = !events.some((event) => event.dueDate >= Date.now())
$: near = getNearest(events)
async function getText (todo: ToDo, near: WorkSlot | undefined): Promise<void> {
const today = new Date()
const tomorrow = new Date(new Date().setDate(new Date().getDate() + 1))
const yesterday = new Date(new Date().setDate(new Date().getDate() - 1))
if (todo.doneOn != null) {
const day = new Date(todo.doneOn)
if (areDatesEqual(day, today)) {
str = await translate(timePlugin.string.Today, {})
return
}
if (areDatesEqual(day, yesterday)) {
str = await translate(timePlugin.string.Yesterday, {})
return
}
if (areDatesEqual(day, tomorrow)) {
str = await translate(timePlugin.string.Tomorrow, {})
return
}
str = new Date(todo.doneOn).toLocaleString('default', {
day: '2-digit',
month: 'short'
})
return
}
if (near !== undefined) {
str = new Date(near.date).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric',
day: '2-digit',
month: 'short'
})
const time = new Date(near.date).toLocaleString('default', {
minute: '2-digit',
hour: 'numeric'
})
const day = new Date(near.date)
if (areDatesEqual(day, today)) {
str = `${await translate(timePlugin.string.Today, {})} ${time}`
return
}
if (areDatesEqual(day, yesterday)) {
str = `${await translate(timePlugin.string.Yesterday, {})} ${time}`
return
}
if (areDatesEqual(day, tomorrow)) {
str = `${await translate(timePlugin.string.Tomorrow, {})} ${time}`
return
}
return
}
str = await translate(timePlugin.string.Inbox, {})
}
let str = ''
$: getText(todo, near)
</script>
<div
class="container"
class:overdue
class:done={todo.doneOn != null}
class:inbox={events.length === 0 && todo.doneOn == null}
>
{str}
</div>
<style lang="scss">
.container {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: nowrap;
padding: 0.125rem 0.5rem;
white-space: nowrap;
font-size: 0.75rem;
color: var(--theme-caption-color);
background-color: var(--theme-navpanel-selected);
border-radius: 0.25rem;
&.overdue {
background-color: var(--highlight-red-press);
}
&.done {
background-color: var(--theme-won-color);
}
&.inbox {
background-color: var(--secondary-button-hovered);
}
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import { translate } from '@hcengineering/platform'
import { DAY, HOUR, MINUTE, themeStore } from '@hcengineering/ui'
import { WorkSlot } from '@hcengineering/time'
import time from '../plugin'
export let events: WorkSlot[]
$: duration = events.reduce((acc, curr) => acc + curr.dueDate - curr.date, 0)
let res: string = ''
async function formatTime (value: number) {
res = ''
const days = Math.floor(value / DAY)
if (days > 0) {
res += await translate(time.string.Days, { days }, $themeStore.language)
}
const hours = Math.floor((value % DAY) / HOUR)
if (hours > 0) {
res += ' '
res += await translate(time.string.Hours, { hours }, $themeStore.language)
}
const minutes = Math.floor((value % HOUR) / MINUTE)
if (minutes > 0) {
res += ' '
res += await translate(time.string.Minutes, { minutes }, $themeStore.language)
}
res = res.trim()
}
$: formatTime(duration)
</script>
{res}

View File

@ -0,0 +1,169 @@
<script lang="ts">
import { SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import {
CheckBox,
Component,
IconMoreH,
IconMoreV2,
Spinner,
eventToHTMLElement,
getEventPositionElement,
showPopup,
showPanel,
Icon
} from '@hcengineering/ui'
import { showMenu, Menu, FixedColumn } from '@hcengineering/view-resources'
import time, { ToDo, WorkSlot } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import EditToDo from './EditToDo.svelte'
import PriorityPresenter from './PriorityPresenter.svelte'
import ToDoDuration from './ToDoDuration.svelte'
import WorkItemPresenter from './WorkItemPresenter.svelte'
export let todo: ToDo
export let size: 'small' | 'large' = 'small'
export let planned: boolean = true
export let draggable: boolean = true
export let isNew: boolean = false
const dispatch = createEventDispatcher()
const client = getClient()
let updating: Promise<any> | undefined = undefined
let isDrag: boolean = false
async function markDone (): Promise<void> {
await updating
updating = client.update(todo, { doneOn: todo.doneOn == null ? Date.now() : null })
await updating
updating = undefined
}
let events: WorkSlot[] = []
const query = createQuery()
$: query.query(
plugin.class.WorkSlot,
{
attachedTo: todo._id
},
(res) => {
events = res
},
{ sort: { date: SortingOrder.Descending } }
)
let hovered = false
async function onMenuClick (ev: MouseEvent): Promise<void> {
hovered = true
showMenu(ev, { object: todo }, () => {
hovered = false
})
}
function dragStart (todo: ToDo, event: DragEvent & { currentTarget: EventTarget & HTMLButtonElement }): void {
event.currentTarget.classList.add('dragged')
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'all'
dispatch('dragstart', todo)
}
function dragEnd (event: DragEvent & { currentTarget: EventTarget & HTMLButtonElement }): void {
event.currentTarget.classList.remove('dragged')
dispatch('dragend')
}
function open (e: MouseEvent): void {
// hovered = true
showPanel(time.component.EditToDo, todo._id, todo._class, 'content')
}
$: isTodo = todo.attachedTo === time.ids.NotAttached
</script>
<button
class="hulyToDoLine-container {size}"
class:hovered
class:isDrag
on:click|stopPropagation={open}
on:contextmenu={(e) => {
showMenu(e, { object: todo })
}}
{draggable}
on:dragstart={(e) => {
isDrag = true
dragStart(todo, e)
}}
on:dragend={(e) => {
isDrag = false
dragEnd(e)
}}
>
<div class="flex-row-top flex-grow flex-gap-2">
<div class="flex-row-center flex-no-shrink">
<button class="hulyToDoLine-dragbox" class:isNew on:contextmenu={onMenuClick}>
<Icon icon={IconMoreV2} size={'small'} />
</button>
<div class="hulyToDoLine-checkbox">
{#if updating !== undefined}
<Spinner size={'small'} />
{:else}
<CheckBox on:value={markDone} checked={todo.doneOn != null} kind={'todo'} size={'medium'} />
{/if}
</div>
</div>
{#if isTodo}
{#if size === 'small'}
<div class="hulyToDoLine-top-align top-12 text-left font-regular-14 secondary-textColor overflow-label">
{todo.title}
</div>
{:else}
<div class="flex-col flex-gap-1 flex-grow text-left">
<div class="hulyToDoLine-top-align top-12 text-left font-regular-14 secondary-textColor">
{todo.title}
</div>
<div class="flex-row-center flex-grow flex-gap-2">
<Component is={tags.component.LabelsPresenter} props={{ object: todo, value: todo.labels, kind: 'todo' }} />
<PriorityPresenter value={todo.priority} />
</div>
</div>
{/if}
{:else}
<div class="flex-col flex-gap-1 flex-grow text-left">
<div
class="hulyToDoLine-top-align text-left top-12 font-bold-12 secondary-textColor"
class:overflow-label={size === 'small'}
>
{todo.title}
</div>
<WorkItemPresenter {todo} kind={'todo-line'} {size} withoutSpace>
{#if size === 'large'}
<div class="flex-row-top flex-grow flex-gap-2">
<Component
is={tags.component.LabelsPresenter}
props={{ object: todo, value: todo.labels, kind: 'todo' }}
/>
<PriorityPresenter value={todo.priority} />
</div>
{/if}
</WorkItemPresenter>
</div>
{/if}
</div>
<div class="flex-row-top flex-no-shrink flex-gap-2">
{#if size === 'small'}
<div class="flex-row-center min-h-6 max-h-6 flex-gap-2">
<Component
is={tags.component.LabelsPresenter}
props={{ object: todo, value: todo.labels, kind: 'todo-compact' }}
/>
<PriorityPresenter value={todo.priority} />
</div>
{/if}
{#if events.length > 0}
<span class="hulyToDoLine-top-align top-12 font-regular-12 secondary-textColor">
<ToDoDuration {events} />
</span>
{/if}
</div>
</button>

View File

@ -0,0 +1,124 @@
<script lang="ts">
import { WithLookup, IdMap, Ref, Space } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../plugin'
import { createEventDispatcher } from 'svelte'
import ToDoDuration from './ToDoDuration.svelte'
import ToDoElement from './ToDoElement.svelte'
import {
AccordionItem,
IconWithEmoji,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore
} from '@hcengineering/ui'
import { ToDosMode } from '..'
import { Project } from '@hcengineering/tracker'
import view from '@hcengineering/view'
export let mode: ToDosMode
export let title: IntlString
export let todos: WithLookup<ToDo>[]
export let showTitle: boolean
export let showDuration: boolean
export let largeSize: boolean = false
export let projects: IdMap<Project>
const dispatch = createEventDispatcher()
function getAllWorkslots (todos: WithLookup<ToDo>[]): WorkSlot[] {
const workslots: WorkSlot[] = []
for (const todo of todos) {
for (const workslot of (todo.$lookup?.workslots ?? []) as WorkSlot[]) {
workslots.push(workslot)
}
}
return workslots
}
let groups: Project[] | undefined = undefined
let withoutProject: boolean = false
$: groups = updateGroups(todos, projects)
const updateGroups = (_todos: WithLookup<ToDo>[], _projects: IdMap<Project>): Project[] | undefined => {
let wp: boolean = false
const _groups: Project[] = []
for (const todo of _todos) {
const id = todo.attachedSpace as Ref<Project>
if (_projects.has(id)) {
if (_groups.findIndex((gr) => gr._id === id) === -1) {
const proj = _projects.get(id)
if (proj) _groups.push(proj)
}
} else wp = true
}
withoutProject = wp
return _groups
}
const hasProject = (proj: Ref<Space> | undefined): boolean => {
return (proj && projects.has(proj as Ref<Project>)) ?? false
}
</script>
{#if showTitle}
<AccordionItem
label={title}
size={'large'}
bottomSpace={false}
counter={todos.length}
duration={showDuration}
isOpen
fixHeader
background={'var(--theme-navpanel-color)'}
>
<svelte:fragment slot="duration">
<ToDoDuration events={getAllWorkslots(todos)} />
</svelte:fragment>
{#if groups}
{#each groups as group}
<AccordionItem
icon={group.icon === view.ids.IconWithEmoji ? IconWithEmoji : group.icon}
iconProps={group.icon === view.ids.IconWithEmoji
? { icon: group.color }
: {
fill:
group.color !== undefined
? getPlatformColorDef(group.color, $themeStore.dark).icon
: getPlatformColorForTextDef(group.name, $themeStore.dark).icon
}}
title={group.name}
size={'medium'}
isOpen
nested
>
{#each todos.filter((td) => td.attachedSpace === group._id) as todo}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="step-tb125" draggable={true} on:dragend on:dragstart={() => dispatch('dragstart', todo)}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</div>
{/each}
</AccordionItem>
{/each}
{/if}
{#if withoutProject}
<AccordionItem label={time.string.WithoutProject} size={'medium'} isOpen nested>
{#each todos.filter((td) => !hasProject(td.attachedSpace)) as todo}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="step-tb125" draggable={true} on:dragend on:dragstart={() => dispatch('dragstart', todo)}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</div>
{/each}
</AccordionItem>
{/if}
</AccordionItem>
{:else}
<div class="flex-col p-4 w-full">
{#each todos as todo}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="step-tb125" draggable={true} on:dragend on:dragstart={() => dispatch('dragstart', todo)}>
<ToDoElement {todo} size={largeSize ? 'large' : 'small'} planned={mode !== 'unplanned'} />
</div>
{/each}
</div>
{/if}

View File

@ -0,0 +1,74 @@
<!--
// 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.
-->
<script lang="ts">
import { ToDo } from '@hcengineering/time'
import time from '../plugin'
import WorkItemPresenter from './WorkItemPresenter.svelte'
import { getCurrentAccount } from '@hcengineering/core'
import calendar from '@hcengineering/calendar'
import { CheckBox, Label } from '@hcengineering/ui'
export let value: ToDo
export let withoutSpace: boolean = false
export let showCheck = false
const me = getCurrentAccount()._id
function isVisible (value: ToDo): boolean {
if (value.createdBy === me) return true
if (value.visibility === 'public') {
return true
}
return false
}
$: visible = isVisible(value)
</script>
{#if showCheck}
<div class="flex-row-center items-start">
<div class="mt-0-5">
<CheckBox readonly checked={value.doneOn != null} kind={'positive'} size={'medium'} />
</div>
<div class="ml-2 flex-col">
<div class="overflow-label flex-no-shrink">
{#if visible}
{value.title}
{:else}
<Label label={calendar.string.Busy} />
{/if}
</div>
{#if value.attachedTo !== time.ids.NotAttached && visible}
<div class:mt-1={value.title}>
<WorkItemPresenter todo={value} {withoutSpace} />
</div>
{/if}
</div>
</div>
{:else}
<div class="flex-col flex-gap-1">
<div class="overflow-label flex-no-shrink">
{#if visible}
{value.title}
{:else}
<Label label={calendar.string.Busy} />
{/if}
</div>
{#if value.attachedTo !== time.ids.NotAttached && visible}
<div>
<WorkItemPresenter todo={value} {withoutSpace} />
</div>
{/if}
</div>
{/if}

View File

@ -0,0 +1,317 @@
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { DocumentQuery, Ref, SortingOrder, WithLookup, getCurrentAccount, IdMap, toIdMap } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Scroller, areDatesEqual, todosSP, defaultSP, Header, ButtonIcon, Label } from '@hcengineering/ui'
import { ToDo, WorkSlot } from '@hcengineering/time'
import { ToDosMode } from '..'
import time from '../plugin'
import { getNearest } from '../utils'
import CreateToDo from './CreateToDo.svelte'
import ToDoGroup from './ToDoGroup.svelte'
import IconDiff from './icons/Diff.svelte'
import tags, { TagElement } from '@hcengineering/tags'
import IconMenu from './icons/Menu.svelte'
import tracker, { Project } from '@hcengineering/tracker'
import view from '@hcengineering/view-resources/src/plugin'
export let mode: ToDosMode
export let tag: Ref<TagElement> | undefined
export let currentDate: Date
const acc = getCurrentAccount() as PersonAccount
const user = acc.person
let largeSize: boolean = false
const doneQuery = createQuery()
const inboxQuery = createQuery()
const activeQuery = createQuery()
const tagsQuery = createQuery()
const projectsQuery = createQuery()
let projects: IdMap<Project> = new Map()
projectsQuery.query(tracker.class.Project, { archived: false }, (result) => {
projects = toIdMap(result)
})
let ids: Ref<ToDo>[] = []
$: updateTags(mode, tag)
function updateTags (mode: ToDosMode, tag: Ref<TagElement> | undefined): void {
if (mode !== 'tag' || tag === undefined) {
tagsQuery.unsubscribe()
ids = []
return
}
tagsQuery.query(
tags.class.TagReference,
{
tag
},
(res) => {
ids = res.map((p) => p.attachedTo as Ref<ToDo>)
}
)
}
function update (mode: ToDosMode, currentDate: Date, ids: Ref<ToDo>[]): void {
let activeQ: DocumentQuery<ToDo> | undefined = undefined
let doneQ: DocumentQuery<ToDo> | undefined = undefined
let inboxQ: DocumentQuery<ToDo> | undefined = undefined
if (mode === 'unplanned') {
activeQ = undefined
doneQ = undefined
inboxQ = {
user,
doneOn: null,
workslots: 0
}
} else if (mode === 'planned') {
inboxQ = undefined
doneQ = {
doneOn: { $gte: currentDate.setHours(0, 0, 0, 0), $lte: currentDate.setHours(23, 59, 59, 999) },
user
}
activeQ = {
user,
doneOn: null,
workslots: { $gt: 0 }
}
} else if (mode === 'all') {
inboxQ = {
doneOn: null,
workslots: 0,
user
}
doneQ = {
doneOn: { $ne: null },
user
}
activeQ = {
user,
doneOn: null,
workslots: { $gt: 0 }
}
} else if (mode === 'tag') {
inboxQ = {
doneOn: null,
workslots: 0,
user,
_id: { $in: ids }
}
doneQ = {
doneOn: { $ne: null },
user,
_id: { $in: ids }
}
activeQ = {
user,
doneOn: null,
workslots: { $gt: 0 },
_id: { $in: ids }
}
}
if (activeQ !== undefined) {
activeQuery.query(
time.class.ToDo,
activeQ,
(res) => {
rawActive = res
},
{
limit: 200,
sort: { modifiedOn: SortingOrder.Ascending },
lookup: { _id: { workslots: time.class.WorkSlot } }
}
)
} else {
activeQuery.unsubscribe()
rawActive = []
}
if (inboxQ !== undefined) {
inboxQuery.query(
time.class.ToDo,
inboxQ,
(res) => {
inbox = res
},
{
limit: 200,
sort: { modifiedOn: SortingOrder.Ascending }
}
)
} else {
inboxQuery.unsubscribe()
inbox = []
}
if (doneQ !== undefined) {
doneQuery.query(
time.class.ToDo,
doneQ,
(res) => {
done = res
},
{ limit: 200, sort: { doneOn: SortingOrder.Descending }, lookup: { _id: { workslots: time.class.WorkSlot } } }
)
} else {
doneQuery.unsubscribe()
done = []
}
}
$: update(mode, currentDate, ids)
let inbox: WithLookup<ToDo>[] = []
let done: WithLookup<ToDo>[] = []
let rawActive: WithLookup<ToDo>[] = []
$: active = filterActive(mode, rawActive, currentDate)
$: groups = group(inbox, done, active)
function filterActive (mode: ToDosMode, raw: WithLookup<ToDo>[], currentDate: Date): WithLookup<ToDo>[] {
if (mode === 'planned') {
const today = areDatesEqual(new Date(), currentDate)
const res: WithLookup<ToDo>[] = []
const endDay = new Date().setHours(23, 59, 59, 999)
for (const todo of raw) {
const nearest = getNearest(getWorkslots(todo))
if (nearest === undefined) {
res.push(todo)
} else {
if (today) {
if (nearest.dueDate < endDay) {
res.push(todo)
}
} else if (areDatesEqual(new Date(nearest.date), currentDate)) {
res.push(todo)
}
}
}
return res
} else {
return raw
}
}
function getWorkslots (todo: WithLookup<ToDo>): WorkSlot[] {
return (todo.$lookup?.workslots ?? []) as WorkSlot[]
}
function group (
unplanned: WithLookup<ToDo>[],
done: WithLookup<ToDo>[],
active: WithLookup<ToDo>[]
): [IntlString, WithLookup<ToDo>[]][] {
const groups = new Map<IntlString, WithLookup<ToDo>[]>([
[time.string.Unplanned, unplanned],
[time.string.ToDos, []],
[time.string.Scheduled, []],
[time.string.Done, done]
])
const now = Date.now()
const todos: {
nearest: WorkSlot | undefined
todo: WithLookup<ToDo>
}[] = []
const scheduled: {
nearest: WorkSlot | undefined
todo: WithLookup<ToDo>
}[] = []
for (const todo of active) {
if (todo.$lookup?.workslots !== undefined) {
todo.$lookup.workslots = getWorkslots(todo).sort((a, b) => a.date - b.date)
}
const nearest = getNearest(getWorkslots(todo))
if (nearest === undefined || nearest.dueDate < now) {
todos.push({
nearest,
todo
})
} else {
scheduled.push({
nearest,
todo
})
}
}
todos.sort((a, b) => (a.nearest?.date ?? 0) - (b.nearest?.date ?? 0))
scheduled.sort((a, b) => (a.nearest?.date ?? 0) - (b.nearest?.date ?? 0))
groups.set(
time.string.ToDos,
todos.map((p) => p.todo)
)
groups.set(
time.string.Scheduled,
scheduled.map((p) => p.todo)
)
return Array.from(groups).filter((p) => p[1].length > 0)
}
const getDateStr = (date: Date): string => {
return date.toLocaleDateString('default', { month: 'long', day: 'numeric', year: 'numeric' })
}
</script>
<div class="toDos-container">
<Header type={'type-panel'} hideSeparator>
<ButtonIcon icon={IconMenu} kind={'tertiary'} size={'small'} />
<div class="heading-bold-20 ml-4">
<Label label={time.string.ToDoColon} />
{#if mode === 'date'}
{getDateStr(currentDate)}
{:else}
<Label
label={mode === 'all'
? time.string.All
: mode === 'planned'
? time.string.Planned
: mode === 'unplanned'
? time.string.Unplanned
: view.string.Labels}
/>
{/if}
</div>
<svelte:fragment slot="actions">
<ButtonIcon
icon={IconDiff}
size={'small'}
kind={'tertiary'}
pressed={largeSize}
on:click={() => (largeSize = !largeSize)}
/>
</svelte:fragment>
</Header>
<CreateToDo fullSize />
<Scroller fade={groups.length > 1 ? todosSP : defaultSP} noStretch>
{#each groups as group}
<ToDoGroup
todos={group[1]}
title={group[0]}
showTitle={groups.length > 1}
showDuration={group[0] !== time.string.Unplanned}
{mode}
{projects}
{largeSize}
on:dragstart
on:dragend
/>
{/each}
</Scroller>
</div>
<style lang="scss">
/* Global styles in components.scss */
.toDos-container {
display: flex;
flex-direction: column;
width: 100%;
// height: 100%;
min-width: 0;
min-height: 0;
}
</style>

View File

@ -0,0 +1,233 @@
<script lang="ts">
import { PersonAccount } from '@hcengineering/contact'
import { Ref, getCurrentAccount } from '@hcengineering/core'
import { Asset, IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import tagsPlugin, { TagElement as TagElementType } from '@hcengineering/tags'
import ui, {
Label,
Separator,
NavItem,
NavGroup,
Scroller,
Month,
getPlatformColorDef,
themeStore,
areDatesEqual
} from '@hcengineering/ui'
import { ToDosMode } from '..'
import time from '../plugin'
export let mode: ToDosMode
export let tag: Ref<TagElementType> | undefined
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
export let currentDate: Date
const acc = getCurrentAccount() as PersonAccount
const user = acc.person
interface IMode {
label: IntlString
value: ToDosMode
icon: Asset
}
const modes: IMode[] = [
{
label: time.string.Unplanned,
value: 'unplanned',
icon: time.icon.Inbox
},
{
label: time.string.Planned,
value: 'planned',
icon: time.icon.Planned
},
{
label: time.string.All,
value: 'all',
icon: time.icon.All
}
]
let allTags: TagElementType[] = []
let tags: TagElementType[] = []
let myTags = new Set<Ref<TagElementType>>()
const tagsQuery = createQuery()
const myTagsQuery = createQuery()
tagsQuery.query(
tagsPlugin.class.TagElement,
{
category: time.category.Other,
refCount: { $gt: 0 },
targetClass: { $in: [time.class.ToDo, time.class.ProjectToDo] }
},
(result) => {
allTags = result
}
)
$: myTagsQuery.query(
tagsPlugin.class.TagReference,
{
createdBy: acc._id,
tag: { $in: allTags.map((p) => p._id) }
},
(result) => {
myTags = new Set(result.map((p) => p.tag))
}
)
$: tags = allTags.filter((p) => myTags.has(p._id))
const unplannedQuery = createQuery()
unplannedQuery.query(
time.class.ToDo,
{
user,
doneOn: null,
workslots: 0
},
(res) => {
counters.unplanned = res.total
},
{
total: true,
limit: 1
}
)
const counters: Record<string, number> = {}
const today: Date = new Date()
</script>
<div class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'} border-left">
<div class="antiPanel-wrap__content hulyNavPanel-container">
<div class="hulyNavPanel-header">
<Label label={time.string.Planner} />
</div>
<Scroller shrink>
{#each modes as _mode}
{@const counter = counters[_mode.value] ?? 0}
<NavItem
icon={_mode.icon}
label={_mode.label}
selected={mode === _mode.value}
count={counter > 0 ? counter : null}
on:click={() => {
mode = _mode.value
tag = undefined
localStorage.setItem('todos_last_mode', mode)
localStorage.removeItem('todos_last_tag')
}}
/>
{/each}
<div class="hulyAccordionItem-container border pb-2">
<Month
currentDate={mode === 'date' ? currentDate : null}
on:update={(event) => {
if (event.detail) {
tag = undefined
currentDate = event.detail
mode = 'date'
}
}}
>
<svelte:fragment slot="header">
<div class="flex-col mx-2 gapV-1 flex-no-shrink flex-grow">
<div class="calendar-slot-row">
<div class="dot red" />
<span class="overflow-label upperFirstLetter"><Label label={ui.string.DueDate} /></span>
</div>
<div class="calendar-slot-row">
<div class="dot blue" />
<span class="overflow-label upperFirstLetter"><Label label={time.string.Scheduled} /></span>
</div>
</div>
</svelte:fragment>
<svelte:fragment let:day>
<!-- {#if areDatesEqual(day.date, today)}
<div class="dots">
<div class="dot red" />
<div class="dot blue" />
</div>
{/if} -->
</svelte:fragment>
</Month>
</div>
{#if tags.length > 0}
<NavGroup label={tagsPlugin.string.Tags} selected={mode === 'tag'} categoryName={'tags'} second>
{#each tags as _tag}
{@const color = getPlatformColorDef(_tag.color ?? 0, $themeStore.dark)}
<NavItem
color={color.color}
title={_tag.title}
selected={tag === _tag._id}
type={'type-tag'}
on:click={() => {
mode = 'tag'
tag = _tag._id
localStorage.setItem('todos_last_mode', mode)
localStorage.setItem('todos_last_tag', tag)
}}
/>
{/each}
</NavGroup>
{/if}
</Scroller>
</div>
<Separator
name={'time'}
float={navFloat ? 'navigator' : true}
index={0}
disabledWhen={['panel-aside']}
color={'var(--theme-navpanel-border)'}
/>
</div>
<style lang="scss">
.calendar-slot-row {
display: flex;
align-items: center;
padding: 0 var(--spacing-0_5);
min-width: 0;
.dot {
margin-right: var(--spacing-0_75);
}
span {
font-weight: 400;
font-size: 0.625rem;
line-height: 0.5rem;
color: var(--global-secondary-TextColor);
}
}
.dot {
flex-shrink: 0;
width: var(--spacing-0_5);
height: var(--spacing-0_5);
border-radius: 50%;
box-shadow: 0 0 0 1px var(--theme-navpanel-color);
&.red {
background-color: var(--global-error-TextColor);
}
&.blue {
background-color: var(--global-accent-TextColor);
}
}
.dots {
position: absolute;
display: flex;
align-items: center;
gap: 1px;
top: 0.1875rem;
right: 0.1875rem;
}
</style>

View File

@ -0,0 +1,94 @@
<!--
// 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.
-->
<script lang="ts">
import { Calendar, generateEventId } from '@hcengineering/calendar'
import contact, { PersonAccount } from '@hcengineering/contact'
import { Ref, getCurrentAccount } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { closePopup, showPopup } from '@hcengineering/ui'
import { deleteObjects } from '@hcengineering/view-resources'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../plugin'
import Workslots from './Workslots.svelte'
export let todo: ToDo
let slots: WorkSlot[] = []
const q = createQuery()
q.query(time.class.WorkSlot, { attachedTo: todo._id }, (res) => {
slots = res
})
const client = getClient()
async function change (e: CustomEvent<{ startDate: number, dueDate: number, slot: Ref<WorkSlot> }>): Promise<void> {
const { startDate, dueDate, slot } = e.detail
const workslot = slots.find((s) => s._id === slot)
if (workslot !== undefined) {
await client.update(workslot, { date: startDate, dueDate })
}
}
async function dueChange (e: CustomEvent<{ dueDate: number, slot: Ref<WorkSlot> }>): Promise<void> {
const { dueDate, slot } = e.detail
const workslot = slots.find((s) => s._id === slot)
if (workslot !== undefined) {
await client.update(workslot, { dueDate })
}
}
async function create (): Promise<void> {
const defaultDuration = 30 * 60 * 1000
const now = Date.now()
const date = Math.ceil(now / (30 * 60 * 1000)) * (30 * 60 * 1000)
const currentUser = getCurrentAccount() as PersonAccount
const space = `${currentUser._id}_calendar` as Ref<Calendar>
const dueDate = date + defaultDuration
await client.addCollection(time.class.WorkSlot, space, todo._id, todo._class, 'workslots', {
eventId: generateEventId(),
date,
dueDate,
description: todo.description,
participants: [currentUser.person],
title: todo.title,
allDay: false,
access: 'owner',
visibility: todo.visibility === 'public' ? 'public' : 'freeBusy',
reminders: []
})
}
async function remove (e: CustomEvent<{ _id: Ref<WorkSlot> }>): Promise<void> {
const object = slots.find((p) => p._id === e.detail._id)
if (object) {
showPopup(
contact.component.DeleteConfirmationPopup,
{
object,
deleteAction: async () => {
const objs = Array.isArray(object) ? object : [object]
await deleteObjects(getClient(), objs).catch((err) => {
console.error(err)
})
closePopup()
}
},
undefined
)
}
}
</script>
<Workslots {slots} on:change={change} on:dueChange={dueChange} on:create={create} on:remove={remove} />

View File

@ -0,0 +1,53 @@
<script lang="ts">
import { Class, Doc } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Component, navigate } from '@hcengineering/ui'
import view, { ObjectPanel } from '@hcengineering/view'
import { getObjectLinkFragment } from '@hcengineering/view-resources'
import { ItemPresenter, ToDo } from '@hcengineering/time'
import time from '../plugin'
export let todo: ToDo
export let kind: 'default' | 'todo-line' = 'default'
export let size: 'small' | 'large' = 'small'
export let withoutSpace: boolean = false
export let isEditable: boolean = false
export let shouldShowAvatar: boolean = false
const client = getClient()
const hierarchy = client.getHierarchy()
$: presenter = hierarchy.classHierarchyMixin<Doc, ItemPresenter>(todo.attachedToClass, time.mixin.ItemPresenter)
let doc: Doc | undefined = undefined
const docQuery = createQuery()
$: docQuery.query(todo.attachedToClass, { _id: todo.attachedTo }, (res) => {
doc = res[0]
})
async function click (ev: MouseEvent) {
ev.stopPropagation()
if (!doc) return
const panelComponent = hierarchy.classHierarchyMixin<Class<Doc>, ObjectPanel>(doc._class, view.mixin.ObjectPanel)
const component = panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, doc, {}, component)
navigate(loc)
}
</script>
{#if presenter?.presenter && doc}
{#if kind === 'default'}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="cursor-pointer clear-mins" on:click|stopPropagation={click}>
<Component is={presenter.presenter} props={{ value: doc, withoutSpace, isEditable, shouldShowAvatar }} />
</div>
{:else}
<Component
is={presenter.presenter}
props={{ value: doc, withoutSpace, isEditable, kind: size === 'large' ? 'todo-line-large' : 'todo-line' }}
on:click={click}
>
<slot />
</Component>
{/if}
{/if}

View File

@ -0,0 +1,38 @@
<!--
// 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.
-->
<script lang="ts">
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'
export let event: WorkSlot
export let oneRow: boolean = false
export let hideDetails: boolean = false
let todo: ToDo
const query = createQuery()
$: query.query(event.attachedToClass, { _id: event.attachedTo }, (res) => {
todo = res[0]
})
</script>
{#if hideDetails}
<Label label={time.string.WorkSlot} />
{:else if todo}
<ToDoPresenter value={todo} withoutSpace={oneRow} />
{/if}

View File

@ -0,0 +1,82 @@
<!--
// 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.
-->
<script lang="ts">
import { EventTimeEditor } from '@hcengineering/calendar-resources'
import { ActionIcon, Button, Icon, IconCircleAdd, IconClose, Scroller } from '@hcengineering/ui'
import { WorkSlot } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import time from '../plugin'
export let slots: WorkSlot[] = []
const dispatch = createEventDispatcher()
async function change (e: CustomEvent<{ startDate: number, dueDate: number }>, slot: WorkSlot): Promise<void> {
const { startDate, dueDate } = e.detail
dispatch('change', { startDate, dueDate, slot: slot._id })
}
async function dueChange (e: CustomEvent<{ dueDate: number }>, slot: WorkSlot): Promise<void> {
const { dueDate } = e.detail
dispatch('dueChange', { dueDate, slot: slot._id })
}
</script>
<div class="flex-col container w-full flex-gap-1">
<Scroller>
{#each slots as slot}
<div class="flex-between w-full pr-4 slot">
<EventTimeEditor
allDay={false}
startDate={slot.date}
bind:dueDate={slot.dueDate}
on:change={(e) => change(e, slot)}
on:dueChange={(e) => dueChange(e, slot)}
/>
<div class="tool">
<ActionIcon
icon={IconClose}
size={'small'}
action={() => {
dispatch('remove', { _id: slot._id })
}}
/>
</div>
</div>
{/each}
</Scroller>
<div class="flex-row-center">
<div class="mr-1-5">
<Icon icon={IconCircleAdd} size="small" />
</div>
<Button padding={'0 .5rem'} kind="ghost" label={time.string.AddSlot} on:click={() => dispatch('create')} />
</div>
</div>
<style lang="scss">
.container {
max-height: 10rem;
}
.slot {
.tool {
visibility: hidden;
}
&:hover {
.tool {
visibility: visible;
}
}
}
</style>

View File

@ -0,0 +1,36 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { IconSize } from '@hcengineering/ui'
export let size: IconSize = 'small'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path
d="M7 11.0004C7 10.4481 7.44772 10.0004 8 10.0004H24C24.5523 10.0004 25 10.4481 25 11.0004C25 11.5527 24.5523 12.0004 24 12.0004H8C7.44772 12.0004 7 11.5527 7 11.0004Z"
/>
<path
d="M7 16.0004C7 15.4481 7.44772 15.0004 8 15.0004H20C20.5523 15.0004 21 15.4481 21 16.0004C21 16.5527 20.5523 17.0004 20 17.0004H8C7.44772 17.0004 7 16.5527 7 16.0004Z"
/>
<path
d="M8 20.0004C7.44772 20.0004 7 20.4481 7 21.0004C7 21.5527 7.44772 22.0004 8 22.0004H23C23.5523 22.0004 24 21.5527 24 21.0004C24 20.4481 23.5523 20.0004 23 20.0004H8Z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2 8.00012C2 5.79098 3.79086 4.00012 6 4.00012H26C28.2091 4.00012 30 5.79098 30 8.00012V24.0001C30 26.2093 28.2091 28.0001 26 28.0001H6C3.79086 28.0001 2 26.2093 2 24.0001V8.00012ZM6 6.00012H26C27.1046 6.00012 28 6.89555 28 8.00012V24.0001C28 25.1047 27.1046 26.0001 26 26.0001H6C4.89543 26.0001 4 25.1047 4 24.0001V8.00012C4 6.89555 4.89543 6.00012 6 6.00012Z"
/>
</svg>

View File

@ -0,0 +1,27 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { IconSize } from '@hcengineering/ui'
export let size: IconSize = 'small'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 5C4.44772 5 4 5.44772 4 6C4 6.55228 4.44772 7 5 7H27C27.5523 7 28 6.55228 28 6C28 5.44772 27.5523 5 27 5H5ZM5 15C4.44772 15 4 15.4477 4 16C4 16.5523 4.44772 17 5 17H15C15.5523 17 16 16.5523 16 16C16 15.4477 15.5523 15 15 15H5ZM19 16L24.3 10.7C24.6866 10.3134 25.3134 10.3134 25.7 10.7C26.0866 11.0866 26.0866 11.7134 25.7 12.1L21.8 16L25.7 19.9C26.0866 20.2866 26.0866 20.9134 25.7 21.3C25.3134 21.6866 24.6866 21.6866 24.3 21.3L19 16ZM4 26C4 25.4477 4.44772 25 5 25H27C27.5523 25 28 25.4477 28 26C28 26.5523 27.5523 27 27 27H5C4.44772 27 4 26.5523 4 26Z"
/>
</svg>

View File

@ -0,0 +1,67 @@
<!--
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { IconSize } from '@hcengineering/ui'
export let size: IconSize = 'small'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8,4.2C5.9,4.2,4.2,5.9,4.2,8s1.7,3.8,3.8,3.8s3.8-1.7,3.8-3.8S10.1,4.2,8,4.2z M8,10.8c-1.6,0-2.8-1.3-2.8-2.8S6.4,5.2,8,5.2c1.6,0,2.8,1.3,2.8,2.8S9.6,10.8,8,10.8z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8,2.5c0.3,0,0.5-0.2,0.5-0.5V0.7c0-0.3-0.2-0.5-0.5-0.5c-0.3,0-0.5,0.2-0.5,0.5V2C7.5,2.3,7.7,2.5,8,2.5z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8,13.5c-0.3,0-0.5,0.2-0.5,0.5v1.3c0,0.3,0.2,0.5,0.5,0.5c0.3,0,0.5-0.2,0.5-0.5V14C8.5,13.7,8.3,13.5,8,13.5z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.4,4.1c0.2,0.2,0.5,0.2,0.7,0s0.2-0.5,0-0.7L3.2,2.5C3,2.3,2.7,2.3,2.5,2.5C2.3,2.7,2.3,3,2.5,3.2L3.4,4.1z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.6,11.9c-0.2-0.2-0.5-0.2-0.7,0c-0.2,0.2-0.2,0.5,0,0.7l0.9,0.9c0.2,0.2,0.5,0.2,0.7,0c0.2-0.2,0.2-0.5,0-0.7L12.6,11.9z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.5,8c0-0.3-0.2-0.5-0.5-0.5H0.7C0.4,7.5,0.2,7.7,0.2,8c0,0.3,0.2,0.5,0.5,0.5H2C2.3,8.5,2.5,8.3,2.5,8z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.3,7.5H14c-0.3,0-0.5,0.2-0.5,0.5c0,0.3,0.2,0.5,0.5,0.5h1.3c0.3,0,0.5-0.2,0.5-0.5C15.8,7.7,15.6,7.5,15.3,7.5z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.4,11.9l-0.9,0.9c-0.2,0.2-0.2,0.5,0,0.7c0.2,0.2,0.5,0.2,0.7,0l0.9-0.9c0.2-0.2,0.2-0.5,0-0.7S3.6,11.7,3.4,11.9z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.6,4.1l0.9-0.9c0.2-0.2,0.2-0.5,0-0.7c-0.2-0.2-0.5-0.2-0.7,0l-0.9,0.9c-0.2,0.2-0.2,0.5,0,0.7C12.1,4.3,12.4,4.3,12.6,4.1z"
/>
</svg>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import contact, { getName } from '@hcengineering/contact'
import core, { Space } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import recruit, { Applicant, Candidate } from '@hcengineering/recruit'
import { Icon, Label } from '@hcengineering/ui'
export let value: Applicant
export let withoutSpace: boolean
let space: Space | undefined = undefined
const query = createQuery()
const contactQ = createQuery()
const client = getClient()
$: query.query(core.class.Space, { _id: value.space }, (res) => {
space = res[0]
})
let candidate: Candidate | undefined = undefined
$: contactQ.query(contact.class.Contact, { _id: value.attachedTo }, (res) => {
candidate = res[0]
})
</script>
{#if !withoutSpace}
<div>
<Label label={recruit.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center flex-gap-1">
<div class="icon">
<Icon icon={recruit.icon.Application} size={'small'} />
</div>
{#if candidate}
{getName(client.getHierarchy(), candidate)}
{/if}
</div>

View File

@ -0,0 +1,28 @@
<script lang="ts">
import board, { Card } from '@hcengineering/board'
import core, { Space } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
export let value: Card
export let withoutSpace: boolean
let space: Space | undefined = undefined
const query = createQuery()
$: query.query(core.class.Space, { _id: value.space }, (res) => {
space = res[0]
})
</script>
{#if !withoutSpace}
<div>
<Label label={board.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center flex-gap-1">
{value.title}
</div>

View File

@ -0,0 +1,162 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getStates } from '@hcengineering/task'
import { typeStore } from '@hcengineering/task-resources'
import tracker, { Issue, IssueStatus, Project } from '@hcengineering/tracker'
import { AssigneeEditor, IssueStatusIcon, StatusPresenter } from '@hcengineering/tracker-resources'
import { activeProjects } from '@hcengineering/tracker-resources/src/utils'
import { Label, SelectPopup, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
export let value: Issue
export let withoutSpace: boolean
export let isEditable: boolean = false
export let shouldShowAvatar: boolean = false
export let kind: 'todo-line' | 'todo-line-large' | undefined = undefined
let space: Project | undefined = undefined
const defaultIssueStatus: Ref<IssueStatus> | undefined = undefined
const client = getClient()
$: space = $activeProjects.get(value.space)
$: st = $statusStore.byId.get(value.status)
const changeStatus = async (newStatus: Ref<IssueStatus> | undefined, refocus: boolean = true) => {
if (!isEditable || newStatus === undefined || value.status === newStatus) {
return
}
if ('_class' in value) {
await client.update(value, { status: newStatus })
}
}
function getSelectedStatus (
statuses: IssueStatus[] | undefined,
value: Issue,
defaultStatus: Ref<IssueStatus> | undefined
): IssueStatus | undefined {
if (value.status !== undefined) {
const current = statuses?.find((status) => status._id === value.status)
if (current) return current
}
if (defaultIssueStatus !== undefined) {
const res = statuses?.find((status) => status._id === defaultStatus)
changeStatus(res?._id, false)
return res
}
}
$: selectedStatus = getSelectedStatus(statuses, value, defaultIssueStatus)
$: statuses = getStates(space, $typeStore, $statusStore.byId)
$: statusesInfo = statuses?.map((s) => {
return {
id: s._id,
component: StatusPresenter,
props: { value: s, size: 'small', space: value.space },
isSelected: selectedStatus?._id === s._id ?? false
}
})
const handleStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(SelectPopup, { value: statusesInfo }, eventToHTMLElement(event), changeStatus)
}
</script>
{#if kind === 'todo-line'}
<button class="flex-row-top flex-grow relative" on:click>
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hulyToDoLine-icon"
class:cursor-pointer={isEditable}
on:click|stopPropagation={handleStatusEditorOpened}
>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<span class="hulyToDoLine-label overflow-label font-regular-14 text-left secondary-textColor ml-2">
{value.title}
</span>
</button>
{:else if kind === 'todo-line-large'}
<button class="flex-row-top flex-grow relative" on:click>
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="hulyToDoLine-icon"
class:cursor-pointer={isEditable}
on:click|stopPropagation={handleStatusEditorOpened}
>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<div class="flex-col flex-gap-1 flex-grow text-left ml-2">
<div class="hulyToDoLine-label large font-regular-14 secondary-textColor">
{value.title}
</div>
<slot />
</div>
</button>
{:else if shouldShowAvatar}
<div class="flex-between">
<div class="flex-col flex-grow mr-3">
{#if !withoutSpace}
<div class="flex-row-center">
<Label label={tracker.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center">
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-no-shrink" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<span class="ml-1-5 overflow-label">{value.title}</span>
</div>
</div>
<div class="hideOnDrag flex-no-shrink">
<AssigneeEditor object={value} avatarSize={'smaller'} shouldShowName={false} />
</div>
</div>
{:else}
{#if !withoutSpace}
<div class="flex-row-center">
<Label label={tracker.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center">
{#if st}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-no-shrink mr-1-5" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
<IssueStatusIcon value={st} size={'small'} space={value.space} />
</div>
{/if}
<span class="overflow-label">{value.title}</span>
</div>
{/if}
<style lang="scss">
button {
margin: 0;
padding: 0;
text-align: left;
border: none;
outline: none;
}
</style>

View File

@ -0,0 +1,41 @@
<script lang="ts">
import { getName } from '@hcengineering/contact'
import core, { Space } from '@hcengineering/core'
import lead, { Customer, Lead } from '@hcengineering/lead'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Label } from '@hcengineering/ui'
export let value: Lead
export let withoutSpace: boolean
const client = getClient()
let space: Space | undefined = undefined
const query = createQuery()
const customerQ = createQuery()
$: query.query(core.class.Space, { _id: value.space }, (res) => {
space = res[0]
})
let customer: Customer | undefined = undefined
$: customerQ.query(lead.mixin.Customer, { _id: value.attachedTo }, (res) => {
customer = res[0]
})
</script>
{#if !withoutSpace}
<div>
<Label label={lead.string.ConfigLabel} />
/
{space?.name}
</div>
{/if}
<div class="flex-row-center flex-gap-1">
{value.title}
{#if customer}
- {getName(client.getHierarchy(), customer)}
{/if}
</div>

View File

@ -0,0 +1,27 @@
<!--
// 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.
-->
<script lang="ts">
import { HOUR, Label, MINUTE } from '@hcengineering/ui'
import time from '../../plugin'
export let value: number
$: days = Math.floor(value / HOUR / 24)
$: hours = Math.floor(value / HOUR) % 24
$: minutes = Math.floor((value % HOUR) / MINUTE)
</script>
<div class="flex-nowrap no-word-wrap">
<Label label={time.string.TotalGroupTime} params={{ days, hours, minutes }} />
</div>

View File

@ -0,0 +1,81 @@
<!--
// 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.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Project } from '@hcengineering/task'
import { Label, ModeSelector, Separator, defineSeparators } from '@hcengineering/ui'
import time from '../../plugin'
import { teamSeparators } from '../../utils'
import TeamNavigator from './TeamNavigator.svelte'
import Agenda from './agenda/Agenda.svelte'
import Calendar from './calendar/Calendar.svelte'
export let visibleNav: boolean = true
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
let currentDate: Date = new Date()
let space: Ref<Project> | undefined = undefined
function changeMode (_mode: string): void {
mode = _mode
}
const config: Array<[string, IntlString, object]> = [
['agenda', time.string.Agenda, {}],
['calendar', time.string.Calendar, {}]
]
let mode = config[0][0]
defineSeparators('team', teamSeparators)
</script>
<div class="background-comp-header-color w-full h-full flex-row-top">
{#if visibleNav}
<TeamNavigator {navFloat} {appsDirection} bind:selected={space} />
<Separator
name={'team'}
float={navFloat}
index={0}
disabledWhen={['panel-aside']}
color={'var(--theme-navpanel-border)'}
/>
{/if}
<div class="background-comp-header-color flex-col w-full h-full">
<div class="ac-header full divide caption-height header-with-mode-selector">
<div class="ac-header__wrap-title">
<span class="ac-header__title flex-row-center mr-2">
<Label label={time.string.Team} />
</span>
<ModeSelector
props={{
mode,
config,
onChange: changeMode
}}
/>
</div>
</div>
{#if space}
{#if mode === 'calendar'}
<Calendar {space} bind:currentDate />
{:else}
<Agenda {space} bind:currentDate />
{/if}
{/if}
</div>
</div>

View File

@ -0,0 +1,90 @@
<!--
// 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.
-->
<script lang="ts">
import { Ref, getCurrentAccount } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { Project } from '@hcengineering/task'
import tracker, { Project as TrackerProject } from '@hcengineering/tracker'
import { Label, Separator } from '@hcengineering/ui'
import { ObjectPresenter, TreeNode } from '@hcengineering/view-resources'
import time from '../../plugin'
export let navFloat: boolean = false
export let appsDirection: 'vertical' | 'horizontal' = 'horizontal'
export let selected: Ref<Project> | undefined = (localStorage.getItem('team_last_mode') as Ref<Project>) ?? undefined
let memberProjects: Project[] = []
let projectsPublic: Project[] = []
const projectsQuery = createQuery()
const publicQuery = createQuery()
$: projectsQuery.query(
task.class.Project,
{
archived: false,
members: getCurrentAccount()._id
},
(result) => {
memberProjects = result
}
)
$: publicQuery.query(
tracker.class.Project,
{
_id: { $nin: memberProjects.map((it) => it._id as Ref<TrackerProject>) },
archived: false,
private: { $ne: true }
},
(result) => {
projectsPublic = result
}
)
$: finalProjects = memberProjects.concat(projectsPublic)
</script>
<div class="antiPanel-navigator {appsDirection === 'horizontal' ? 'portrait' : 'landscape'}">
<div class="antiPanel-wrap__content">
<div class="antiNav-header overflow-label">
<Label label={time.string.Team} />
<Label label={time.string.Planner} />
</div>
<TreeNode _id={'projects-planning'} label={time.string.Team} node>
{#each finalProjects as _project}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="antiNav-element parent"
class:selected={selected === _project._id}
on:click={() => {
selected = _project._id
localStorage.setItem('team_last_mode', selected)
}}
>
<ObjectPresenter objectId={_project._id} _class={_project._class} value={_project} />
</div>
{/each}
</TreeNode>
<div class="antiNav-divider line" />
</div>
<Separator
name={'time'}
float={navFloat ? 'navigator' : true}
index={0}
disabledWhen={['panel-aside']}
color={'var(--theme-navpanel-border)'}
/>
</div>

View File

@ -0,0 +1,110 @@
<!--
// 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.
-->
<script lang="ts">
import calendar, { Calendar, Event } from '@hcengineering/calendar'
import { calendarStore, hidePrivateEvents } from '@hcengineering/calendar-resources'
import contact, { Person, PersonAccount } from '@hcengineering/contact'
import { IdMap, Ref, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import task, { Project } from '@hcengineering/task'
import time, { ToDo, WorkSlot } from '@hcengineering/time'
export let space: Ref<Project>
export let fromDate: number
export let toDate: number
export let project: Project | undefined
export let calendars: IdMap<Calendar> = new Map()
export let personAccounts: PersonAccount[] = []
export let slots: WorkSlot[] = []
export let events: Event[] = []
export let todos: IdMap<ToDo> = new Map()
export let persons: Ref<Person>[] = []
const client = getClient()
const spaceQuery = createQuery()
$: spaceQuery.query(task.class.Project, { _id: space }, (res) => {
;[project] = res
})
const query = createQuery()
const queryR = createQuery()
let raw: Event[] = []
let rawEvent: Event[] = []
let rawReq: Event[] = []
let calendarIds: Ref<Calendar>[] = []
const accountsQuery = createQuery()
$: accountsQuery.query(
contact.class.PersonAccount,
{ _id: { $in: project?.members.map((it) => it as Ref<PersonAccount>) ?? [] } },
(res) => {
persons = res.flatMap((it) => it.person).filter((it, idx, arr) => arr.indexOf(it) === idx)
}
)
$: query.query(
calendar.class.Event,
{
_class: { $nin: [calendar.class.ReccuringEvent] },
space: { $in: calendarIds },
date: { $lte: toDate },
dueDate: { $gte: fromDate },
participants: { $in: persons } as any
},
(res) => {
rawEvent = res
}
)
$: queryR.query(
calendar.class.ReccuringEvent,
{ space: { $in: calendarIds }, participants: { $in: persons } as any },
(res) => {
rawReq = res
}
)
$: raw = rawEvent.concat(rawReq)
$: visible = hidePrivateEvents(raw, $calendarStore, false)
const todoQuery = createQuery()
$: slots = visible.filter((it) => client.getHierarchy().isDerived(it._class, time.class.WorkSlot)) as WorkSlot[]
$: events = visible.filter((it) => !client.getHierarchy().isDerived(it._class, time.class.WorkSlot))
$: todoQuery.query(
time.class.ToDo,
{
_id: { $in: slots.map((it) => it.attachedTo).filter((it, idx, arr) => arr.indexOf(it) === idx) }
},
(res) => {
todos = toIdMap(res)
}
)
const calendarQuery = createQuery()
$: calendarQuery.query(calendar.class.Calendar, { archived: false }, (res) => {
calendarIds = res.map((p) => p._id)
calendars = toIdMap(res)
})
const personMapQuery = createQuery()
$: personMapQuery.query(contact.class.PersonAccount, { person: { $in: persons } }, (res) => {
personAccounts = res
})
</script>

View File

@ -0,0 +1,101 @@
<script lang="ts">
import { Calendar, Event, getAllEvents } from '@hcengineering/calendar'
import { IdMap, Ref } from '@hcengineering/core'
import { Project } from '@hcengineering/task'
import Border from '../../Border.svelte'
import WithTeamData from '../WithTeamData.svelte'
import DayPlan from './DayPlan.svelte'
import { toSlots } from '../utils'
import { Person, PersonAccount } from '@hcengineering/contact'
import { ToDo, WorkSlot } from '@hcengineering/time'
import Header from '../../Header.svelte'
export let space: Ref<Project>
export let currentDate: Date
$: today = new Date(currentDate)
$: yesterday = new Date(new Date(today).setDate(today.getDate() - 1))
$: tomorrow = new Date(new Date(today).setDate(today.getDate() + 1))
$: yesterdayFrom = new Date(yesterday).setHours(0, 0, 0, 0)
$: yesterdayTo = new Date(today).setHours(0, 0, 0, 0)
$: todayFrom = new Date(today).setHours(0, 0, 0, 0)
$: todayTo = new Date(tomorrow).setHours(0, 0, 0, 0)
let project: Project | undefined
let calendars: IdMap<Calendar> = new Map()
let personAccounts: PersonAccount[] = []
let slots: WorkSlot[] = []
let events: Event[] = []
let todos: IdMap<ToDo> = new Map()
let persons: Ref<Person>[] = []
$: yesterdaySlots = toSlots(getAllEvents(slots, yesterdayFrom, yesterdayTo))
$: yesterdayEvents = getAllEvents(events, yesterdayFrom, yesterdayTo)
$: yesterdayEventsMap = new Map(yesterdayEvents.map((e) => [e._id, e]))
$: todaySlots = toSlots(getAllEvents(slots, todayFrom, todayTo))
$: todayEvents = getAllEvents(
events.filter((it) => !yesterdayEventsMap.has(it._id)),
todayFrom,
todayTo
)
</script>
<WithTeamData
{space}
fromDate={yesterdayFrom}
toDate={todayTo}
bind:project
bind:calendars
bind:personAccounts
bind:todos
bind:slots
bind:events
bind:persons
/>
<Header bind:currentDate />
{#if project}
<div class="flex-row-top background-body-color h-full">
<div class="item flex-col">
<DayPlan
day={yesterday}
slots={yesterdaySlots}
events={yesterdayEvents}
showAssignee
{persons}
{personAccounts}
{project}
{calendars}
{todos}
/>
</div>
<div class="flex-no-shrink">
<Border />
</div>
<div class="item flex-col">
<DayPlan
day={today}
slots={todaySlots}
events={todayEvents}
showAssignee
{persons}
{personAccounts}
{project}
{calendars}
{todos}
/>
</div>
</div>
{/if}
<style lang="scss">
.item {
flex-shrink: 0;
flex-grow: 1;
width: 50%;
height: 100%;
// margin: 2rem;
}
</style>

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { Calendar, Event } from '@hcengineering/calendar'
import { Person, PersonAccount } from '@hcengineering/contact'
import { IdMap, Ref, Timestamp } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { Project } from '@hcengineering/task'
import { Label, Scroller, areDatesEqual, ticker } from '@hcengineering/ui'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../../../plugin'
import PlanGroup from './PlanGroup.svelte'
export let day: Date
export let slots: WorkSlot[]
export let events: Event[]
export let showAssignee: boolean = false
export let persons: Ref<Person>[]
export let personAccounts: PersonAccount[]
export let calendars: IdMap<Calendar>
export let project: Project
export let todos: IdMap<ToDo>
function getTitle (day: Date, now: Timestamp): IntlString {
const today = new Date(now)
const tomorrow = new Date(new Date(now).setDate(new Date(now).getDate() + 1))
const yesterday = new Date(new Date(now).setDate(new Date(now).getDate() - 1))
if (areDatesEqual(day, today)) return time.string.Today
if (areDatesEqual(day, yesterday)) return time.string.Yesterday
if (areDatesEqual(day, tomorrow)) return time.string.Tomorrow
const isCurrentYear = day.getFullYear() === new Date().getFullYear()
return getEmbeddedLabel(
day.toLocaleDateString('default', {
month: 'long',
day: 'numeric',
year: isCurrentYear ? undefined : 'numeric'
})
)
}
$: title = getTitle(day, $ticker)
</script>
<div class="caption-color text-xl p-4">
<Label label={title} />
</div>
<Scroller padding={'0 1rem'} noStretch shrink>
<PlanGroup {slots} {events} {showAssignee} {personAccounts} {calendars} {todos} />
</Scroller>
<div class="antiVSpacer x4" />

View File

@ -0,0 +1,44 @@
<script lang="ts">
import calendar, { Event } from '@hcengineering/calendar'
import { DateRangeMode } from '@hcengineering/core'
import { Icon } from '@hcengineering/ui'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
import ArrowRight from '@hcengineering/ui/src/components/icons/ArrowRight.svelte'
import TimePresenter from '../../presenters/TimePresenter.svelte'
export let item: Event
export let showTime = false
$: dueTime = item.dueDate - item.date
</script>
<div class="item flex-between">
<div class="flex-col">
<div class="flex-row-center">
<Icon icon={calendar.icon.Calendar} size={'medium'} />
<span class="ml-1 select-text">
{item.title}
</span>
</div>
{#if showTime}
<div class="flex-col ml-4 mt-2">
<div class="flex-row-center">
<DatePresenter mode={DateRangeMode.TIMEONLY} value={item.date} />
<div class="p-1">
<Icon icon={ArrowRight} size={'small'} />
</div>
<DatePresenter mode={DateRangeMode.TIMEONLY} value={item.dueDate} />
</div>
</div>
{/if}
</div>
<div class="flex-row-center whitespace-nowra flex-gap-4 flex-no-shrink ml-4">
<TimePresenter value={dueTime} />
</div>
</div>
<style lang="scss">
.item {
margin: 0.25rem 1rem 0.25rem 1rem;
}
</style>

View File

@ -0,0 +1,42 @@
<script lang="ts">
import { Calendar, Event } from '@hcengineering/calendar'
import { calendarStore } from '@hcengineering/calendar-resources'
import { PersonAccount } from '@hcengineering/contact'
import { IdMap, getCurrentAccount } from '@hcengineering/core'
import { ToDo, WorkSlot } from '@hcengineering/time'
import { groupTeamData } from '../utils'
import PlanPerson from './PlanPerson.svelte'
export let slots: WorkSlot[]
export let events: Event[]
export let showAssignee: boolean = false
export let personAccounts: PersonAccount[]
export let calendars: IdMap<Calendar>
export let todos: IdMap<ToDo>
const me = (getCurrentAccount() as PersonAccount).person
$: grouped = groupTeamData(slots, todos, events, personAccounts, calendars, me, $calendarStore)
</script>
<div class="container flex-col background-comp-header-color">
{#each grouped as gitem, i}
{#if i}
<div class="divider" />
{/if}
<PlanPerson {gitem} {showAssignee} />
{/each}
</div>
<style lang="scss">
.divider {
border-top: 1px solid var(--theme-table-border-color);
}
.container {
margin-top: 0.75rem;
border: 1px solid var(--theme-table-border-color);
border-radius: 0.5rem;
}
</style>

View File

@ -0,0 +1,57 @@
<script lang="ts">
import calendarPlugin from '@hcengineering/calendar'
import { Icon, Label } from '@hcengineering/ui'
import { WorkSlotMapping } from '../../../types'
import ToDoPresenter from '../../ToDoPresenter.svelte'
import TimePresenter from '../../presenters/TimePresenter.svelte'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
import { DateRangeMode } from '@hcengineering/core'
import ArrowRight from '@hcengineering/ui/src/components/icons/ArrowRight.svelte'
export let item: WorkSlotMapping
export let showAssignee: boolean = false
export let showSlots = false
$: dueTime = item.slots.reduce((it, itm) => it + (itm.dueDate - itm.date), 0)
$: overlap = item.slots.reduce((it, itm) => it + (itm.overlap ?? 0), 0)
</script>
<div class="item flex-between items-baseline">
<div class="flex-col ml-0-5">
{#if item.todo !== undefined}
<ToDoPresenter value={item.todo} showCheck />
{:else}
<div class="overflow-label flex-no-shrink">
<Label label={calendarPlugin.string.Busy} />
</div>
{/if}
{#if showSlots}
<div class="flex-col ml-4 mt-2">
{#each item.slots as slot}
<div class="flex-row-center">
<DatePresenter mode={DateRangeMode.TIMEONLY} value={slot.date} />
<div class="p-1">
<Icon icon={ArrowRight} size={'small'} />
</div>
<DatePresenter mode={DateRangeMode.TIMEONLY} value={slot.dueDate} />
</div>
{/each}
</div>
{/if}
</div>
<div class="flex-row-center whitespace-nowrap flex-no-shrink ml-4 no-word-wrap">
<TimePresenter value={dueTime} />
{#if overlap > 0}
<div class="flex-row-center ml-1 text-sm no-word-wrap">
(-<TimePresenter value={overlap} />)
</div>
{/if}
</div>
</div>
<style lang="scss">
.item {
margin: 0.25rem 1rem 0.25rem 1rem;
}
</style>

View File

@ -0,0 +1,94 @@
<script lang="ts">
import calendarPlugin from '@hcengineering/calendar'
import { PersonAccount } from '@hcengineering/contact'
import { PersonPresenter } from '@hcengineering/contact-resources'
import { getCurrentAccount } from '@hcengineering/core'
import { Chevron, Label } from '@hcengineering/ui'
import { EventPersonMapping } from '../../../types'
import TimePresenter from '../../presenters/TimePresenter.svelte'
import { isVisibleMe } from '../utils'
import EventItem from './EventItem.svelte'
import PlanItem from './PlanItem.svelte'
let expanded: boolean = false
const mePerson = (getCurrentAccount() as PersonAccount).person
export let gitem: EventPersonMapping
export let showAssignee: boolean = false
</script>
<div class="header flex-between px-2 flexn-no-shrink">
<div class="flex-row-center flex-grow flex-between">
<div class="label ml-1-5">
<PersonPresenter value={gitem.user} shouldShowAvatar shouldShowName={true} />
</div>
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="mr-2 flex-row-center">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="ml-2 p-1"
on:click|preventDefault|stopPropagation={() => {
expanded = !expanded
}}
>
<div class="flex-row-center">
<Chevron {expanded} marginRight={'.5rem'} />
</div>
</div>
<TimePresenter value={gitem.total} />
</div>
</div>
{#each gitem.mappings as item}
<PlanItem {item} {showAssignee} showSlots={expanded} />
{/each}
{#each gitem.events as event}
<EventItem item={event} showTime={expanded} />
{/each}
{#if gitem.busy.slots.length > 0}
<PlanItem item={gitem.busy} {showAssignee} showSlots={expanded} />
{/if}
{#if gitem.busyTotal > 0}
<div class="item flex-between items-baseline">
<div class="flex-col">
<div class="overflow-label flex-no-shrink">
<Label label={calendarPlugin.string.Busy} />
{#each gitem.busyEvents as event}
{#if isVisibleMe(event, mePerson)}
<EventItem item={event} showTime={expanded} />
{/if}
{/each}
</div>
</div>
<div class="flex-row-center whitespace-nowra flex-gap-4 flex-no-shrink ml-4">
<TimePresenter value={gitem.busyTotal} />
</div>
</div>
{/if}
<style lang="scss">
.header {
margin-top: 1.75rem;
}
.item {
margin: 0.25rem 1rem 0.25rem 1rem;
}
.label {
color: var(--theme-caption-color);
font-weight: 500;
}
.divider {
border-top: 1px solid var(--theme-table-border-color);
}
.container {
margin-top: 0.75rem;
border: 1px solid var(--theme-table-border-color);
border-radius: 0.5rem;
}
</style>

View File

@ -0,0 +1,62 @@
<!--
// 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.
-->
<script lang="ts">
import { Project } from '@hcengineering/task'
import TeamCalendar from './TeamCalendar.svelte'
import TeamCalendarDay from './TeamCalendarDay.svelte'
import { Ref } from '@hcengineering/core'
import Header from '../../Header.svelte'
import { DropdownLabels, DropdownLabelsIntl } from '@hcengineering/ui'
import time from '../../../plugin'
export let space: Ref<Project>
export let currentDate: Date
let mode: 'week' | 'day' = 'day'
let timeMode: '1hour' | '30mins' | '15mins'
</script>
<Header bind:currentDate>
<svelte:fragment>
<div class="p-1 flex-row-center">
{#if mode === 'day'}
<DropdownLabels
items={[
{ id: '1hour', label: '1 hour' },
{ id: '30mins', label: '30 mins' },
{ id: '15mins', label: '15 mins' }
]}
bind:selected={timeMode}
kind={'ghost'}
/>
{/if}
<DropdownLabelsIntl
items={[
{ id: 'day', label: time.string.DayCalendar },
{ id: 'week', label: time.string.WeekCalendar }
]}
bind:selected={mode}
kind={'ghost'}
/>
</div>
</svelte:fragment>
</Header>
{#if mode === 'week'}
<TeamCalendar {space} {currentDate} />
{:else if mode === 'day'}
<TeamCalendarDay {space} {currentDate} {timeMode} />
{/if}

View File

@ -0,0 +1,92 @@
<!--
// 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.
-->
<script lang="ts">
import calendar, { CalendarEventPresenter, Event } from '@hcengineering/calendar'
import { EventPresenter, calendarStore, isVisible } from '@hcengineering/calendar-resources'
import { Doc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component, MILLISECONDS_IN_MINUTE, showPopup, tooltip } from '@hcengineering/ui'
import view, { ObjectEditor } from '@hcengineering/view'
import { showMenu } from '@hcengineering/view-resources'
export let event: Event
export let hour: number
export let top: number
$: width = (hour * (event.dueDate - event.date)) / MILLISECONDS_IN_MINUTE / 60
$: left = (hour / 60) * new Date(event.date).getMinutes()
$: empty = false
function click (): void {
if (visible) {
const editor = hierarchy.classHierarchyMixin<Doc, ObjectEditor>(event._class, view.mixin.ObjectEditor)
if (editor?.editor !== undefined) {
showPopup(editor.editor, { object: event })
}
}
}
const client = getClient()
const hierarchy = client.getHierarchy()
$: presenter = hierarchy.classHierarchyMixin<Doc, CalendarEventPresenter>(
event._class,
calendar.mixin.CalendarEventPresenter
)
let div: HTMLDivElement
$: visible = isVisible(event, $calendarStore)
function onContext (e: MouseEvent): void {
showMenu(e, { object: event })
}
</script>
{#if event}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
bind:this={div}
class="event-container"
class:oneRow={true}
class:empty
style:width="{width}rem"
style:margin-left="{left}rem"
style:margin-top="{-3 * top}rem"
use:tooltip={{ component: EventPresenter, props: { value: event, hideDetails: !visible } }}
on:click|stopPropagation={click}
on:contextmenu={onContext}
>
{#if !empty && presenter?.presenter}
<Component is={presenter.presenter} props={{ event, narrow: false, oneRow: true, hideDetails: !visible }} />
{/if}
</div>
{/if}
<style lang="scss">
.event-container {
pointer-events: auto;
overflow: hidden;
height: 3rem;
min-width: 0;
min-height: 0;
font-size: 0.8125rem;
background-color: #f3f6fb;
border: 1px solid rgba(43, 81, 144, 0.2);
border-left: 0.25rem solid #2b5190;
border-radius: 0.25rem;
cursor: pointer;
padding: 0.25rem 0.5rem 0.25rem 1rem;
}
</style>

View File

@ -0,0 +1,319 @@
<!--
// 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.
-->
<script lang="ts">
import contact, { Person } from '@hcengineering/contact'
import { PersonPresenter } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import {
Label,
Scroller,
areDatesEqual,
daysInMonth,
deviceOptionsStore as deviceInfo,
day as getDay,
getWeekDayName,
isWeekend,
resizeObserver
} from '@hcengineering/ui'
export let headerHeightRem = 4.375
const minColWidthRem = 2.5
export let rowHeightRem = 8
export let currentDate: Date = new Date()
export let startDate: Date
export let maxDays: number = 33
export let persons: Ref<Person>[]
export let multipler = 1
export let highlightToday = true
const todayDate = new Date()
function getColumnWidth (gridWidth: number, currentDate: Date, maxDays: number): number {
const width = gridWidth / Math.min(daysInMonth(currentDate), maxDays)
return Math.max(width, minColWidthRem)
}
export function getCellStyle (): string {
return `width: ${columnWidthRem}rem;`
}
export function getRowStyle (): string {
return `height: ${rowHeightRem}rem;`
}
export function getHeaderStyle (): string {
return `height: ${headerHeightRem}rem;`
}
let headerWidth: number
$: headerWidthRem = headerWidth / $deviceInfo.fontSize
let containerWidth: number = window.outerWidth
$: containerWidthRem = containerWidth / $deviceInfo.fontSize
$: sideDays = Math.round((maxDays - 1) / 2)
$: values = [
...Array.from(Array(sideDays).keys())
.reverse()
.map((it) => currentDate.getDate() - (it + 1)),
currentDate.getDate(),
...Array.from(Array(sideDays).keys()).map((it) => currentDate.getDate() + (it + 1))
]
$: columnWidthRem = getColumnWidth(containerWidthRem - headerWidthRem, currentDate, maxDays) * multipler
</script>
<Scroller horizontal fade={{ multipler: { top: headerHeightRem, left: headerWidthRem } }} noFade>
<div
use:resizeObserver={(evt) => {
containerWidth = evt.clientWidth
}}
class="timeline"
>
{#key [containerWidthRem, columnWidthRem, headerWidthRem]}
<!-- Resource Header -->
<div
use:resizeObserver={(evt) => {
headerWidth = evt.clientWidth
}}
class="timeline-header timeline-resource-header"
>
<div class="timeline-row" style={getHeaderStyle()}>
<div class="timeline-resource-cell flex-row-center">
<!-- <div class="timeline-resource-header__title">----</div> -->
<div class="timeline-resource-header__subtitle">
<Label label={contact.string.NumberMembers} params={{ count: persons.length }} />
</div>
</div>
</div>
</div>
<!-- Resource Content -->
<div class="timeline-resource-content">
{#each persons as person}
<div class="timeline-row" style={getRowStyle()}>
<div class="timeline-resource-cell">
<PersonPresenter value={person} />
</div>
</div>
{/each}
</div>
<!-- Grid Header -->
<div class="timeline-header timeline-grid-header">
<div class="timeline-row flex" style={getHeaderStyle()}>
{#each values as value}
{@const day = getDay(startDate, value - currentDate.getDate())}
{@const today = areDatesEqual(todayDate, day)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="flex-col">
<div class="timeline-cell timeline-day-header flex-col-center justify-center" style={getCellStyle()}>
{#if maxDays > 1}
<div
class="timeline-day-header__day flex-col-center justify-center"
class:timeline-day-header__day--today={today}
>
{day.getDate()}
</div>
<div class="timeline-day-header__weekday">{getWeekDayName(day, 'short')}</div>
{/if}
<slot name="day-header" {day} width={columnWidthRem} />
</div>
</div>
{/each}
</div>
</div>
<!-- Grid Content -->
<div class="timeline-grid-content timeline-grid-bg">
{#each persons as person}
<div class="timeline-row flex" style={getRowStyle()}>
<div class="timeline-events" />
{#each values as value, i}
{@const day = getDay(startDate, value - currentDate.getDate())}
{@const today = areDatesEqual(todayDate, day)}
{@const weekend = isWeekend(day)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="timeline-cell"
class:timeline-cell--today={today}
class:timeline-cell--weekend={weekend}
style={getCellStyle()}
>
<div class:timeline-cell-today-marker={today && highlightToday}>
<slot name="day" {day} {today} {weekend} {person} height={rowHeightRem} width={columnWidthRem} />
</div>
</div>
{/each}
</div>
{/each}
</div>
{/key}
</div>
</Scroller>
<style lang="scss">
$timeline-header-height: 4.5rem;
$timeline-column-width: 2rem;
$timeline-bg-color: var(--theme-comp-header-color);
$timeline-border-color: var(--theme-bg-divider-color);
$timeline-border: 1px solid $timeline-border-color;
$timeline-weekend-stroke-color: var(--theme-calendar-weekend-stroke-color);
.timeline {
width: 100%;
display: grid;
grid-auto-flow: column;
grid-template-columns: auto 1fr;
grid-template-rows: auto 1fr;
}
.timeline-header {
background-color: $timeline-bg-color;
}
.timeline-header {
position: sticky;
top: 0;
z-index: 1;
&.timeline-resource-header {
left: 0;
z-index: 2;
}
}
.timeline-resource-header__title {
white-space: nowrap;
font-size: 0.875rem;
font-weight: 500;
}
.timeline-resource-header__subtitle {
white-space: nowrap;
font-size: 0.6875rem;
font-weight: 400;
line-height: 1.25rem;
opacity: 0.4;
}
.timeline-resource-content {
background-color: $timeline-bg-color;
position: sticky;
left: 0;
z-index: 1;
}
.timeline-day-header {
cursor: pointer;
.timeline-day-header__day {
width: 1.3125rem;
height: 1.3125rem;
font-size: 0.8125rem;
font-weight: 500;
&.timeline-day-header__day--today {
color: white;
background-color: #3871e0;
border-radius: 0.375rem;
}
}
.timeline-day-header__weekday {
font-size: 0.6875rem;
font-weight: 400;
line-height: 1.25rem;
opacity: 0.4;
}
}
.timeline-grid-bg {
background-image: linear-gradient(
135deg,
$timeline-weekend-stroke-color 10%,
$timeline-bg-color 10%,
$timeline-bg-color 50%,
$timeline-weekend-stroke-color 50%,
$timeline-weekend-stroke-color 60%,
$timeline-bg-color 60%,
$timeline-bg-color 100%
);
background-size: 7px 7px;
}
.timeline-row {
position: relative;
border-bottom: $timeline-border;
}
.timeline-events {
position: absolute;
width: 100%;
top: 1em;
bottom: 1em;
pointer-events: none;
}
.timeline-cell {
border-right: $timeline-border;
width: $timeline-column-width;
height: 100%;
&:not(.timeline-cell--weekend, .timeline-cell--holiday) {
background-color: $timeline-bg-color;
}
&.timeline-cell--holiday {
background-color: transparent;
}
&.timeline-cell--weekend {
background-color: transparent;
}
&.timeline-cell--today {
background-color: $timeline-bg-color;
}
.timeline-cell-today-marker {
width: 100%;
height: 100%;
background-color: var(--theme-calendar-today-bgcolor);
}
}
.timeline-resource-cell {
border-right: $timeline-border;
width: 100%;
height: 100%;
padding: 1rem 2rem;
}
.timeline-event-wrapper {
position: absolute;
height: 1.5rem;
padding-left: 0.125rem;
padding-right: 0.125rem;
pointer-events: all;
}
</style>

View File

@ -0,0 +1,240 @@
<!--
// 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.
-->
<script lang="ts">
import calendar, { Calendar, Event, getAllEvents } from '@hcengineering/calendar'
import { calendarStore } from '@hcengineering/calendar-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
import core, {
Account,
Doc,
IdMap,
Ref,
Timestamp,
Tx,
TxCUD,
TxCreateDoc,
TxProcessor,
TxUpdateDoc,
getCurrentAccount
} from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Project } from '@hcengineering/task'
import { Icon, tooltip } from '@hcengineering/ui'
import view from '@hcengineering/view'
import { ToDo, WorkSlot } from '@hcengineering/time'
import time from '../../../plugin'
import TimePresenter from '../../presenters/TimePresenter.svelte'
import WithTeamData from '../WithTeamData.svelte'
import { groupTeamData, toSlots } from '../utils'
import PersonCalendar from './PersonCalendar.svelte'
import TxPanel from './TxPanel.svelte'
export let space: Ref<Project>
export let currentDate: Date
export let maxDays = 5
$: fromDate = new Date(currentDate).setDate(currentDate.getDate() - Math.round(maxDays / 2 + 1))
$: toDate = new Date(currentDate).setDate(currentDate.getDate() + Math.round(maxDays / 2 + 1))
const me = (getCurrentAccount() as PersonAccount).person
let project: Project | undefined
let calendars: IdMap<Calendar> = new Map()
let personAccounts: PersonAccount[] = []
let slots: WorkSlot[] = []
let events: Event[] = []
let todos: IdMap<ToDo> = new Map()
let persons: Ref<Person>[] = []
const txCreateQuery = createQuery()
let txes = new Map<Ref<Account>, Tx[]>()
$: txCreateQuery.query(
core.class.Tx,
{ modifiedBy: { $in: Array.from(personAccounts.map((it) => it._id)) }, modifiedOn: { $gt: fromDate, $lt: toDate } },
(res) => {
const map = new Map<Ref<Account>, Tx[]>()
for (const _t of res) {
const t = TxProcessor.extractTx(_t)
const account = t.createdBy ?? t.modifiedBy
map.set(account, [...(map.get(account) ?? []), t])
}
txes = map
}
)
const client = getClient()
function group (
txMap: Map<Ref<Account>, Tx[]>,
persons: Ref<Account>[],
from: Timestamp,
to: Timestamp
): { add: Map<Asset, { count: number, tx: TxCUD<Doc>[] }>, change: Map<Asset, { count: number, tx: TxCUD<Doc>[] }> } {
const txes = persons.flatMap((it) => txMap.get(it))
const add = new Map<Asset, { count: number, tx: TxCUD<Doc>[] }>()
const change = new Map<Asset, { count: number, tx: TxCUD<Doc>[] }>()
const h = client.getHierarchy()
for (const tx of txes) {
if (tx === undefined || tx.modifiedOn < from || tx.modifiedOn > to) {
continue
}
if (tx._class === core.class.TxCreateDoc) {
const txAdd = tx as TxCreateDoc<Doc>
if (h.isDerived(txAdd.objectClass, core.class.Space)) {
continue
}
try {
h.getClass(txAdd.objectClass)
} catch (err) {
continue
}
const cl = h.getClass(txAdd.objectClass)
const presenter = h.classHierarchyMixin(txAdd.objectClass, view.mixin.ObjectPresenter)
if (cl.icon !== undefined && presenter !== undefined) {
const v = add.get(cl.icon) ?? { count: 0, tx: [] }
v.count++
v.tx.push(txAdd)
add.set(cl.icon, v)
}
}
if (tx._class === core.class.TxUpdateDoc) {
const txUpd = tx as TxUpdateDoc<Doc>
try {
h.getClass(txUpd.objectClass)
} catch (err) {
continue
}
if (h.isDerived(txUpd.objectClass, core.class.Space)) {
continue
}
const cl = h.getClass(txUpd.objectClass)
const presenter = h.classHierarchyMixin(txUpd.objectClass, view.mixin.ObjectPresenter)
if (cl.icon !== undefined && presenter !== undefined) {
const v = change.get(cl.icon) ?? { count: 0, tx: [] }
v.count++
v.tx.push(txUpd)
change.set(cl.icon, v)
}
}
}
return { add, change }
}
$: allSlots = getAllEvents(slots, fromDate, toDate)
$: allEvents = getAllEvents(events, fromDate, toDate)
</script>
<WithTeamData
{space}
{fromDate}
{toDate}
bind:project
bind:calendars
bind:personAccounts
bind:todos
bind:slots
bind:events
bind:persons
/>
<PersonCalendar {persons} startDate={currentDate} {maxDays}>
<svelte:fragment slot="day" let:day let:today let:weekend let:person let:height>
{@const dayFrom = new Date(day).setHours(0, 0, 0, 0)}
{@const dayTo = new Date(day).setHours(23, 59, 59, 999)}
{@const grouped = groupTeamData(
toSlots(getAllEvents(allSlots, dayFrom, dayTo)),
todos,
getAllEvents(allEvents, dayFrom, dayTo),
personAccounts,
calendars,
me,
$calendarStore
)}
{@const gitem = grouped.find((it) => it.user === person)}
{@const planned = gitem?.mappings.reduce((it, val) => it + val.total, 0) ?? 0}
{@const pevents = gitem?.events.reduce((it, val) => it + (val.dueDate - val.date), 0) ?? 0}
{@const busy = gitem?.busy.slots.reduce((it, val) => it + (val.dueDate - val.date), 0) ?? 0}
{@const accounts = personAccounts.filter((it) => it.person === person).map((it) => it._id)}
{@const txInfo = group(txes, accounts, dayFrom, dayTo)}
<div style:overflow="auto" style:height="{height}rem" class="p-1">
<div class="flex-row-center p-1">
<Icon icon={time.icon.Team} size={'small'} />
<TimePresenter value={gitem?.total ?? 0} />
</div>
<div class="flex flex-row-center">
{#if planned > 0}
<div class="flex-row-center p-1 flex-nowrap">
<Icon icon={time.icon.FilledFlag} size={'small'} fill={'var(--positive-button-default)'} />
<TimePresenter value={planned} />
</div>
{/if}
{#if pevents > 0}
<div class="flex-row-center p-1 flex-nowrap">
<Icon icon={calendar.icon.Calendar} size={'small'} fill={'var(--positive-button-default)'} />
<TimePresenter value={pevents} />
</div>
{/if}
{#if busy > 0}
<div class="flex-row-center p-1 flex-nowrap">
<Icon icon={calendar.icon.Private} size={'small'} fill={'var(--positive-button-default)'} />
<TimePresenter value={busy} />
</div>
{/if}
</div>
{#if txInfo.add.size > 0}
<div class="flex">
{#each Array.from(txInfo.add.entries()) as add}
<div
class="flex-row-center p-1 no-word-wrap flex-nowrap"
use:tooltip={{
component: TxPanel,
props: { tx: add[1].tx }
}}
>
<span class="mr-1">
<Icon icon={add[0]} size={'small'} fill={'var(--positive-button-default)'} />
</span>
{add[1].count}
</div>
{/each}
</div>
{/if}
{#if txInfo.change.size > 0}
<div class="flex">
{#each Array.from(txInfo.change.entries()) as change}
<div
class="flex-row-center p-1 no-word-wrap flex-nowrap"
use:tooltip={{
component: TxPanel,
props: { tx: change[1].tx }
}}
>
<span class="mr-1">
<Icon icon={change[0]} size={'small'} fill={'var(--activity-status-busy)'} />
</span>
{change[1].count}
</div>
{/each}
</div>
{/if}
</div>
</svelte:fragment>
</PersonCalendar>

View File

@ -0,0 +1,171 @@
<!--
// 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.
-->
<script lang="ts">
import { Calendar, Event, getAllEvents } from '@hcengineering/calendar'
import { calendarStore } from '@hcengineering/calendar-resources'
import { Person, PersonAccount } from '@hcengineering/contact'
import { IdMap, Ref, getCurrentAccount } from '@hcengineering/core'
import { Project } from '@hcengineering/task'
import { ToDo, WorkSlot } from '@hcengineering/time'
import WithTeamData from '../WithTeamData.svelte'
import { groupTeamData, toSlots } from '../utils'
import EventElement from './EventElement.svelte'
import PersonCalendar from './PersonCalendar.svelte'
export let space: Ref<Project>
export let currentDate: Date
export let timeMode: '1hour' | '30mins' | '15mins'
const maxDays = 1
$: fromDate = new Date(currentDate).setDate(currentDate.getDate() - Math.round(maxDays / 2 + 1))
$: toDate = new Date(currentDate).setDate(currentDate.getDate() + Math.round(maxDays / 2 + 1))
const me = (getCurrentAccount() as PersonAccount).person
let project: Project | undefined
let calendars: IdMap<Calendar> = new Map()
let personAccounts: PersonAccount[] = []
let slots: WorkSlot[] = []
let events: Event[] = []
let todos: IdMap<ToDo> = new Map()
let persons: Ref<Person>[] = []
function calcHourWidth (events: Event[], totalWidth: number): number[] {
const hours = new Map<number, number>()
for (const e of events) {
const h1 = new Date(e.date).getHours()
const h2 = new Date(e.dueDate).getHours()
for (let i = h1; i <= h2; i++) {
hours.set(i, hours.get(i) ?? 0 + 1)
}
}
const width: number[] = []
for (let i = 0; i < 24; i++) {
if (!hours.has(i)) {
width.push(0)
} else {
width.push((totalWidth - 1) / hours.size)
}
}
return width
}
const timeModes: Record<typeof timeMode, number> = {
'15mins': 4,
'1hour': 1,
'30mins': 2
}
function calcTop (event: Event, prevEvent?: Event): number {
if (prevEvent === undefined) {
return 0
}
if (new Date(prevEvent.date).getMinutes() === new Date(event.date).getMinutes()) {
return 0
}
if (prevEvent.dueDate <= event.date) {
return 1
}
return 0
}
</script>
<WithTeamData
{space}
{fromDate}
{toDate}
bind:project
bind:calendars
bind:personAccounts
bind:todos
bind:slots
bind:events
bind:persons
/>
<PersonCalendar
{persons}
startDate={currentDate}
{maxDays}
rowHeightRem={6.5}
headerHeightRem={2}
multipler={timeModes[timeMode]}
highlightToday={false}
>
<svelte:fragment slot="day-header" let:day let:width>
{@const dayFrom = new Date(day).setHours(0, 0, 0, 0)}
{@const dayTo = new Date(day).setHours(23, 59, 59, 999)}
{@const totalSlots = toSlots(getAllEvents(slots, dayFrom, dayTo))}
{@const totalEvents = getAllEvents(events, dayFrom, dayTo)}
{@const hourWidths = calcHourWidth([...totalSlots, ...totalEvents], width)}
<div class="flex-nowrap w-full p-1" style:display={'inline-flex'}>
{#each Array.from(Array(25).keys()) as hour}
{@const width = hourWidths[hour]}
<div class="flex-row-center" style:width="{width}rem">
{#if width > 0 || hour === 24}
{#if timeMode === '30mins'}
<span style:width="{width / 2}rem">{hour === 24 ? '00' : hour}:00</span>
{#if hour !== 24}
<span style:width="{width / 2}rem">{hour}:30</span>
{/if}
{:else if timeMode === '15mins'}
<span style:width="{width / 4}rem">{hour === 24 ? '00' : hour}:00</span>
{#if hour !== 24}
<span style:width="{width / 4}rem">{hour}:15</span>
<span style:width="{width / 4}rem">{hour}:30</span>
<span style:width="{width / 4}rem">{hour}:45</span>
{/if}
{:else}
{hour === 24 ? '00' : hour}:00
{/if}
{/if}
</div>
{/each}
</div>
</svelte:fragment>
<svelte:fragment slot="day" let:day let:today let:weekend let:person let:height let:width>
{@const dayFrom = new Date(day).setHours(0, 0, 0, 0)}
{@const dayTo = new Date(day).setHours(23, 59, 59, 999)}
{@const totalSlots = toSlots(getAllEvents(slots, dayFrom, dayTo))}
{@const totalEvents = getAllEvents(events, dayFrom, dayTo)}
{@const grouped = groupTeamData(totalSlots, todos, totalEvents, personAccounts, calendars, me, $calendarStore)}
{@const gitem = grouped.find((it) => it.user === person)}
{@const hourWidths = calcHourWidth([...totalSlots, ...totalEvents], width)}
{#if gitem}
{@const slots = [
...Array.from(gitem.mappings.flatMap((it) => it.slots)),
...gitem.events,
...gitem.busyEvents,
...gitem.busy.slots
].toSorted((a, b) => a.date - b.date)}
<div style:overflow-x={'hidden'} style:overflow-y={'auto'} style:height="{height}rem">
<div class="flex flex-row-center">
<div class="flex-nowrap p-1 w-full" style:display={'inline-flex'}>
{#each Array.from(Array(24).keys()) as hour}
{@const _slots = slots
.filter((it) => new Date(it.date).getHours() === hour)
.toSorted((a, b) => a.date - b.date)}
{@const cwidth = hourWidths[hour]}
<div class="flex-col" style:width="{cwidth}rem">
{#each _slots as m, i}
<!-- <div class="flex-col mr-1"> -->
<!-- <TimePresenter value={m.dueDate - m.date} /> -->
<EventElement event={m} hour={cwidth} top={calcTop(m, _slots[i - 1])} />
<!-- </div> -->
{/each}
</div>
{/each}
</div>
</div>
</div>
{/if}
</svelte:fragment>
</PersonCalendar>

View File

@ -0,0 +1,70 @@
<!--
// 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.
-->
<script lang="ts">
import { Doc, Ref, TxCUD } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Component, resizeObserver } from '@hcengineering/ui'
import { DocNavLink, ObjectPresenter } from '@hcengineering/view-resources'
import { ItemPresenter } from '@hcengineering/time'
import { createEventDispatcher } from 'svelte'
import time from '../../../plugin'
export let tx: TxCUD<Doc>[]
const dispatch = createEventDispatcher()
const client = getClient()
interface ObjData {
doc?: Doc
txes: TxCUD<Doc>[]
itemPresenter?: ItemPresenter
}
let objects: ObjData[] = []
async function group (txes: TxCUD<Doc>[]): Promise<void> {
const h = client.getHierarchy()
const objs = new Map<Ref<Doc>, ObjData>()
for (const tx of txes.slice(0, 100)) {
const dta: ObjData = objs.get(tx.objectId) ?? {
doc: await client.findOne(tx.objectClass, { _id: tx.objectId }),
txes: [],
itemPresenter: h.classHierarchyMixin(tx.objectClass, time.mixin.ItemPresenter)
}
dta.txes.push(tx)
objs.set(tx.objectId, dta)
}
objects = Array.from(objs.values())
}
$: group(tx)
</script>
<div use:resizeObserver={() => dispatch('changeContent')} class="p-1" style:overflow={'auto'}>
{#each objects as object}
<div class="p-1">
{#if object.itemPresenter && object.doc !== undefined}
<DocNavLink object={object.doc}>
<ObjectPresenter objectId={object.doc?._id} _class={object.doc._class} value={object.doc} />
<Component
is={object.itemPresenter.presenter}
props={{ value: object.doc, withoutSpace: true, isEditable: false, shouldShowAvatar: true }}
/>
</DocNavLink>
{:else if object.doc !== undefined}
<ObjectPresenter objectId={object.doc?._id} _class={object.doc._class} value={object.doc} />
{/if}
</div>
{/each}
</div>

View File

@ -0,0 +1,201 @@
// 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.
import { type Calendar, type Event } from '@hcengineering/calendar'
import { isVisible } from '@hcengineering/calendar-resources'
import { type Contact, type Person, type PersonAccount } from '@hcengineering/contact'
import { type IdMap, type Ref } from '@hcengineering/core'
import { type ToDo, type WorkSlot } from '@hcengineering/time'
import { type EventPersonMapping } from '../../types'
export function isVisibleMe (value: Event, me: Ref<Contact>): boolean {
if (value.participants.includes(me)) {
return true
}
return false
}
function isVisibleAll (value: ToDo): boolean {
if (value.visibility === 'public' || value.visibility === undefined) {
return true
}
return false
}
/**
* @public
*/
export function groupTeamData (
items: WorkSlot[],
todos: IdMap<ToDo>,
events: Event[],
personAccounts: PersonAccount[],
calendars: IdMap<Calendar>,
mePerson: Ref<Person>,
calendarStore: IdMap<Calendar>
): EventPersonMapping[] {
const result = new Map<Ref<Person>, EventPersonMapping>()
const totalEventsMap = new Map<Ref<Person>, EventVars[]>()
for (const slot of items) {
const todo = todos.get(slot.attachedTo)
if (todo === undefined) {
continue
}
const mapping: EventPersonMapping = result.get(todo.user) ?? {
busy: {
slots: [],
total: 0,
user: todo.user
},
mappings: [],
user: todo.user,
total: 0,
events: [],
busyTotal: 0,
busyEvents: []
}
result.set(todo.user, mapping)
const totalEvents = totalEventsMap.get(todo.user) ?? []
const over = calcOverlap(totalEvents, slot)
totalEvents.push(...over.events)
totalEventsMap.set(todo.user, totalEvents)
if (isVisibleAll(todo)) {
let mm = mapping.mappings.find((it) => it.todo?._id === todo._id)
if (mm === undefined) {
mm = {
todo,
slots: [],
user: todo.user,
total: 0
}
mapping.mappings.push(mm)
}
mm.total += over.total
mm.slots.push({ ...slot, overlap: slot.dueDate - slot.date - over.total })
} else {
mapping.busy.slots.push(slot)
mapping.busy.total += over.total
}
mapping.total += over.total
}
for (const event of events) {
const space = calendars.get(event.space)
if (space === undefined) {
continue
}
for (const p of event.participants) {
const accounts = personAccounts.filter((it) => it.person === p)
if (!accounts.some((it) => space.members.includes(it._id))) {
continue
}
const mapping: EventPersonMapping = result.get(p) ?? {
busy: {
slots: [],
total: 0,
user: p
},
mappings: [],
user: p,
total: 0,
events: [],
busyTotal: 0,
busyEvents: []
}
result.set(p, mapping)
if (mapping.events.find((it) => it.eventId === event.eventId) === undefined) {
const totalEvents = totalEventsMap.get(p) ?? []
const over = calcOverlap(totalEvents, event)
totalEvents.push(...over.events)
totalEventsMap.set(p, totalEvents)
if (isVisible(event, calendarStore) || isVisibleMe(event, mePerson)) {
mapping.total += over.total
mapping.events.push({ ...event, overlap: event.dueDate - event.date - over.total })
} else {
mapping.busyTotal += over.total
mapping.busyEvents.push(event)
}
}
}
}
console.log(
Array.from(totalEventsMap.entries()).map(([k, it]) => [
k,
it.toSorted((a, b) => a.date - b.date).map((q) => [new Date(q.date), new Date(q.dueDate)])
])
)
return Array.from(result.values())
}
/**
* @public
*/
export const toSlots = (events: Event[]): WorkSlot[] => events as WorkSlot[]
type EventVars = Pick<Event, 'date' | 'dueDate'>
/**
* Inside:
* A: ------------------
* B: ....----------....
*
* Before:
* A: ...------------------
* B: vvv--------..........
*
* After:
* A: -------------------...
* B: ....---------------vvv
*
* Outside:
* A: ...-------------------...
* B: vvv-------------------vvv
*/
function crossWith (a: EventVars, b: EventVars): EventVars[] {
const newTmp: EventVars[] = []
// Before
if (b.date <= a.date) {
const n = { date: b.date, dueDate: Math.min(a.date, b.dueDate) }
if (n.dueDate - n.date > 0) {
newTmp.push(n)
}
}
// After
if (a.dueDate <= b.dueDate) {
const n = { date: Math.max(a.dueDate, b.date), dueDate: b.dueDate }
if (n.dueDate - n.date > 0) {
newTmp.push(n)
}
}
return newTmp
}
/**
*
* @param events - without overlaps
* @param event -
* @returns
*/
function calcOverlap (events: EventVars[], event: Event): { events: EventVars[], total: number } {
let tmp: EventVars[] = [{ date: event.date, dueDate: event.dueDate }]
for (const a of events) {
const newTmp: EventVars[] = []
for (const b of tmp) {
newTmp.push(...crossWith(a, b))
}
tmp = newTmp
}
return { events: tmp, total: tmp.reduce((v, it) => v + (it.dueDate - it.date), 0) }
}

View File

@ -0,0 +1,51 @@
//
// 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.
//
import { type Resources } from '@hcengineering/platform'
import Me from './components/Me.svelte'
import Team from './components/team/Team.svelte'
import IssuePresenter from './components/presenters/IssuePresenter.svelte'
import CardPresenter from './components/presenters/CardPresenter.svelte'
import LeadPresenter from './components/presenters/LeadPresenter.svelte'
import ApplicantPresenter from './components/presenters/ApplicantPresenter.svelte'
import WorkSlotElement from './components/WorkSlotElement.svelte'
import EditWorkSlot from './components/EditWorkSlot.svelte'
import EditToDo from './components/EditToDo.svelte'
import CreateToDoPopup from './components/CreateToDoPopup.svelte'
import NotificationToDoPresenter from './components/NotificationToDoPresenter.svelte'
import PriorityEditor from './components/PriorityEditor.svelte'
import { ToDoTitleProvider } from './utils'
export type ToDosMode = 'unplanned' | 'planned' | 'all' | 'tag' | 'date'
export default async (): Promise<Resources> => ({
component: {
Me,
Team,
IssuePresenter,
CardPresenter,
LeadPresenter,
ApplicantPresenter,
EditWorkSlot,
WorkSlotElement,
CreateToDoPopup,
EditToDo,
NotificationToDoPresenter,
PriorityEditor
},
function: {
ToDoTitleProvider
}
})

View File

@ -0,0 +1,60 @@
//
// 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.
//
import { type Ref } from '@hcengineering/core'
import { type IntlString, mergeIds } from '@hcengineering/platform'
import { type TagCategory } from '@hcengineering/tags'
import time, { timeId } from '@hcengineering/time'
export default mergeIds(timeId, time, {
category: {
Other: '' as Ref<TagCategory>
},
string: {
Today: '' as IntlString,
TodayColon: '' as IntlString,
ToDoColon: '' as IntlString,
Tomorrow: '' as IntlString,
Yesterday: '' as IntlString,
Completed: '' as IntlString,
Now: '' as IntlString,
Scheduled: '' as IntlString,
Schedule: '' as IntlString,
WithoutProject: '' as IntlString,
TotalGroupTime: '' as IntlString,
Tasks: '' as IntlString,
WorkItem: '' as IntlString,
WorkSlot: '' as IntlString,
CreateToDo: '' as IntlString,
Inbox: '' as IntlString,
Days: '' as IntlString,
Hours: '' as IntlString,
Minutes: '' as IntlString,
All: '' as IntlString,
Done: '' as IntlString,
ToDos: '' as IntlString,
Unplanned: '' as IntlString,
Planned: '' as IntlString,
AddSlot: '' as IntlString,
HighPriority: '' as IntlString,
MediumPriority: '' as IntlString,
LowPriority: '' as IntlString,
NoPriority: '' as IntlString,
AddTo: '' as IntlString,
AddTitle: '' as IntlString,
MyWork: '' as IntlString,
WorkSchedule: '' as IntlString
}
})

View File

@ -0,0 +1,27 @@
import { type Event } from '@hcengineering/calendar'
import { type Person } from '@hcengineering/contact'
import { type Ref } from '@hcengineering/core'
import { type ToDo, type WorkSlot } from '@hcengineering/time'
/**
* @public
*/
export interface WorkSlotMapping {
slots: Array<WorkSlot & { overlap?: number }>
todo?: ToDo
user: Ref<Person>
total: number
}
/**
* @public
*/
export interface EventPersonMapping {
user: Ref<Person>
mappings: WorkSlotMapping[]
busy: WorkSlotMapping
busyTotal: number
busyEvents: Event[]
events: Array<Event & { overlap?: number }>
total: number
}

View File

@ -0,0 +1,35 @@
import { type Client, type Ref } from '@hcengineering/core'
import { type DefSeparators } from '@hcengineering/ui'
import time, { type WorkSlot, type ToDo } from '@hcengineering/time'
export function getNearest (events: WorkSlot[]): WorkSlot | undefined {
const now = Date.now()
events.sort((a, b) => a.date - b.date)
return (
events.find((event) => event.date <= now && event.dueDate >= now) ??
events.find((event) => event.date >= now) ??
events[events.length - 1]
)
}
/**
* @public
*/
export const timeSeparators: DefSeparators = [
{ minSize: 18, size: 18, maxSize: 22.5, float: 'navigator' },
{ minSize: 15, size: 35, maxSize: 45, float: 'planner' },
null
]
/**
* @public
*/
export const teamSeparators: DefSeparators = [{ minSize: 12.5, size: 17.5, maxSize: 22.5, float: 'navigator' }, null]
export async function ToDoTitleProvider (client: Client, ref: Ref<ToDo>, doc?: ToDo): Promise<string> {
const object = doc ?? (await client.findOne(time.class.ToDo, { _id: ref }))
if (object === undefined) return ''
return object.title
}

View File

@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

4
plugins/time/.npmignore Normal file
View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}

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"]
}

43
plugins/time/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "@hcengineering/time",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Uberflow Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/core": "^0.6.28",
"@hcengineering/contact": "^0.6.20",
"@hcengineering/calendar": "^0.6.17",
"@hcengineering/task": "^0.6.13",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/ui": "^0.6.11"
}
}

140
plugins/time/src/index.ts Normal file
View File

@ -0,0 +1,140 @@
//
// 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.
//
import { Event, Visibility } from '@hcengineering/calendar'
import { Person } from '@hcengineering/contact'
import { AttachedDoc, Class, Doc, Hierarchy, Markup, Mixin, Ref, Space, Timestamp, Type } from '@hcengineering/core'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import { AnyComponent } from '@hcengineering/ui'
/**
* @public
*/
export const timeId = 'time' as Plugin
/**
* @public
*/
export interface WorkSlot extends Event {
attachedTo: Ref<ToDo>
attachedToClass: Ref<Class<ToDo>>
}
/**
* @public
*/
export interface ToDo extends AttachedDoc {
attachedTo: Ref<Doc>
attachedToClass: Ref<Class<Doc>>
workslots: number
title: string
description: Markup
dueDate?: Timestamp | null
priority: ToDoPriority
visibility: Visibility
doneOn?: Timestamp | null
user: Ref<Person>
attachedSpace?: Ref<Space>
labels?: number
}
/**
* @public
*/
export enum ToDoPriority {
High,
Medium,
Low,
NoPriority
}
/**
* @public
*/
export interface ProjectToDo extends ToDo {
attachedSpace: Ref<Space>
}
/**
* @public
*/
export interface ItemPresenter extends Class<Doc> {
presenter: AnyComponent
}
/**
* @public
*/
export type TodoDoneTester = (
client: {
findAll: Storage['findAll']
hierarchy: Hierarchy
},
todo: ToDo
) => Promise<boolean>
/**
* A helper class to control classic project todo automation.
*/
export interface TodoAutomationHelper extends Doc {
onDoneTester: Resource<TodoDoneTester>
}
export default plugin(timeId, {
component: {
Me: '' as AnyComponent,
Team: '' as AnyComponent,
EditToDo: '' as AnyComponent
},
class: {
WorkSlot: '' as Ref<Class<WorkSlot>>,
ToDo: '' as Ref<Class<ToDo>>,
ProjectToDo: '' as Ref<Class<ProjectToDo>>,
TypeToDoPriority: '' as Ref<Class<Type<ToDoPriority>>>,
TodoAutomationHelper: '' as Ref<Class<TodoAutomationHelper>>
},
mixin: {
ItemPresenter: '' as Ref<Mixin<ItemPresenter>>
},
ids: {
NotAttached: '' as Ref<Doc>
},
space: {
ToDos: '' as Ref<Space>
},
icon: {
Team: '' as Asset,
Hashtag: '' as Asset,
Inbox: '' as Asset,
Target: '' as Asset,
Flag: '' as Asset,
FilledFlag: '' as Asset,
Planned: '' as Asset,
All: '' as Asset
},
string: {
Planner: '' as IntlString,
Calendar: '' as IntlString,
Agenda: '' as IntlString,
Me: '' as IntlString,
Team: '' as IntlString,
WeekCalendar: '' as IntlString,
DayCalendar: '' as IntlString,
CreatedToDo: '' as IntlString,
AddToDo: '' as IntlString,
NewToDoDetails: '' as IntlString
}
})

View File

@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -1581,6 +1581,41 @@
"packageName": "@hcengineering/auth-providers", "packageName": "@hcengineering/auth-providers",
"projectFolder": "pods/authProviders", "projectFolder": "pods/authProviders",
"shouldPublish": false "shouldPublish": false
} },
{
"packageName": "@hcengineering/time",
"projectFolder": "plugins/time",
"shouldPublish": false
},
{
"packageName": "@hcengineering/time-assets",
"projectFolder": "plugins/time-assets",
"shouldPublish": false
},
{
"packageName": "@hcengineering/time-resources",
"projectFolder": "plugins/time-resources",
"shouldPublish": false
},
{
"packageName": "@hcengineering/model-time",
"projectFolder": "models/time",
"shouldPublish": false
},
{
"packageName": "@hcengineering/server-time",
"projectFolder": "server-plugins/time",
"shouldPublish": false
},
{
"packageName": "@hcengineering/server-time-resources",
"projectFolder": "server-plugins/time-resources",
"shouldPublish": false
},
{
"packageName": "@hcengineering/model-server-time",
"projectFolder": "models/server-time",
"shouldPublish": false
},
] ]
} }

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}

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"]
}

View File

@ -0,0 +1,47 @@
{
"name": "@hcengineering/server-time-resources",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"prettier-plugin-svelte": "^3.1.0"
},
"dependencies": {
"@hcengineering/contact": "^0.6.20",
"@hcengineering/core": "^0.6.28",
"@hcengineering/notification": "^0.6.16",
"@hcengineering/platform": "^0.6.9",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/server-notification-resources": "^0.6.0",
"@hcengineering/task": "^0.6.13",
"@hcengineering/tracker": "^0.6.13",
"@hcengineering/server-time": "^0.6.0",
"@hcengineering/time": "^0.6.0"
}
}

View File

@ -0,0 +1,586 @@
//
// 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.
//
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import core, {
AttachedData,
Class,
Data,
Doc,
DocumentUpdate,
Ref,
Status,
Tx,
TxCUD,
TxCreateDoc,
TxFactory,
TxProcessor,
TxUpdateDoc,
toIdMap
} from '@hcengineering/core'
import notification, { CommonInboxNotification } from '@hcengineering/notification'
import { getResource } from '@hcengineering/platform'
import type { TriggerControl } from '@hcengineering/server-core'
import {
getNotificationContent,
isShouldNotify,
pushInboxNotifications
} from '@hcengineering/server-notification-resources'
import task from '@hcengineering/task'
import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker'
import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time'
import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time'
/**
* @public
*/
export async function OnTask (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Doc>
const mixin = control.hierarchy.classHierarchyMixin<Class<Doc>, ToDoFactory>(
actualTx.objectClass,
serverTime.mixin.ToDoFactory
)
if (mixin !== undefined) {
if (actualTx._class !== core.class.TxRemoveDoc) {
const factory = await getResource(mixin.factory)
return await factory(tx, control)
} else {
const todos = await control.findAll(time.class.ToDo, { attachedTo: actualTx.objectId })
return todos.map((p) => control.txFactory.createTxRemoveDoc(p._class, p.space, p._id))
}
}
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 []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)) return []
const workslot = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<WorkSlot>)
const workslots = await control.findAll(time.class.WorkSlot, { attachedTo: workslot.attachedTo })
if (workslots.length > 1) return []
const todo = (await control.findAll(time.class.ToDo, { _id: workslot.attachedTo }))[0]
if (todo === undefined) return []
if (!control.hierarchy.isDerived(todo.attachedToClass, tracker.class.Issue)) return []
const issue = (await control.findAll(tracker.class.Issue, { _id: todo.attachedTo as Ref<Issue> }))[0]
if (issue === undefined) return []
const project = (await control.findAll(task.class.Project, { _id: issue.space }))[0]
if (project !== undefined) {
const type = (await control.queryFind(task.class.ProjectType, {})).find((it) => it._id === project.type)
if (type?.classic === true) {
const taskType = (await control.queryFind(task.class.TaskType, {})).find((it) => it._id === issue.kind)
if (taskType !== undefined) {
const statuses = await control.findAll(core.class.Status, { _id: { $in: taskType.statuses } })
const statusMap = toIdMap(statuses)
const typeStatuses = taskType.statuses.map((p) => statusMap.get(p)).filter((p) => p !== undefined) as Status[]
const current = statusMap.get(issue.status)
if (current === undefined) return []
if (current.category !== task.statusCategory.UnStarted && current.category !== task.statusCategory.ToDo) {
return []
}
const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.Active)
if (nextStatus !== undefined) {
const factory = new TxFactory(control.txFactory.account)
const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, {
status: nextStatus._id
})
const outerTx = factory.createTxCollectionCUD(
issue.attachedToClass,
issue.attachedTo,
issue.space,
issue.collection,
innerTx
)
await control.apply([outerTx], true)
return []
}
}
}
}
return []
}
export async function OnToDoRemove (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<ToDo>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)) return []
const todo = control.removedMap.get(actualTx.objectId) as ToDo
if (todo === undefined) return []
// it was closed, do nothing
if (todo.doneOn != null) return []
const todos = await control.findAll(time.class.ToDo, { attachedTo: todo.attachedTo })
if (todos.length > 0) return []
const issue = (await control.findAll(tracker.class.Issue, { _id: todo.attachedTo as Ref<Issue> }))[0]
if (issue === undefined) return []
const project = (await control.findAll(task.class.Project, { _id: issue.space }))[0]
if (project !== undefined) {
const type = (await control.queryFind(task.class.ProjectType, {})).find((it) => it._id === project.type)
if (type !== undefined && type.classic) {
const factory = new TxFactory(control.txFactory.account)
const taskType = (await control.queryFind(task.class.TaskType, {})).find((it) => it._id === issue.kind)
if (taskType !== undefined) {
const statuses = await control.findAll(core.class.Status, { _id: { $in: taskType.statuses } })
const statusMap = toIdMap(statuses)
const typeStatuses = taskType.statuses.map((p) => statusMap.get(p)).filter((p) => p !== undefined) as Status[]
const current = statusMap.get(issue.status)
if (current === undefined) return []
if (current.category !== task.statusCategory.Active && current.category !== task.statusCategory.ToDo) return []
const nextStatus = typeStatuses.find((p) => p.category === task.statusCategory.UnStarted)
if (nextStatus !== undefined) {
const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, {
status: nextStatus._id
})
const outerTx = factory.createTxCollectionCUD(
issue.attachedToClass,
issue.attachedTo,
issue.space,
issue.collection,
innerTx
)
await control.apply([outerTx], true)
return []
}
}
}
}
return []
}
export async function OnToDoCreate (tx: TxCUD<Doc>, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const createTx = TxProcessor.extractTx(tx) as TxCreateDoc<ToDo>
if (!hierarchy.isDerived(createTx.objectClass, time.class.ToDo)) return []
if (!hierarchy.isDerived(createTx._class, core.class.TxCreateDoc)) return []
const mixin = hierarchy.classHierarchyMixin(
createTx.objectClass as Ref<Class<Doc>>,
notification.mixin.ClassCollaborators
)
if (mixin === undefined) {
return []
}
const todo = TxProcessor.createDoc2Doc(createTx)
const account = await getPersonAccount(todo.user, control)
if (account === undefined) {
return []
}
const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: todo._id })
const res: Tx[] = []
const notifyResult = await isShouldNotify(control, createTx, tx, todo, account._id, true, false)
if (notifyResult.allowed) {
const content = await getNotificationContent(tx, account._id, todo, control)
const details = todo.description != null && todo.description.length > 0 ? todo.description : todo.title
const data: Partial<Data<CommonInboxNotification>> = {
...content,
header: time.string.CreatedToDo,
message: time.string.NewToDoDetails,
props: { details }
}
await pushInboxNotifications(
control,
res,
account._id,
todo._id,
todo._class,
todo.space,
notifyContexts,
data,
notification.class.CommonInboxNotification,
createTx.modifiedOn
)
}
return res
}
/**
* @public
*/
export async function OnToDoUpdate (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<ToDo>
if (!control.hierarchy.isDerived(actualTx.objectClass, time.class.ToDo)) return []
if (!control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) return []
const updTx = actualTx as TxUpdateDoc<ToDo>
const doneOn = updTx.operations.doneOn
const title = updTx.operations.title
const description = updTx.operations.description
const visibility = updTx.operations.visibility
if (doneOn != null) {
const events = await control.findAll(time.class.WorkSlot, { attachedTo: updTx.objectId })
const res: Tx[] = []
const resEvents: WorkSlot[] = []
for (const event of events) {
if (event.date > doneOn) {
const innerTx = control.txFactory.createTxRemoveDoc(event._class, event.space, event._id)
const outerTx = control.txFactory.createTxCollectionCUD(
event.attachedToClass,
event.attachedTo,
event.space,
event.collection,
innerTx
)
res.push(outerTx)
} else if (event.dueDate > doneOn) {
const upd: DocumentUpdate<WorkSlot> = {
dueDate: doneOn
}
if (title !== undefined) {
upd.title = title
}
if (description !== undefined) {
upd.description = description
}
const innerTx = control.txFactory.createTxUpdateDoc(event._class, event.space, event._id, upd)
const outerTx = control.txFactory.createTxCollectionCUD(
event.attachedToClass,
event.attachedTo,
event.space,
event.collection,
innerTx
)
res.push(outerTx)
resEvents.push({
...event,
dueDate: doneOn
})
} else {
resEvents.push(event)
}
}
const todo = (await control.findAll(time.class.ToDo, { _id: updTx.objectId }))[0]
if (todo === undefined) return res
const funcs = control.hierarchy.classHierarchyMixin<Class<Doc>, OnToDo>(
todo.attachedToClass,
serverTime.mixin.OnToDo
)
if (funcs !== undefined) {
const func = await getResource(funcs.onDone)
const todoRes = await func(control, resEvents, todo)
await control.apply(todoRes, true)
}
return res
}
if (title !== undefined || description !== undefined || visibility !== undefined) {
const events = await control.findAll(time.class.WorkSlot, { attachedTo: updTx.objectId })
const res: Tx[] = []
for (const event of events) {
const upd: DocumentUpdate<WorkSlot> = {}
if (title !== undefined) {
upd.title = title
}
if (description !== undefined) {
upd.description = description
}
if (visibility !== undefined) {
const newVisibility = visibility === 'public' ? 'public' : 'freeBusy'
if (event.visibility !== newVisibility) {
upd.visibility = newVisibility
}
}
const innerTx = control.txFactory.createTxUpdateDoc(event._class, event.space, event._id, upd)
const outerTx = control.txFactory.createTxCollectionCUD(
event.attachedToClass,
event.attachedTo,
event.space,
event.collection,
innerTx
)
res.push(outerTx)
}
return res
}
return []
}
/**
* @public
*/
export async function IssueToDoFactory (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = TxProcessor.extractTx(tx) as TxCUD<Issue>
if (!control.hierarchy.isDerived(actualTx.objectClass, tracker.class.Issue)) return []
if (control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)) {
const issue = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<Issue>)
return await createIssueHandler(issue, control)
} else if (control.hierarchy.isDerived(actualTx._class, core.class.TxUpdateDoc)) {
const updateTx = actualTx as TxUpdateDoc<Issue>
return await updateIssueHandler(updateTx, control)
}
return []
}
/**
* @public
*/
export async function IssueToDoDone (control: TriggerControl, workslots: WorkSlot[], todo: ToDo): Promise<Tx[]> {
const res: Tx[] = []
let total = 0
for (const workslot of workslots) {
total += (workslot.dueDate - workslot.date) / 1000 / 60
}
const factory = new TxFactory(control.txFactory.account)
const issue = (await control.findAll<Issue>(todo.attachedToClass, { _id: todo.attachedTo as Ref<Issue> }))[0]
if (issue !== undefined) {
const project = (await control.findAll(task.class.Project, { _id: issue.space }))[0]
if (project !== undefined) {
const type = (await control.queryFind(task.class.ProjectType, {})).find((it) => it._id === project.type)
if (type?.classic === true) {
const taskType = (await control.queryFind(task.class.TaskType, {})).find((it) => it._id === issue.kind)
if (taskType !== undefined) {
const index = taskType.statuses.findIndex((p) => p === issue.status)
const helpers = await control.modelDb.findAll<TodoAutomationHelper>(time.class.TodoAutomationHelper, {})
const testers = await Promise.all(helpers.map((it) => getResource(it.onDoneTester)))
let allowed = true
for (const tester of testers) {
if (!(await tester(control, todo))) {
allowed = false
break
}
}
if (index !== -1 && allowed) {
const nextStatus = taskType.statuses[index + 1]
if (nextStatus !== undefined) {
const currentStatus = taskType.statuses[index]
const current = (await control.findAll(core.class.Status, { _id: currentStatus }))[0]
const next = (await control.findAll(core.class.Status, { _id: nextStatus }))[0]
if (
current.category !== task.statusCategory.Lost &&
next.category !== task.statusCategory.Lost &&
current.category !== task.statusCategory.Won
) {
const innerTx = factory.createTxUpdateDoc(issue._class, issue.space, issue._id, {
status: nextStatus
})
const outerTx = factory.createTxCollectionCUD(
issue.attachedToClass,
issue.attachedTo,
issue.space,
issue.collection,
innerTx
)
res.push(outerTx)
}
}
}
}
}
}
if (total > 0) {
// round to nearest 15 minutes
total = Math.round(total / 15) * 15
const data: AttachedData<TimeSpendReport> = {
employee: todo.user as Ref<Employee>,
date: new Date().getTime(),
value: total / 60,
description: ''
}
const innerTx = factory.createTxCreateDoc(
tracker.class.TimeSpendReport,
issue.space,
data as Data<TimeSpendReport>
)
const outerTx = factory.createTxCollectionCUD(issue._class, issue._id, issue.space, 'reports', innerTx)
res.push(outerTx)
}
}
return res
}
async function createIssueHandler (issue: Issue, control: TriggerControl): Promise<Tx[]> {
if (issue.assignee != null) {
const project = (await control.findAll(task.class.Project, { _id: issue.space }))[0]
if (project === undefined) return []
const type = (await control.queryFind(task.class.ProjectType, {})).find((it) => it._id === project.type)
if (type?.classic !== true) return []
const status = (await control.findAll(core.class.Status, { _id: issue.status }))[0]
if (status === undefined) return []
if (status.category === task.statusCategory.Active || status.category === task.statusCategory.ToDo) {
const tx = await getCreateToDoTx(issue, issue.assignee, control)
if (tx !== undefined) {
await control.apply([tx], true)
}
}
}
return []
}
async function getPersonAccount (person: Ref<Person>, control: TriggerControl): Promise<PersonAccount | undefined> {
const account = (
await control.modelDb.findAll(
contact.class.PersonAccount,
{
person
},
{ limit: 1 }
)
)[0]
return account
}
async function getIssueToDoData (
issue: Issue,
user: Ref<Person>,
control: TriggerControl
): Promise<AttachedData<ProjectToDo> | undefined> {
const acc = await getPersonAccount(user, control)
if (acc === undefined) return
const data: AttachedData<ProjectToDo> = {
attachedSpace: issue.space,
workslots: 0,
description: issue.title,
priority: ToDoPriority.NoPriority,
visibility: 'public',
title: issue.identifier,
user: acc.person
}
return data
}
async function getCreateToDoTx (issue: Issue, user: Ref<Person>, control: TriggerControl): Promise<Tx | undefined> {
const data = await getIssueToDoData(issue, user, control)
if (data === undefined) return
const innerTx = control.txFactory.createTxCreateDoc(
time.class.ProjectToDo,
time.space.ToDos,
data as Data<ProjectToDo>
)
innerTx.space = core.space.Tx
const outerTx = control.txFactory.createTxCollectionCUD(issue._class, issue._id, time.space.ToDos, 'todos', innerTx)
outerTx.space = core.space.Tx
return outerTx
}
async function changeIssueAssigneeHandler (
control: TriggerControl,
newAssignee: Ref<Person>,
issueId: Ref<Issue>
): Promise<Tx[]> {
const issue = (await control.findAll(tracker.class.Issue, { _id: issueId }))[0]
if (issue !== undefined) {
const status = (await control.findAll(core.class.Status, { _id: issue.status }))[0]
if (status === undefined) return []
if (status.category === task.statusCategory.Active) {
const tx = await getCreateToDoTx(issue, newAssignee, control)
if (tx !== undefined) return [tx]
}
}
return []
}
async function changeIssueStatusHandler (
control: TriggerControl,
newStatus: Ref<IssueStatus>,
issueId: Ref<Issue>
): Promise<Tx[]> {
const status = (await control.findAll(core.class.Status, { _id: newStatus }))[0]
if (status === undefined) return []
if (status.category === task.statusCategory.Active || status.category === task.statusCategory.ToDo) {
const issue = (await control.findAll(tracker.class.Issue, { _id: issueId }))[0]
if (issue?.assignee != null) {
const todos = await control.findAll(time.class.ToDo, {
attachedTo: issue._id,
user: issue.assignee
})
if (todos.length === 0) {
const tx = await getCreateToDoTx(issue, issue.assignee, control)
if (tx !== undefined) {
await control.apply([tx], true)
}
}
}
}
return []
}
async function changeIssueNumberHandler (control: TriggerControl, issueId: Ref<Issue>): Promise<Tx[]> {
const res: Tx[] = []
const issue = (await control.findAll(tracker.class.Issue, { _id: issueId }))[0]
if (issue !== undefined) {
const todos = await control.findAll(time.class.ToDo, {
attachedTo: issue._id
})
for (const todo of todos) {
const data = await getIssueToDoData(issue, todo.user, control)
if (data === undefined) continue
const update: DocumentUpdate<ToDo> = {}
if (data.title !== todo.title) {
update.title = data.title
}
if (data.description !== todo.description) {
update.description = data.description
}
if (data.attachedSpace !== todo.attachedSpace) {
update.attachedSpace = data.attachedSpace
}
if (Object.keys(update).length > 0) {
const innerTx = control.txFactory.createTxUpdateDoc(todo._class, todo.space, todo._id, update)
const outerTx = control.txFactory.createTxCollectionCUD(
issue._class,
issue._id,
time.space.ToDos,
'todos',
innerTx
)
res.push(outerTx)
}
}
}
return res
}
async function updateIssueHandler (tx: TxUpdateDoc<Issue>, control: TriggerControl): Promise<Tx[]> {
const res: Tx[] = []
const project = (await control.findAll(task.class.Project, { _id: tx.objectSpace as Ref<Project> }))[0]
if (project === undefined) return []
const type = (await control.queryFind(task.class.ProjectType, {})).find((it) => it._id === project.type)
if (type?.classic !== true) return []
const newAssignee = tx.operations.assignee
if (newAssignee != null) {
res.push(...(await changeIssueAssigneeHandler(control, newAssignee, tx.objectId)))
}
const newStatus = tx.operations.status
if (newStatus !== undefined) {
res.push(...(await changeIssueStatusHandler(control, newStatus, tx.objectId)))
}
const number = tx.operations.number
if (number !== undefined) {
res.push(...(await changeIssueNumberHandler(control, tx.objectId)))
}
return res
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
IssueToDoFactory,
IssueToDoDone
},
trigger: {
OnTask,
OnToDoUpdate,
OnToDoRemove,
OnToDoCreate,
OnWorkSlotCreate
}
})

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