aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-09-19 20:38:35 -0400
committerbobzel <zzzman@gmail.com>2024-09-19 20:38:35 -0400
commit06d460562004a29060d9972857d5c76e4ee51461 (patch)
treefe4b8ee0b3c46f2c737d4b3a60a40fa972f39f72
parent34e2b4d4c098e08d6cfe7d48c3a4b27fcda091b3 (diff)
parentcf7216aec2fd61bc7de43240586532b5d54e7d8b (diff)
merged with master
-rw-r--r--eslint.config.mjs5
-rw-r--r--package-lock.json768
-rw-r--r--package.json18
-rw-r--r--src/client/apis/gpt/GPT.ts45
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/util/CurrentUserUtils.ts96
-rw-r--r--src/client/util/DragManager.ts5
-rw-r--r--src/client/views/DocumentButtonBar.scss9
-rw-r--r--src/client/views/DocumentButtonBar.tsx54
-rw-r--r--src/client/views/DocumentDecorations.scss7
-rw-r--r--src/client/views/DocumentDecorations.tsx24
-rw-r--r--src/client/views/FilterPanel.scss92
-rw-r--r--src/client/views/FilterPanel.tsx229
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/PropertiesView.tsx4
-rw-r--r--src/client/views/StyleProvider.tsx7
-rw-r--r--src/client/views/TagsView.tsx163
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss40
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx531
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx1
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss15
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx232
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx3
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx1
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx3
-rw-r--r--src/client/views/collections/CollectionSubView.tsx6
-rw-r--r--src/client/views/collections/TabDocView.tsx175
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx4
-rw-r--r--src/client/views/global/globalScripts.ts191
-rw-r--r--src/client/views/nodes/DocumentView.tsx5
-rw-r--r--src/client/views/nodes/FieldView.tsx4
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx6
-rw-r--r--src/client/views/nodes/IconTagBox.scss26
-rw-r--r--src/client/views/nodes/IconTagBox.tsx95
-rw-r--r--src/client/views/nodes/ImageBox.tsx3
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx3
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts4
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss92
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx375
-rw-r--r--src/client/views/search/FaceRecognitionHandler.tsx9
-rw-r--r--src/fields/Doc.ts8
43 files changed, 2286 insertions, 1088 deletions
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 619966f20..aebdc20d0 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -36,6 +36,7 @@ export default [
'no-multi-assign': 'off',
'no-underscore-dangle': 'off',
'no-nested-ternary': 'off',
+ 'no-param-reassign': 'error',
'lines-between-class-members': 'off',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'warn',
@@ -47,9 +48,9 @@ export default [
'no-return-assign': 'error',
'no-await-in-loop': 'error',
'no-loop-func': 'error',
- 'no-cond-assign': 'error',
+ '@typescript-eslint/no-cond-assign': 'error',
'no-use-before-define': 'error',
- 'no-explicit-any': 'error',
+ '@typescript-eslint/no-explicit-any': 'error',
'no-restricted-globals': ['error', 'event'],
},
},
diff --git a/package-lock.json b/package-lock.json
index 51cb32764..a821ff776 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,7 +51,7 @@
"@types/reveal": "^4.2.0",
"@types/supercluster": "^7.1.3",
"@types/textfit": "^2.4.4",
- "@types/web": "^0.0.162",
+ "@types/web": "^0.0.163",
"@types/webpack-hot-middleware": "^2.25.9",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"adm-zip": "^0.5.10",
@@ -94,7 +94,7 @@
"D": "^1.0.0",
"d3": "^7.8.5",
"depcheck": "^1.4.7",
- "dotenv": "^16.3.1",
+ "dotenv": "^16.4.5",
"eslint-webpack-plugin": "^4.1.0",
"exif": "^0.6.0",
"exifr": "^7.1.3",
@@ -109,7 +109,7 @@
"file-saver": "^2.0.5",
"find-in-files": "^0.5.0",
"fit-curve": "^0.2.0",
- "flexlayout-react": "^0.7.15",
+ "flexlayout-react": "^0.8.0",
"fluent-ffmpeg": "^2.1.2",
"forever-agent": "^0.6.1",
"fork-ts-checker-webpack-plugin": "^9.0.2",
@@ -134,6 +134,7 @@
"image-data-uri": "^2.0.1",
"image-size": "^1.0.2",
"image-size-stream": "^1.1.0",
+ "install": "^0.13.0",
"is-plain-obj": "^4.1.0",
"jimp": "^1.0.4",
"jpeg-autorotate": "^9.0.0",
@@ -158,7 +159,7 @@
"node-stream-zip": "^1.15.0",
"nodemailer": "^6.9.7",
"nodemon": "^3.0.2",
- "npm": "^10.2.5",
+ "npm": "^10.8.1",
"openai": "^4.26.0",
"p-limit": "^6.1.0",
"passport": "^0.7.0",
@@ -192,6 +193,7 @@
"react-compound-slider": "^3.4.0",
"react-datepicker": "^7.3.0",
"react-dom": "^18.2.0",
+ "react-draggable": "^4.4.6",
"react-grid-layout": "^1.4.4",
"react-icons": "^5.0.1",
"react-jsx-parser": "^2.0.0",
@@ -252,7 +254,7 @@
"xregexp": "^5.1.1"
},
"devDependencies": {
- "@eslint/js": "^9.9.1",
+ "@eslint/js": "^9.10.0",
"@types/adm-zip": "^0.5.5",
"@types/animejs": "^3.1.12",
"@types/archiver": "^6.0.2",
@@ -301,8 +303,8 @@
"@types/youtube": "^0.1.0",
"chai": "^5.0.0",
"cross-env": "^7.0.3",
- "eslint": "^9.9.1",
- "eslint-plugin-react": "^7.35.0",
+ "eslint": "^9.10.0",
+ "eslint-plugin-react": "^7.36.1",
"globals": "^15.9.0",
"jsdom": "^25.0.0",
"mocha": "^10.2.0",
@@ -312,7 +314,7 @@
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
- "typescript-eslint": "^8.3.0",
+ "typescript-eslint": "^8.6.0",
"webpack-dev-server": "^5.0.4"
},
"engines": {
@@ -452,9 +454,9 @@
}
},
"node_modules/@azure/core-auth": {
- "version": "1.7.2",
- "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz",
- "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.8.0.tgz",
+ "integrity": "sha512-YvFMowkXzLbXNM11yZtVLhUCmuG0ex7JKOH366ipjmHBhL3vpDcPAeWF+jf0X+jVXwFqo3UhsWUq4kH0ZPdu/g==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.1.0",
@@ -564,12 +566,12 @@
}
},
"node_modules/@azure/core-rest-pipeline": {
- "version": "1.16.3",
- "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.3.tgz",
- "integrity": "sha512-VxLk4AHLyqcHsfKe4MZ6IQ+D+ShuByy+RfStKfSjxJoL3WBWq17VNmrz8aT8etKzqc2nAeIyLxScjpzsS4fz8w==",
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.17.0.tgz",
+ "integrity": "sha512-62Vv8nC+uPId3j86XJ0WI+sBf0jlqTqPUFCBNrGtlaUeQUIXWV/D8GE5A1d+Qx8H7OQojn2WguC8kChD6v0shA==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
- "@azure/core-auth": "^1.4.0",
+ "@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.9.0",
"@azure/logger": "^1.0.0",
@@ -604,9 +606,9 @@
}
},
"node_modules/@azure/core-util": {
- "version": "1.9.2",
- "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.2.tgz",
- "integrity": "sha512-l1Qrqhi4x1aekkV+OlcqsJa4AnAkj5p0JV8omgwjaV9OAbP41lvrMvs+CptfetKkeEaGRGSzby7sjPZEX7+kkQ==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.10.0.tgz",
+ "integrity": "sha512-dqLWQsh9Nro1YQU+405POVtXnwrIVqPyfUzc4zXCbThTg7+vNNaiMkwbX9AMXKyoFYFClxmB3s25ZFr3+jZkww==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"tslib": "^2.6.2"
@@ -2591,9 +2593,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.11.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz",
- "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==",
+ "version": "4.11.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+ "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
@@ -2704,6 +2706,7 @@
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
+ "license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
@@ -2720,6 +2723,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
+ "license": "Apache-2.0",
"dependencies": {
"levn": "^0.4.1"
},
@@ -2755,29 +2759,29 @@
}
},
"node_modules/@floating-ui/core": {
- "version": "1.6.7",
- "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz",
- "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==",
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+ "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
"dependencies": {
- "@floating-ui/utils": "^0.2.7"
+ "@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/dom": {
- "version": "1.6.10",
- "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz",
- "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==",
+ "version": "1.6.11",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
+ "integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
"dependencies": {
"@floating-ui/core": "^1.6.0",
- "@floating-ui/utils": "^0.2.7"
+ "@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/react": {
- "version": "0.26.23",
- "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.23.tgz",
- "integrity": "sha512-9u3i62fV0CFF3nIegiWiRDwOs7OW/KhSUJDNx2MkQM3LbE5zQOY01sL3nelcVBXvX7Ovvo3A49I8ql+20Wg/Hw==",
+ "version": "0.26.24",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.24.tgz",
+ "integrity": "sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==",
"dependencies": {
- "@floating-ui/react-dom": "^2.1.1",
- "@floating-ui/utils": "^0.2.7",
+ "@floating-ui/react-dom": "^2.1.2",
+ "@floating-ui/utils": "^0.2.8",
"tabbable": "^6.0.0"
},
"peerDependencies": {
@@ -2786,9 +2790,9 @@
}
},
"node_modules/@floating-ui/react-dom": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
- "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
@@ -2798,9 +2802,9 @@
}
},
"node_modules/@floating-ui/utils": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz",
- "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA=="
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+ "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.0.0",
@@ -3011,9 +3015,9 @@
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="
},
"node_modules/@iconify/utils": {
- "version": "2.1.32",
- "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.32.tgz",
- "integrity": "sha512-LeifFZPPKu28O3AEDpYJNdEbvS4/ojAPyIW+pF/vUpJTYnbTiXUHkCh0bwgFRzKvdpb8H4Fbfd/742++MF4fPQ==",
+ "version": "2.1.33",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.33.tgz",
+ "integrity": "sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==",
"dependencies": {
"@antfu/install-pkg": "^0.4.0",
"@antfu/utils": "^0.7.10",
@@ -3873,6 +3877,45 @@
"langium": "3.0.0"
}
},
+ "node_modules/@module-federation/runtime": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.5.1.tgz",
+ "integrity": "sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@module-federation/sdk": "0.5.1"
+ }
+ },
+ "node_modules/@module-federation/runtime-tools": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.5.1.tgz",
+ "integrity": "sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@module-federation/runtime": "0.5.1",
+ "@module-federation/webpack-bundler-runtime": "0.5.1"
+ }
+ },
+ "node_modules/@module-federation/sdk": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.5.1.tgz",
+ "integrity": "sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==",
+ "optional": true,
+ "peer": true
+ },
+ "node_modules/@module-federation/webpack-bundler-runtime": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.5.1.tgz",
+ "integrity": "sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@module-federation/runtime": "0.5.1",
+ "@module-federation/sdk": "0.5.1"
+ }
+ },
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
@@ -7189,6 +7232,71 @@
"unicode-trie": "^0.3.0"
}
},
+ "node_modules/@rspack/binding": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.0.0.tgz",
+ "integrity": "sha512-eLyqSEM1h/exJYn98k+9MRktP8AYDB13x5oVn8hoxVucuhk0TubFqQSX8h9SQcZp1O3j/Z8eWWwOaNPe3JU40Q==",
+ "optional": true,
+ "peer": true,
+ "optionalDependencies": {
+ "@rspack/binding-darwin-arm64": "1.0.0",
+ "@rspack/binding-darwin-x64": "1.0.0",
+ "@rspack/binding-linux-arm64-gnu": "1.0.0",
+ "@rspack/binding-linux-arm64-musl": "1.0.0",
+ "@rspack/binding-linux-x64-gnu": "1.0.0",
+ "@rspack/binding-linux-x64-musl": "1.0.0",
+ "@rspack/binding-win32-arm64-msvc": "1.0.0",
+ "@rspack/binding-win32-ia32-msvc": "1.0.0",
+ "@rspack/binding-win32-x64-msvc": "1.0.0"
+ }
+ },
+ "node_modules/@rspack/binding-darwin-x64": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz",
+ "integrity": "sha512-qhTXm9wUhv2lBjsqqfCu59RchH1/2jursdPAmTqGc7zMReZdZvtJs2Ri6Ma1M48BLLu+7fS4fbL8Rw1g78TOOQ==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true
+ },
+ "node_modules/@rspack/core": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.0.0.tgz",
+ "integrity": "sha512-F4RA9uOLLvD1oTKa96Gcly+Sro1qaqPNENadFyiPwepa7DrwexQa/ym6CQKbvKMOYGKlVSFDPUmgFAirz35ETg==",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@module-federation/runtime-tools": "0.5.1",
+ "@rspack/binding": "1.0.0",
+ "@rspack/lite-tapable": "1.0.0",
+ "caniuse-lite": "^1.0.30001616"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.1"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rspack/lite-tapable": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.0.tgz",
+ "integrity": "sha512-7MZf4lburSUZoEenwazwUDKHhqyfnLCGnQ/tKcUtztfmVzfjZfRn/EaiT0AKkYGnL2U8AGsw89oUeVyvaOLVCw==",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@@ -9581,9 +9689,9 @@
"dev": true
},
"node_modules/@types/mocha": {
- "version": "10.0.7",
- "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.7.tgz",
- "integrity": "sha512-GN8yJ1mNTcFcah/wKEFIJckJx9iJLoMSzWcfRRuxz/Jk+U6KQNnml+etbtxFK8lPjzOw3zp4Ha/kjSst9fsHYw==",
+ "version": "10.0.8",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.8.tgz",
+ "integrity": "sha512-HfMcUmy9hTMJh66VNcmeC9iVErIZJli2bszuXc6julh5YGuRb/W5OnkHjwLNYdFlMis0sY3If5SEAp+PktdJjw==",
"dev": true
},
"node_modules/@types/ms": {
@@ -9592,9 +9700,9 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
- "version": "22.5.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
- "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
+ "version": "22.5.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz",
+ "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==",
"dependencies": {
"undici-types": "~6.19.2"
}
@@ -9618,9 +9726,9 @@
}
},
"node_modules/@types/nodemailer": {
- "version": "6.4.15",
- "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz",
- "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==",
+ "version": "6.4.16",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz",
+ "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
@@ -9708,14 +9816,15 @@
"integrity": "sha512-+gbBHbNCVGGYw1S9lAIIvrHW47UYOhMIFUsJcMkMrzy1Jf0vulBN3XQIjPgnoOXveMuHnF3b57fXROnY/Or7eg=="
},
"node_modules/@types/prop-types": {
- "version": "15.7.12",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
- "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
+ "version": "15.7.13",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
+ "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA=="
},
"node_modules/@types/qs": {
- "version": "6.9.15",
- "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
- "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg=="
+ "version": "6.9.16",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
+ "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==",
+ "dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
@@ -9733,9 +9842,9 @@
}
},
"node_modules/@types/react": {
- "version": "18.3.5",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
- "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.7.tgz",
+ "integrity": "sha512-KUnDCJF5+AiZd8owLIeVHqmW9yM4sqmDVf2JRJiBMFkGvkoZ4/WyV2lL4zVsoinmRS/W3FeEdZLEWFRofnT2FQ==",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -9993,9 +10102,9 @@
"dev": true
},
"node_modules/@types/web": {
- "version": "0.0.162",
- "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.162.tgz",
- "integrity": "sha512-uYssYXblX0BTDdpxllMQ84uDEk0EeU+dcfo8Whyl715cpfsYk5+msRQ6ul0TJGHbCr0XftFybVQGpy1eU262Aw=="
+ "version": "0.0.163",
+ "resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.163.tgz",
+ "integrity": "sha512-5Pg2gKfulo186wFnv+YXx0luJGWQ94cCY2/Dy8lU5WAE50FdBoOK45uBbp8FceOSpLJ4UW3dmTW5tvsN9uuX7A=="
},
"node_modules/@types/webgl-ext": {
"version": "0.0.30",
@@ -10079,16 +10188,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
- "integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
+ "integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.5.0",
- "@typescript-eslint/type-utils": "8.5.0",
- "@typescript-eslint/utils": "8.5.0",
- "@typescript-eslint/visitor-keys": "8.5.0",
+ "@typescript-eslint/scope-manager": "8.6.0",
+ "@typescript-eslint/type-utils": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -10112,15 +10222,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
- "integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
+ "integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.5.0",
- "@typescript-eslint/types": "8.5.0",
- "@typescript-eslint/typescript-estree": "8.5.0",
- "@typescript-eslint/visitor-keys": "8.5.0",
+ "@typescript-eslint/scope-manager": "8.6.0",
+ "@typescript-eslint/types": "8.6.0",
+ "@typescript-eslint/typescript-estree": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0",
"debug": "^4.3.4"
},
"engines": {
@@ -10140,13 +10251,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
- "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
+ "integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.5.0",
- "@typescript-eslint/visitor-keys": "8.5.0"
+ "@typescript-eslint/types": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10157,13 +10269,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
- "integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
+ "integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.5.0",
- "@typescript-eslint/utils": "8.5.0",
+ "@typescript-eslint/typescript-estree": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -10181,10 +10294,11 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
- "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
+ "integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -10194,13 +10308,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
- "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
+ "integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "8.5.0",
- "@typescript-eslint/visitor-keys": "8.5.0",
+ "@typescript-eslint/types": "8.6.0",
+ "@typescript-eslint/visitor-keys": "8.6.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -10226,6 +10341,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@@ -10241,6 +10357,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
+ "license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -10249,15 +10366,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
- "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
+ "integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.5.0",
- "@typescript-eslint/types": "8.5.0",
- "@typescript-eslint/typescript-estree": "8.5.0"
+ "@typescript-eslint/scope-manager": "8.6.0",
+ "@typescript-eslint/types": "8.6.0",
+ "@typescript-eslint/typescript-estree": "8.6.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -10271,12 +10389,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
- "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
+ "integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.5.0",
+ "@typescript-eslint/types": "8.6.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -10292,6 +10411,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
+ "license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
@@ -10305,55 +10425,55 @@
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.4.tgz",
- "integrity": "sha512-oNwn+BAt3n9dK9uAYvI+XGlutwuTq/wfj4xCBaZCqwwVIGtD7D6ViihEbyYZrDHIHTDE3Q6oL3/hqmAyFEy9DQ==",
+ "version": "3.5.6",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.6.tgz",
+ "integrity": "sha512-r+gNu6K4lrvaQLQGmf+1gc41p3FO2OUJyWmNqaIITaJU6YFiV5PtQSFZt8jfztYyARwqhoCayjprC7KMvT3nRA==",
"dependencies": {
"@babel/parser": "^7.25.3",
- "@vue/shared": "3.5.4",
+ "@vue/shared": "3.5.6",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.4.tgz",
- "integrity": "sha512-yP9RRs4BDLOLfldn6ah+AGCNovGjMbL9uHvhDHf5wan4dAHLnFGOkqtfE7PPe4HTXIqE7l/NILdYw53bo1C8jw==",
+ "version": "3.5.6",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.6.tgz",
+ "integrity": "sha512-xRXqxDrIqK8v8sSScpistyYH0qYqxakpsIvqMD2e5sV/PXQ1mTwtXp4k42yHK06KXxKSmitop9e45Ui/3BrTEw==",
"dependencies": {
- "@vue/compiler-core": "3.5.4",
- "@vue/shared": "3.5.4"
+ "@vue/compiler-core": "3.5.6",
+ "@vue/shared": "3.5.6"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.4.tgz",
- "integrity": "sha512-P+yiPhL+NYH7m0ZgCq7AQR2q7OIE+mpAEgtkqEeH9oHSdIRvUO+4X6MPvblJIWcoe4YC5a2Gdf/RsoyP8FFiPQ==",
+ "version": "3.5.6",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.6.tgz",
+ "integrity": "sha512-pjWJ8Kj9TDHlbF5LywjVso+BIxCY5wVOLhkEXRhuCHDxPFIeX1zaFefKs8RYoHvkSMqRWt93a0f2gNJVJixHwg==",
"dependencies": {
"@babel/parser": "^7.25.3",
- "@vue/compiler-core": "3.5.4",
- "@vue/compiler-dom": "3.5.4",
- "@vue/compiler-ssr": "3.5.4",
- "@vue/shared": "3.5.4",
+ "@vue/compiler-core": "3.5.6",
+ "@vue/compiler-dom": "3.5.6",
+ "@vue/compiler-ssr": "3.5.6",
+ "@vue/shared": "3.5.6",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.11",
- "postcss": "^8.4.44",
+ "postcss": "^8.4.47",
"source-map-js": "^1.2.0"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.4.tgz",
- "integrity": "sha512-acESdTXsxPnYr2C4Blv0ggx5zIFMgOzZmYU2UgvIff9POdRGbRNBHRyzHAnizcItvpgerSKQbllUc9USp3V7eg==",
+ "version": "3.5.6",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.6.tgz",
+ "integrity": "sha512-VpWbaZrEOCqnmqjE83xdwegtr5qO/2OPUC6veWgvNqTJ3bYysz6vY3VqMuOijubuUYPRpG3OOKIh9TD0Stxb9A==",
"dependencies": {
- "@vue/compiler-dom": "3.5.4",
- "@vue/shared": "3.5.4"
+ "@vue/compiler-dom": "3.5.6",
+ "@vue/shared": "3.5.6"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.4.tgz",
- "integrity": "sha512-L2MCDD8l7yC62Te5UUyPVpmexhL9ipVnYRw9CsWfm/BGRL5FwDX4a25bcJ/OJSD3+Hx+k/a8LDKcG2AFdJV3BA=="
+ "version": "3.5.6",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.6.tgz",
+ "integrity": "sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA=="
},
"node_modules/@webassemblyjs/ast": {
"version": "1.12.1",
@@ -11170,9 +11290,9 @@
}
},
"node_modules/babel-loader": {
- "version": "9.1.3",
- "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz",
- "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==",
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz",
+ "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==",
"dependencies": {
"find-cache-dir": "^4.0.0",
"schema-utils": "^4.0.0"
@@ -15496,14 +15616,6 @@
"node": ">=6"
}
},
- "node_modules/clone-deep/node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -16975,6 +17087,17 @@
"node": ">=0.10.0"
}
},
+ "node_modules/deep-rename-keys/node_modules/kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
+ "dependencies": {
+ "is-buffer": "^1.1.5"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -17392,9 +17515,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
- "version": "1.5.19",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.19.tgz",
- "integrity": "sha512-kpLJJi3zxTR1U828P+LIUDZ5ohixyo68/IcYOHLqnbTPr/wdgn4i1ECvmALN9E16JPA6cvCG5UG79gVwVdEK5w=="
+ "version": "1.5.24",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.24.tgz",
+ "integrity": "sha512-0x0wLCmpdKFCi9ulhvYZebgcPmHTkFVUfU2wzDykadkslKwT4oAmDTHEKLnlrDsMGZe4B+ksn8quZfZjYsBetA=="
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -17521,9 +17644,9 @@
}
},
"node_modules/envinfo": {
- "version": "7.13.0",
- "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
- "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
+ "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"bin": {
"envinfo": "dist/cli.js"
},
@@ -17733,6 +17856,7 @@
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
+ "license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
@@ -17788,10 +17912,11 @@
}
},
"node_modules/eslint-plugin-react": {
- "version": "7.35.2",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz",
- "integrity": "sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ==",
+ "version": "7.36.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
+ "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
@@ -18151,9 +18276,9 @@
}
},
"node_modules/express": {
- "version": "4.20.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.20.0.tgz",
- "integrity": "sha512-pLdae7I6QqShF5PnNTCVn4hI91Dx0Grkn2+IAsMTgMIKuQVte2dN9PeGSSAME2FR8anOhVA62QDIUaWVfEXVLw==",
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
+ "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -18167,7 +18292,7 @@
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
- "finalhandler": "1.2.0",
+ "finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
@@ -18176,11 +18301,11 @@
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7",
- "qs": "6.11.0",
+ "qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
- "serve-static": "1.16.0",
+ "serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
@@ -18279,20 +18404,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
- "node_modules/express/node_modules/qs": {
- "version": "6.11.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
- "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
- "dependencies": {
- "side-channel": "^1.0.4"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -18356,6 +18467,7 @@
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -18372,6 +18484,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
+ "license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -18557,12 +18670,12 @@
}
},
"node_modules/finalhandler": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
- "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"dependencies": {
"debug": "2.6.9",
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
@@ -18581,14 +18694,6 @@
"ms": "2.0.0"
}
},
- "node_modules/finalhandler/node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -18691,9 +18796,9 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
},
"node_modules/flexlayout-react": {
- "version": "0.7.15",
- "resolved": "https://registry.npmjs.org/flexlayout-react/-/flexlayout-react-0.7.15.tgz",
- "integrity": "sha512-ydTMdEoQO5BniylxVkSxa59rEY0+96lqqRII+QK+yq6028eHywPuxZawt4g45y5pMb9ptP4N9HPAQXAFsxwowQ==",
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/flexlayout-react/-/flexlayout-react-0.8.0.tgz",
+ "integrity": "sha512-nTkEOxVw438/6ugzKb0nBFRzJkWPYOgx8laGhDmTsXMuJQMEEC4dxyRSswcjt8IWsCKl8zSQHlsxSG+PrdJENQ==",
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
@@ -19572,6 +19677,7 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
"integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=18"
},
@@ -19725,7 +19831,8 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/grid-index": {
"version": "1.1.0",
@@ -19871,9 +19978,9 @@
}
},
"node_modules/hast-util-from-html": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.2.tgz",
- "integrity": "sha512-HwOHwxdt2zC5KQ/CNoybBntRook2zJvfZE/u5/Ap7aLPe22bDqen7KwGkOqOyzL5zIqKwiYX/OTtE0FWgr6XXA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz",
+ "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.1.0",
@@ -20851,9 +20958,9 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/inline-style-parser": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.3.tgz",
- "integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g=="
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
+ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="
},
"node_modules/inspect-function": {
"version": "0.3.4",
@@ -20973,6 +21080,14 @@
"inspect-function": "^0.3.1"
}
},
+ "node_modules/install": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
+ "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/internal-slot": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
@@ -22267,12 +22382,9 @@
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
},
"node_modules/kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==",
- "dependencies": {
- "is-buffer": "^1.1.5"
- },
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"engines": {
"node": ">=0.10.0"
}
@@ -23099,9 +23211,9 @@
}
},
"node_modules/mdast-util-mdx-expression": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz",
- "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
"dependencies": {
"@types/estree-jsx": "^1.0.0",
"@types/hast": "^3.0.0",
@@ -23279,14 +23391,15 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/mermaid": {
- "version": "11.2.0",
- "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.2.0.tgz",
- "integrity": "sha512-ZinOa063lk81lujX8vkINNqmFaNMk1A95Z4kCL7fE6QLAi01CxeiUJVw+tpXU+lAM73utO39G+2PLjxS2GYS/w==",
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.2.1.tgz",
+ "integrity": "sha512-F8TEaLVVyxTUmvKswVFyOkjPrlJA5h5vNR1f7ZnSWSpqxgEZG1hggtn/QCa7znC28bhlcrNh10qYaIiill7q4A==",
"dependencies": {
"@braintree/sanitize-url": "^7.0.1",
"@iconify/utils": "^2.1.32",
@@ -24215,9 +24328,9 @@
}
},
"node_modules/mongodb": {
- "version": "6.8.1",
- "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.1.tgz",
- "integrity": "sha512-qsS+gl5EJb+VzJqUjXSZ5Y5rbuM/GZlZUEJ2OIVYP10L9rO9DQ0DGp+ceTzsmoADh6QYMWd9MSdG9IxRyYUkEA==",
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.9.0.tgz",
+ "integrity": "sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0",
@@ -24292,9 +24405,9 @@
}
},
"node_modules/mongoose": {
- "version": "8.6.1",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.1.tgz",
- "integrity": "sha512-dppGcYqvsdg+VcnqXR5b467V4a+iNhmvkfYNpEPi6AjaUxnz6ioEDmrMLOi+sOWjvoHapuwPOigV4f2l7HC6ag==",
+ "version": "8.6.3",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.3.tgz",
+ "integrity": "sha512-++yRmm7hjMbqVA/8WeiygTnEfrFbiy+OBjQi49GFJIvCQuSYE56myyQWo4j5hbpcHjhHQU8NukMNGTwAWFWjIw==",
"dependencies": {
"bson": "^6.7.0",
"kareem": "2.6.3",
@@ -24629,9 +24742,9 @@
}
},
"node_modules/nodemon": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",
- "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.5.tgz",
+ "integrity": "sha512-V5UtfYc7hjFD4SI3EzD5TR8ChAHEZ+Ns7Z5fBk8fAbTVAj+q3G+w7sHJrHxXBkVn6ApLVTljau8wfHwqmGUjMw==",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
@@ -27326,19 +27439,17 @@
}
},
"node_modules/openai": {
- "version": "4.58.2",
- "resolved": "https://registry.npmjs.org/openai/-/openai-4.58.2.tgz",
- "integrity": "sha512-hIalypYELt7/PcryFpz4Gi1z/8ZDzukWyOhr+jKM6L/GVE+t4NseaENXKt+OxnkkIm/1R2EkdGxgnHrZ0kB5bQ==",
+ "version": "4.62.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.62.0.tgz",
+ "integrity": "sha512-cPSsarEXoJENNwYMx/Xh/wuvnyYf8lPSR4zDVSnRvbcMHmKkDIzXhUVvPPfuI4M4T83x25gVnlW7huWEGKG+SA==",
"dependencies": {
"@types/node": "^18.11.18",
"@types/node-fetch": "^2.6.4",
- "@types/qs": "^6.9.15",
"abort-controller": "^3.0.0",
"agentkeepalive": "^4.2.1",
"form-data-encoder": "1.7.2",
"formdata-node": "^4.3.2",
- "node-fetch": "^2.6.7",
- "qs": "^6.10.3"
+ "node-fetch": "^2.6.7"
},
"bin": {
"openai": "bin/cli"
@@ -28104,9 +28215,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.45",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
- "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
+ "version": "8.4.47",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+ "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [
{
"type": "opencollective",
@@ -28123,8 +28234,8 @@
],
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.1",
- "source-map-js": "^1.2.0"
+ "picocolors": "^1.1.0",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -29053,15 +29164,15 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
},
"node_modules/react-jsx-parser": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/react-jsx-parser/-/react-jsx-parser-2.0.0.tgz",
- "integrity": "sha512-1siP+vM47gWE+x2VTF0ano5jJPRilOf7sRIoy4dzb1FmNoDpwe3htuzgcEfvzsc7YThzwmMQNUfy4vAv1ATs2g==",
- "hasInstallScript": true,
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/react-jsx-parser/-/react-jsx-parser-2.1.0.tgz",
+ "integrity": "sha512-cGp8ceqA6j7OylsqeK2kuCGRitHpWzximsxsQyqkQO+1fwyG82Mb1l2nx9ImrfdCO6GT2kgt7C6cX9vJ46B7ow==",
"dependencies": {
"acorn": "^8.12.1",
- "acorn-jsx": "^5.3.2",
- "browserslist": "^4.23.1",
- "core-js": "^3.37.1"
+ "acorn-jsx": "^5.3.2"
+ },
+ "engines": {
+ "bun": "^1.1.27"
},
"optionalDependencies": {
"@types/react": "^18.3.3",
@@ -29501,9 +29612,9 @@
"integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="
},
"node_modules/regenerate-unicode-properties": {
- "version": "10.1.1",
- "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz",
- "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==",
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
+ "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
"dependencies": {
"regenerate": "^1.4.2"
},
@@ -30248,11 +30359,11 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
- "version": "1.78.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz",
- "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==",
+ "version": "1.79.1",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.1.tgz",
+ "integrity": "sha512-+mA7svoNKeL0DiJqZGeR/ZGUu8he4I8o3jyUcOFyo4eBJrwNgIMmAEwCMo/N2Y3wdjOBcRzoNxZIOtrtMX8EXg==",
"dependencies": {
- "chokidar": ">=3.0.0 <4.0.0",
+ "chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
@@ -30302,6 +30413,32 @@
}
}
},
+ "node_modules/sass/node_modules/chokidar": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.0.tgz",
+ "integrity": "sha512-mxIojEAQcuEvT/lyXq+jf/3cO/KoA6z4CeNDGGevTybECPOMFCnQy3OPahluUkbqgPNGw5Bi78UC7Po6Lhy+NA==",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/sass/node_modules/readdirp": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz",
+ "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==",
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
@@ -30590,69 +30727,14 @@
}
},
"node_modules/serve-static": {
- "version": "1.16.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.0.tgz",
- "integrity": "sha512-pDLK8zwl2eKaYrs8mrPZBJua4hMplRWJ1tIFksVC3FtBEBnl8dxgeHtsaMS8DhS9i4fLObaon6ABoc4/hQGdPA==",
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"dependencies": {
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
- "send": "0.18.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/serve-static/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/serve-static/node_modules/debug/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
- },
- "node_modules/serve-static/node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/serve-static/node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/serve-static/node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~1.0.2",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "0.5.2",
- "http-errors": "2.0.0",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "2.0.1"
+ "send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -30728,14 +30810,6 @@
"node": ">=8"
}
},
- "node_modules/shallow-clone/node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
@@ -30950,11 +31024,11 @@
}
},
"node_modules/simple-xml-to-json": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.2.tgz",
- "integrity": "sha512-bmJJf5YiYL60eOQk3gaVxbM6vgYuwrFydCEAA2x3jccHUTsAffiPyblS/yQGr8GDUQVxSDm3WwLNL5HmRqDUcg==",
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.3.tgz",
+ "integrity": "sha512-kWJDCr9EWtZ+/EYYM5MareWj2cRnZGF93YDNpH4jQiHB+hBIZnfPFSQiVMzZOdk+zXWqTZ/9fTeQNu2DqeiudA==",
"engines": {
- "node": ">=14.20.0"
+ "node": ">=20.12.2"
}
},
"node_modules/simplify-js": {
@@ -31360,9 +31434,9 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/streamx": {
- "version": "2.20.0",
- "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.0.tgz",
- "integrity": "sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==",
+ "version": "2.20.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
+ "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
"dependencies": {
"fast-fifo": "^1.3.2",
"queue-tick": "^1.0.1",
@@ -31676,11 +31750,11 @@
}
},
"node_modules/style-to-object": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.7.tgz",
- "integrity": "sha512-uSjr59G5u6fbxUfKbb8GcqMGT3Xs9v5IbPkjb0S16GyOeBLAzSRK0CixBv5YrYvzO6TDLzIS6QCn78tkqWngPw==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz",
+ "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==",
"dependencies": {
- "inline-style-parser": "0.2.3"
+ "inline-style-parser": "0.2.4"
}
},
"node_modules/styled-components": {
@@ -31883,9 +31957,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/terser": {
- "version": "5.32.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.32.0.tgz",
- "integrity": "sha512-v3Gtw3IzpBJ0ugkxEX8U0W6+TnPKRRCWGh1jC/iM/e3Ki5+qvO1L1EAZ56bZasc64aXHwRHNIQEzm6//i5cemQ==",
+ "version": "5.33.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.33.0.tgz",
+ "integrity": "sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g==",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -31990,9 +32064,9 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/text-decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz",
- "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz",
+ "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==",
"dependencies": {
"b4a": "^1.6.4"
}
@@ -32055,20 +32129,20 @@
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
},
"node_modules/tldts": {
- "version": "6.1.44",
- "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.44.tgz",
- "integrity": "sha512-Id5Kq3vy4fAHgrMasofrYZ4ghMa9rX64BfstBFgnGN+CAPdXYeZ0xWA2BXk3F+VgS+/jJ9PdYEQrP8LEZvGrCw==",
+ "version": "6.1.47",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.47.tgz",
+ "integrity": "sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==",
"dependencies": {
- "tldts-core": "^6.1.44"
+ "tldts-core": "^6.1.47"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
- "version": "6.1.44",
- "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.44.tgz",
- "integrity": "sha512-DMOTcn8pGmuY65zDPH+M1xAOVtBj7Phrah0HxuspKEu33hOArCDmk3R4UEVymtuN+HveeUlVgX0+RNf4mFkoAw=="
+ "version": "6.1.47",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.47.tgz",
+ "integrity": "sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA=="
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
@@ -32240,6 +32314,7 @@
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
"integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=16"
},
@@ -32938,14 +33013,15 @@
"integrity": "sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ=="
},
"node_modules/typescript-eslint": {
- "version": "8.5.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.5.0.tgz",
- "integrity": "sha512-uD+XxEoSIvqtm4KE97etm32Tn5MfaZWgWfMMREStLxR6JzvHkc2Tkj7zhTEK5XmtpTmKHNnG8Sot6qDfhHtR1Q==",
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.6.0.tgz",
+ "integrity": "sha512-eEhhlxCEpCd4helh3AO1hk0UP2MvbRi9CtIAJTVPQjuSXOOO2jsEacNi4UdcJzZJbeuVg1gMhtZ8UYb+NFYPrA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.5.0",
- "@typescript-eslint/parser": "8.5.0",
- "@typescript-eslint/utils": "8.5.0"
+ "@typescript-eslint/eslint-plugin": "8.6.0",
+ "@typescript-eslint/parser": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -33041,9 +33117,9 @@
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
},
"node_modules/unicode-canonical-property-names-ecmascript": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
- "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
"engines": {
"node": ">=4"
}
@@ -33061,9 +33137,9 @@
}
},
"node_modules/unicode-match-property-value-ecmascript": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz",
- "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
"engines": {
"node": ">=4"
}
@@ -33854,9 +33930,9 @@
}
},
"node_modules/webpack-dev-middleware/node_modules/memfs": {
- "version": "4.11.1",
- "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.1.tgz",
- "integrity": "sha512-LZcMTBAgqUUKNXZagcZxvXXfgF1bHX7Y7nQ0QyEiNbRJgE29GhgPd8Yna1VQcLlPiHt/5RFJMWYN9Uv/VPNvjQ==",
+ "version": "4.11.2",
+ "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.11.2.tgz",
+ "integrity": "sha512-VcR7lEtgQgv7AxGkrNNeUAimFLT+Ov8uGu1LuOfbe/iF/dKoh/QgpoaMZlhfejvLtMxtXYyeoT7Ar1jEbWdbPA==",
"dependencies": {
"@jsonjoy.com/json-pack": "^1.0.3",
"@jsonjoy.com/util": "^1.3.0",
diff --git a/package.json b/package.json
index 9afe52152..51f28abca 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
"tsc": "tsc -t es5"
},
"devDependencies": {
- "@eslint/js": "^9.9.1",
+ "@eslint/js": "^9.10.0",
"@types/adm-zip": "^0.5.5",
"@types/animejs": "^3.1.12",
"@types/archiver": "^6.0.2",
@@ -72,8 +72,8 @@
"@types/youtube": "^0.1.0",
"chai": "^5.0.0",
"cross-env": "^7.0.3",
- "eslint": "^9.9.1",
- "eslint-plugin-react": "^7.35.0",
+ "eslint": "^9.10.0",
+ "eslint-plugin-react": "^7.36.1",
"globals": "^15.9.0",
"jsdom": "^25.0.0",
"mocha": "^10.2.0",
@@ -83,7 +83,7 @@
"ts-loader": "^9.5.1",
"ts-node": "^10.9.1",
"ts-node-dev": "^2.0.0",
- "typescript-eslint": "^8.3.0",
+ "typescript-eslint": "^8.6.0",
"webpack-dev-server": "^5.0.4"
},
"dependencies": {
@@ -130,7 +130,7 @@
"@types/reveal": "^4.2.0",
"@types/supercluster": "^7.1.3",
"@types/textfit": "^2.4.4",
- "@types/web": "^0.0.162",
+ "@types/web": "^0.0.163",
"@types/webpack-hot-middleware": "^2.25.9",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"adm-zip": "^0.5.10",
@@ -173,7 +173,7 @@
"D": "^1.0.0",
"d3": "^7.8.5",
"depcheck": "^1.4.7",
- "dotenv": "^16.3.1",
+ "dotenv": "^16.4.5",
"eslint-webpack-plugin": "^4.1.0",
"exif": "^0.6.0",
"exifr": "^7.1.3",
@@ -188,7 +188,7 @@
"file-saver": "^2.0.5",
"find-in-files": "^0.5.0",
"fit-curve": "^0.2.0",
- "flexlayout-react": "^0.7.15",
+ "flexlayout-react": "^0.8.0",
"fluent-ffmpeg": "^2.1.2",
"forever-agent": "^0.6.1",
"fork-ts-checker-webpack-plugin": "^9.0.2",
@@ -213,6 +213,7 @@
"image-data-uri": "^2.0.1",
"image-size": "^1.0.2",
"image-size-stream": "^1.1.0",
+ "install": "^0.13.0",
"is-plain-obj": "^4.1.0",
"jimp": "^1.0.4",
"jpeg-autorotate": "^9.0.0",
@@ -237,7 +238,7 @@
"node-stream-zip": "^1.15.0",
"nodemailer": "^6.9.7",
"nodemon": "^3.0.2",
- "npm": "^10.2.5",
+ "npm": "^10.8.1",
"openai": "^4.26.0",
"p-limit": "^6.1.0",
"passport": "^0.7.0",
@@ -271,6 +272,7 @@
"react-compound-slider": "^3.4.0",
"react-datepicker": "^7.3.0",
"react-dom": "^18.2.0",
+ "react-draggable": "^4.4.6",
"react-grid-layout": "^1.4.4",
"react-icons": "^5.0.1",
"react-jsx-parser": "^2.0.0",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 20c810651..209e8e87e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,4 +1,4 @@
-import { ChatCompletionMessageParam } from 'openai/resources';
+import { ChatCompletionMessageParam, Image } from 'openai/resources';
import { openai } from './setup';
enum GPTCallType {
@@ -14,6 +14,10 @@ enum GPTCallType {
DATA = 'data',
DRAW = 'draw',
COLOR = 'color',
+ RUBRIC = 'rubric',
+ TYPE = 'type',
+ SUBSET = 'subset',
+ INFO = 'info',
}
type GPTCallOpts = {
@@ -28,6 +32,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled. Do not label each flashcard and do not include asterisks: ' },
+
completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: "You are a helpful assistant. Answer the user's prompt." },
mermaid: {
model: 'gpt-4-turbo',
@@ -44,8 +49,8 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
sort: {
model: 'gpt-4o',
maxTokens: 2048,
- temp: 0.5,
- prompt: "I'm going to give you a list of descriptions. Each one is seperated by ====== on either side. They will vary in length, so make sure to only seperate when you see ======. Sort them into lists by shared content. MAKE SURE EACH DESCRIPTOR IS IN ONLY ONE LIST. Generate only the list with each list seperated by ====== with the elements seperated by ~~~~~~. Try to do around 4 groups, but a little more or less is ok.",
+ temp: 0.25,
+ prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas",
},
describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
@@ -53,7 +58,35 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
model: 'gpt-4-turbo',
maxTokens: 1024,
temp: 0,
- prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct',
+ prompt: "BRIEFLY (<50 words) describe any differences between the rubric and the user's answer answer in second person. If there are no differences, say correct",
+ },
+
+ rubric: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "BRIEFLY (<25 words) provide a definition for the following term. It will be used as a rubric to evaluate the user's understanding of the topic",
+ },
+
+ type: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "I'm going to provide you with a question. Based on the question, is the user asking you to 1. Assigns docs with tags(like star / heart etc)/labels, 2. Filter docs, 3. Provide information about a specific doc 4. Provide a specific doc based on a question/information 5. Provide general information 6. Put cards in a specific order. Answer with only the number for 2-6. For number one, provide the number (1) and the appropriate tag",
+ },
+
+ subset: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "I'm going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and no commas",
+ },
+
+ info: {
+ model: 'gpt-4-turbo',
+ maxTokens: 1024,
+ temp: 0,
+ prompt: "Answer the user's question with a short (<100 word) response. If a particular document is selected I will provide that information (which may help with your response)",
},
draw: {
model: 'gpt-4o',
@@ -84,7 +117,7 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: a
try {
lastCall = inputText;
- const usePrompt = prompt ? opts.prompt + prompt : opts.prompt;
+ const usePrompt = prompt ? prompt + opts.prompt : opts.prompt;
const messages: ChatCompletionMessageParam[] = [
{ role: 'system', content: usePrompt },
{ role: 'user', content: inputText },
@@ -110,7 +143,7 @@ const gptImageCall = async (prompt: string, n?: number) => {
n: n ?? 1,
size: '1024x1024',
});
- return response.data.map((data: any) => data.url);
+ return response.data.map((data: Image) => data.url);
// return response.data.data[0].url;
} catch (err) {
console.error(err);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 26a937817..ba1bc8b7a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -273,6 +273,7 @@ export class DocumentOptions {
_layout_showTitle?: string; // field name to display in header (:hover is an optional suffix)
_layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
_layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
+ _layout_showTags?: BOOLt = new BoolInfo('whether to show the list of document tags at the bottom of a DocView');
_chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden');
hideClickBehaviors?: BOOLt = new BoolInfo('whether to hide click behaviors in context menu');
@@ -487,8 +488,7 @@ export class DocumentOptions {
userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
cardSort?: STRt = new StrInfo('way cards are sorted in deck view');
- cardSort_customField?: STRt = new StrInfo('field key used for sorting cards');
- cardSort_visibleSortGroups?: List<number>; // which sorting values are being filtered (shown)
+ cardSort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
}
export const DocOptions = new DocumentOptions();
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 079ab46cf..076269afc 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,7 +2,7 @@
import { reaction, runInAction } from "mobx";
import * as rp from 'request-promise';
import { ClientUtils, OmitKeys } from "../../ClientUtils";
-import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, FieldType, Opt, StrListCast } from "../../fields/Doc";
import { DocData } from "../../fields/DocSymbols";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
@@ -40,11 +40,12 @@ import { ColorScheme } from "./SettingsManager";
import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
-interface Button {
+export interface Button {
// DocumentOptions fields a button can set
title?: string;
toolTip?: string;
icon?: string;
+ isSystem?: boolean;
btnType?: ButtonType;
numBtnMin?: number;
numBtnMax?: number;
@@ -690,32 +691,32 @@ pie title Minerals in my tap water
}
static cardTools(): Button[] {
return [
- { title: "Time", icon:"hourglass-half", toolTip:"Sort by most recent document creation", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Time", icon:"hourglass-half", toolTip:"Sort by most recent document creation", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Tags", icon:"bolt", toolTip:"Sort by document's tags", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Pile", icon:"layer-group", toolTip:"View the cards as a pile in the free form view!",btnType: ButtonType.ClickButton, expertMode: false, toolType:"pile", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Chat Popup",icon:"lightbulb", toolTip:"Toggle the chat popup's visibility!", width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+
+ { title: "Sort", icon: "sort" , toolTip: "Manage sort order / lock status", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
+ subMenu: [
+ { title: "Ascending", toolTip: "Sort the cards in ascending order", btnType: ButtonType.ToggleButton, icon: "sort-up", toolType:"up", ignoreClick: true, scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+ { title: "Descending",toolTip: "Sort the cards in descending order",btnType: ButtonType.ToggleButton, icon: "sort-down",toolType:"down",ignoreClick: true, scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+ ]},
]
}
- static labelTools(): Button[] {
- return [
- { title: "AI", icon:"robot", toolTip:"Add AI labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"chat", funcs: {hidden:`showFreeform ("chat", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "AIs", icon:"AI Sort", toolTip:"Filter AI labels", subMenu: this.cardGroupTools("chat"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("chat", true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} },
- { title: "Like", icon:"heart", toolTip:"Add Like labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"like", funcs: {hidden:`showFreeform ("like", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Likes", icon:"Likes", toolTip:"Filter likes", width: 10, subMenu: this.cardGroupTools("heart"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("like", true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} },
- { title: "Star", icon:"star", toolTip:"Add Star labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"star", funcs: {hidden:`showFreeform ("star", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Stars", icon:"Stars", toolTip:"Filter stars", width: 80, subMenu: this.cardGroupTools("star"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("star", true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} },
- { title: "Idea", icon:"satellite", toolTip:"Add Idea labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"idea", funcs: {hidden:`showFreeform ("idea", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Ideas", icon:"Ideas", toolTip:"Filter ideas", width: 80, subMenu: this.cardGroupTools("satellite"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("idea", true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} },
- ]
- }
- static cardGroupTools(icon: string): Button[] {
+
+ static tagGroupTools(): Button[] {
+ const defaultTagButtonDescs = [
+ { title: "Star", isSystem: false,icon: "star", toolTip:"Click to toggle visibility of Star tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#star", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ { title: "Like", isSystem: false,icon: "heart", toolTip:"Click to toggle visibility of Like tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#like", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ { title: "Todo", isSystem: false,icon: "bolt", toolTip:"Click to toggle visibility of Todo tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#todo", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ { title: "Idea", isSystem: false,icon: "cloud", toolTip:"Click to toggle visibility of Idea tagged Docs", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"#idea", funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ ];
+ // hack: if there's no dashboard, create default filters. otherwise, just make sure that the Options button is preserved
return [
- { title: "1", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"1", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "2", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"2", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "3", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"3", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "4", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"4", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "5", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"5", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "6", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"6", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "7", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"7", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title:"Options",isSystem: true,icon: "gear", toolTip:"Click to customize list of filter buttons", btnType: ButtonType.ClickButton, expertMode: false, toolType:"-opts-",funcs: {}, scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}'}},
+ ...(Doc.UserDoc().activeDashboard ? [] : defaultTagButtonDescs)
]
}
static viewTools(): Button[] {
@@ -734,8 +735,8 @@ pie title Minerals in my tap water
{ title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'},
btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
{ title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 },
- { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}},
- { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'}},
+ { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
+ { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(this.toolType, value, _readOnly_);}'} },
{ title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -744,12 +745,12 @@ pie title Minerals in my tap water
{ title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
subMenu: [
- { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}' }},
+ { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
]},
- { title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}},
- { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}},
+ { title: "Elide", toolTip: "Elide selection", btnType: ButtonType.ToggleButton, icon: "eye", toolType:"elide", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
+ { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
// { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
// { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
@@ -821,25 +822,29 @@ pie title Minerals in my tap water
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
{ title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
- { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
- { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Card", icon: "Sort", toolTip: "Card sort", subMenu: CurrentUserUtils.cardTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Label", icon: "Label", toolTip: "Assign card labels", subMenu: CurrentUserUtils.labelTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
- { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
- { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
- { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
- { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(),expertMode: false,toolType:CollectionViewType.Schema,funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Schema is selected
- ];
+
+ { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+ { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+
+ { title: "Card", icon: "Card", toolTip: "Card View Tools", subMenu: CurrentUserUtils.cardTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ // { title: "Create", icon: "Create", toolTip: "Assign card labels", subMenu: CurrentUserUtils.labelTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
+ { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
+ { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
+ { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
+ { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected
+ { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 150},
+ ];
}
/// initializes a context menu button for the top bar context menu
static setupContextMenuButton(params:Button, btnDoc?:Doc, btnContainer?:Doc) {
const reqdOpts:DocumentOptions = {
- ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit as {[key:string]: string|undefined},
- color: Colors.WHITE, isSystem: true,
+ isSystem: true,
+ ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
+ color: Colors.WHITE,
_nativeWidth: params.width ?? 30, _width: params.width ?? 30,
_height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
toolType: params.toolType, expertMode: params.expertMode,
@@ -1008,6 +1013,7 @@ pie title Minerals in my tap water
doc.filterDocCount = 0;
doc.treeView_FreezeChildren = "remove|add";
doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
+
this.setupLinkDocs(doc, linkDatabaseId);
this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
@@ -1143,4 +1149,4 @@ ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.import
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() });
+ScriptingGlobals.add(function getSharingDoc() { return Doc.SharingDoc() }); \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 7db13689d..81ea840f1 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -29,7 +29,7 @@ import { dropActionType } from './DropActionTypes';
import { SnappingManager } from './SnappingManager';
import { UndoManager } from './UndoManager';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { contextMenuZindex } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
/**
@@ -101,7 +101,6 @@ export namespace DragManager {
// event called when the drag operation results in a drop action
export class DropEvent {
- // eslint-disable-next-line no-useless-constructor
constructor(
readonly x: number,
readonly y: number,
@@ -569,7 +568,7 @@ export namespace DragManager {
AbortDrag();
await finishDrag?.(new DragCompleteEvent(true, docDragData));
DragManager.StartWindowDrag?.(e, docDragData.droppedDocuments, aborted => {
- if (!aborted && (docDragData?.dropAction === 'move' || docDragData?.dropAction === 'same')) {
+ if (!aborted && (docDragData?.dropAction === dropActionType.move || docDragData?.dropAction === dropActionType.same)) {
docDragData.removeDocument?.(docDragData?.draggedDocuments[0]);
}
});
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 11614d627..ede277aae 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -29,6 +29,11 @@ $linkGap: 3px;
background: black;
height: 20px;
align-items: center;
+
+ .tags {
+ width: 40px;
+
+ }
}
.documentButtonBar-followTypes {
width: 20px;
@@ -153,6 +158,10 @@ $linkGap: 3px;
&:hover {
background-color: $black;
+
+ .documentButtonBar-pinTypes {
+ display: flex;
+ }
}
}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 0cfdfa9f7..e228f648c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -9,7 +9,7 @@ import * as React from 'react';
import { FaEdit } from 'react-icons/fa';
import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
import { Cast, DocCast } from '../../fields/Types';
import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
import { CalendarManager } from '../util/CalendarManager';
@@ -278,16 +278,57 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
}
@computed
- get keywordButton() {
- return !DocumentView.Selected().length ? null : (
- <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
- <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => DocumentView.Selected().map(dv => (dv.dataDoc.showTags = !dv.dataDoc.showTags))}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
+ get calendarButton() {
+ const targetDoc = this.view0?.Document;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-calendar-button">Open calendar menu</div>}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ color: 'white' }}
+ onClick={() => {
+ CalendarManager.Instance.open(this.view0, targetDoc);
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={faCalendarDays as IconLookup} />
</div>
</Tooltip>
);
}
+ /**
+ * Allows for both the keywords and the icon tags to be shown using a quasi- multitoggle
+ */
+ @computed
+ get keywordButton() {
+ const targetDoc = this.view0?.Document;
+
+ return !targetDoc ? null : (
+ <div className="documentButtonBar-icon">
+ {/* <div className="documentButtonBar-pinTypes" style={{ width: '40px' }}>
+ {metaBtn('tags', 'star')}
+ {metaBtn('keywords', 'id-card')}
+ </div> */}
+
+ <Tooltip title={<div className="dash-keyword-button">Open keyword menu</div>}>
+ <div
+ className="documentButtonBar-icon"
+ style={{ color: 'white' }}
+ onClick={undoable(e => {
+ const showing = DocumentView.Selected().some(dv => dv.showTags);
+ DocumentView.Selected().forEach(dv => {
+ dv.layoutDoc._layout_showTags = !showing;
+ if (e.shiftKey)
+ DocListCast(dv.Document[Doc.LayoutFieldKey(dv.Document) + '_annotations']).forEach(doc => {
+ if (doc.face) doc.hidden = showing;
+ });
+ });
+ }, 'show Doc tags')}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="tag" />
+ </div>
+ </Tooltip>
+ </div>
+ );
+ }
+
@observable _isRecording = false;
_stopFunc: () => void = emptyFunction;
@computed
@@ -458,6 +499,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
<div className="documentButtonBar-button">{this.pinButton}</div>
<div className="documentButtonBar-button">{this.saveAnnoButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
+ <div className="documentButtonBar-button">{this.calendarButton}</div>
<div className="documentButtonBar-button">{this.keywordButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
<div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 67e1054c3..346df10d5 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -38,6 +38,13 @@ $resizeHandler: 8px;
background: green;
border-radius: 50%;
}
+ .documentDecorations-tagsView {
+ position: absolute;
+ height: 100%;
+ pointer-events: all;
+ border-radius: 50%;
+ color: black;
+ }
}
.documentDecorations-container {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8552003eb..7f6cfbb87 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -35,6 +35,7 @@ import { DocumentView } from './nodes/DocumentView';
import { ImageBox } from './nodes/ImageBox';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { TagsView } from './TagsView';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -172,7 +173,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
!this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString);
this._editingTitle = true;
this._keyinput.current && setTimeout(this._keyinput.current.focus);
- })
+ }),
+ false // can't preventDefault since that will mess up goldenlayout if you drag over the tab bar. so just stop propagation below.
);
e.stopPropagation();
}
@@ -194,7 +196,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (containers.size > 1) return false;
const { left, top } = dragDocView.getBounds || { left: 0, top: 0 };
const dragData = new DragManager.DocumentDragData(DocumentView.SelectedDocs(), dragDocView._props.dropAction);
- dragData.offset = dragDocView.screenToContentsTransform().transformDirection(e.x - left, e.y - top);
+ dragData.offset = dragDocView.screenToViewTransform().transformDirection(e.x - left, e.y - top);
dragData.moveDocument = dragDocView._props.moveDocument;
dragData.removeDocument = dragDocView._props.removeDocument;
dragData.isDocDecorationMove = true;
@@ -236,7 +238,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) {
DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
}
- iconView._props.removeDocument?.(iconView.Document);
+ if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
+ else iconView._props.removeDocument?.(iconView.Document);
}
});
views.forEach(DocumentView.DeselectView);
@@ -342,7 +345,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => {
const selDoc = seldocview.Document;
- const newloccentern = seldocview.screenToContentsTransform().transformPoint(rotCenter[0], rotCenter[1]);
+ const newloccentern = seldocview.screenToViewTransform().transformPoint(rotCenter[0], rotCenter[1]);
const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI);
selDoc._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
@@ -637,7 +640,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
@computed get rotCenter() {
const lastView = DocumentView.Selected().lastElement();
if (lastView) {
- const invXf = lastView.screenToContentsTransform().inverse();
+ const invXf = lastView.screenToViewTransform().inverse();
const seldoc = lastView.layoutDoc;
const loccenter = Utils.rotPt(NumCast(seldoc._rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc._rotation_centerY) * NumCast(seldoc._height), invXf.Rotate);
return invXf.transformPoint(loccenter.x + NumCast(seldoc._width) / 2, loccenter.y + NumCast(seldoc._height) / 2);
@@ -648,7 +651,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
render() {
const { b, r, x, y } = this.Bounds;
const seldocview = DocumentView.Selected().lastElement();
- const doc = DocumentView.SelectedDocs().lastElement();
if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
setTimeout(
action(() => {
@@ -840,12 +842,20 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
<div
className="link-button-container"
style={{
- top: `${doc[DocData].showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ top: DocumentView.Selected().length > 1 ? 0 : `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
}}>
<DocumentButtonBar views={() => DocumentView.Selected()} />
</div>
)}
+ <div
+ className="documentDecorations-tagsView"
+ style={{
+ top: `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`,
+ transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
+ }}>
+ {DocumentView.Selected().length > 1 ? <TagsView Views={DocumentView.Selected()} /> : null}
+ </div>
</div>
{useRotation && (
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index d6d2956aa..508b1ee1f 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -1,3 +1,4 @@
+
.filterBox-flyout {
display: block;
text-align: left;
@@ -228,6 +229,97 @@
vertical-align: middle;
}
+.filterHotKey-button {
+ pointer-events: auto;
+ width: 100%; //width: 25px;
+ border-radius: 5px;
+ margin-top: 8px;
+ border-color: #d3d3d3;
+ border-style: solid;
+ border-width: thin;
+ transition: all 0.3s ease-out;
+ display: flex;
+ flex-direction: row;
+ padding: 5px;
+
+
+ &:hover{
+ border-color: #e9e9e9;
+ background-color: #6d6c6c
+ }
+
+ .hotKey-icon, .hotKey-close{
+ background-color: transparent;
+ border-radius: 10%;
+ padding: 5px;
+
+
+ &:hover{
+ background-color: #616060;
+ }
+ }
+
+ .hotKey-close{
+ right: 30px;
+ position: fixed;
+ padding-top: 10px;
+
+}
+
+ .hotkey-title{
+ top: 6px;
+ position: relative;
+ cursor: text;
+
+ }
+
+ .hotkey-title-input{
+ background-color: transparent;
+ border: none;
+ border-color: transparent;
+ outline: none;
+ cursor: text;
+
+ }
+}
+
+.hotKeyButtons {
+ position: relative;
+ width: 100%;
+
+}
+
+.hotKey-icon-button {
+
+ background-color: transparent;
+
+
+}
+
+.icon-panel {
+ position: absolute;
+ z-index: 10000;
+ border-color: black;
+ border-style: solid;
+ border-width: medium;
+ border-radius: 10%;
+ background-color: #323232;
+
+ .icon-panel-button{
+ background-color: #323232;
+ border-radius: 10%;
+
+
+ &:hover{
+ background-color:#7a7878
+ }
+ }
+
+
+
+}
+
+
// .sliderBox-outerDiv {
// width: 30%;// width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box
// height: 40; // height: 100%;
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 2f6d1fbaa..e34b66963 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,22 +1,161 @@
-/* eslint-disable react/jsx-props-no-spreading */
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
import { action, computed, makeObservable, observable, ObservableMap } from 'mobx';
-import { observer } from 'mobx-react';
+import { observer, useLocalObservable } from 'mobx-react';
import * as React from 'react';
+import { useEffect, useRef } from 'react';
import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider';
import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai';
import { CiCircleRemove } from 'react-icons/ci';
import { Doc, DocListCast, Field, FieldType, LinkedTo, StrListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
+import { DocCast, StrCast } from '../../fields/Types';
+import { Button, CurrentUserUtils } from '../util/CurrentUserUtils';
import { SearchUtil } from '../util/SearchUtil';
import { SnappingManager } from '../util/SnappingManager';
import { undoable } from '../util/UndoManager';
import { FieldsDropdown } from './FieldsDropdown';
import './FilterPanel.scss';
import { DocumentView } from './nodes/DocumentView';
+import { ButtonType } from './nodes/FontIconBox/FontIconBox';
import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components';
import { ObservableReactComponent } from './ObservableReactComponent';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+interface HotKeyButtonProps {
+ hotKey: Doc;
+ selected?: Doc;
+}
+
+/**
+ * Renders the buttons that correspond to each icon tag in the properties view. Allows users to change the icon,
+ * title, and delete.
+ */
+const HotKeyIconButton: React.FC<HotKeyButtonProps> = observer(({ hotKey /*, selected */ }) => {
+ const state = useLocalObservable(() => ({
+ isActive: false,
+ isEditing: false,
+ myHotKey: hotKey,
+
+ toggleActive() { this.isActive = !this.isActive; },
+ deactivate() { this.isActive = false; },
+ startEditing() { this.isEditing = true; },
+ stopEditing() { this.isEditing = false; },
+ setHotKey(newHotKey: string) { this.myHotKey.title = newHotKey; },
+ })); // prettier-ignore
+
+ const panelRef = useRef<HTMLDivElement>(null);
+ const inputRef = useRef<HTMLInputElement>(null);
+
+ const handleClick = () => state.toggleActive();
+
+ /**
+ * Updates the list of hotkeys based on the users input. replaces the old title with the new one and then assigns this new
+ * hotkey with the current icon
+ */
+ const updateFromInput = undoable(() => {
+ hotKey.title = StrCast(state.myHotKey.title);
+ hotKey.toolTip = `Click to toggle the ${StrCast(hotKey.title)}'s group's visibility`;
+ }, '');
+
+ /**
+ * Deselects if the user clicks outside the button
+ * @param event
+ */
+ const handleClickOutside = (event: MouseEvent) => {
+ if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
+ state.deactivate();
+ if (state.isEditing) {
+ state.stopEditing();
+
+ updateFromInput();
+ }
+ }
+ };
+
+ useEffect(() => {
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => document.removeEventListener('mousedown', handleClickOutside);
+ }, []);
+
+ const iconOpts = ['star', 'heart', 'bolt', 'satellite', 'palette', 'robot', 'lightbulb', 'highlighter', 'book', 'chalkboard'] as IconProp[];
+
+ /**
+ * Panel of icons the user can choose from to represent their tag
+ */
+ const iconPanel = iconOpts.map(icon => (
+ <button
+ key={icon.toString()}
+ onClick={undoable(e => {
+ e.stopPropagation;
+ hotKey[DocData].icon = icon.toString();
+ }, '')}
+ className="icon-panel-button">
+ <FontAwesomeIcon icon={icon} color={SnappingManager.userColor} />
+ </button>
+ ));
+
+ /**
+ * Actually renders the buttons
+ */
+
+ return (
+ <div
+ className={`filterHotKey-button`}
+ onClick={e => {
+ e.stopPropagation();
+ state.startEditing();
+ setTimeout(() => inputRef.current?.focus(), 0);
+ }}>
+ <div className={`hotKey-icon-button ${state.isActive ? 'active' : ''}`} ref={panelRef}>
+ <Tooltip title={<div className="dash-tooltip">Click to customize this hotkey&apos;s icon</div>}>
+ <button
+ type="button"
+ className="hotKey-icon"
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ handleClick();
+ }}>
+ <FontAwesomeIcon icon={hotKey.icon as IconProp} size="2xl" color={SnappingManager.userColor} />
+ </button>
+ </Tooltip>
+ {state.isActive && <div className="icon-panel">{iconPanel}</div>}
+ </div>
+ {state.isEditing ? (
+ <input
+ ref={inputRef}
+ type="text"
+ value={StrCast(state.myHotKey.title).toUpperCase()}
+ onChange={e => state.setHotKey(e.target.value)}
+ onBlur={() => {
+ state.stopEditing();
+ updateFromInput();
+ }}
+ onKeyDown={e => {
+ if (e.key === 'Enter') {
+ state.stopEditing();
+ updateFromInput();
+ }
+ }}
+ className="hotkey-title-input"
+ />
+ ) : (
+ <p className="hotkey-title">{StrCast(hotKey.title).toUpperCase()}</p>
+ )}
+ <button
+ className="hotKey-close"
+ onClick={(e: React.MouseEvent) => {
+ e.stopPropagation();
+ Doc.RemFromFilterHotKeys(hotKey);
+ }}>
+ <FontAwesomeIcon icon={'x' as IconProp} color={SnappingManager.userColor} />
+ </button>
+ </div>
+ );
+});
interface filterProps {
Document: Doc;
@@ -24,13 +163,16 @@ interface filterProps {
@observer
export class FilterPanel extends ObservableReactComponent<filterProps> {
- @observable _selectedFacetHeaders = new Set<string>();
+ // eslint-disable-next-line no-use-before-define
+ public static Instance: FilterPanel;
constructor(props: filterProps) {
super(props);
makeObservable(this);
+ FilterPanel.Instance = this;
}
+ @observable _selectedFacetHeaders = new Set<string>();
/**
* @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
*/
@@ -129,7 +271,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
@computed get activeRenderedFacetInfos() {
return new Set(
Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => {
- const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
+ const facetValues = facetHeader.startsWith('#') ? { strings: [] } : FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.Document.childFilters));
let nonNumbers = 0;
let minVal = Number.MAX_VALUE;
@@ -147,7 +289,10 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
if (facetHeader === 'text') {
return { facetHeader, renderType: 'text' };
}
- if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
+ if (facetHeader.startsWith('#')) {
+ return { facetHeader, renderType: 'togglebox' };
+ }
+ if (facetHeader !== 'tags' && !facetHeader.startsWith('#') && nonNumbers / facetValues.strings.length < 0.1) {
const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
const ranged: number[] | undefined = Doc.readDocRangeFilter(this.Document, facetHeader); // not the filter range, but the zooomed in range on the filter
@@ -211,12 +356,59 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2));
};
+ /**
+ * Allows users to add a filter hotkey to the properties panel. Will also update the multitoggle at the top menu and the
+ * icontags tht are displayed on the documents themselves
+ * @param hotKey tite of the new hotkey
+ */
+ addHotkey = (hotKey: string) => {
+ const buttons = DocCast(Doc.UserDoc().myContextMenuBtns);
+ const filter = DocCast(buttons.Filter);
+ const title = hotKey.startsWith('#') ? hotKey.substring(1) : hotKey;
+
+ const newKey: Button = {
+ title,
+ icon: 'question',
+ toolTip: `Click to toggle the ${title}'s group's visibility`,
+ btnType: ButtonType.ToggleButton,
+ expertMode: false,
+ toolType: '#' + title,
+ funcs: {},
+ scripts: { onClick: '{ return handleTags(this.toolType, _readOnly_);}' },
+ };
+
+ const newBtn = CurrentUserUtils.setupContextMenuBtn(newKey, filter);
+ newBtn.isSystem = newBtn[DocData].isSystem = undefined;
+
+ Doc.AddToFilterHotKeys(newBtn);
+ };
+
+ /**
+ * Renders the newly formed hotkey icon buttons
+ * @returns the buttons to be rendered
+ */
+ hotKeyButtons = () => {
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const hotKeys = Doc.MyFilterHotKeys;
+
+ // Selecting a button should make it so that the icon on the top filter panel becomes said icon
+ const buttons = hotKeys.map(hotKey => (
+ <Tooltip key={StrCast(hotKey.title)} title={<div className="dash-tooltip">Click to customize this hotkey&apos;s icon</div>}>
+ <HotKeyIconButton hotKey={hotKey} selected={selected} />
+ </Tooltip>
+ ));
+
+ return buttons;
+ };
+
+ // @observable iconPanelMap: Map<string, number> = new Map();
+
render() {
return (
<div className="filterBox-treeView">
<div className="filterBox-select">
<div style={{ width: '100%' }}>
- <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl_Guest', LinkedTo]} />
+ <FieldsDropdown Document={this.Document} selectFunc={this.facetClick} showPlaceholder placeholder="add a filter" addedFields={['acl_Guest', LinkedTo, 'Star', 'Heart', 'Bolt', 'Cloud']} />
</div>
{/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
{/* <div className="filterBox-select-bool">
@@ -277,6 +469,15 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
)
)}
</div>
+ <div>
+ <div className="filterBox-select">
+ <div style={{ width: '100%' }}>
+ <FieldsDropdown Document={this.Document} selectFunc={this.addHotkey} showPlaceholder placeholder="add a hotkey" addedFields={['acl_Guest', LinkedTo]} />
+ </div>
+ </div>
+ </div>
+
+ <div>{this.hotKeyButtons()}</div>
</div>
);
}
@@ -317,6 +518,22 @@ export class FilterPanel extends ObservableReactComponent<filterProps> {
</div>
);
});
+ case 'togglebox':
+ return (
+ <div>
+ <input
+ style={{ width: 20, marginLeft: 20 }}
+ checked={['check', 'exists'].includes(
+ StrListCast(this.Document._childFilters)
+ .find(filter => filter.split(Doc.FilterSep)[0] === 'tags' && filter.split(Doc.FilterSep)[1] === facetHeader)
+ ?.split(Doc.FilterSep)[2] ?? ''
+ )}
+ type={'checkbox'}
+ onChange={undoable(e => Doc.setDocFilter(this.Document, 'tags', facetHeader, e.target.checked ? 'check' : 'remove'), 'set filter')}
+ />
+ -set-
+ </div>
+ );
case 'range':
{
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0b1e5f857..41a933431 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -556,6 +556,10 @@ export class MainView extends ObservableReactComponent<object> {
fa.faRobot,
fa.faSatellite,
fa.faStar,
+ fa.faCloud,
+ fa.faBolt,
+ fa.faLightbulb,
+ fa.faX,
]
);
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 9299705ee..26a695b43 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -58,7 +58,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
private _widthUndo?: UndoManager.Batch;
// eslint-disable-next-line no-use-before-define
- public static Instance: PropertiesView | undefined;
+ public static Instance: PropertiesView;
constructor(props: PropertiesViewProps) {
super(props);
makeObservable(this);
@@ -1277,7 +1277,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
@computed get filtersSubMenu() {
return (
// prettier-ignore
- <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => { this.openFilters = bool; }} onDoubleClick={this.CloseAll}>
+ <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={action(bool => { this.openFilters = bool; })} onDoubleClick={this.CloseAll}>
<div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
<FilterPanel Document={this.selectedDoc ?? Doc.ActiveDashboard!} />
</div>
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 513953d17..3545afcee 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -19,12 +19,11 @@ import { SnappingManager } from '../util/SnappingManager';
import { undoable, UndoManager } from '../util/UndoManager';
import { TreeSort } from './collections/TreeSort';
import { Colors } from './global/globalEnums';
-import { TagsView } from './TagsView';
-import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView';
import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
import { StyleProp } from './StyleProp';
import './StyleProvider.scss';
+import { TagsView } from './TagsView';
function toggleLockedPosition(doc: Doc) {
UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground');
@@ -325,7 +324,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
type={Type.TERT}
dropdownType={DropdownType.CLICK}
fillWidth
- // eslint-disable-next-line react/no-unstable-nested-components
iconProvider={() => <div className='styleProvider-filterShift'><FaFilter/></div>}
closeOnSelect
setSelectedVal={((dvValue: unknown) => {
@@ -365,7 +363,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
</Tooltip>
);
};
- const tags = () => props?.DocumentView?.() ? <TagsView View={props.DocumentView()}/> : null;
+ const tags = () => docView?.() ? <TagsView Views={[docView?.()]}/> : null;
+
return (
<>
{paint()}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 8047363d9..f44fd1d03 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -9,7 +9,7 @@ import { emptyFunction } from '../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../fields/Doc';
import { DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { DocCast, StrCast } from '../../fields/Types';
+import { DocCast, NumCast, StrCast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
import { SnappingManager } from '../util/SnappingManager';
@@ -18,6 +18,8 @@ import { ObservableReactComponent } from './ObservableReactComponent';
import './TagsView.scss';
import { DocumentView } from './nodes/DocumentView';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
+import { IconTagBox } from './nodes/IconTagBox';
+import { Id } from '../../fields/FieldSymbols';
/**
* The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection.
@@ -27,13 +29,13 @@ import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
* the user can drag them off in order to display a collection of all documents that share the tag value.
*
* The tags that are added using the panel are the same as the #tags that can entered in a text Doc.
- * Note that tags starting with #@ display a metadata key/value pair instead of the tag itself.
- * e.g., '#@author' shows the document author
+ * Note that tags starting with @ display a metadata key/value pair instead of the tag itself.
+ * e.g., '@author' shows the document author
*
*/
interface TagItemProps {
- doc: Doc;
+ docs: Doc[];
tag: string;
tagDoc: Opt<Doc>;
showRemoveUI: boolean;
@@ -59,7 +61,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
* @param tag tag string
* @returns tag collection Doc or undefined
*/
- public static findTagCollectionDoc = (tag: String) => TagItem.AllTagCollectionDocs.find(doc => doc.title === tag);
+ public static findTagCollectionDoc = (tag: string) => TagItem.AllTagCollectionDocs.find(doc => doc.title === tag);
/**
* Creates a Doc that collects Docs with the specified tag / value
@@ -82,6 +84,9 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
*/
public static allDocsWithTag = (tag: string) => DocListCast(TagItem.findTagCollectionDoc(tag)?.[DocData].docs);
+ public static docHasTag = (doc: Doc, tag: string) => {
+ return StrListCast(doc?.tags).includes(tag);
+ };
/**
* Adds a tag to the metadata of this document and adds the Doc to the corresponding tag collection Doc (or creates it)
* @param tag tag string
@@ -148,7 +153,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
private _ref: React.RefObject<HTMLDivElement>;
- constructor(props: any) {
+ constructor(props: TagItemProps) {
super(props);
makeObservable(this);
this._ref = React.createRef();
@@ -172,12 +177,12 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
docData.data = new List<Doc>(newEmbeddings);
docData.title = this._props.tag;
docData.tags = new List<string>([this._props.tag]);
- docData.showTags = true;
docData.freeform_fitContentsToBox = true;
doc._freeform_panX = doc._freeform_panY = 0;
doc._width = 900;
doc._height = 900;
doc.layout_fitWidth = true;
+ doc._layout_showTags = true;
return doc;
})(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true));
newEmbeddings.forEach(embed => Doc.SetContainer(embed, newCollection));
@@ -202,39 +207,45 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
return false;
},
returnFalse,
- emptyFunction
+ clickEv => {
+ clickEv.stopPropagation();
+ this._props.setToEditing();
+ }
);
e.preventDefault();
};
+ @computed get doc() {
+ return this._props.docs.lastElement();
+ }
+
render() {
- this._props.tagDoc && setTimeout(() => TagItem.addTagToDoc(this._props.doc, this._props.tag)); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
- const tag = this._props.tag.replace(/^#/, '');
- const metadata = tag.startsWith('@') ? tag.replace(/^@/, '') : '';
+ this._props.tagDoc && setTimeout(() => this._props.docs.forEach(doc => TagItem.addTagToDoc(doc, this._props.tag))); // bcz: hack to make sure that Docs are added to their tag Doc collection since metadata can get set anywhere without a guard triggering an add to the collection
+ const metadata = this._props.tag.startsWith('@') ? this._props.tag.replace(/^@/, '') : '';
return (
- <div className={'tagItem' + (!this._props.tagDoc ? ' faceItem' : '')} onClick={this._props.setToEditing} onPointerDown={this.handleDragStart} ref={this._ref}>
+ <div className={'tagItem' + (!this._props.tagDoc ? ' faceItem' : '')} onPointerDown={this.handleDragStart} ref={this._ref}>
{metadata ? (
<span>
- <b style={{ fontSize: 'smaller' }}>{tag}&nbsp;</b>
- {typeof this._props.doc[metadata] === 'boolean' ? (
+ <b style={{ fontSize: 'smaller' }}>{'@' + metadata}&nbsp;</b>
+ {typeof this.doc[metadata] === 'boolean' ? (
<input
type="checkbox"
onClick={e => e.stopPropagation()}
onPointerDown={e => e.stopPropagation()}
- onChange={undoable(e => (this._props.doc[metadata] = !this._props.doc[metadata]), 'metadata toggle')}
- checked={this._props.doc[metadata] as boolean}
+ onChange={undoable(() => (this.doc[metadata] = !this.doc[metadata]), 'metadata toggle')}
+ checked={this.doc[metadata] as boolean}
/>
) : (
- Field.toString(this._props.doc[metadata])
+ Field.toString(this.doc[metadata])
)}
</span>
) : (
- tag
+ this._props.tag
)}
{this.props.showRemoveUI && this._props.tagDoc && (
<IconButton
tooltip="Remove tag"
- onPointerDown={undoable(() => TagItem.removeTagFromDoc(this._props.doc, this._props.tag, this._props.tagDoc), `remove tag ${this._props.tag}`)}
+ onPointerDown={undoable(() => this._props.docs.forEach(doc => TagItem.removeTagFromDoc(doc, this._props.tag, this._props.tagDoc)), `remove tag ${this._props.tag}`)}
icon={<FontAwesomeIcon icon="times" size="sm" />}
style={{ width: '8px', height: '8px', marginLeft: '10px' }}
/>
@@ -245,7 +256,7 @@ export class TagItem extends ObservableReactComponent<TagItemProps> {
}
interface TagViewProps {
- View: DocumentView;
+ Views: DocumentView[];
}
/**
@@ -253,7 +264,7 @@ interface TagViewProps {
*/
@observer
export class TagsView extends ObservableReactComponent<TagViewProps> {
- constructor(props: any) {
+ constructor(props: TagViewProps) {
super(props);
makeObservable(this);
}
@@ -261,13 +272,13 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
@observable _panelHeightDirty = 0;
@observable _currentInput = '';
- @observable _isEditing = !StrListCast(this._props.View.dataDoc.tags).length;
+ @observable _isEditing = !StrListCast(this.View.dataDoc.tags).length;
_heightDisposer: IReactionDisposer | undefined;
componentDidMount() {
this._heightDisposer = reaction(
- () => this._props.View.screenToContentsTransform(),
- xf => {
+ () => this.View.screenToContentsTransform(),
+ () => {
this._panelHeightDirty = this._panelHeightDirty + 1;
}
);
@@ -276,11 +287,21 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
this._heightDisposer?.();
}
+ @computed get View() {
+ return this._props.Views.lastElement();
+ }
+
+ // x: 1 => 1/vs 0 => 1 1/(vs - (1-x)*(vs-1))
@computed get currentScale() {
- return Math.max(1, 1 / this._props.View.screenToLocalScale());
+ if (this._props.Views.length > 1) return 1;
+ const x = NumCast(this.View.Document.height) / this.View.screenToContentsTransform().Scale / 80;
+ const xscale = x >= 1 ? 0 : 1 / (1 + x * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35 / this.View.screenToLocalScale() - ;
+ const y = NumCast(this.View.Document.width) / this.View.screenToContentsTransform().Scale / 200;
+ const yscale = y >= 1 ? 0 : 1 / (1 + y * (this.View.screenToLocalScale() - 1)); //docheight / this.View.screenToContentsTransform().Scale / 35 / this.View.screenToLocalScale() - ;
+ return Math.max(xscale, yscale, 1 / this.View.screenToLocalScale());
}
@computed get isEditing() {
- return this._isEditing && DocumentView.SelectedDocs().includes(this._props.View.Document);
+ return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View)));
}
/**
@@ -290,7 +311,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
@action
setToEditing = (editing = true) => {
this._isEditing = editing;
- editing && this._props.View.select(false);
+ editing && this._props.Views.length === 1 && this.View.select(false);
};
/**
@@ -303,8 +324,10 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
action((tag: string) => {
const submittedLabel = tag.trim().replace(/^#/, '').split(':');
if (submittedLabel[0]) {
- TagItem.addTagToDoc(this._props.View.Document, '#' + submittedLabel[0]);
- if (submittedLabel.length > 1) Doc.SetField(this._props.View.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+ this._props.Views.forEach(view => {
+ TagItem.addTagToDoc(view.Document, (submittedLabel[0].startsWith('@') ? '' : '#') + submittedLabel[0]);
+ if (submittedLabel.length > 1) Doc.SetField(view.Document, submittedLabel[0].replace(/^@/, ''), ':' + submittedLabel[1]);
+ });
}
this._currentInput = ''; // Clear the input box
}),
@@ -312,25 +335,26 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
);
/**
- * When 'showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc).
+ * When 'layout_showTags' is set on a Doc, this displays a wrapping panel of tagItemViews corresponding to all the tags set on the Doc).
* When the dropdown is clicked, this will toggle an extended UI that allows additional tags to be added/removed.
*/
render() {
- const tagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags));
- const chatTagsList = new Set<string>(StrListCast(this._props.View.dataDoc.tags_chat));
+ const tagsList = new Set<string>(StrListCast(this.View.dataDoc.tags));
+ const chatTagsList = new Set<string>(StrListCast(this.View.dataDoc.tags_chat));
const facesList = new Set<string>(
- DocListCast(this._props.View.dataDoc[Doc.LayoutFieldKey(this._props.View.Document) + '_annotations'])
- .concat(this._props.View.Document)
+ DocListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_annotations'])
+ .concat(this.View.Document)
.filter(d => d.face)
.map(doc => StrCast(DocCast(doc.face)?.title))
);
this._panelHeightDirty;
- return !this._props.View.Document.showTags ? null : (
+ return this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1) ? null : (
<div
className="tagsView-container"
- ref={r => r && new ResizeObserver(action(() => (this._props.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
+ ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)}
style={{
+ display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined,
transformOrigin: 'top left',
maxWidth: `${100 * this.currentScale}%`,
width: 'max-content',
@@ -338,18 +362,38 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
position: 'relative',
- top: `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+ top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
}}>
<div className="tagsView-content" style={{ width: '100%' }}>
<div className="tagsView-list">
- {!tagsList.size && !facesList.size ? null : ( //
- <IconButton style={{ width: '8px' }} tooltip="Close Menu" onPointerDown={() => this.setToEditing(!this._isEditing)} icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />} />
+ {this._props.Views.length === 1 && !this.View.showTags ? null : ( //
+ <IconButton
+ style={{ width: '8px' }}
+ tooltip="Close Menu"
+ onPointerDown={e =>
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, upEv => {
+ this.setToEditing(!this._isEditing);
+ upEv.stopPropagation();
+ })
+ }
+ icon={<FontAwesomeIcon icon={this._isEditing ? 'chevron-up' : 'chevron-down'} size="sm" />}
+ />
)}
- {Array.from(tagsList).map((tag, i) => (
- <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
- ))}
- {Array.from(facesList).map((tag, i) => (
- <TagItem key={i} doc={this._props.View.Document} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
+ <IconTagBox Views={this._props.Views} IsEditing={this._isEditing} />
+ {Array.from(tagsList)
+ .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag))
+ .map(tag => (
+ <TagItem
+ key={tag}
+ docs={this._props.Views.map(view => view.Document)}
+ tag={tag}
+ tagDoc={TagItem.findTagCollectionDoc(tag) ?? TagItem.createTagCollectionDoc(tag)}
+ setToEditing={this.setToEditing}
+ showRemoveUI={this.isEditing}
+ />
+ ))}
+ {Array.from(facesList).map(tag => (
+ <TagItem key={tag} docs={this._props.Views.map(view => view.Document)} tag={tag} tagDoc={undefined} setToEditing={this.setToEditing} showRemoveUI={this.isEditing} />
))}
</div>
{this.isEditing ? (
@@ -371,9 +415,9 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
/>
</div>
<div className="tagsView-suggestions-box">
- {TagItem.AllTagCollectionDocs.map((doc, i) => {
- const tag = StrCast(doc.title);
- return (
+ {TagItem.AllTagCollectionDocs.map(doc => StrCast(doc.title))
+ .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag))
+ .map(tag => (
<Button
style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightblue', color: 'black' }}
text={tag}
@@ -382,20 +426,17 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
onClick={() => this.submitTag(tag)}
key={tag}
/>
- );
- })}
- {Array.from(chatTagsList).map(tag => {
- return (
- <Button
- style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightpink', color: 'black' }}
- text={tag}
- color={SnappingManager.userVariantColor}
- tooltip="Add existing tag"
- onClick={() => this.submitTag(tag)}
- key={tag}
- />
- );
- })}
+ ))}
+ {Array.from(chatTagsList).map(tag => (
+ <Button
+ style={{ margin: '2px 2px', border: '1px solid black', backgroundColor: 'lightpink', color: 'black' }}
+ text={tag}
+ color={SnappingManager.userVariantColor}
+ tooltip="Add existing tag"
+ onClick={() => this.submitTag(tag)}
+ key={tag}
+ />
+ ))}
</div>
</div>
) : null}
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index cc797d0bd..e5fb7aba6 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -12,7 +12,6 @@
height: 35px;
border-radius: 50%;
background-color: $dark-gray;
- // border-color: $medium-blue;
margin: 5px; // transform: translateY(-50px);
}
}
@@ -20,7 +19,6 @@
.card-wrapper {
display: grid;
grid-template-columns: repeat(10, 1fr);
- // width: 100%;
transform-origin: top left;
position: absolute;
@@ -31,40 +29,13 @@
transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
-.card-button-container {
- display: flex;
- padding: 3px;
- // width: 300px;
- background-color: rgb(218, 218, 218); /* Background color of the container */
- border-radius: 50px; /* Rounds the corners of the container */
- transform: translateY(75px);
- // box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Optional: Adds shadow for depth */
- align-items: center; /* Centers buttons vertically */
- justify-content: start; /* Centers buttons horizontally */
+.no-card-span {
+ position: relative;
+ width: fit-content;
+ text-align: center;
+ font-size: 65px;
}
-// button:hover {
-// transform: translateY(-50px);
-// }
-
-// .card-wrapper::after {
-// content: "";
-// width: 100%; /* Forces wrapping */
-// }
-
-// .card-wrapper > .card-item:nth-child(10n)::after {
-// content: "";
-// width: 100%; /* Forces wrapping after every 10th item */
-// }
-
-// .card-row{
-// display: flex;
-// position: absolute;
-// align-items: center;
-// transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
-
-// }
-
.card-item-inactive,
.card-item-active,
.card-item {
@@ -79,6 +50,5 @@
}
.card-item-active {
- position: absolute;
z-index: 100;
}
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 28a769896..bcb42111f 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -2,15 +2,18 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
-import { numberRange } from '../../../Utils';
-import { Doc, NumListCast } from '../../../fields/Doc';
+import { emptyFunction } from '../../../Utils';
+import { Doc, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
-import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { List } from '../../../fields/List';
+import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
import { URLField } from '../../../fields/URLField';
import { gptImageLabel } from '../../apis/gpt/GPT';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
+import { dropActionType } from '../../util/DropActionTypes';
+import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable } from '../../util/UndoManager';
@@ -25,24 +28,30 @@ enum cardSortings {
Type = 'type',
Color = 'color',
Custom = 'custom',
+ Chat = 'chat',
+ Tag = 'tag',
None = '',
}
+
+/**
+ * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
+ * sort and filter using presets, and customize your experience with chat gpt.
+ *
+ * This file contains code as to how the docs are to be rendered (there place geographically and also in regards to sorting),
+ * and callback functions for the gpt popup
+ */
@observer
export class CollectionCardView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
- private _childDocumentWidth = 600; // target width of a Doc...
private _disposers: { [key: string]: IReactionDisposer } = {};
private _textToDoc = new Map<string, Doc>();
@observable _forceChildXf = false;
- @observable _isLoading = false;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
@observable _maxRowCount = 10;
-
- static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined {
- return Cast(doc[groupFieldKey], 'number', null);
- }
+ @observable _docDraggedIndex: number = -1;
+ @observable overIndex: number = -1;
static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
try {
@@ -61,22 +70,46 @@ export class CollectionCardView extends CollectionSubView() {
}
};
+ constructor(props: SubCollectionViewProps) {
+ super(props);
+ makeObservable(this);
+ this.setRegenerateCallback();
+ }
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
this._dropDisposer?.();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
};
+ /**
+ * Callback to ensure gpt's text versions of the child docs are updated
+ */
+ setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);
- constructor(props: SubCollectionViewProps) {
- super(props);
- makeObservable(this);
- }
+ /**
+ * update's gpt's doc-text list and initializes callbacks
+ */
+ @action
+ childPairStringListAndUpdateSortDesc = async () => {
+ const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
+ GPTPopup.Instance.setSortDesc(sortDesc.join());
+ GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag);
+ GPTPopup.Instance.onQuizRandom = () => this.quizMode();
+ };
+
+ componentDidMount() {
+ this.Document.childFilters_boolean = 'OR'; // bcz: really shouldn't be assigning to fields from within didMount -- this should be a default/override beahavior somehow
- componentDidMount(): void {
+ // Reaction to cardSort changes
this._disposers.sort = reaction(
- () => ({ cardSort: this.cardSort, field: this.cardSort_customField }),
- ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false))
+ () => GPTPopup.Instance.visible,
+ isVis => {
+ if (isVis) {
+ this.openChatPopup();
+ } else {
+ this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort;
+ }
+ }
);
}
@@ -85,56 +118,40 @@ export class CollectionCardView extends CollectionSubView() {
this._dropDisposer?.();
}
- @computed get cardSort_customField() {
- return StrCast(this.Document.cardSort_customField) as 'chat' | 'star' | 'idea' | 'like';
- }
-
@computed get cardSort() {
return StrCast(this.Document.cardSort) as cardSortings;
}
+
+ /**
+ * The child documents to be rendered-- either all of them except the Links or the docs in the currently active
+ * custom group
+ */
+ @computed get childDocsWithoutLinks() {
+ return this.childDocs.filter(l => l.type !== DocumentType.LINK);
+ }
+
/**
* how much to scale down the contents of the view so that everything will fit
*/
@computed get fitContentScale() {
const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount);
- return (this._childDocumentWidth * length) / this._props.PanelWidth();
- }
-
- @computed get translateWrapperX() {
- let translate = 0;
-
- if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) {
- translate += this.panelWidth() / 2;
- }
- return translate;
+ return (this.childPanelWidth() * length) / this._props.PanelWidth();
}
/**
- * The child documents to be rendered-- either all of them except the Links or the docs in the currently active
- * custom group
+ * When in quiz mode, randomly selects a document
*/
- @computed get childDocsWithoutLinks() {
- const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK);
- const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups);
-
- if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) {
- return regularDocs.filter(doc => {
- // Get the group number for the current index
- const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
- // Check if the group number is in the active groups
- return groupNumber !== undefined && activeGroups.includes(groupNumber);
- });
- }
-
- // Default return for non-custom cardSort or other cases, filtering out links
- return regularDocs;
- }
+ quizMode = () => {
+ const randomIndex = Math.floor(Math.random() * this.childDocs.length);
+ SelectionManager.DeselectAll();
+ DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false);
+ };
/**
- * Determines the order in which the cards will be rendered depending on the current sort type
+ * Number of rows of cards to be rendered
*/
- @computed get sortedDocs() {
- return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc));
+ @computed get numRows() {
+ return Math.ceil(this.sortedDocs.length / this._maxRowCount);
}
@action
@@ -157,8 +174,7 @@ export class CollectionCardView extends CollectionSubView() {
*/
inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d));
- panelWidth = () => this._childDocumentWidth;
- panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width);
+ childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
isChildContentActive = () => !!this.isContentActive();
@@ -170,6 +186,8 @@ export class CollectionCardView extends CollectionSubView() {
* @returns
*/
rotate = (amCards: number, index: number) => {
+ if (amCards == 1) return 0;
+
const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
@@ -188,11 +206,12 @@ export class CollectionCardView extends CollectionSubView() {
translateY = (amCards: number, index: number, realIndex: number) => {
const evenOdd = amCards % 2;
const apex = (amCards - evenOdd) / 2;
- const stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
+ const Magnitude = this.childPanelWidth() / 2; // 400
+ const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
let rowOffset = 0;
if (realIndex > this._maxRowCount - 1) {
- rowOffset = 400 * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
+ rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
}
if (evenOdd === 1 || index < apex - 1) {
return Math.abs(stepMag * (apex - index)) - rowOffset;
@@ -205,24 +224,137 @@ export class CollectionCardView extends CollectionSubView() {
};
/**
- * Translates the selected node to the middle fo the screen
- * @param index
- * @returns
+ * When dragging a card, determines the index the card should be set to if dropped
+ * @param mouseX mouse's x location
+ * @param mouseY mouses' y location
+ * @returns the card's new index
*/
- translateSelected = (index: number): number => {
- // if (this.isSelected(index)) {
- const middleOfPanel = this._props.PanelWidth() / 2;
- const scaledNodeWidth = this.panelWidth() * 1.25;
+ findCardDropIndex = (mouseX: number, mouseY: number) => {
+ const amCardsTotal = this.sortedDocs.length;
+ let index = 0;
+ const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount;
+
+ // Calculate the adjusted X position accounting for the initial offset
+ let adjustedX = mouseX;
- // Calculate the position of the node's left edge before scaling
- const nodeLeftEdge = index * this.panelWidth();
- // Find the center of the node after scaling
- const scaledNodeCenter = nodeLeftEdge + scaledNodeWidth / 2;
+ const amRows = Math.ceil(amCardsTotal / this._maxRowCount);
+ const rowHeight = this._props.PanelHeight() / amRows;
+ const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0
+
+ if (adjustedX < 0) {
+ console.log('DROP INDEX NO ');
+ return 0; // Before the first column
+ }
- // Calculate the translation needed to align the scaled node's center with the panel's center
- const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4;
+ if (amCardsTotal < this._maxRowCount) {
+ index = Math.floor(adjustedX / cardWidth);
+ } else if (currRow != amRows - 1) {
+ index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
+ } else {
+ const rowAmCards = amCardsTotal - currRow * this._maxRowCount;
+ const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth;
+ adjustedX = mouseX - offset;
- return translation;
+ index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
+ }
+ console.log('DROP INDEX = ' + index);
+ return index;
+ };
+
+ /**
+ * Checks to see if a card is being dragged and calls the appropriate methods if so
+ * @param e the current pointer event
+ */
+
+ @action
+ onPointerMove = (x: number, y: number) => {
+ this._docDraggedIndex = -1;
+ if (DragManager.docsBeingDragged.length) {
+ const newIndex = this.findCardDropIndex(x, y);
+
+ if (newIndex !== this._docDraggedIndex && newIndex != -1) {
+ this._docDraggedIndex = newIndex;
+ }
+ }
+ };
+
+ /**
+ * Handles external drop of images/PDFs etc from outside Dash.
+ */
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
+ super.onExternalDrop(e, {});
+ };
+
+ /**
+ * Resets all the doc dragging vairables once a card is dropped
+ * @param e
+ * @param de drop event
+ * @returns true if a card has been dropped, falls if not
+ */
+ onInternalDrop = undoable(
+ action((e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ const dragIndex = this._docDraggedIndex;
+ const draggedDoc = DragManager.docsBeingDragged[0];
+ if (dragIndex > -1 && draggedDoc) {
+ this._docDraggedIndex = -1;
+ const sorted = this.sortedDocs;
+ const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
+
+ this.Document.cardSort = '';
+ originalIndex !== -1 && sorted.splice(originalIndex, 1);
+ sorted.splice(dragIndex, 0, draggedDoc);
+ if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
+ this.dataDoc[this.fieldKey] = new List<Doc>(sorted);
+ }
+ }
+ e.stopPropagation();
+ return true;
+ }
+ return false;
+ }),
+ ''
+ );
+
+ @computed get sortedDocs() {
+ return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.Document.cardSort_isDesc), this._docDraggedIndex);
+ }
+
+ /**
+ * Used to determine how to sort cards based on tags. The lestmost tags are given lower values while cards to the right are
+ * given higher values. Decimals are used to determine placement for cards with multiple tags
+ * @param doc the doc whose value is being determined
+ * @returns its value based on its tags
+ */
+
+ tagValue = (doc: Doc) => {
+ const keys = Doc.MyFilterHotKeys;
+
+ const isTagActive = (key: Doc) => BoolCast(key.active);
+
+ let base = '';
+ let fraction = '';
+
+ keys.forEach(key => {
+ if (isTagActive(key)) {
+ if (base === '') {
+ base = StrCast(key.toolType); // First active tag becomes the base
+ } else {
+ fraction += StrCast(key.toolType); // Subsequent active tags become part of the fraction
+ }
+ }
+ });
+
+ // If no tag was active, return 0 by default
+ if (base === '') {
+ return 0;
+ }
+
+ // Construct the final number by appending the fraction if it exists
+ const numberString = fraction ? `${base}.${fraction}` : base;
+
+ // Convert the result to a number and return
+ return Number(numberString);
};
/**
@@ -233,35 +365,43 @@ export class CollectionCardView extends CollectionSubView() {
* @param isDesc
* @returns
*/
- sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => {
- if (sortType === cardSortings.None) return docs;
- docs.sort((docA, docB) => {
- const [typeA, typeB] = (() => {
- switch (sortType) {
- case cardSortings.Time:
- return [DateCast(docA.author_date)?.date ?? Date.now(),
- DateCast(docB.author_date)?.date ?? Date.now()];
- case cardSortings.Color:
- return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string
- DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string
- case cardSortings.Custom:
- return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0,
- CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0];
- default: return [StrCast(docA.type), // If docA.type is undefined, use an empty string
- StrCast(docB.type)]; // If docB.type is undefined, use an empty string
- } // prettier-ignore
- })();
-
- const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
- return isDesc ? -out : out; // Reverse the sort order if descending is true
- });
+ sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
+ sortType &&
+ docs.sort((docA, docB) => {
+ const [typeA, typeB] = (() => {
+ switch (sortType) {
+ case cardSortings.Time:
+ return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
+ case cardSortings.Color: {
+ const d1 = DashColor(StrCast(docA.backgroundColor));
+ const d2 = DashColor(StrCast(docB.backgroundColor));
+ return [d1.hsv().hue(), d2.hsv().hue()];
+ }
+ case cardSortings.Tag:
+ return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999];
+ case cardSortings.Chat:
+ return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999];
+ default:
+ return [StrCast(docA.type), StrCast(docB.type)];
+ }
+ })();
+
+ const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
+ return isDesc ? out : -out;
+ });
+ if (dragIndex != -1) {
+ const draggedDoc = DragManager.docsBeingDragged[0];
+ const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+ originalIndex !== -1 && docs.splice(originalIndex, 1);
+ docs.splice(dragIndex, 0, draggedDoc);
+ }
- return docs;
+ return [...docs]; // need to spread docs into a new object list since sort() modifies the incoming list which confuses mobx caching
};
displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => (
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))}
Document={doc}
@@ -273,10 +413,14 @@ export class CollectionCardView extends CollectionSubView() {
LayoutTemplate={this._props.childLayoutTemplate}
LayoutTemplateString={this._props.childLayoutString}
ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
- isContentActive={this.isChildContentActive}
+ isContentActive={emptyFunction}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight(doc)}
+ PanelWidth={this.childPanelWidth}
+ PanelHeight={() => this._props.PanelHeight() * this.fitContentScale}
+ dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
+ dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
+ showTags={true}
+ dontHideOnDrag
/>
);
@@ -286,24 +430,24 @@ export class CollectionCardView extends CollectionSubView() {
* @returns
*/
overflowAmCardsCalc = (index: number) => {
- if (this.inactiveDocs().length < this._maxRowCount) {
- return this.inactiveDocs().length;
+ if (this.sortedDocs.length < this._maxRowCount) {
+ return this.sortedDocs.length;
}
// 13 - 3 = 10
- const totalCards = this.inactiveDocs().length;
+ const totalCards = this.sortedDocs.length;
// if 9 or less
- if (index < totalCards - (totalCards % 10)) {
+ if (index < totalCards - (totalCards % this._maxRowCount)) {
return this._maxRowCount;
}
// (3)
- return totalCards % 10;
+ return totalCards % this._maxRowCount;
};
/**
* Determines the index a card is in in a row
* @param realIndex
* @returns
*/
- overflowIndexCalc = (realIndex: number) => realIndex % 10;
+ overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
/**
* Translates the cards in the second rows and beyond over to the right
* @param realIndex
@@ -311,7 +455,7 @@ export class CollectionCardView extends CollectionSubView() {
* @param calcRowCards
* @returns
*/
- translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (10 - calcRowCards) * (this.panelWidth() / 2));
+ translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));
/**
* Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected
@@ -323,22 +467,15 @@ export class CollectionCardView extends CollectionSubView() {
* @returns
*/
calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
- if (isSelected) return 50 * this.fitContentScale;
- const trans = isHovered ? this.translateHover(realIndex) : 0;
- return trans + this.translateY(amCards, calcRowIndex, realIndex);
+ const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
+ const rowIndex = Math.trunc(realIndex / this._maxRowCount);
+ const rowToCenterShift = this.numRows / 2 - rowIndex;
+ if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2;
+ if (amCards == 1) return 50 * this.fitContentScale;
+ return this.translateY(amCards, calcRowIndex, realIndex);
};
/**
- * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt
- * @param childPairIndex
- * @param buttonID
- * @param doc
- */
- toggleButton = undoable((buttonID: number, doc: Doc) => {
- this.cardSort_customField && (doc[this.cardSort_customField] = buttonID);
- }, 'toggle custom button');
-
- /**
* A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
* Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
* inputted into the gpt prompt to sort everything together
@@ -355,6 +492,7 @@ export class CollectionCardView extends CollectionSubView() {
};
const docTextPromises = this.childDocsWithoutLinks.map(async doc => {
const docText = (await docToText(doc)) ?? '';
+ doc['gptInputText'] = docText;
this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
return `======${docText.replace(/\n/g, ' ').trim()}======`;
});
@@ -377,78 +515,107 @@ export class CollectionCardView extends CollectionSubView() {
image[DocData].description = response.trim();
return response; // Return the response from gptImageLabel
} catch (error) {
- console.log('bad things have happened');
+ console.log(error);
}
return '';
};
/**
- * Converts the gpt output into a hashmap that can be used for sorting. lists are seperated by ==== while elements within the list are seperated by ~~~~~~
+ * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
+ * usable code
* @param gptOutput
*/
- processGptOutput = (gptOutput: string) => {
+ @action
+ processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => {
// Split the string into individual list items
const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
+
+ if (questionType === '2' || questionType === '4') {
+ this.childDocs.forEach(d => {
+ d['chatFilter'] = false;
+ });
+ }
+
+ if (questionType === '6') {
+ this.Document.cardSort = 'chat';
+ }
+
listItems.forEach((item, index) => {
- // Split the item by '~~~~~~' to get all descriptors
- const parts = item.split('~~~~~~').map(part => part.trim());
-
- parts.forEach(part => {
- // Find the corresponding Doc in the textToDoc map
- const doc = this._textToDoc.get(part);
- if (doc) {
- doc.chat = index;
+ const normalizedItem = item.trim();
+ // find the corresponding Doc in the textToDoc map
+ const doc = this._textToDoc.get(normalizedItem);
+
+ if (doc) {
+ switch (questionType) {
+ case '6':
+ doc.chatIndex = index;
+ break;
+ case '1': {
+ const allHotKeys = Doc.MyFilterHotKeys;
+
+ let myTag = '';
+
+ if (tag) {
+ for (let i = 0; i < allHotKeys.length; i++) {
+ // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED
+ const keyTag = StrCast(allHotKeys[i].toolType);
+ if (tag.includes(keyTag)) {
+ myTag = keyTag;
+ break;
+ }
+ }
+
+ if (myTag != '') {
+ doc[myTag] = true;
+ }
+ }
+ break;
+ }
+ case '2':
+ case '4':
+ doc['chatFilter'] = true;
+ Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match');
+ break;
}
- });
+ } else {
+ console.warn(`No matching document found for item: ${normalizedItem}`);
+ }
});
- };
+ }, '');
+
/**
* Opens up the chat popup and starts the process for smart sorting.
*/
openChatPopup = async () => {
GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.SORT);
- const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
+ GPTPopup.Instance.setMode(GPTPopupMode.CARD);
GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
- GPTPopup.Instance.setSortDesc(sortDesc.join());
- GPTPopup.Instance.onSortComplete = (sortResult: string) => this.processGptOutput(sortResult);
+ await this.childPairStringListAndUpdateSortDesc();
};
/**
- * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups
- * @param childPairIndex
- * @param doc
- * @returns
- */
- renderButtons = (doc: Doc, cardSort: cardSortings) => {
- if (cardSort !== cardSortings.Custom) return '';
- const amButtons = Math.max(4, this.childDocs?.reduce((set, d) => this.cardSort_customField && set.add(NumCast(d[this.cardSort_customField])), new Set<number>()).size ?? 0);
- const activeButtonIndex = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
- const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6;
- return (
- <div className="card-button-container" style={{ width: `${totalWidth}px` }}>
- {numberRange(amButtons).map(i => (
- <button
- key={i}
- type="button"
- style={{ backgroundColor: activeButtonIndex === i ? '#4476f7' : '#323232' }} //
- onClick={() => this.toggleButton(i, doc)}
- />
- ))}
- </div>
- );
- };
- /**
* Actually renders all the cards
*/
- renderCards = () => {
+ @computed get renderCards() {
+ const sortedDocs = this.sortedDocs;
const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc));
+ const isEmpty = this.childDocsWithoutLinks.length === 0;
+
+ if (isEmpty) {
+ return (
+ <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}>
+ Sorry ! There are no cards in this group
+ </span>
+ );
+ }
+
// Map sorted documents to their rendered components
- return this.sortedDocs.map((doc, index) => {
- const realIndex = this.sortedDocs.filter(sortDoc => !DocumentView.SelectedDocs().includes(sortDoc)).indexOf(doc);
+ return sortedDocs.map((doc, index) => {
+ const realIndex = sortedDocs.indexOf(doc);
const calcRowIndex = this.overflowIndexCalc(realIndex);
const amCards = this.overflowAmCardsCalc(realIndex);
- const isSelected = DocumentView.SelectedDocs().includes(doc);
+ const view = DocumentView.getDocumentView(doc, this.DocumentView?.());
+ const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false;
const childScreenToLocal = () => {
this._forceChildXf;
@@ -459,41 +626,56 @@ export class CollectionCardView extends CollectionSubView() {
.scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
};
+ const translateIfSelected = () => {
+ const indexInRow = index % this._maxRowCount;
+ const rowIndex = Math.trunc(index / this._maxRowCount);
+ const rowCenterIndex = Math.min(this._maxRowCount, sortedDocs.length - rowIndex * this._maxRowCount) / 2;
+ return (rowCenterIndex - indexInRow) * 100 - 50;
+ };
+ const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
+ const vscale = Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()),
+ (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10))); // prettier-ignore
+ const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
return (
<div
key={doc[Id]}
className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
onPointerUp={() => {
+ if (DocumentView.SelectedDocs().includes(doc)) return;
// this turns off documentDecorations during a transition, then turns them back on afterward.
- SnappingManager.SetIsResizing(this.Document[Id]);
+ SnappingManager.SetIsResizing(doc[Id]);
setTimeout(
action(() => {
SnappingManager.SetIsResizing(undefined);
this._forceChildXf = !this._forceChildXf;
}),
- 700
+ 900
);
}}
style={{
- width: this.panelWidth(),
- height: 'max-content', // this.panelHeight(childPair.layout)(),
+ width: this.childPanelWidth(),
+ height: 'max-content',
transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px)
- translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px)
+ translateX(calc(${isSelected ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px))
rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
- scale(${isSelected ? 1.25 : 1})`,
- }}
+ scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`,
+ }} // prettier-ignore
onMouseEnter={() => this.setHoveredNodeIndex(index)}>
{this.displayDoc(doc, childScreenToLocal)}
- {this.renderButtons(doc, this.cardSort)}
</div>
);
});
- };
+ }
+
render() {
+ const isEmpty = this.childDocsWithoutLinks.length === 0;
+
return (
<div
className="collectionCardView-outer"
- ref={this.createDashEventsTarget}
+ ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
+ onPointerMove={e => this.onPointerMove(e.clientX, e.clientY)}
+ onDrop={this.onExternalDrop.bind(this)}
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
@@ -501,11 +683,12 @@ export class CollectionCardView extends CollectionSubView() {
<div
className="card-wrapper"
style={{
- transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`,
- height: `${100 * this.fitContentScale}%`,
+ ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
+ ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }),
+ gridAutoRows: `${100 / this.numRows}%`,
}}
onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
- {this.renderCards()}
+ {this.renderCards}
</div>
</div>
);
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 0473bb163..4951590dc 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -77,6 +77,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
NativeWidth={returnZero}
NativeHeight={returnZero}
fitWidth={this._props.childLayoutFitWidth}
+ containerViewPath={this.childContainerViewPath}
onDoubleClickScript={this.onChildDoubleClick}
renderDepth={this._props.renderDepth + 1}
LayoutTemplate={this._props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index f115bb40a..01b20d6d3 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -12,6 +12,21 @@
user-select: none;
}
}
+.collectionCarouselView-addFlashcards {
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ z-index: -1;
+ pointer-events: none;
+}
+.collectionCarouselView-recentlyMissed {
+ color: red;
+ z-index: 999;
+ position: relative;
+ left: 10px;
+ top: 10px;
+ pointer-events: none;
+}
.carouselView-back,
.carouselView-fwd,
.carouselView-star,
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 264a34680..9741c45fe 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,16 +1,15 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { computed, makeObservable } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
-import { Doc, Opt } from '../../../fields/Doc';
-import { Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { ContextMenu } from '../ContextMenu';
import { StyleProp } from '../StyleProp';
+import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -30,15 +29,36 @@ enum practiceVal {
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore
- get starField() { return this.fieldKey + "_star"; } // prettier-ignore
+ get starField() { return "#star"; } // prettier-ignore
+
+ _fadeTimer: NodeJS.Timeout | undefined;
+ _resetter: IReactionDisposer | undefined;
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
}
+ @observable _last_index = this.carouselIndex;
+ @observable _last_opacity = 1;
+
+ componentDidMount() {
+ this._resetter = reaction(
+ // automatically reset practice fields when all cards have been marked as correct
+ () => this.carouselItems.length,
+ itemsCount => {
+ if (this.layoutDoc.filterOp === cardMode.PRACTICE && !itemsCount) {
+ this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode
+ this.carouselItems.forEach(item => { // reset all the practice values
+ item[this.practiceField] = undefined;
+ });
+ }
+ } // prettier-ignore
+ );
+ }
componentWillUnmount() {
this._dropDisposer?.();
+ this._resetter?.();
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
@@ -48,43 +68,24 @@ export class CollectionCarouselView extends CollectionSubView() {
}
};
+ @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
+ @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore
@computed get carouselItems() {
- return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
- }
- @computed get marginX() {
- return NumCast(this.layoutDoc.caption_xMargin, 50);
+ return DocListCast(this.childDocList)
+ .filter(doc => doc.type !== DocumentType.LINK)
+ .filter(doc => {
+ switch (StrCast(this.layoutDoc.filterOp)) {
+ case cardMode.STAR: return !!doc[this.starField]; // show only cards that are starred
+ case cardMode.PRACTICE: return doc[this.practiceField] !== practiceVal.CORRECT;// show only cards that aren't marked as correct
+ default: return true;
+ } // prettier-ignore
+ });
}
- move = (dir: number) => {
- const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => {
- let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length;
- while (!match(this.carouselItems?.[startInd].layout) && (startInd + dir + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
- startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length;
- }
- if (match(this.carouselItems?.[startInd].layout)) {
- this.layoutDoc._carousel_index = startInd;
- return true;
- }
- return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout);
- };
- switch (StrCast(this.layoutDoc.filterOp)) {
- case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't
- if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) {
- this.layoutDoc.filterOp = undefined; // if there aren't any starred, show all cards
- }
- break;
- case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct
- if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) {
- this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode
-
- this.carouselItems.forEach(item => { // reset all the practice values
- item.layout[this.practiceField] = undefined;
- });
- }
- break;
- default: moveToCardWithField(returnTrue);
- } // prettier-ignore
- };
+ move = action((dir: number) => {
+ this._last_index = this.carouselIndex;
+ this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length;
+ });
/**
* Goes to the next Doc in the stack subject to the currently selected filter option.
@@ -107,8 +108,11 @@ export class CollectionCarouselView extends CollectionSubView() {
*/
star = (e: React.MouseEvent) => {
e.stopPropagation();
- const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)];
- curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true;
+ const curDoc = this.carouselItems[this.carouselIndex];
+ if (curDoc) {
+ if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField);
+ else TagItem.addTagToDoc(curDoc, this.starField);
+ }
};
/*
@@ -116,8 +120,8 @@ export class CollectionCarouselView extends CollectionSubView() {
*/
setPracticeVal = (e: React.MouseEvent, val: string) => {
e.stopPropagation();
- const curDoc = this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)];
- curDoc.layout[this.practiceField] = val;
+ const curDoc = this.carouselItems[this.carouselIndex];
+ curDoc && (curDoc[this.practiceField] = val);
this.advance(e);
};
@@ -132,7 +136,6 @@ export class CollectionCarouselView extends CollectionSubView() {
captionWidth = () => this._props.PanelWidth() - 2 * this.marginX;
specificMenu = (): void => {
const cm = ContextMenu.Instance;
-
const revealOptions = cm.findByDescription('Filter Flashcards');
const revealItems = revealOptions?.subitems ?? [];
revealItems.push({description: 'All', event: () => {this.layoutDoc.filterOp = undefined;}, icon: 'layer-group',}); // prettier-ignore
@@ -142,34 +145,78 @@ export class CollectionCarouselView extends CollectionSubView() {
!revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
};
childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
+
+ isChildContentActive = () =>
+ this._props.isContentActive?.() === false
+ ? false
+ : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ ? true
+ : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+ ? false
+ : undefined;
+
+ renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
+ return (
+ <DocumentView
+ {...this._props}
+ ref={overlayFunc}
+ Document={doc}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ fitWidth={undefined}
+ containerViewPath={this.childContainerViewPath}
+ setContentViewBox={undefined}
+ onDoubleClickScript={this.onContentDoubleClick}
+ onClickScript={this.onContentClick}
+ isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
+ isContentActive={this.isChildContentActive}
+ hideCaptions={showCaptions}
+ renderDepth={this._props.renderDepth + 1}
+ LayoutTemplate={this._props.childLayoutTemplate}
+ LayoutTemplateString={this._props.childLayoutString}
+ TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
+ PanelHeight={this.panelHeight}
+ />
+ );
+ };
+ /**
+ * Display an overlay of the previous card that crossfades to the next card
+ */
+ @computed get overlay() {
+ const fadeTime = 500;
+ const lastDoc = this.carouselItems?.[this._last_index];
+ return !lastDoc || this.carouselIndex === this._last_index ? null : (
+ <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}>
+ {this.renderDoc(
+ lastDoc,
+ false, // hide captions if the carousel is configured to show the captions
+ action((r: DocumentView | null) => {
+ if (r) {
+ this._fadeTimer && clearTimeout(this._fadeTimer);
+ this._last_opacity = 0;
+ this._fadeTimer = setTimeout(
+ action(() => {
+ this._last_index = -1;
+ this._last_opacity = 1;
+ }),
+ fadeTime
+ );
+ }
+ })
+ )}
+ </div>
+ );
+ }
@computed get content() {
- const index = NumCast(this.layoutDoc._carousel_index);
+ const index = this.carouselIndex;
const curDoc = this.carouselItems?.[index];
const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption);
- return !(curDoc?.layout instanceof Doc) ? null : (
+ return !curDoc ? null : (
<>
<div className="collectionCarouselView-image" key="image">
- <DocumentView
- {...this._props}
- // NativeWidth={returnZero}
- // NativeHeight={returnZero}
- fitWidth={returnTrue}
- setContentViewBox={undefined}
- containerViewPath={this._props.docViewPath}
- onDoubleClickScript={this.onContentDoubleClick}
- onClickScript={this.onContentClick}
- isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
- isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction}
- hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions
- renderDepth={this._props.renderDepth + 1}
- LayoutTemplate={this._props.childLayoutTemplate}
- LayoutTemplateString={this._props.childLayoutString}
- Document={curDoc.layout}
- TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)}
- PanelHeight={this.panelHeight}
- PanelWidth={this._props.PanelWidth}
- />
+ {this.renderDoc(curDoc, !!carouselShowsCaptions)}
+ {this.overlay}
</div>
{!carouselShowsCaptions ? null : (
<div
@@ -182,14 +229,14 @@ export class CollectionCarouselView extends CollectionSubView() {
marginLeft: this.marginX,
width: `calc(100% - ${this.marginX * 2}px)`,
}}>
- <FormattedTextBox key={index} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} TemplateDataDocument={undefined} />
+ <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} />
</div>
)}
</>
);
}
@computed get buttons() {
- if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null;
+ if (!this.carouselItems?.[this.carouselIndex]) return null;
return (
<>
<div key="back" className="carouselView-back" onClick={this.goback}>
@@ -199,7 +246,7 @@ export class CollectionCarouselView extends CollectionSubView() {
<FontAwesomeIcon icon="chevron-right" size="2x" />
</div>
<div key="star" className="carouselView-star" onClick={this.star}>
- <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" />
+ <FontAwesomeIcon icon="star" color={TagItem.docHasTag(this.carouselItems?.[this.carouselIndex], this.starField) ? 'yellow' : 'gray'} size="1x" />
</div>
<div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}>
<FontAwesomeIcon icon="xmark" color="red" size="1x" />
@@ -211,6 +258,24 @@ export class CollectionCarouselView extends CollectionSubView() {
);
}
+ /**
+ * Prompts user to add more flashcaards if they are in practice mode but there are no flashcards
+ */
+ renderAddFlashcards = () => <p
+ className="collectionCarouselView-addFlashcards"
+ style={{display: !this.carouselItems?.[this.carouselIndex] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none'}}>
+ Add flashcards!
+ </p> // prettier-ignore
+
+ /**
+ * Displays message that a flashcard was recently missed if it had previously been marked as wrong.
+ * */
+ renderRecentlyMissed = () => <p
+ className="collectionCarouselView-recentlyMissed"
+ style={{display: this.carouselItems?.[this.carouselIndex]?.[this.practiceField] === practiceVal.MISSED ? 'block' : 'none'}}>
+ Recently missed!
+ </p> // prettier-ignore
+
render() {
return (
<div
@@ -222,29 +287,8 @@ export class CollectionCarouselView extends CollectionSubView() {
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
}}>
{this.content}
- {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */}
- <p
- style={{
- display: !this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none',
- justifyContent: 'center',
- alignItems: 'center',
- height: '100%',
- zIndex: '-1',
- }}>
- Add flashcards!
- </p>
- {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */}
- <p
- style={{
- color: 'red',
- zIndex: '999',
- position: 'relative',
- left: '10px',
- top: '10px',
- display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] ? (this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED ? 'block' : 'none') : 'none',
- }}>
- Recently missed!
- </p>
+ {this.renderAddFlashcards()}
+ {this.renderRecentlyMissed()}
{this.Document._chromeHidden ? null : this.buttons}
</div>
);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e0aa79c7b..028133a6e 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -31,6 +31,7 @@ import { ScriptingRepl } from '../ScriptingRepl';
import { UndoStack } from '../UndoStack';
import './CollectionDockingView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { TabHTMLElement } from './TabDocView';
@observer
export class CollectionDockingView extends CollectionSubView() {
@@ -544,7 +545,7 @@ export class CollectionDockingView extends CollectionSubView() {
tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => {
this.tabMap.add(tab);
// InitTab is added to the tab's HTMLElement in TabDocView
- const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as { InitTab?: (tab: object) => void };
+ const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as TabHTMLElement;
tabdocviewContent?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
};
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index e1f0a3e41..ac8e37358 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -653,7 +653,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const sections = Array.from(this.Sections.entries());
return sections.reduce((list, sec, i) => {
list.push(this.sectionNoteTaking(sec[0], sec[1]));
- // eslint-disable-next-line react/no-array-index-key
i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
return list;
}, [] as JSX.Element[]);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index e97ee713e..1ac0b6d70 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as CSS from 'csstype';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
@@ -540,7 +539,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
if (this.pivotField) {
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
- // eslint-disable-next-line prefer-destructuring
type = types[0];
}
}
@@ -577,7 +575,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
- // eslint-disable-next-line prefer-destructuring
type = types[0];
}
const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))));
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 6aca8f2ca..5d32482c3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -122,6 +122,9 @@ export function CollectionSubView<X>() {
);
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
+ /**
+ * This is the raw, stored list of children on a collection. If you modify this list, the database will be updated
+ */
@computed get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
@@ -218,7 +221,6 @@ export function CollectionSubView<X>() {
if (!cursors) {
proto.cursors = cursors = new List<CursorField>();
}
- // eslint-disable-next-line no-cond-assign
if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
cursors[ind].setPosition(pos);
} else {
@@ -251,7 +253,7 @@ export function CollectionSubView<X>() {
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
const { docDragData } = de.complete;
- if (docDragData) {
+ if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) {
let added;
const dropAction = docDragData.dropAction || docDragData.userDropAction;
const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 31b6be927..f56ea9d76 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -57,6 +57,7 @@ interface TabMiniThumbProps {
miniLeft: () => number;
}
+export type TabHTMLElement = HTMLDivElement & { InitTab?: (tab: object) => void };
@observer
class TabMiniThumb extends React.Component<TabMiniThumbProps> {
render() {
@@ -193,8 +194,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
.filter(tv => tv._document)
.map(tv => tv._document!);
}
- _mainCont: HTMLDivElement | null = null;
+ _mainCont: TabHTMLElement | null = null;
_tabReaction: IReactionDisposer | undefined;
+ _lastSelection = 0; // time when view was last selected - used to re-select views that get invalidated when selected
/**
* Adds a document to the presentation view
@@ -273,19 +275,24 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
+ // Flag indicating that when a tab is activated, it should not select it's document.
+ // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected)
+ public static DontSelectOnActivate = 'dontSelectOnActivate';
+
+ public static IsSelected = (doc?: Doc) => {
+ return DocumentView.getViews(doc).some(dv => dv?.IsSelected);
+ };
+
static Activate = (tabDoc: Doc) => {
const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
- // static ActivateTabView(doc: Doc) {
- // const tabView = Array.from(TabDocView._allTabs).find(view => view._document === doc);
- // if (!tabView?._activated && tabView?._document) {
- // TabDocView.Activate(tabView?._document);
- // return tabView;
- // }
- // return undefined;
- // }
+
+ get stack() { return this._props.glContainer.parent.parent; } // prettier-ignore
+ get tab() { return this._props.glContainer.tab; } // prettier-ignore
+ get view() { return this._view; } // prettier-ignore
+
constructor(props: TabDocViewProps) {
super(props);
makeObservable(this);
@@ -299,37 +306,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
@observable _hovering = false;
@observable _isActive: boolean = false;
@observable _isAnyChildContentActive = false;
- public static IsSelected = (doc?: Doc) => {
- if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) {
- return true;
- }
- return false;
- };
- @computed get _isUserActivated() {
- return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive;
- }
- get _isContentActive() {
- return this._isUserActivated || this._hovering;
- }
@observable _document: Doc | undefined = undefined;
@observable _view: DocumentView | undefined = undefined;
+ @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver. this hack allows the resize observer to trigger a react update
- @computed get layoutDoc() {
- return this._document && Doc.Layout(this._document);
- }
-
- get stack() {
- return this._props.glContainer.parent.parent;
- }
- get tab() {
- return this._props.glContainer.tab;
- }
- get view() {
- return this._view;
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- _lastTab: any;
- _lastView: DocumentView | undefined;
+ @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore
+ @computed get isUserActivated() { return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore
+ @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore
@action
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -413,7 +396,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
color === variant
? DashColor(color)
.fade(
- this._isUserActivated
+ this.isUserActivated
? 0
: this._hovering
? 0.25
@@ -522,19 +505,14 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
this._props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged);
}
- // Flag indicating that when a tab is activated, it should not select it's document.
- // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected)
- public static DontSelectOnActivate = 'dontSelectOnActivate';
-
- @action.bound
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- private onActiveContentItemChanged(contentItem: any) {
+ onActiveContentItemChanged = action((contentItem: any) => {
if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false));
!this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
}
- }
+ });
// adds a tab to the layout based on the locaiton parameter which can be:
// close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab,
@@ -551,7 +529,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
const panelName = whereFields.length > 1 ? whereFields.lastElement() : '';
if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]);
- // prettier-ignore
switch (whereFields[0]) {
case undefined:
case OpenWhere.lightbox: return LightboxView.Instance.AddDocTab(docs[0], location);
@@ -559,7 +536,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
case OpenWhere.replace: return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue);
case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue);
case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue);
- }
+ } // prettier-ignore
};
remDocTab = (doc: Doc | Doc[]) => {
if (doc === this._document) {
@@ -571,8 +548,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
};
getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
-
- @action
focusFunc = () => {
if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
@@ -580,7 +555,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
return undefined;
};
active = () => this._isActive;
- @observable _forceInvalidateScreenToLocal = 0;
ScreenToLocalTransform = () => {
this._forceInvalidateScreenToLocal;
const { translateX, translateY } = ClientUtils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement);
@@ -594,47 +568,44 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
whenChildContentActiveChanges = (isActive: boolean) => {
this._isAnyChildContentActive = isActive;
};
- isContentActive = () => this._isContentActive;
+ isContentActiveFunc = () => this.isContentActive;
waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
- @computed get docView() {
- return !this._activated || !this._document ? null : (
- <>
- <DocumentView
- key={this._document[Id]}
- ref={action((r: DocumentView) => {
- this._lastView && DocumentView.removeView(this._lastView);
- this._view = r;
- this._lastView = this._view;
- })}
- renderDepth={0}
- LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined}
- hideTitle={this._props.keyValue}
- Document={this._document}
- TemplateDataDocument={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
- waitForDoubleClickToClick={this.waitForDoubleClick}
- isContentActive={this.isContentActive}
- isDocumentActive={returnFalse}
- PanelWidth={this.PanelWidth}
- PanelHeight={this.PanelHeight}
- styleProvider={DefaultStyleProvider}
- childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter}
- childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter}
- searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
- addDocument={undefined}
- removeDocument={this.remDocTab}
- addDocTab={this.addDocTab}
- suppressSetHeight={!!this._document._layout_fitWidth}
- ScreenToLocalTransform={this.ScreenToLocalTransform}
- dontCenter="y"
- whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
- focus={this.focusFunc}
- containerViewPath={returnEmptyDocViewList}
- pinToPres={TabDocView.PinDoc}
- />
- {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />}
- </>
- );
- }
+ renderDocView = (doc: Doc) => (
+ <DocumentView
+ key={doc[Id]}
+ ref={action((r: DocumentView) => {
+ const now = Date.now();
+ this._lastSelection = this._view?.IsSelected ? now : this._lastSelection;
+ if (this._view) DocumentView.removeView(this._view);
+ this._view = r;
+ if (this._view && now - this._lastSelection < 1000) this._view.select(false);
+ })}
+ renderDepth={0}
+ LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined}
+ hideTitle={this._props.keyValue}
+ Document={doc}
+ TemplateDataDocument={!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined}
+ waitForDoubleClickToClick={this.waitForDoubleClick}
+ isContentActive={this.isContentActiveFunc}
+ isDocumentActive={returnFalse}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
+ styleProvider={DefaultStyleProvider}
+ childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter}
+ childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter}
+ searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
+ addDocument={undefined}
+ removeDocument={this.remDocTab}
+ addDocTab={this.addDocTab}
+ suppressSetHeight={!!doc._layout_fitWidth}
+ ScreenToLocalTransform={this.ScreenToLocalTransform}
+ dontCenter="y"
+ whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
+ focus={this.focusFunc}
+ containerViewPath={returnEmptyDocViewList}
+ pinToPres={TabDocView.PinDoc}
+ />
+ );
render() {
return (
@@ -647,23 +618,21 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
onPointerLeave={action(() => { this._hovering = false; })} // prettier-ignore
onDragOver={action(() => { this._hovering = true; })} // prettier-ignore
onDragLeave={action(() => { this._hovering = false; })} // prettier-ignore
- ref={ref => {
+ ref={(ref: TabHTMLElement) => {
+ // "add" an InitTab function to this div to call from tabCreated in CollectionDockingView when div is reused
this._mainCont = ref;
if (this._mainCont) {
- if (this._lastTab) {
- this._view && DocumentView.removeView(this._view);
- }
- this._lastTab = this.tab;
- (this._mainCont as { InitTab?: (tab: object) => void }).InitTab = (tab: object) => this.init(tab, this._document);
- DocServer.GetRefField(this._props.documentId).then(
- action(doc => {
- doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
- })
- );
- ref && new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref);
+ this._mainCont.InitTab = (tab: object) => this.init(tab, this._document);
+ DocServer.GetRefField(this._props.documentId).then(action(doc => {
+ doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
+ })); // prettier-ignore
+ new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(this._mainCont);
}
}}>
- {this.docView}
+ {!this._activated || !this._document ? null : this.renderDocView(this._document)}
+ {this.disableMinimap() || !this._document ? null : (
+ <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />
+ )}
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9aac724cc..57b2d1ccf 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { Bezier } from 'bezier-js';
import { Colors } from 'browndash-components';
import { Property } from 'csstype';
@@ -1178,7 +1177,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
for (let j = 0; j < otherCtrlPts.length - 3; j += 4) {
const neighboringSegment = i === j || i === j - 4 || i === j + 4;
// Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
- // eslint-disable-next-line no-continue
if (ink?.Document === otherInk.Document && neighboringSegment) continue;
const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
@@ -1550,8 +1548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const childData = entry.pair.data;
return (
<CollectionFreeFormDocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any
- {...(OmitKeys(entry, ['replica', 'pair']).omit as any)}
+ {...(OmitKeys(entry, ['replica', 'pair']).omit as { x: number; y: number; z: number; width: number; height: number })}
key={childLayout[Id] + (entry.replica || '')}
Document={childLayout}
reactParent={this}
@@ -1855,7 +1852,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!code.includes('dashDiv')) {
const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true });
if (script.compiled) script.run({ this: this.DocumentView?.() });
- // eslint-disable-next-line no-eval
} else code && !first && eval?.(code);
},
{ fireImmediately: true }
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index f9f6c81ab..534f67927 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -179,7 +179,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
})}>
{FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => {
- const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.');
+ const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png'];
return (
<div
className="image-wrapper"
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
index e419e522c..033d1590d 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx
@@ -139,9 +139,9 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
toggleDisplayInformation = () => {
this._displayImageInformation = !this._displayImageInformation;
if (this._displayImageInformation) {
- this._selectedImages.forEach(doc => (doc[DocData].showTags = true));
+ this._selectedImages.forEach(doc => (doc._layout_showTags = true));
} else {
- this._selectedImages.forEach(doc => (doc[DocData].showTags = false));
+ this._selectedImages.forEach(doc => (doc._layout_showTags = false));
}
};
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index afb4e2eff..a4bbec6b1 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -2,20 +2,23 @@
import { Colors } from 'browndash-components';
import { action, runInAction } from 'mobx';
import { aggregateBounds } from '../../../Utils';
-import { Doc, DocListCast, FieldType, NumListCast, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
-import { List } from '../../../fields/List';
import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { Gestures } from '../../../pen-gestures/GestureTypes';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SnappingManager } from '../../util/SnappingManager';
import { UndoManager, undoable } from '../../util/UndoManager';
import { GestureOverlay } from '../GestureOverlay';
import { InkingStroke } from '../InkingStroke';
import { CollectionFreeFormView, MarqueeView } from '../collections/collectionFreeForm';
+import { MainView } from '../MainView';
+import { PropertiesView } from '../PropertiesView';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import {
ActiveEraserWidth,
@@ -36,6 +39,7 @@ import { ImageBox } from '../nodes/ImageBox';
import { VideoBox } from '../nodes/VideoBox';
import { WebBox } from '../nodes/WebBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function IsNoneSelected() {
@@ -133,20 +137,25 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
const selected = DocumentView.SelectedDocs().lastElement();
+
+ function isAttrFiltered(attribute: string) {
+ return StrListCast(selected._childFilters).some(filter => filter.includes(attribute));
+ }
+
// prettier-ignore
- const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4',
+ const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'tag',
{
waitForRender?: boolean;
checkResult: (doc: Doc) => boolean;
setDoc: (doc: Doc, dv: DocumentView) => void;
}> = new Map([
['grid', {
- checkResult: (doc:Doc) => BoolCast(doc?._freeform_backgroundGrid, false),
- setDoc: (doc:Doc) => { doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid; },
+ checkResult: (doc: Doc) => BoolCast(doc?._freeform_backgroundGrid, false),
+ setDoc: (doc: Doc) => { doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid; },
}],
['snaplines', {
- checkResult: (doc:Doc) => BoolCast(doc?._freeform_snapLines, false),
- setDoc: (doc:Doc) => { doc._freeform_snapLines = !doc._freeform_snapLines; },
+ checkResult: (doc: Doc) => BoolCast(doc?._freeform_snapLines, false),
+ setDoc: (doc: Doc) => { doc._freeform_snapLines = !doc._freeform_snapLines; },
}],
['viewAll', {
checkResult: (doc: Doc) => BoolCast(doc?._freeform_fitContentsToBox, false),
@@ -166,120 +175,128 @@ ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid'
}],
['clusters', {
waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
- checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false),
- setDoc: (doc:Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
+ checkResult: (doc: Doc) => BoolCast(doc?._freeform_useClusters, false),
+ setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
}],
['flashcards', {
checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
- setDoc: (doc: Doc, dv: DocumentView) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards,
+ setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
}],
['time', {
checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "time",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "time",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "time" ? doc.cardSort = '' : doc.cardSort = 'time'}, // prettier-ignore
}],
['docType', {
checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "type",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "type",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "type" ? doc.cardSort = '' : doc.cardSort = 'type'}, // prettier-ignore
}],
['color', {
checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "color",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "color",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "color" ? doc.cardSort = '' : doc.cardSort = 'color'}, // prettier-ignore
}],
- ['links', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "links",
- setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "links",
+ ['tag', {
+ checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "tag",
+ setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "tag" ? doc.cardSort = '' : doc.cardSort = 'tag'}, // prettier-ignore
}],
- ['like', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "like",
+ ['up', {
+ checkResult: (doc: Doc) => BoolCast(!doc?.cardSort_isDesc),
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "like";
- doc.cardSort_visibleSortGroups = new List<number>();
- }
+ doc.cardSort_isDesc = false;
+ },
}],
- ['star', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "star",
+ ['down', {
+ checkResult: (doc: Doc) => BoolCast(doc?.cardSort_isDesc),
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "star";
- doc.cardSort_visibleSortGroups = new List<number>();
- }
+ doc.cardSort_isDesc = true;
+ },
}],
- ['idea', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "idea",
+ ['toggle-chat', {
+ checkResult: (doc: Doc) => GPTPopup.Instance.visible,
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "idea";
- doc.cardSort_visibleSortGroups = new List<number>();
- }
+ if (GPTPopup.Instance.visible){
+ doc.cardSort = ''
+ GPTPopup.Instance.setVisible(false);
+
+ } else {
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.CARD);
+ GPTPopup.Instance.setCardsDoneLoading(true);
+
+ }
+
+
+ },
}],
- ['chat', {
- checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "chat",
+ ['pile', {
+ checkResult: (doc: Doc) => doc._type_collection == CollectionViewType.Freeform,
setDoc: (doc: Doc, dv: DocumentView) => {
- doc.cardSort = "custom";
- doc.cardSort_customField = "chat";
- doc.cardSort_visibleSortGroups = new List<number>();
+ doc._type_collection = CollectionViewType.Freeform;
+ const newCol = Docs.Create.CarouselDocument(DocListCast(doc[Doc.LayoutFieldKey(doc)]), {
+ _width: 250,
+ _height: 200,
+ _layout_fitWidth: false,
+ _layout_autoHeight: true,
+ });
+
+
+ const iconMap: { [key: number]: string } = {
+ 0: 'star',
+ 1: 'heart',
+ 2: 'cloud',
+ 3: 'bolt'
+ };
+
+ for (let i=0; i<4; i++){
+ if (isAttrFiltered(iconMap[i])){
+ newCol[iconMap[i]] = true
+ }
+ }
+
+ newCol && dv.ComponentView?.addDocument?.(newCol);
+ DocumentView.showDocument(newCol, { willZoomCentered: true })
+
},
}],
]);
- for (let i = 0; i < 8; i++) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- map.set((i + 1 + '') as any, {
- checkResult: (doc: Doc) => NumListCast(doc?.cardSort_visibleSortGroups).includes(i),
- setDoc: (doc: Doc, dv: DocumentView) => {
- const list = NumListCast(doc.cardSort_visibleSortGroups);
- doc.cardSort_visibleSortGroups = new List<number>(list.includes(i) ? list.filter(d => d !== i) : [...list, i]);
- },
- });
- }
if (checkResult) {
return map.get(attr)?.checkResult(selected);
}
+
const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
DocumentView.Selected().map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
setTimeout(() => batch.end(), 100);
return undefined;
});
-ScriptingGlobals.add(function cardHasLabel(label: string) {
+/**
+ * Applies a filter to the selected document (or, if the settings button is pressed, opens the filter panel)
+ */
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function handleTags(value: string, checkResult?: boolean) {
const selected = DocumentView.SelectedDocs().lastElement();
- const labelNum = Number(label) - 1;
- return labelNum < 4 || (selected && DocListCast(selected[Doc.LayoutFieldKey(selected)]).some(doc => doc[StrCast(selected.cardSort_customField)] == labelNum));
-}, '');
+ const isOptions = value === '-opts-';
-// ScriptingGlobals.add(function setCardSortAttr(attr: 'time' | 'docType' | 'color', value: any, checkResult?: boolean) {
-// // const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-// const selected = SelectionManager.Docs.lastElement();
-// // prettier-ignore
-// const map: Map<'time' | 'docType' | 'color', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc, dv:DocumentView) => void;}> = new Map([
-// ['time', {
-// checkResult: (doc:Doc) => StrCast(doc?.cardSort),
-// setDoc: (doc:Doc,dv:DocumentView) => doc.cardSort = "time",
-// }],
-// ['docType', {
-// checkResult: (doc:Doc) => StrCast(doc?.cardSort),
-// setDoc: (doc:Doc,dv:DocumentView) => doc.cardSort = "type",
-// }],
-// ['color', {
-// checkResult: (doc:Doc) => StrCast(doc?.cardSort),
-// setDoc: (doc:Doc,dv:DocumentView) => doc.cardSort = "color",
-// }],
-// // ['custom', {
-// // checkResult: () => RichTextMenu.Instance.textAlign,
-// // setDoc: () => value && editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value),
-// // }]
-// // ,
-// ]);
-
-// if (checkResult) {
-// return map.get(attr)?.checkResult(selected);
-// }
-
-// console.log('hey')
-// SelectionManager.Views.map(dv => map.get(attr)?.setDoc(dv.layoutDoc, dv));
-// console.log('success')
-// });
+ const isAttrFiltered = (attr: string) =>
+ StrListCast(selected._childFilters)
+ .map(filter => filter.split(Doc.FilterSep))
+ .some(([key, val]) => key === 'tags' && val === attr);
+
+ if (checkResult) {
+ return isOptions ? false : isAttrFiltered(value);
+ }
+
+ if (!isOptions) {
+ isAttrFiltered(value) ? Doc.setDocFilter(selected, 'tags', value, 'remove') : Doc.setDocFilter(selected, 'tags', value, 'check');
+ } else {
+ SnappingManager.PropertiesWidth < 5 && SnappingManager.SetPropertiesWidth(0);
+ SnappingManager.SetPropertiesWidth(MainView.Instance.propertiesWidth() < 15 ? 250 : 0);
+ PropertiesView.Instance?.CloseAll();
+ runInAction(() => (PropertiesView.Instance.openFilters = SnappingManager.PropertiesWidth > 5));
+ }
+
+ return undefined;
+}, '');
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: string | number, checkResult?: boolean) {
@@ -314,6 +331,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
]);
if (checkResult) {
+ // console.log(map.get(attr)?.checkResult() + "font check result")
return map.get(attr)?.checkResult();
}
map.get(attr)?.setDoc?.();
@@ -474,6 +492,7 @@ function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult?
ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
+// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function activeEraserTool() {
return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser);
}, 'returns the current eraser tool');
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c279badf4..85fd42ddf 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -67,6 +67,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
hideCaptions?: boolean;
contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
dontCenter?: 'x' | 'y' | 'xy';
+ showTags?: boolean;
childHideDecorationTitle?: boolean;
childHideResizeHandles?: boolean;
childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
@@ -1137,6 +1138,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
@observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing
@observable public TagPanelHeight = 0;
+ @computed get showTags() {
+ return this.Document._layout_showTags || this._props.showTags;
+ }
+
@computed private get shouldNotScale() {
return (this.layout_fitWidth && !this.nativeWidth) || this.ComponentView?.isUnstyledView?.();
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index c269c7bcb..683edba16 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,5 +1,3 @@
-/* eslint-disable react/no-unused-prop-types */
-/* eslint-disable react/require-default-props */
import { Property } from 'csstype';
import { computed } from 'mobx';
import { observer } from 'mobx-react';
@@ -21,6 +19,7 @@ export type FocusFuncType = (doc: Doc, options: FocusViewOptions) => Opt<number>
// eslint-disable-next-line no-use-before-define
export type StyleProviderFuncType = (
doc: Opt<Doc>,
+ // eslint-disable-next-line no-use-before-define
props: Opt<FieldViewProps>,
property: string
) =>
@@ -65,6 +64,7 @@ export interface FieldViewSharedProps {
containerViewPath?: () => DocumentView[];
fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
isGroupActive?: () => string | undefined; // is this document part of a group that is active
+ // eslint-disable-next-line no-use-before-define
setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
PanelWidth: () => number;
PanelHeight: () => number;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 7a09ad9e2..0e4288574 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -192,7 +192,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
} else {
text = script?.script.run({ this: this.Document, value: '', _readOnly_: true }).result as string;
// text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- getStyle = (val: string) => ({ fontFamily: val });
+ if (this.Document.title === 'Font') getStyle = (val: string) => ({ fontFamily: val }); // bcz: major hack to style the font dropdown items --- needs to become part of the dropdown's metadata
}
// Get items to place into the list
@@ -267,6 +267,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
const items = DocListCast(this.dataDoc.data);
const multiDoc = this.Document;
+ const selectedItem = StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType ?? StrCast(multiDoc.toolType));
+ console.log(selectedItem);
return (
<MultiToggle
tooltip={`Toggle ${tooltip}`}
@@ -282,7 +284,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
tooltip: StrCast(item.toolTip),
val: StrCast(item.toolType),
}))}
- selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType ?? StrCast(multiDoc.toolType))}
+ selectedVal={selectedItem}
setSelectedVal={(val: string | number) => {
const itemDoc = items.find(item => item.toolType === val);
itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: val, _readOnly_: false });
diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss
new file mode 100644
index 000000000..90cc06092
--- /dev/null
+++ b/src/client/views/nodes/IconTagBox.scss
@@ -0,0 +1,26 @@
+@import '../global/globalCssVariables.module.scss';
+
+.card-button-container {
+ display: flex;
+ position: relative;
+ pointer-events: none;
+ background-color: rgb(218, 218, 218);
+ border-radius: 50px;
+ align-items: center;
+ gap: 5px;
+ padding-left: 5px;
+ padding-right: 5px;
+ padding-top: 2px;
+ padding-bottom: 2px;
+
+ button {
+ pointer-events: auto;
+ width: 20px;
+ height: 20px;
+ margin: auto;
+ padding: 0;
+ border-radius: 50%;
+ background-color: $dark-gray;
+ background-color: transparent;
+ }
+}
diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx
new file mode 100644
index 000000000..292f9b523
--- /dev/null
+++ b/src/client/views/nodes/IconTagBox.tsx
@@ -0,0 +1,95 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import { action, computed, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { StrCast } from '../../../fields/Types';
+import { SnappingManager } from '../../util/SnappingManager';
+import { undoable } from '../../util/UndoManager';
+import { MainView } from '../MainView';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { PropertiesView } from '../PropertiesView';
+import { TagItem } from '../TagsView';
+import { DocumentView } from './DocumentView';
+import './IconTagBox.scss';
+
+export interface IconTagProps {
+ Views: DocumentView[];
+ IsEditing: boolean;
+}
+
+/**
+ * Renders the icon tags that rest under the document. The icons rendered are determined by the values of
+ * each icon in the userdoc.
+ */
+@observer
+export class IconTagBox extends ObservableReactComponent<IconTagProps> {
+ constructor(props: IconTagProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ @computed get View() { return this._props.Views.lastElement(); } // prettier-ignore
+ @computed get currentScale() { return this.View?.screenToLocalScale(); } // prettier-ignore
+
+ /**
+ * Sets or removes the specified tag
+ * @param tag tag name (should begin with '#')
+ * @param state flag to add or remove the metadata
+ */
+ setIconTag = undoable((tag: string, state: boolean) => {
+ this._props.Views.forEach(view => {
+ state && TagItem.addTagToDoc(view.dataDoc, tag);
+ !state && TagItem.removeTagFromDoc(view.dataDoc, tag);
+ });
+ }, 'toggle card tag');
+
+ /**
+ * Returns a renderable version of the button Doc that is colorized to indicate
+ * whether the doc has the associated tag set on it or not.
+ * @param doc doc to test
+ * @param key metadata icon button
+ * @returns an icon for the metdata button
+ */
+ getButtonIcon = (doc: Doc, key: Doc): JSX.Element => {
+ const icon = StrCast(key.icon) as IconProp;
+ const tag = StrCast(key.toolType);
+ const isActive = TagItem.docHasTag(doc, tag);
+ const color = isActive ? '#4476f7' : '#323232'; // TODO should use theme colors
+
+ return <FontAwesomeIcon icon={icon} style={{ color, height: '20px', width: '20px' }} />;
+ };
+
+ /**
+ * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups
+ */
+ render() {
+ const buttons = Doc.MyFilterHotKeys
+ .map(key => ({ key, tag: StrCast(key.toolType) }))
+ .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected.length === 1 && this.View.IsSelected))
+ .map(({ key, tag }) => (
+ <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove this card from the {tag} group</div>}>
+ <button
+ type="button"
+ onPointerDown={e =>
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => {
+ this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag));
+ clickEv.stopPropagation();
+ })
+ }>
+ {this.getButtonIcon(this.View.Document, key)}
+ </button>
+ </Tooltip>
+ )); // prettier-ignore
+
+ return !buttons.length ? null : (
+ <div className="card-button-container" style={{ fontSize: '50px' }}>
+ {buttons}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index be3525544..aa4376bb2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -368,7 +368,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
TraceMobx();
const backColor = DashColor((this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string) ?? Colors.WHITE);
- const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1;
+ // allow use case where the image is transparent when the alpha value is to smallest possible value from UI (alpha = 1 out of 255)
+ const backAlpha = backColor.alpha() < 0.015 && backColor.alpha() > 0 ? backColor.alpha() : 1;
const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
const { nativeWidth, nativeHeight /* , nativeOrientation */ } = this.nativeSize;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 1d3251496..84d3fc748 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -283,6 +283,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
ele.append(contents);
}
this._selectionHTML = ele?.innerHTML;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
/* empty */
}
@@ -569,7 +570,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const draggedDoc = dragData.droppedDocuments.lastElement();
let added: Opt<boolean>;
const dropAction = dragData.dropAction || dragData.userDropAction;
- if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) {
+ if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) {
// replace text contents when dragging with Alt
if (de.altKey) {
const fieldKey = Doc.LayoutFieldKey(draggedDoc);
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index e0d6c7c05..f58434906 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -404,9 +404,9 @@ export class RichTextRules {
if (!tags.includes(tag)) {
tags.push(tag);
this.Document[DocData].tags = new List<string>(tags);
- this.Document[DocData].showTags = true;
+ this.Document._layout_showTags = true;
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: tag.startsWith('@') ? tag.replace(/^@/, '') : '#' + tag });
return state.tr
.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end)))
.replaceSelectionWith(fieldView, true)
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 6d8793f82..0247dc10c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -7,10 +7,13 @@ $highlightedText: #82e0ff;
.summary-box {
position: fixed;
- bottom: 10px;
- right: 10px;
+ top: 115px;
+ left: 75px;
width: 250px;
+ height: 200px;
min-height: 200px;
+ min-width: 180px;
+
border-radius: 16px;
padding: 16px;
padding-bottom: 0;
@@ -21,6 +24,18 @@ $highlightedText: #82e0ff;
background-color: #ffffff;
box-shadow: 0 2px 5px #7474748d;
color: $textgrey;
+ resize: both; /* Allows resizing */
+ overflow: auto;
+
+ .resize-handle {
+ width: 10px;
+ height: 10px;
+ background: #ccc;
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ cursor: se-resize;
+ }
.summary-heading {
display: flex;
@@ -51,25 +66,76 @@ $highlightedText: #82e0ff;
.content-wrapper {
padding-top: 10px;
min-height: 50px;
- max-height: 150px;
+ // max-height: 150px;
overflow-y: auto;
+ height: 100%
}
.btns-wrapper-gpt {
- height: 50px;
+ height: 100%;
display: flex;
justify-content: center;
align-items: center;
- transform: translateY(30px);
+ flex-direction: column;
+
+ .inputWrapper{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 60px;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ background-color: white;
+
+ }
.searchBox-input{
- transform: translateY(-15px);
- height: 50px;
+ height: 40px;
border-radius: 10px;
+ position: absolute;
+ bottom: 10px;
border-color: #5b97ff;
+ width: 90%
}
+ .chat-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ max-height: calc(100vh - 80px);
+ overflow-y: auto;
+ padding-bottom: 60px;
+ }
+
+ .chat-bubbles {
+ margin-top: 20px;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ }
+
+ .chat-bubble {
+ padding: 10px;
+ margin-bottom: 10px;
+ border-radius: 10px;
+ max-width: 60%;
+ }
+
+ .user-message {
+ background-color: #283d53;
+ align-self: flex-end;
+ color: whitesmoke;
+ }
+
+ .chat-message {
+ background-color: #367ae7;
+ align-self: flex-start;
+ color:whitesmoke;
+ }
+
+
.summarizing {
@@ -78,16 +144,14 @@ $highlightedText: #82e0ff;
}
- }
- button {
- font-size: 9px;
- padding: 10px;
- color: #ffffff;
- background-color: $button;
- border-radius: 5px;
+
+
+
}
+
+
.text-btn {
&:hover {
background-color: $button;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index a37e73e27..d5f5f620c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -3,7 +3,7 @@ import { Button, IconButton, Type } from 'browndash-components';
import { action, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { CgClose } from 'react-icons/cg';
+import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
import ReactLoading from 'react-loading';
import { TypeAnimation } from 'react-type-animation';
import { ClientUtils } from '../../../../ClientUtils';
@@ -11,13 +11,14 @@ import { Doc } from '../../../../fields/Doc';
import { NumCast, StrCast } from '../../../../fields/Types';
import { Networking } from '../../../Network';
import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
-import { Docs } from '../../../documents/Documents';
import { DocUtils } from '../../../documents/DocUtils';
+import { Docs } from '../../../documents/Documents';
+import { SettingsManager } from '../../../util/SettingsManager';
+import { SnappingManager } from '../../../util/SnappingManager';
import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { DocumentView } from '../../nodes/DocumentView';
import { AnchorMenu } from '../AnchorMenu';
import './GPTPopup.scss';
-import { SettingsManager } from '../../../util/SettingsManager';
-import { SnappingManager } from '../../../util/SnappingManager';
export enum GPTPopupMode {
SUMMARY,
@@ -25,7 +26,15 @@ export enum GPTPopupMode {
IMAGE,
FLASHCARD,
DATA,
+ CARD,
SORT,
+ QUIZ,
+}
+
+export enum GPTQuizType {
+ CURRENT = 0,
+ CHOOSE = 1,
+ MULTIPLE = 2,
}
interface GPTPopupProps {}
@@ -34,6 +43,8 @@ interface GPTPopupProps {}
export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
// eslint-disable-next-line no-use-before-define
static Instance: GPTPopup;
+ private messagesEndRef: React.RefObject<HTMLDivElement>;
+
@observable private chatMode: boolean = false;
private correlatedColumns: string[] = [];
@@ -63,7 +74,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
};
@observable
public dataJson: string = '';
- public dataChatPrompt: string | null = null;
+ public dataChatPrompt: string | undefined = undefined;
@action
public setDataJson = (text: string) => {
if (text === '') this.dataChatPrompt = '';
@@ -140,7 +151,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.sortDesc = t;
};
- @observable onSortComplete?: (sortResult: string) => void;
+ @observable onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
+ @observable onQuizRandom?: () => void;
@observable cardsDoneLoading = false;
@action setCardsDoneLoading(done: boolean) {
@@ -148,22 +160,157 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
this.cardsDoneLoading = done;
}
+ @observable sortRespText: string = '';
+
+ @action setSortRespText(resp: string) {
+ this.sortRespText = resp;
+ }
+
+ @observable chatSortPrompt: string = '';
+
+ sortPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.chatSortPrompt = e.target.value;
+ });
+
+ @observable quizAnswer: string = '';
+
+ quizAnswerChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.quizAnswer = e.target.value;
+ });
+
+ @observable conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
+
+ /**
+ * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
+ * @returns
+ */
+ generateQuiz = async () => {
+ this.setLoading(true);
+ this.setSortDone(false);
+
+ const quizType = this.quizMode;
+
+ const selected = DocumentView.SelectedDocs().lastElement();
+
+ const questionText = 'Question: ' + StrCast(selected['gptInputText']);
+
+ if (StrCast(selected['gptRubric']) === '') {
+ const rubricText = 'Rubric: ' + (await this.generateRubric(StrCast(selected['gptInputText']), selected));
+ }
+
+ const rubricText = 'Rubric: ' + StrCast(selected['gptRubric']);
+ const queryText = questionText + ' UserAnswer: ' + this.quizAnswer + '. ' + 'Rubric' + rubricText;
+
+ try {
+ const res = await gptAPICall(queryText, GPTCallType.QUIZ);
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ this.setQuizResp(res);
+ this.conversationArray.push(res);
+
+ this.setLoading(false);
+ this.setSortDone(true);
+ } catch (err) {
+ console.error('GPT call failed');
+ }
+
+ if (this.onQuizRandom) {
+ this.onQuizRandom();
+ }
+ };
+
+ /**
+ * Generates a rubric by which to compare the user's answer to
+ * @param inputText user's answer
+ * @param doc the doc the user is providing info about
+ * @returns gpt's response
+ */
+ generateRubric = async (inputText: string, doc: Doc) => {
+ try {
+ const res = await gptAPICall(inputText, GPTCallType.RUBRIC);
+ doc['gptRubric'] = res;
+ return res;
+ } catch (err) {
+ console.error('GPT call failed');
+ }
+ };
+
+ @observable private regenerateCallback: (() => Promise<void>) | null = null;
+
+ /**
+ * Callback function that causes the card view to update the childpair string list
+ * @param callback
+ */
+ @action public setRegenerateCallback(callback: () => Promise<void>) {
+ this.regenerateCallback = callback;
+ }
+
public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
public createFilteredDoc: (axes?: string[]) => boolean = () => false;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+ @observable quizRespText: string = '';
+
+ @action setQuizResp(resp: string) {
+ this.quizRespText = resp;
+ }
+
/**
- * Sorts cards in the CollectionCardDeckView
+ * Generates a response to the user's question depending on the type of their question
*/
- generateSort = async () => {
+ generateCard = async () => {
+ console.log(this.chatSortPrompt + 'USER PROMPT');
this.setLoading(true);
this.setSortDone(false);
+ if (this.regenerateCallback) {
+ await this.regenerateCallback();
+ }
+
try {
- const res = await gptAPICall(this.sortDesc, GPTCallType.SORT);
+ // const res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
+ const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
+ const questionNumber = questionType.split(' ')[0];
+ console.log(questionType);
+ let res = '';
+
+ switch (questionNumber) {
+ case '1':
+ case '2':
+ case '4':
+ res = await gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
+ break;
+ case '6':
+ res = await gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
+ break;
+ default:
+ const selected = DocumentView.SelectedDocs().lastElement();
+ const questionText = StrCast(selected!['gptInputText']);
+
+ res = await gptAPICall(questionText, GPTCallType.INFO, this.chatSortPrompt);
+ break;
+ }
+
// Trigger the callback with the result
if (this.onSortComplete) {
- this.onSortComplete(res || 'Something went wrong :(');
+ this.onSortComplete(res || 'Something went wrong :(', questionNumber, questionType.split(' ').slice(1).join(' '));
+
+ let explanation = res;
+
+ if (questionType != '5' && questionType != '3') {
+ // Extract explanation surrounded by ------ at the top or both at the top and bottom
+ const explanationMatch = res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) || [];
+ explanation = explanationMatch[1] ? explanationMatch[1].trim() : 'No explanation found';
+ }
+
+ // Set the extracted explanation to sortRespText
+ this.setSortRespText(explanation);
+ this.conversationArray.push(this.sortRespText);
+ this.scrollToBottom();
+
console.log(res);
}
} catch (err) {
@@ -305,67 +452,143 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
super(props);
makeObservable(this);
GPTPopup.Instance = this;
+ this.messagesEndRef = React.createRef();
}
+ scrollToBottom = () => {
+ setTimeout(() => {
+ // Code to execute after 1 second (1000 ms)
+ if (this.messagesEndRef.current) {
+ this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
+ }
+ }, 50);
+ };
+
componentDidUpdate = () => {
if (this.loading) {
this.setDone(false);
}
};
+ @observable quizMode: GPTQuizType = GPTQuizType.CURRENT;
+ @action setQuizMode(g: GPTQuizType) {
+ this.quizMode = g;
+ }
+
+ cardMenu = () => (
+ <div className="btns-wrapper-gpt">
+ <Button
+ tooltip="Have ChatGPT sort, tag, define, or filter your cards for you!"
+ text="Modify/Sort Cards!"
+ onClick={() => this.setMode(GPTPopupMode.SORT)}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ style={{
+ width: '100%',
+ height: '40%',
+ textAlign: 'center',
+ color: '#ffffff',
+ fontSize: '16px',
+ marginBottom: '10px',
+ }}
+ />
+ <Button
+ tooltip="Test your knowledge with ChatGPT!"
+ text="Quiz Cards!"
+ onClick={() => {
+ this.conversationArray = ['Define the selected card!'];
+ this.setMode(GPTPopupMode.QUIZ);
+ if (this.onQuizRandom) {
+ this.onQuizRandom();
+ }
+ }}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ type={Type.TERT}
+ style={{
+ width: '100%',
+ textAlign: 'center',
+ color: '#ffffff',
+ fontSize: '16px',
+ height: '40%',
+ }}
+ />
+ </div>
+ );
+
+ handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => {
+ if (e.key === 'Enter') {
+ e.stopPropagation();
+
+ if (isSort) {
+ this.conversationArray.push(this.chatSortPrompt);
+ await this.generateCard();
+ this.chatSortPrompt = '';
+ } else {
+ this.conversationArray.push(this.quizAnswer);
+ await this.generateQuiz();
+ this.quizAnswer = '';
+ }
+
+ this.scrollToBottom();
+ }
+ };
+
+ cardActual = (opt: GPTPopupMode) => {
+ const isSort = opt === GPTPopupMode.SORT;
+ return (
+ <div className="btns-wrapper-gpt">
+ <div className="chat-wrapper">
+ <div className="chat-bubbles">
+ {this.conversationArray.map((message, index) => (
+ <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}>
+ {message}
+ </div>
+ ))}
+ {(!this.cardsDoneLoading || this.loading) && <div className={`chat-bubble chat-message`}>...</div>}
+ </div>
+
+ <div ref={this.messagesEndRef} style={{ height: '100px' }} />
+ </div>
+
+ <div className="inputWrapper">
+ <input
+ className="searchBox-input"
+ defaultValue=""
+ value={isSort ? this.chatSortPrompt : this.quizAnswer} // Controlled input
+ autoComplete="off"
+ onChange={isSort ? this.sortPromptChanged : this.quizAnswerChanged}
+ onKeyDown={e => {
+ this.handleKeyPress(e, isSort);
+ }}
+ type="text"
+ placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`}
+ />
+ </div>
+ </div>
+ );
+ };
+
sortBox = () => (
- <>
- <div>
- {this.heading('SORTING')}
- {this.loading ? (
+ <div style={{ height: '80%' }}>
+ {this.heading(this.mode === GPTPopupMode.SORT ? 'SORTING' : 'QUIZ')}
+ <>
+ {!this.cardsDoneLoading ? (
<div className="content-wrapper">
<div className="loading-spinner">
<ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- <span>Loading...</span>
+ {this.loading ? <span>Loading...</span> : <span>Reading Cards...</span>}
</div>
</div>
+ ) : this.mode === GPTPopupMode.CARD ? (
+ this.cardMenu()
) : (
- <>
- {!this.cardsDoneLoading ? (
- <div className="content-wrapper">
- <div className="loading-spinner">
- <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
- <span>Reading Cards...</span>
- </div>
- </div>
- ) : (
- !this.sortDone && (
- <div className="btns-wrapper-gpt">
- <Button
- tooltip="Have ChatGPT sort your cards for you!"
- text="Sort!"
- onClick={this.generateSort}
- color={StrCast(Doc.UserDoc().userVariantColor)}
- type={Type.TERT}
- style={{
- width: '90%', // Almost as wide as the container
- textAlign: 'center',
- color: '#ffffff', // White text
- fontSize: '16px', // Adjust font size as needed
- }}
- />
- </div>
- )
- )}
-
- {this.sortDone && (
- <div>
- <div className="content-wrapper">
- <p>{this.text === 'Something went wrong :(' ? 'Something went wrong :(' : 'Sorting done! Feel free to move things around / regenerate :) !'}</p>
- <IconButton tooltip="Generate Again" onClick={() => this.setSortDone(false)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
- </div>
- </div>
- )}
- </>
- )}
- </div>
- </>
+ this.cardActual(this.mode)
+ ) // Call the functions to render JSX
+ }
+ </>
+ </div>
);
+
imageBox = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{this.heading('GENERATED IMAGE')}
@@ -511,14 +734,52 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
heading = (headingText: string) => (
<div className="summary-heading">
<label className="summary-text">{headingText}</label>
- {this.loading ? <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} /> : <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />}
+ {this.loading ? (
+ <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />
+ ) : (
+ <>
+ {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && (
+ <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
+ )}
+ <IconButton
+ color={StrCast(SettingsManager.userVariantColor)}
+ tooltip="close"
+ icon={<CgClose size="16px" />}
+ onClick={() => {
+ this.setVisible(false);
+ }}
+ />
+ </>
+ )}
</div>
);
render() {
+ let content;
+
+ switch (this.mode) {
+ case GPTPopupMode.SUMMARY:
+ content = this.summaryBox();
+ break;
+ case GPTPopupMode.DATA:
+ content = this.dataAnalysisBox();
+ break;
+ case GPTPopupMode.IMAGE:
+ content = this.imageBox();
+ break;
+ case GPTPopupMode.SORT:
+ case GPTPopupMode.CARD:
+ case GPTPopupMode.QUIZ:
+ content = this.sortBox();
+ break;
+ default:
+ content = null;
+ }
+
return (
<div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
- {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : this.mode === GPTPopupMode.SORT ? this.sortBox() : null}
+ {content}
+ <div className="resize-handle" />
</div>
);
}
diff --git a/src/client/views/search/FaceRecognitionHandler.tsx b/src/client/views/search/FaceRecognitionHandler.tsx
index 4f6f5d314..6f70e96ab 100644
--- a/src/client/views/search/FaceRecognitionHandler.tsx
+++ b/src/client/views/search/FaceRecognitionHandler.tsx
@@ -30,6 +30,7 @@ import { DocumentManager } from '../../util/DocumentManager';
* face_annos - a list of face annotations, where each anno has
*/
export class FaceRecognitionHandler {
+ // eslint-disable-next-line no-use-before-define
static _instance: FaceRecognitionHandler;
private _apiModelReady = false;
private _pendingAPIModelReadyDocs: Doc[] = [];
@@ -119,8 +120,7 @@ export class FaceRecognitionHandler {
* @param faceDoc - unique face Doc
*/
public static UniqueFaceRemoveFaceImage = (faceAnno: Doc, faceDoc: Doc) => {
- Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', faceAnno);
- faceAnno.face = undefined;
+ FaceRecognitionHandler.ImageDocFaceAnnos(faceAnno).forEach(face => Doc.RemoveDocFromList(faceDoc[DocData], 'face_annos', face) && (face.face = undefined));
};
constructor() {
@@ -221,7 +221,8 @@ export class FaceRecognitionHandler {
.then(imgDocFaceDescriptions => { // For each face detected, find a match.
const annos = [] as Doc[];
const scale = NumCast(imgDoc.data_nativeWidth) / img.width;
- imgDocFaceDescriptions.forEach((fd, i) => {
+ const showTags= imgDocFaceDescriptions.length > 1;
+ imgDocFaceDescriptions.forEach(fd => {
const faceDescriptor = new List<number>(Array.from(fd.descriptor));
const matchedUniqueFace = this.findMatchingFaceDoc(fd.descriptor) ?? this.createUniqueFaceDoc(activeDashboard);
const faceAnno = Docs.Create.FreeformDocument([], {
@@ -234,12 +235,14 @@ export class FaceRecognitionHandler {
y: fd.alignedRect.box.top * scale,
_width: fd.alignedRect.box.width * scale,
_height: fd.alignedRect.box.height * scale,
+ _layout_showTags: showTags
})
FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, matchedUniqueFace); // add image/faceDescriptor to matched unique face
annos.push(faceAnno);
});
imgDoc[DocData].data_annotations = new List<Doc>(annos);
+ imgDoc._layout_showTags = annos.length > 0;
return imgDocFaceDescriptions;
})
); // prettier-ignore
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e7d6a9460..b3a17de8f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -258,7 +258,13 @@ export class Doc extends RefField {
public static set ActivePresentation(val) { Doc.ActiveDashboard && (Doc.ActiveDashboard.activePresentation = val) } // prettier-ignore
public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); } // prettier-ignore
public static set ActiveDashboard(val: Opt<Doc>) { Doc.UserDoc().activeDashboard = val; } // prettier-ignore
-
+ public static get MyFilterHotKeys() { return DocListCast(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter)?.data).filter(key => key.toolType !== "-opts-"); } // prettier-ignore
+ public static RemFromFilterHotKeys(doc: Doc) {
+ return Doc.RemoveDocFromList(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter), 'data', doc);
+ }
+ public static AddToFilterHotKeys(doc: Doc) {
+ return Doc.AddDocToList(DocCast(DocCast(Doc.UserDoc().myContextMenuBtns)?.Filter), 'data', doc);
+ }
public static IsInMyOverlay(doc: Doc) { return Doc.MyOverlayDocs.includes(doc); } // prettier-ignore
public static AddToMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.AddDocToList(Doc.ActiveDashboard, 'myOverlayDocs', doc) : Doc.AddDocToList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore
public static RemFromMyOverlay(doc: Doc) { return Doc.ActiveDashboard ? Doc.RemoveDocFromList(Doc.ActiveDashboard,'myOverlayDocs', doc) : Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myOverlayDocs), undefined, doc); } // prettier-ignore