mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-21 16:09:12 +03:00
parent
f6826dd5f9
commit
287652f279
@ -338,6 +338,9 @@ dependencies:
|
||||
'@rush-temp/model-server-templates':
|
||||
specifier: file:./projects/model-server-templates.tgz
|
||||
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':
|
||||
specifier: file:./projects/model-server-tracker.tgz
|
||||
version: file:projects/model-server-tracker.tgz(svelte@4.2.11)
|
||||
@ -368,6 +371,9 @@ dependencies:
|
||||
'@rush-temp/model-text-editor':
|
||||
specifier: file:./projects/model-text-editor.tgz
|
||||
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':
|
||||
specifier: file:./projects/model-tracker.tgz
|
||||
version: file:projects/model-tracker.tgz(svelte@4.2.11)
|
||||
@ -578,6 +584,12 @@ dependencies:
|
||||
'@rush-temp/server-templates':
|
||||
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)
|
||||
'@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':
|
||||
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)
|
||||
@ -668,6 +680,15 @@ dependencies:
|
||||
'@rush-temp/theme':
|
||||
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)
|
||||
'@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':
|
||||
specifier: file:./projects/tool.tgz
|
||||
version: file:projects/tool.tgz(bufferutil@4.0.8)(svelte@4.2.11)
|
||||
@ -19942,6 +19963,27 @@ packages:
|
||||
- svelte
|
||||
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):
|
||||
resolution: {integrity: sha512-8TqauIOhrL/TrZIwsKyaixz5G2XlQjulPp9aMFm7gMxRHm7b3bjUiPI7SZ0bC4+kP99o5mxepmyiLsU/XrYSig==, tarball: file:projects/model-server-tracker.tgz}
|
||||
id: file:projects/model-server-tracker.tgz
|
||||
@ -20152,6 +20194,27 @@ packages:
|
||||
- svelte
|
||||
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):
|
||||
resolution: {integrity: sha512-JekAX6mPwRvA0RvyDDELrdcyCDDGYtSeqseUfklkZZKSj0t3zrAT1KQlmIFTe05x3uqOMCcQXgPRVBvtgsPqUA==, tarball: file:projects/model-tracker.tgz}
|
||||
id: file:projects/model-tracker.tgz
|
||||
@ -22537,6 +22600,70 @@ packages:
|
||||
- ts-node
|
||||
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):
|
||||
resolution: {integrity: sha512-njkCF8ZsS1x6rIwZPjyN368EONN8FrN9R+6f3yYoMR+2luiJt/GknPNNopZwuvEIcorJ43fUC79ECsJFMzHfMg==, tarball: file:projects/server-token.tgz}
|
||||
id: file:projects/server-token.tgz
|
||||
@ -23746,6 +23873,116 @@ packages:
|
||||
- ts-node
|
||||
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):
|
||||
resolution: {integrity: sha512-u/v+y38hfzb8fBpMNT1IkihE0UA06lngEqh/bQWl6rMb4EMKOVc6V5N6YPq41mLIQvDnqMz4mzDsVRlq6c4HDQ==, tarball: file:projects/tool.tgz}
|
||||
id: file:projects/tool.tgz
|
||||
|
7
models/server-time/.eslintrc.js
Normal file
7
models/server-time/.eslintrc.js
Normal 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/server-time/.npmignore
Normal file
4
models/server-time/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
5
models/server-time/config/rig.json
Normal file
5
models/server-time/config/rig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "model"
|
||||
}
|
41
models/server-time/package.json
Normal file
41
models/server-time/package.json
Normal 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"
|
||||
}
|
||||
}
|
84
models/server-time/src/index.ts
Normal file
84
models/server-time/src/index.ts
Normal 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'
|
10
models/server-time/tsconfig.json
Normal file
10
models/server-time/tsconfig.json
Normal 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
7
models/time/.eslintrc.js
Normal 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
4
models/time/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
5
models/time/config/rig.json
Normal file
5
models/time/config/rig.json
Normal 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
55
models/time/package.json
Normal 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
369
models/time/src/index.ts
Normal 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'
|
165
models/time/src/migration.ts
Normal file
165
models/time/src/migration.ts
Normal 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
68
models/time/src/plugin.ts
Normal 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
10
models/time/tsconfig.json
Normal 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
plugins/time-assets/.eslintrc.js
Normal file
7
plugins/time-assets/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
47
plugins/time-assets/assets/icons.svg
Normal file
47
plugins/time-assets/assets/icons.svg
Normal 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 |
5
plugins/time-assets/config/rig.json
Normal file
5
plugins/time-assets/config/rig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "assets"
|
||||
}
|
7
plugins/time-assets/jest.config.js
Normal file
7
plugins/time-assets/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
54
plugins/time-assets/lang/en.json
Normal file
54
plugins/time-assets/lang/en.json
Normal 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"
|
||||
}
|
||||
}
|
54
plugins/time-assets/lang/ru.json
Normal file
54
plugins/time-assets/lang/ru.json
Normal 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": "Расписание работы"
|
||||
}
|
||||
}
|
40
plugins/time-assets/package.json
Normal file
40
plugins/time-assets/package.json
Normal 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"
|
||||
}
|
||||
}
|
6
plugins/time-assets/src/__tests__/lang.test.ts
Normal file
6
plugins/time-assets/src/__tests__/lang.test.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { makeLocalesTest } from '@hcengineering/platform'
|
||||
|
||||
it(
|
||||
'Locales are equale',
|
||||
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
|
||||
)
|
29
plugins/time-assets/src/index.ts
Normal file
29
plugins/time-assets/src/index.ts
Normal 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`
|
||||
})
|
11
plugins/time-assets/tsconfig.json
Normal file
11
plugins/time-assets/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
4
plugins/time-resources/.eslintrc.js
Normal file
4
plugins/time-resources/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
|
||||
parserOptions: { tsconfigRootDir: __dirname }
|
||||
}
|
5
plugins/time-resources/config/rig.json
Normal file
5
plugins/time-resources/config/rig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig",
|
||||
"rigProfile": "ui"
|
||||
}
|
7
plugins/time-resources/jest.config.js
Normal file
7
plugins/time-resources/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
67
plugins/time-resources/package.json
Normal file
67
plugins/time-resources/package.json
Normal 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"
|
||||
}
|
||||
}
|
5
plugins/time-resources/postcss.config.js
Normal file
5
plugins/time-resources/postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('autoprefixer')
|
||||
]
|
||||
}
|
6
plugins/time-resources/src/components/Border.svelte
Normal file
6
plugins/time-resources/src/components/Border.svelte
Normal 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>
|
65
plugins/time-resources/src/components/CreateToDo.svelte
Normal file
65
plugins/time-resources/src/components/CreateToDo.svelte
Normal 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>
|
295
plugins/time-resources/src/components/CreateToDoPopup.svelte
Normal file
295
plugins/time-resources/src/components/CreateToDoPopup.svelte
Normal 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>
|
55
plugins/time-resources/src/components/DueDateEditor.svelte
Normal file
55
plugins/time-resources/src/components/DueDateEditor.svelte
Normal 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>
|
271
plugins/time-resources/src/components/EditToDo.svelte
Normal file
271
plugins/time-resources/src/components/EditToDo.svelte
Normal 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>
|
194
plugins/time-resources/src/components/EditWorkSlot.svelte
Normal file
194
plugins/time-resources/src/components/EditWorkSlot.svelte
Normal 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>
|
@ -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}
|
81
plugins/time-resources/src/components/Header.svelte
Normal file
81
plugins/time-resources/src/components/Header.svelte
Normal 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>
|
11
plugins/time-resources/src/components/Me.svelte
Normal file
11
plugins/time-resources/src/components/Me.svelte
Normal 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>
|
@ -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>
|
98
plugins/time-resources/src/components/PlanView.svelte
Normal file
98
plugins/time-resources/src/components/PlanView.svelte
Normal 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>
|
243
plugins/time-resources/src/components/PlanningCalendar.svelte
Normal file
243
plugins/time-resources/src/components/PlanningCalendar.svelte
Normal 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>
|
89
plugins/time-resources/src/components/PriorityEditor.svelte
Normal file
89
plugins/time-resources/src/components/PriorityEditor.svelte
Normal 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)
|
||||
}}
|
||||
/>
|
@ -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}
|
42
plugins/time-resources/src/components/TaskSelector.svelte
Normal file
42
plugins/time-resources/src/components/TaskSelector.svelte
Normal 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}
|
107
plugins/time-resources/src/components/ToDoDatePresenter.svelte
Normal file
107
plugins/time-resources/src/components/ToDoDatePresenter.svelte
Normal 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>
|
35
plugins/time-resources/src/components/ToDoDuration.svelte
Normal file
35
plugins/time-resources/src/components/ToDoDuration.svelte
Normal 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}
|
169
plugins/time-resources/src/components/ToDoElement.svelte
Normal file
169
plugins/time-resources/src/components/ToDoElement.svelte
Normal 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>
|
124
plugins/time-resources/src/components/ToDoGroup.svelte
Normal file
124
plugins/time-resources/src/components/ToDoGroup.svelte
Normal 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}
|
74
plugins/time-resources/src/components/ToDoPresenter.svelte
Normal file
74
plugins/time-resources/src/components/ToDoPresenter.svelte
Normal 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}
|
317
plugins/time-resources/src/components/ToDos.svelte
Normal file
317
plugins/time-resources/src/components/ToDos.svelte
Normal 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>
|
233
plugins/time-resources/src/components/ToDosNavigator.svelte
Normal file
233
plugins/time-resources/src/components/ToDosNavigator.svelte
Normal 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>
|
94
plugins/time-resources/src/components/TodoWorkslots.svelte
Normal file
94
plugins/time-resources/src/components/TodoWorkslots.svelte
Normal 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} />
|
@ -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}
|
38
plugins/time-resources/src/components/WorkSlotElement.svelte
Normal file
38
plugins/time-resources/src/components/WorkSlotElement.svelte
Normal 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}
|
82
plugins/time-resources/src/components/Workslots.svelte
Normal file
82
plugins/time-resources/src/components/Workslots.svelte
Normal 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>
|
36
plugins/time-resources/src/components/icons/Diff.svelte
Normal file
36
plugins/time-resources/src/components/icons/Diff.svelte
Normal 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>
|
27
plugins/time-resources/src/components/icons/Menu.svelte
Normal file
27
plugins/time-resources/src/components/icons/Menu.svelte
Normal 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>
|
67
plugins/time-resources/src/components/icons/Sun.svelte
Normal file
67
plugins/time-resources/src/components/icons/Sun.svelte
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
81
plugins/time-resources/src/components/team/Team.svelte
Normal file
81
plugins/time-resources/src/components/team/Team.svelte
Normal 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>
|
@ -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>
|
110
plugins/time-resources/src/components/team/WithTeamData.svelte
Normal file
110
plugins/time-resources/src/components/team/WithTeamData.svelte
Normal 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>
|
101
plugins/time-resources/src/components/team/agenda/Agenda.svelte
Normal file
101
plugins/time-resources/src/components/team/agenda/Agenda.svelte
Normal 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>
|
@ -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" />
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
201
plugins/time-resources/src/components/team/utils.ts
Normal file
201
plugins/time-resources/src/components/team/utils.ts
Normal 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) }
|
||||
}
|
51
plugins/time-resources/src/index.ts
Normal file
51
plugins/time-resources/src/index.ts
Normal 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
|
||||
}
|
||||
})
|
60
plugins/time-resources/src/plugin.ts
Normal file
60
plugins/time-resources/src/plugin.ts
Normal 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
|
||||
}
|
||||
})
|
27
plugins/time-resources/src/types.ts
Normal file
27
plugins/time-resources/src/types.ts
Normal 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
|
||||
}
|
35
plugins/time-resources/src/utils.ts
Normal file
35
plugins/time-resources/src/utils.ts
Normal 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
|
||||
}
|
5
plugins/time-resources/svelte.config.js
Normal file
5
plugins/time-resources/svelte.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
const sveltePreprocess = require('svelte-preprocess')
|
||||
|
||||
module.exports = {
|
||||
preprocess: sveltePreprocess()
|
||||
};
|
9
plugins/time-resources/tsconfig.json
Normal file
9
plugins/time-resources/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types"
|
||||
}
|
||||
}
|
7
plugins/time/.eslintrc.js
Normal file
7
plugins/time/.eslintrc.js
Normal 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
4
plugins/time/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
4
plugins/time/config/rig.json
Normal file
4
plugins/time/config/rig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
7
plugins/time/jest.config.js
Normal file
7
plugins/time/jest.config.js
Normal 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
43
plugins/time/package.json
Normal 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
140
plugins/time/src/index.ts
Normal 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
|
||||
}
|
||||
})
|
10
plugins/time/tsconfig.json
Normal file
10
plugins/time/tsconfig.json
Normal 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"
|
||||
}
|
||||
}
|
37
rush.json
37
rush.json
@ -1581,6 +1581,41 @@
|
||||
"packageName": "@hcengineering/auth-providers",
|
||||
"projectFolder": "pods/authProviders",
|
||||
"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
|
||||
},
|
||||
]
|
||||
}
|
||||
|
7
server-plugins/time-resources/.eslintrc.js
Normal file
7
server-plugins/time-resources/.eslintrc.js
Normal 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
server-plugins/time-resources/.npmignore
Normal file
4
server-plugins/time-resources/.npmignore
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
4
server-plugins/time-resources/config/rig.json
Normal file
4
server-plugins/time-resources/config/rig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
7
server-plugins/time-resources/jest.config.js
Normal file
7
server-plugins/time-resources/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
47
server-plugins/time-resources/package.json
Normal file
47
server-plugins/time-resources/package.json
Normal 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"
|
||||
}
|
||||
}
|
586
server-plugins/time-resources/src/index.ts
Normal file
586
server-plugins/time-resources/src/index.ts
Normal 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
Loading…
Reference in New Issue
Block a user