diff --git a/package-lock.json b/package-lock.json index 73319ee546..608f0ff2eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16112,9 +16112,9 @@ "dev": true }, "node_modules/@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, "node_modules/@types/rimraf": { @@ -16556,204 +16556,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@uppy/companion-client": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.10.2.tgz", - "integrity": "sha512-5RmsNF9UBvUqmqQz48SoiLvkpGmvQTgwNM4bJX8xwVozv/6goRpFrsMJGLwqFcHS/9xj6STKOqrM582g8exVwQ==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "namespace-emitter": "^2.0.1", - "qs-stringify": "^1.1.0", - "url-parse": "^1.4.7" - } - }, - "node_modules/@uppy/core": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/@uppy/core/-/core-1.20.1.tgz", - "integrity": "sha512-Z0yGixSNOSMgT/2aLylXQaEBB6X32RqGLQUgDJDK08jI0ZcMha5glNhD2RU1Gs9noQOAR/f7QwBssSnYJUNRfg==", - "dev": true, - "dependencies": { - "@transloadit/prettier-bytes": "0.0.7", - "@uppy/store-default": "^1.2.7", - "@uppy/utils": "^3.6.2", - "cuid": "^2.1.1", - "lodash.throttle": "^4.1.1", - "mime-match": "^1.0.2", - "namespace-emitter": "^2.0.1", - "preact": "8.2.9" - } - }, - "node_modules/@uppy/dashboard": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-1.21.1.tgz", - "integrity": "sha512-psMwBVxxhAZxYkRds4e//+Sx3zkFYRnYpt4jaF4hmwpL9SehlyhQKwaB/scZz/O4yshmgTN8Sri0mYK5FSh5SQ==", - "dev": true, - "dependencies": { - "@transloadit/prettier-bytes": "0.0.7", - "@uppy/informer": "^1.6.6", - "@uppy/provider-views": "^1.12.3", - "@uppy/status-bar": "^1.9.6", - "@uppy/thumbnail-generator": "^1.7.11", - "@uppy/utils": "^3.6.2", - "classnames": "^2.2.6", - "cuid": "^2.1.1", - "is-shallow-equal": "^1.0.1", - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1", - "memoize-one": "^5.0.4", - "preact": "8.2.9", - "resize-observer-polyfill": "^1.5.0" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/drag-drop": { - "version": "1.4.31", - "resolved": "https://registry.npmjs.org/@uppy/drag-drop/-/drag-drop-1.4.31.tgz", - "integrity": "sha512-a9/WKOdAhz9mfFYI9JJNEGLP3TI8RQChndpFkjlxbsD82x2WHSB1TyWTslMdSCK6Ed3pV5IYyBVqDGsTqgveYg==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/file-input": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@uppy/file-input/-/file-input-1.5.2.tgz", - "integrity": "sha512-EI7IROt2qyGm3EoGDmb4UiWNe/P8YsGAcoCZZqFlTLkBlK7Yen5yxzQ4+KH7jWZYM6BynYnHl18aMdRcDWf/UA==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/informer": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-1.6.6.tgz", - "integrity": "sha512-9rZoAqNrKQN/HINnGg8rGnKEliLgc+9/tQQ0f9QcBgRIu/rnbBCTwS+qnGGdjYBdEJTSbHx+U7X9ufjrrjB+CA==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/progress-bar": { - "version": "1.3.30", - "resolved": "https://registry.npmjs.org/@uppy/progress-bar/-/progress-bar-1.3.30.tgz", - "integrity": "sha512-MAn20wBMzKc1p9M/Mot4+bV/707EO/DVgoFcvoP8rmA5oZOGMINpvFGR+rUtWQoBFMvKtvs/Wkp8mcR22rCMrw==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/provider-views": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-1.12.3.tgz", - "integrity": "sha512-r2kra3IftmGLeKMEgZbmQM1qXixulWUUzydgpHcZqJOpeNIjJcpspJruYRctrVqaLz/8asw87V4KxDk0U4xGzw==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "classnames": "^2.2.6", - "preact": "8.2.9" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/react": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@uppy/react/-/react-1.12.2.tgz", - "integrity": "sha512-d4bn08hc5SIuufRtfCAzSC3LdmuHEwg1mG3nFBEzR9xS++KLGtr3FlI6S57wvNCQiA99qB/V27bKGUINe6YdXw==", - "dev": true, - "dependencies": { - "@uppy/dashboard": "^1.21.1", - "@uppy/drag-drop": "^1.4.31", - "@uppy/file-input": "^1.5.2", - "@uppy/progress-bar": "^1.3.30", - "@uppy/status-bar": "^1.9.6", - "@uppy/utils": "^3.6.2", - "prop-types": "^15.6.1" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0", - "react": "^16.0.0 || ^17.0.0" - } - }, - "node_modules/@uppy/status-bar": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-1.9.6.tgz", - "integrity": "sha512-U/KPs5SwZ5d4hJFiCNAdriGHSk1Uhrl+iQmpJS8hoM+8r8rPfwScdua2/ehLuH69Ymwp6k7DpK2DU7UG2XZ+ag==", - "dev": true, - "dependencies": { - "@transloadit/prettier-bytes": "0.0.7", - "@uppy/utils": "^3.6.2", - "classnames": "^2.2.6", - "lodash.throttle": "^4.1.1", - "preact": "8.2.9" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/store-default": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-1.2.7.tgz", - "integrity": "sha512-58IG9yk/i/kYQ9uEwAwMFl1H2V3syOoODrYoFfVHlxaqv+9MkXBg2tHE2gk40iaAIxcCErcPxZkBOvkqzO1SQA==", - "dev": true - }, - "node_modules/@uppy/thumbnail-generator": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-1.7.11.tgz", - "integrity": "sha512-qo9ZD8ByDMM6gIJ4JPN0V/dWlruYMhmYifhUvDUu0qhPAOTJAqh2hLQ+dlmUXTns8RnDorCXScreICSQ09FuLQ==", - "dev": true, - "dependencies": { - "@uppy/utils": "^3.6.2", - "exifr": "^6.0.0", - "math-log2": "^1.0.1" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, - "node_modules/@uppy/utils": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-3.6.2.tgz", - "integrity": "sha512-wGTZma7eywIojfuE1vXlT0fxPSpmCRMkfgFWYc+6TL2FfGqWInmePoB+yal6/M2AnjeKHz6XYMhIpZkjOxFvcw==", - "dev": true, - "dependencies": { - "abortcontroller-polyfill": "^1.4.0", - "lodash.throttle": "^4.1.1" - } - }, - "node_modules/@uppy/xhr-upload": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-1.7.5.tgz", - "integrity": "sha512-Itnc9j9k/PemcmT5KrZ1BEw3pTc6WJg0yyyOcE+hLO8Hjv60Fm7c/I2ZknarOroIjT1WiTSyuxTBPp+9UGkxNA==", - "dev": true, - "dependencies": { - "@uppy/companion-client": "^1.10.2", - "@uppy/utils": "^3.6.2", - "cuid": "^2.1.1" - }, - "peerDependencies": { - "@uppy/core": "^1.0.0" - } - }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -17044,12 +16846,6 @@ "node": ">=6.5" } }, - "node_modules/abortcontroller-polyfill": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz", - "integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==", - "dev": true - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -21485,12 +21281,6 @@ "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" }, - "node_modules/cuid": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/cuid/-/cuid-2.1.8.tgz", - "integrity": "sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==", - "dev": true - }, "node_modules/cypress": { "version": "13.3.3", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.3.tgz", @@ -24437,12 +24227,6 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/exifr": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/exifr/-/exifr-6.3.0.tgz", - "integrity": "sha512-NCSOP15py+4QyvD90etFN0QOVj12ygVE8kfEDG8GDc+SXf9YAOxua2x5kGp6WvxbGjufA5C3r/1ZKHOpHbEWFg==", - "dev": true - }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -28503,6 +28287,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.0.1.tgz", + "integrity": "sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -30776,12 +30572,6 @@ "lodash._isnative": "~2.4.1" } }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=", - "dev": true - }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -31242,15 +31032,6 @@ "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", "dev": true }, - "node_modules/math-log2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", - "integrity": "sha1-+4lBvl9evol55xjmJzsXjlhpRWU=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/mathjax": { "version": "2.7.9", "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.9.tgz", @@ -31331,12 +31112,6 @@ "node": ">= 4.0.0" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "dev": true - }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -33310,6 +33085,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.4.1.tgz", + "integrity": "sha512-vRpMXmIkYF2/1hLBKisKeVYJZ8S2tZ0zEAmIJgdVKP2nq0nh4qCdf8bgw+ZgKrkh71AOCaqzwbJJk1WtdcF3VA==", + "dev": true, + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^5.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/p-queue/node_modules/p-timeout": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", + "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", @@ -35563,13 +35372,6 @@ "node": ">= 8" } }, - "node_modules/preact": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.2.9.tgz", - "integrity": "sha512-ThuGXBmJS3VsT+jIP+eQufD3L8pRw/PY3FoCys6O9Pu6aF12Pn9zAJDX99TfwRAFOCEKm/P0lwiPTbqKMJp0fA==", - "dev": true, - "hasInstallScript": true - }, "node_modules/precond": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", @@ -36364,12 +36166,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/qs-stringify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/qs-stringify/-/qs-stringify-1.2.1.tgz", - "integrity": "sha512-2N5xGLGZUxpgAYq1fD1LmBSCbxQVsXYt5JU0nU3FuPWO8PlCnKNFQwXkZgyB6mrTdg7IbexX4wxIR403dJw9pw==", - "dev": true - }, "node_modules/query-string": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", @@ -37789,12 +37585,6 @@ "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", "dev": true }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true - }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -46582,11 +46372,11 @@ "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", - "@uppy/core": "^1.15.0", - "@uppy/dashboard": "^1.11.0", - "@uppy/react": "^1.11.0", - "@uppy/utils": "^4.0.7", - "@uppy/xhr-upload": "^1.6.8", + "@uppy/core": "^3.8.0", + "@uppy/dashboard": "^3.7.1", + "@uppy/react": "^3.2.1", + "@uppy/utils": "^5.7.0", + "@uppy/xhr-upload": "^3.6.0", "abort-controller": "^3.0.0", "acorn": "^7.1.1", "acorn-walk": "^7.1.1", @@ -47115,13 +46905,234 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "services/web/node_modules/@uppy/utils": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.0.7.tgz", - "integrity": "sha512-nKViMT8XchKy+NWpb3DtVKuzZBmW7au26LrMq89EsvTwIOT6UR9+7bmz/+zr3+lc7UC7vMgNChIC6G+/Ya9wWQ==", + "services/web/node_modules/@uppy/companion-client": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-3.7.0.tgz", + "integrity": "sha512-37qJNMkqo01SM9h2gkFbV6e+aXM02s2zAda2dGsRLRsjvl/Tx69NlmxJ3xqG/7HWRnYcbBWtspb7y0tt1i/afg==", "dev": true, "dependencies": { - "lodash.throttle": "^4.1.1" + "@uppy/utils": "^5.7.0", + "namespace-emitter": "^2.0.1", + "p-retry": "^6.1.0" + } + }, + "services/web/node_modules/@uppy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@uppy/core/-/core-3.8.0.tgz", + "integrity": "sha512-C93vVhid929+VLGjaD9CZOLJDg8GkEGMUGveFp3Tyo/wujiG+sB3fOF+c6TzKpzPLfNtVpskU1BnI7tZrq1LWw==", + "dev": true, + "dependencies": { + "@transloadit/prettier-bytes": "0.0.9", + "@uppy/store-default": "^3.2.0", + "@uppy/utils": "^5.7.0", + "lodash": "^4.17.21", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^4.0.0", + "preact": "^10.5.13" + } + }, + "services/web/node_modules/@uppy/core/node_modules/@transloadit/prettier-bytes": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", + "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==", + "dev": true + }, + "services/web/node_modules/@uppy/dashboard": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-3.7.1.tgz", + "integrity": "sha512-qtCMXd2Ymrw0qNGSTlEEMyyDkGUCm+wX5/VrmV9lnfT7JtlSfotUK0K6KvkBeu2v1Chsu27C6Xlq6RddZMR2xQ==", + "dev": true, + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/informer": "^3.0.4", + "@uppy/provider-views": "^3.7.0", + "@uppy/status-bar": "^3.2.5", + "@uppy/thumbnail-generator": "^3.0.6", + "@uppy/utils": "^5.6.0", + "classnames": "^2.2.6", + "is-shallow-equal": "^1.0.1", + "lodash": "^4.17.21", + "memoize-one": "^6.0.0", + "nanoid": "^4.0.0", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.7.1" + } + }, + "services/web/node_modules/@uppy/drag-drop": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@uppy/drag-drop/-/drag-drop-3.0.3.tgz", + "integrity": "sha512-0bCgQKxg+9vkxQipTgrX9yQIuK9a0hZrkipm1+Ynq6jTeig49b7II1bWYnoKdiYhi6nRE4UnDJf4z09yCAU7rA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@uppy/utils": "^5.4.3", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.4.0" + } + }, + "services/web/node_modules/@uppy/file-input": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@uppy/file-input/-/file-input-3.0.4.tgz", + "integrity": "sha512-D7Nw9GgpABYTcC8SZluDyxd+ppe7+gJejNbPZqMpQyW1S/ME3me55dkDQaVWn8yrgv7347zO2ciue9Rfmko+rQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@uppy/utils": "^5.5.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.6.0" + } + }, + "services/web/node_modules/@uppy/informer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-3.0.4.tgz", + "integrity": "sha512-gzocdxn8qAFsW2EryehwjghladaBgv6Isjte53FTBV7o/vjaHPP6huKGbYpljyuQi8i9V+KrmvNGslofssgJ4g==", + "dev": true, + "dependencies": { + "@uppy/utils": "^5.5.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.6.0" + } + }, + "services/web/node_modules/@uppy/progress-bar": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@uppy/progress-bar/-/progress-bar-3.0.4.tgz", + "integrity": "sha512-sxv/mG7Uc9uyTnRvfcXBhO+TWd+UqjuW5aHXCKWwTkMgDShHR0T46sEk12q+jwgbFwyeFg3p0GU3hgUxqxiEUQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@uppy/utils": "^5.5.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.6.0" + } + }, + "services/web/node_modules/@uppy/provider-views": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-3.8.0.tgz", + "integrity": "sha512-sTtx5bgsg2WVR+MyF0gnnM3Z7g3CyFx+Stlz//AvB6g27EMqtqO4zwDR3mestMrETkWYov5bhhqUbt2BaeANpA==", + "dev": true, + "dependencies": { + "@uppy/utils": "^5.7.0", + "classnames": "^2.2.6", + "nanoid": "^4.0.0", + "p-queue": "^7.3.4", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.8.0" + } + }, + "services/web/node_modules/@uppy/react": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@uppy/react/-/react-3.2.1.tgz", + "integrity": "sha512-PoLplDF6YDI7f06T8ORnJhav6CcKNSYWJETXqItZR3jcXIve6pdcCuskqd+l0yiYWf4J2IdyLQXtzgGfIJl7xQ==", + "dev": true, + "dependencies": { + "@uppy/utils": "^5.6.0", + "prop-types": "^15.6.1" + }, + "peerDependencies": { + "@uppy/core": "^3.7.1", + "@uppy/dashboard": "^3.7.1", + "@uppy/drag-drop": "^3.0.3", + "@uppy/file-input": "^3.0.4", + "@uppy/progress-bar": "^3.0.4", + "@uppy/status-bar": "^3.2.5", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@uppy/dashboard": { + "optional": true + }, + "@uppy/drag-drop": { + "optional": true + }, + "@uppy/file-input": { + "optional": true + }, + "@uppy/progress-bar": { + "optional": true + }, + "@uppy/status-bar": { + "optional": true + } + } + }, + "services/web/node_modules/@uppy/status-bar": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-3.2.5.tgz", + "integrity": "sha512-bRSxBPio5B+Kuf6w8ll+/i9VUwG8f0FnbZ1yQvCr8J9vxhd0Z5hvwhX4NP8uzHC6ZPJHlEQOTsxzGQ6y+Mdm0A==", + "dev": true, + "dependencies": { + "@transloadit/prettier-bytes": "0.0.9", + "@uppy/utils": "^5.5.2", + "classnames": "^2.2.6", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^3.6.0" + } + }, + "services/web/node_modules/@uppy/status-bar/node_modules/@transloadit/prettier-bytes": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", + "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==", + "dev": true + }, + "services/web/node_modules/@uppy/store-default": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-3.2.0.tgz", + "integrity": "sha512-Y7t0peUG89ZKa30vM4qlRIC6uKxIfOANeMT9Nzjwcxvzz8l7es22jG3eAj9WF2F7YSu7xdsH8ODs6SIrJJ8gow==", + "dev": true + }, + "services/web/node_modules/@uppy/thumbnail-generator": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-3.0.6.tgz", + "integrity": "sha512-gsi/BQBiunHneXCbo8VglFbhEb0CoQXQjCyGNKoEq/deEcbXhBBDxkiGcgv83l5GZJl2jLiKWqXnXAXREkldrQ==", + "dev": true, + "dependencies": { + "@uppy/utils": "^5.5.2", + "exifr": "^7.0.0" + }, + "peerDependencies": { + "@uppy/core": "^3.6.0" + } + }, + "services/web/node_modules/@uppy/utils": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-5.7.0.tgz", + "integrity": "sha512-AJj7gAx5YfMgyevwOxVdIP2h4Nw/O6h57wKA6gj+Lce6tMORcqzGt4yQiKBsrBI0bPyFWCbzA3vX5t0//1JCBA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "preact": "^10.5.13" + } + }, + "services/web/node_modules/@uppy/xhr-upload": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-3.6.0.tgz", + "integrity": "sha512-HgWr+CvJzJXAp639AiZatdEWmRdhhN5LrjTZurAkvm9nPQarpi1bo0DChO+1bpkXWOR/1VarBbZOr8lNecEn7Q==", + "dev": true, + "dependencies": { + "@uppy/companion-client": "^3.7.0", + "@uppy/utils": "^5.7.0", + "nanoid": "^4.0.0" + }, + "peerDependencies": { + "@uppy/core": "^3.8.0" } }, "services/web/node_modules/ansi-styles": { @@ -47360,6 +47371,12 @@ "node": ">=0.8.x" } }, + "services/web/node_modules/exifr": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", + "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==", + "dev": true + }, "services/web/node_modules/fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", @@ -47496,6 +47513,12 @@ "node": ">=12" } }, + "services/web/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "dev": true + }, "services/web/node_modules/method-override": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.10.tgz", @@ -47620,6 +47643,24 @@ "node": ">= 6.0.0" } }, + "services/web/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, "services/web/node_modules/nise": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", @@ -47686,6 +47727,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "services/web/node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "services/web/node_modules/preact": { + "version": "10.19.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", + "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "services/web/node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -55308,11 +55376,11 @@ "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", - "@uppy/core": "^1.15.0", - "@uppy/dashboard": "^1.11.0", - "@uppy/react": "^1.11.0", - "@uppy/utils": "^4.0.7", - "@uppy/xhr-upload": "^1.6.8", + "@uppy/core": "^3.8.0", + "@uppy/dashboard": "^3.7.1", + "@uppy/react": "^3.2.1", + "@uppy/utils": "^5.7.0", + "@uppy/xhr-upload": "^3.6.0", "@xmldom/xmldom": "^0.7.13", "abort-controller": "^3.0.0", "accepts": "^1.3.7", @@ -55797,13 +55865,185 @@ "eslint-visitor-keys": "^3.4.1" } }, - "@uppy/utils": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.0.7.tgz", - "integrity": "sha512-nKViMT8XchKy+NWpb3DtVKuzZBmW7au26LrMq89EsvTwIOT6UR9+7bmz/+zr3+lc7UC7vMgNChIC6G+/Ya9wWQ==", + "@uppy/companion-client": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-3.7.0.tgz", + "integrity": "sha512-37qJNMkqo01SM9h2gkFbV6e+aXM02s2zAda2dGsRLRsjvl/Tx69NlmxJ3xqG/7HWRnYcbBWtspb7y0tt1i/afg==", "dev": true, "requires": { - "lodash.throttle": "^4.1.1" + "@uppy/utils": "^5.7.0", + "namespace-emitter": "^2.0.1", + "p-retry": "^6.1.0" + } + }, + "@uppy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@uppy/core/-/core-3.8.0.tgz", + "integrity": "sha512-C93vVhid929+VLGjaD9CZOLJDg8GkEGMUGveFp3Tyo/wujiG+sB3fOF+c6TzKpzPLfNtVpskU1BnI7tZrq1LWw==", + "dev": true, + "requires": { + "@transloadit/prettier-bytes": "0.0.9", + "@uppy/store-default": "^3.2.0", + "@uppy/utils": "^5.7.0", + "lodash": "^4.17.21", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^4.0.0", + "preact": "^10.5.13" + }, + "dependencies": { + "@transloadit/prettier-bytes": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", + "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==", + "dev": true + } + } + }, + "@uppy/dashboard": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-3.7.1.tgz", + "integrity": "sha512-qtCMXd2Ymrw0qNGSTlEEMyyDkGUCm+wX5/VrmV9lnfT7JtlSfotUK0K6KvkBeu2v1Chsu27C6Xlq6RddZMR2xQ==", + "dev": true, + "requires": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/informer": "^3.0.4", + "@uppy/provider-views": "^3.7.0", + "@uppy/status-bar": "^3.2.5", + "@uppy/thumbnail-generator": "^3.0.6", + "@uppy/utils": "^5.6.0", + "classnames": "^2.2.6", + "is-shallow-equal": "^1.0.1", + "lodash": "^4.17.21", + "memoize-one": "^6.0.0", + "nanoid": "^4.0.0", + "preact": "^10.5.13" + } + }, + "@uppy/drag-drop": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@uppy/drag-drop/-/drag-drop-3.0.3.tgz", + "integrity": "sha512-0bCgQKxg+9vkxQipTgrX9yQIuK9a0hZrkipm1+Ynq6jTeig49b7II1bWYnoKdiYhi6nRE4UnDJf4z09yCAU7rA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@uppy/utils": "^5.4.3", + "preact": "^10.5.13" + } + }, + "@uppy/file-input": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@uppy/file-input/-/file-input-3.0.4.tgz", + "integrity": "sha512-D7Nw9GgpABYTcC8SZluDyxd+ppe7+gJejNbPZqMpQyW1S/ME3me55dkDQaVWn8yrgv7347zO2ciue9Rfmko+rQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@uppy/utils": "^5.5.2", + "preact": "^10.5.13" + } + }, + "@uppy/informer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-3.0.4.tgz", + "integrity": "sha512-gzocdxn8qAFsW2EryehwjghladaBgv6Isjte53FTBV7o/vjaHPP6huKGbYpljyuQi8i9V+KrmvNGslofssgJ4g==", + "dev": true, + "requires": { + "@uppy/utils": "^5.5.2", + "preact": "^10.5.13" + } + }, + "@uppy/progress-bar": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@uppy/progress-bar/-/progress-bar-3.0.4.tgz", + "integrity": "sha512-sxv/mG7Uc9uyTnRvfcXBhO+TWd+UqjuW5aHXCKWwTkMgDShHR0T46sEk12q+jwgbFwyeFg3p0GU3hgUxqxiEUQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@uppy/utils": "^5.5.2", + "preact": "^10.5.13" + } + }, + "@uppy/provider-views": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-3.8.0.tgz", + "integrity": "sha512-sTtx5bgsg2WVR+MyF0gnnM3Z7g3CyFx+Stlz//AvB6g27EMqtqO4zwDR3mestMrETkWYov5bhhqUbt2BaeANpA==", + "dev": true, + "requires": { + "@uppy/utils": "^5.7.0", + "classnames": "^2.2.6", + "nanoid": "^4.0.0", + "p-queue": "^7.3.4", + "preact": "^10.5.13" + } + }, + "@uppy/react": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@uppy/react/-/react-3.2.1.tgz", + "integrity": "sha512-PoLplDF6YDI7f06T8ORnJhav6CcKNSYWJETXqItZR3jcXIve6pdcCuskqd+l0yiYWf4J2IdyLQXtzgGfIJl7xQ==", + "dev": true, + "requires": { + "@uppy/utils": "^5.6.0", + "prop-types": "^15.6.1" + } + }, + "@uppy/status-bar": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-3.2.5.tgz", + "integrity": "sha512-bRSxBPio5B+Kuf6w8ll+/i9VUwG8f0FnbZ1yQvCr8J9vxhd0Z5hvwhX4NP8uzHC6ZPJHlEQOTsxzGQ6y+Mdm0A==", + "dev": true, + "requires": { + "@transloadit/prettier-bytes": "0.0.9", + "@uppy/utils": "^5.5.2", + "classnames": "^2.2.6", + "preact": "^10.5.13" + }, + "dependencies": { + "@transloadit/prettier-bytes": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", + "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==", + "dev": true + } + } + }, + "@uppy/store-default": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-3.2.0.tgz", + "integrity": "sha512-Y7t0peUG89ZKa30vM4qlRIC6uKxIfOANeMT9Nzjwcxvzz8l7es22jG3eAj9WF2F7YSu7xdsH8ODs6SIrJJ8gow==", + "dev": true + }, + "@uppy/thumbnail-generator": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-3.0.6.tgz", + "integrity": "sha512-gsi/BQBiunHneXCbo8VglFbhEb0CoQXQjCyGNKoEq/deEcbXhBBDxkiGcgv83l5GZJl2jLiKWqXnXAXREkldrQ==", + "dev": true, + "requires": { + "@uppy/utils": "^5.5.2", + "exifr": "^7.0.0" + } + }, + "@uppy/utils": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-5.7.0.tgz", + "integrity": "sha512-AJj7gAx5YfMgyevwOxVdIP2h4Nw/O6h57wKA6gj+Lce6tMORcqzGt4yQiKBsrBI0bPyFWCbzA3vX5t0//1JCBA==", + "dev": true, + "requires": { + "lodash": "^4.17.21", + "preact": "^10.5.13" + } + }, + "@uppy/xhr-upload": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-3.6.0.tgz", + "integrity": "sha512-HgWr+CvJzJXAp639AiZatdEWmRdhhN5LrjTZurAkvm9nPQarpi1bo0DChO+1bpkXWOR/1VarBbZOr8lNecEn7Q==", + "dev": true, + "requires": { + "@uppy/companion-client": "^3.7.0", + "@uppy/utils": "^5.7.0", + "nanoid": "^4.0.0" } }, "ansi-styles": { @@ -55967,6 +56207,12 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, + "exifr": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", + "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==", + "dev": true + }, "fs-extra": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", @@ -56064,6 +56310,12 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==" }, + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "dev": true + }, "method-override": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/method-override/-/method-override-2.3.10.tgz", @@ -56161,6 +56413,12 @@ "xtend": "^4.0.0" } }, + "nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "dev": true + }, "nise": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.3.tgz", @@ -56220,6 +56478,23 @@ "p-try": "^2.0.0" } }, + "p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dev": true, + "requires": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + } + }, + "preact": { + "version": "10.19.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz", + "integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==", + "dev": true + }, "propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -61787,9 +62062,9 @@ "dev": true }, "@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", "dev": true }, "@types/rimraf": { @@ -62145,173 +62420,6 @@ } } }, - "@uppy/companion-client": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-1.10.2.tgz", - "integrity": "sha512-5RmsNF9UBvUqmqQz48SoiLvkpGmvQTgwNM4bJX8xwVozv/6goRpFrsMJGLwqFcHS/9xj6STKOqrM582g8exVwQ==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "namespace-emitter": "^2.0.1", - "qs-stringify": "^1.1.0", - "url-parse": "^1.4.7" - } - }, - "@uppy/core": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/@uppy/core/-/core-1.20.1.tgz", - "integrity": "sha512-Z0yGixSNOSMgT/2aLylXQaEBB6X32RqGLQUgDJDK08jI0ZcMha5glNhD2RU1Gs9noQOAR/f7QwBssSnYJUNRfg==", - "dev": true, - "requires": { - "@transloadit/prettier-bytes": "0.0.7", - "@uppy/store-default": "^1.2.7", - "@uppy/utils": "^3.6.2", - "cuid": "^2.1.1", - "lodash.throttle": "^4.1.1", - "mime-match": "^1.0.2", - "namespace-emitter": "^2.0.1", - "preact": "8.2.9" - } - }, - "@uppy/dashboard": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-1.21.1.tgz", - "integrity": "sha512-psMwBVxxhAZxYkRds4e//+Sx3zkFYRnYpt4jaF4hmwpL9SehlyhQKwaB/scZz/O4yshmgTN8Sri0mYK5FSh5SQ==", - "dev": true, - "requires": { - "@transloadit/prettier-bytes": "0.0.7", - "@uppy/informer": "^1.6.6", - "@uppy/provider-views": "^1.12.3", - "@uppy/status-bar": "^1.9.6", - "@uppy/thumbnail-generator": "^1.7.11", - "@uppy/utils": "^3.6.2", - "classnames": "^2.2.6", - "cuid": "^2.1.1", - "is-shallow-equal": "^1.0.1", - "lodash.debounce": "^4.0.8", - "lodash.throttle": "^4.1.1", - "memoize-one": "^5.0.4", - "preact": "8.2.9", - "resize-observer-polyfill": "^1.5.0" - } - }, - "@uppy/drag-drop": { - "version": "1.4.31", - "resolved": "https://registry.npmjs.org/@uppy/drag-drop/-/drag-drop-1.4.31.tgz", - "integrity": "sha512-a9/WKOdAhz9mfFYI9JJNEGLP3TI8RQChndpFkjlxbsD82x2WHSB1TyWTslMdSCK6Ed3pV5IYyBVqDGsTqgveYg==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - } - }, - "@uppy/file-input": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@uppy/file-input/-/file-input-1.5.2.tgz", - "integrity": "sha512-EI7IROt2qyGm3EoGDmb4UiWNe/P8YsGAcoCZZqFlTLkBlK7Yen5yxzQ4+KH7jWZYM6BynYnHl18aMdRcDWf/UA==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - } - }, - "@uppy/informer": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-1.6.6.tgz", - "integrity": "sha512-9rZoAqNrKQN/HINnGg8rGnKEliLgc+9/tQQ0f9QcBgRIu/rnbBCTwS+qnGGdjYBdEJTSbHx+U7X9ufjrrjB+CA==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - } - }, - "@uppy/progress-bar": { - "version": "1.3.30", - "resolved": "https://registry.npmjs.org/@uppy/progress-bar/-/progress-bar-1.3.30.tgz", - "integrity": "sha512-MAn20wBMzKc1p9M/Mot4+bV/707EO/DVgoFcvoP8rmA5oZOGMINpvFGR+rUtWQoBFMvKtvs/Wkp8mcR22rCMrw==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "preact": "8.2.9" - } - }, - "@uppy/provider-views": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-1.12.3.tgz", - "integrity": "sha512-r2kra3IftmGLeKMEgZbmQM1qXixulWUUzydgpHcZqJOpeNIjJcpspJruYRctrVqaLz/8asw87V4KxDk0U4xGzw==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "classnames": "^2.2.6", - "preact": "8.2.9" - } - }, - "@uppy/react": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@uppy/react/-/react-1.12.2.tgz", - "integrity": "sha512-d4bn08hc5SIuufRtfCAzSC3LdmuHEwg1mG3nFBEzR9xS++KLGtr3FlI6S57wvNCQiA99qB/V27bKGUINe6YdXw==", - "dev": true, - "requires": { - "@uppy/dashboard": "^1.21.1", - "@uppy/drag-drop": "^1.4.31", - "@uppy/file-input": "^1.5.2", - "@uppy/progress-bar": "^1.3.30", - "@uppy/status-bar": "^1.9.6", - "@uppy/utils": "^3.6.2", - "prop-types": "^15.6.1" - } - }, - "@uppy/status-bar": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-1.9.6.tgz", - "integrity": "sha512-U/KPs5SwZ5d4hJFiCNAdriGHSk1Uhrl+iQmpJS8hoM+8r8rPfwScdua2/ehLuH69Ymwp6k7DpK2DU7UG2XZ+ag==", - "dev": true, - "requires": { - "@transloadit/prettier-bytes": "0.0.7", - "@uppy/utils": "^3.6.2", - "classnames": "^2.2.6", - "lodash.throttle": "^4.1.1", - "preact": "8.2.9" - } - }, - "@uppy/store-default": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-1.2.7.tgz", - "integrity": "sha512-58IG9yk/i/kYQ9uEwAwMFl1H2V3syOoODrYoFfVHlxaqv+9MkXBg2tHE2gk40iaAIxcCErcPxZkBOvkqzO1SQA==", - "dev": true - }, - "@uppy/thumbnail-generator": { - "version": "1.7.11", - "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-1.7.11.tgz", - "integrity": "sha512-qo9ZD8ByDMM6gIJ4JPN0V/dWlruYMhmYifhUvDUu0qhPAOTJAqh2hLQ+dlmUXTns8RnDorCXScreICSQ09FuLQ==", - "dev": true, - "requires": { - "@uppy/utils": "^3.6.2", - "exifr": "^6.0.0", - "math-log2": "^1.0.1" - } - }, - "@uppy/utils": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-3.6.2.tgz", - "integrity": "sha512-wGTZma7eywIojfuE1vXlT0fxPSpmCRMkfgFWYc+6TL2FfGqWInmePoB+yal6/M2AnjeKHz6XYMhIpZkjOxFvcw==", - "dev": true, - "requires": { - "abortcontroller-polyfill": "^1.4.0", - "lodash.throttle": "^4.1.1" - } - }, - "@uppy/xhr-upload": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-1.7.5.tgz", - "integrity": "sha512-Itnc9j9k/PemcmT5KrZ1BEw3pTc6WJg0yyyOcE+hLO8Hjv60Fm7c/I2ZknarOroIjT1WiTSyuxTBPp+9UGkxNA==", - "dev": true, - "requires": { - "@uppy/companion-client": "^1.10.2", - "@uppy/utils": "^3.6.2", - "cuid": "^2.1.1" - } - }, "@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -62565,12 +62673,6 @@ "event-target-shim": "^5.0.0" } }, - "abortcontroller-polyfill": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz", - "integrity": "sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==", - "dev": true - }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -65958,12 +66060,6 @@ "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.6.5.tgz", "integrity": "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==" }, - "cuid": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/cuid/-/cuid-2.1.8.tgz", - "integrity": "sha512-xiEMER6E7TlTPnDxrM4eRiC6TRgjNX9xzEZ5U/Se2YJKr7Mq4pJn/2XEHjl3STcSh96GmkHPcBXLES8M29wyyg==", - "dev": true - }, "cypress": { "version": "13.3.3", "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.3.3.tgz", @@ -68206,12 +68302,6 @@ "exegesis": "^4.1.0" } }, - "exifr": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/exifr/-/exifr-6.3.0.tgz", - "integrity": "sha512-NCSOP15py+4QyvD90etFN0QOVj12ygVE8kfEDG8GDc+SXf9YAOxua2x5kGp6WvxbGjufA5C3r/1ZKHOpHbEWFg==", - "dev": true - }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -71311,6 +71401,12 @@ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" }, + "is-network-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.0.1.tgz", + "integrity": "sha512-OwQXkwBJeESyhFw+OumbJVD58BFBJJI5OM5S1+eyrDKlgDZPX2XNT5gXS56GSD3NPbbwUuMlR1Q71SRp5SobuQ==", + "dev": true + }, "is-number-object": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", @@ -74044,12 +74140,6 @@ "lodash._isnative": "~2.4.1" } }, - "lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=", - "dev": true - }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -74424,12 +74514,6 @@ "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", "dev": true }, - "math-log2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/math-log2/-/math-log2-1.0.1.tgz", - "integrity": "sha1-+4lBvl9evol55xjmJzsXjlhpRWU=", - "dev": true - }, "mathjax": { "version": "2.7.9", "resolved": "https://registry.npmjs.org/mathjax/-/mathjax-2.7.9.tgz", @@ -74491,12 +74575,6 @@ "fs-monkey": "^1.0.3" } }, - "memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "dev": true - }, "memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -76158,6 +76236,30 @@ "p-map": "^4.0.0" } }, + "p-queue": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-7.4.1.tgz", + "integrity": "sha512-vRpMXmIkYF2/1hLBKisKeVYJZ8S2tZ0zEAmIJgdVKP2nq0nh4qCdf8bgw+ZgKrkh71AOCaqzwbJJk1WtdcF3VA==", + "dev": true, + "requires": { + "eventemitter3": "^5.0.1", + "p-timeout": "^5.0.2" + }, + "dependencies": { + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "p-timeout": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz", + "integrity": "sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew==", + "dev": true + } + } + }, "p-retry": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", @@ -77608,12 +77710,6 @@ } } }, - "preact": { - "version": "8.2.9", - "resolved": "https://registry.npmjs.org/preact/-/preact-8.2.9.tgz", - "integrity": "sha512-ThuGXBmJS3VsT+jIP+eQufD3L8pRw/PY3FoCys6O9Pu6aF12Pn9zAJDX99TfwRAFOCEKm/P0lwiPTbqKMJp0fA==", - "dev": true - }, "precond": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", @@ -78283,12 +78379,6 @@ "side-channel": "^1.0.4" } }, - "qs-stringify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/qs-stringify/-/qs-stringify-1.2.1.tgz", - "integrity": "sha512-2N5xGLGZUxpgAYq1fD1LmBSCbxQVsXYt5JU0nU3FuPWO8PlCnKNFQwXkZgyB6mrTdg7IbexX4wxIR403dJw9pw==", - "dev": true - }, "query-string": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", @@ -79386,12 +79476,6 @@ "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", "dev": true }, - "resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true - }, "resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", diff --git a/services/web/cypress/support/component.ts b/services/web/cypress/support/component.ts index 807c3e4bf9..704458cb2b 100644 --- a/services/web/cypress/support/component.ts +++ b/services/web/cypress/support/component.ts @@ -7,9 +7,13 @@ import './shared/exceptions' import './ct/commands' beforeEach(function () { - window.metaAttributesCache = new Map() + cy.window().then(win => { + win.metaAttributesCache = new Map() + }) }) afterEach(function () { - window.metaAttributesCache.clear() + cy.window().then(win => { + win.metaAttributesCache?.clear() + }) }) diff --git a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.jsx b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.jsx index 36bc012ec4..902f28cfa5 100644 --- a/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.jsx +++ b/services/web/frontend/js/features/file-tree/components/file-tree-create/modes/file-tree-upload-doc.jsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react' import PropTypes from 'prop-types' import Uppy from '@uppy/core' import XHRUpload from '@uppy/xhr-upload' -import { Dashboard, useUppy } from '@uppy/react' +import { Dashboard } from '@uppy/react' import { useFileTreeActionable } from '../../../contexts/file-tree-actionable' import { useProjectContext } from '../../../../../shared/context/project-context' import * as eventTracking from '../../../../../infrastructure/event-tracking' @@ -45,13 +45,13 @@ export default function FileTreeUploadDoc() { } // initialise the Uppy object - const uppy = useUppy(() => { + const [uppy] = useState(() => { const endpoint = buildEndpoint(projectId, parentFolderId) return ( new Uppy({ // logger: Uppy.debugLogger, - allowMultipleUploads: false, + allowMultipleUploadBatches: false, restrictions: { maxNumberOfFiles, maxFileSize: maxFileSize || null, diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.jsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.jsx index bac16116ff..f025a69c10 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.jsx +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-draggable.jsx @@ -2,7 +2,6 @@ import { useRef, useEffect, useState } from 'react' import PropTypes from 'prop-types' import { useTranslation } from 'react-i18next' import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles' - import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd' import { HTML5Backend, diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx index 2ca53fc660..0d290beeba 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button/new-project-button-modal.tsx @@ -1,9 +1,10 @@ import BlankProjectModal from './blank-project-modal' import ExampleProjectModal from './example-project-modal' import importOverleafModules from '../../../../../macros/import-overleaf-module.macro' -import { JSXElementConstructor, lazy, Suspense } from 'react' +import { JSXElementConstructor, lazy, Suspense, useCallback } from 'react' import { Nullable } from '../../../../../../types/utils' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' +import { useLocation } from '@/shared/hooks/use-location' const UploadProjectModal = lazy(() => import('./upload-project-modal')) @@ -26,6 +27,15 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) { onHide: () => void }> = importProjectFromGithubModalWrapper?.import.default + const location = useLocation() + + const openProject = useCallback( + (projectId: string) => { + location.assign(`/project/${projectId}`) + }, + [location] + ) + switch (modal) { case 'blank_project': return @@ -34,7 +44,7 @@ function NewProjectButtonModal({ modal, onHide }: NewProjectButtonModalProps) { case 'upload_project': return ( }> - + ) case 'import_from_github': diff --git a/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx b/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx index f02c56dc3f..92e5d4f0ea 100644 --- a/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx +++ b/services/web/frontend/js/features/project-list/components/new-project-button/upload-project-modal.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { Button, Modal } from 'react-bootstrap' import { useTranslation } from 'react-i18next' import Uppy from '@uppy/core' -import { Dashboard, useUppy } from '@uppy/react' +import { Dashboard } from '@uppy/react' import XHRUpload from '@uppy/xhr-upload' import AccessibleModal from '../../../../shared/components/accessible-modal' import getMeta from '../../../../utils/meta' @@ -10,7 +10,6 @@ import { ExposedSettings } from '../../../../../../types/exposed-settings' import '@uppy/core/dist/style.css' import '@uppy/dashboard/dist/style.css' -import { useLocation } from '../../../../shared/hooks/use-location' type UploadResponse = { project_id: string @@ -18,19 +17,19 @@ type UploadResponse = { type UploadProjectModalProps = { onHide: () => void + openProject: (projectId: string) => void } -function UploadProjectModal({ onHide }: UploadProjectModalProps) { +function UploadProjectModal({ onHide, openProject }: UploadProjectModalProps) { const { t } = useTranslation() const { maxUploadSize, projectUploadTimeout } = getMeta( 'ol-ExposedSettings' ) as ExposedSettings const [ableToUpload, setAbleToUpload] = useState(false) - const location = useLocation() - const uppy: Uppy.Uppy = useUppy(() => { - return Uppy({ - allowMultipleUploads: false, + const [uppy] = useState(() => { + return new Uppy({ + allowMultipleUploadBatches: false, restrictions: { maxNumberOfFiles: 1, maxFileSize: maxUploadSize, @@ -62,7 +61,7 @@ function UploadProjectModal({ onHide }: UploadProjectModalProps) { const { project_id: projectId }: UploadResponse = response.body if (projectId) { - location.assign(`/project/${projectId}`) + openProject(projectId) } }) .on('restriction-failed', () => { diff --git a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx index 87b7280661..7a280ddd73 100644 --- a/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx +++ b/services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx @@ -2,10 +2,10 @@ import { FC, useCallback, useEffect, useState } from 'react' import { useFigureModalContext } from '../figure-modal-context' import { useCurrentProjectFolders } from '../../../hooks/use-current-project-folders' import { File } from '../../../utils/file' -import { Dashboard, useUppy } from '@uppy/react' +import { Dashboard } from '@uppy/react' import '@uppy/core/dist/style.css' import '@uppy/dashboard/dist/style.css' -import { Uppy, UppyFile } from '@uppy/core' +import { Uppy, type UppyFile } from '@uppy/core' import XHRUpload from '@uppy/xhr-upload' import { refreshProjectMetadata } from '../../../../file-tree/util/api' import { useProjectContext } from '../../../../../shared/context/project-context' @@ -41,10 +41,9 @@ export const FigureModalUploadFileSource: FC = () => { const [name, setName] = useState('') const [uploading, setUploading] = useState(false) const [uploadError, setUploadError] = useState(null) - - const uppy = useUppy(() => + const [uppy] = useState(() => new Uppy({ - allowMultipleUploads: false, + allowMultipleUploadBatches: false, restrictions: { maxNumberOfFiles: 1, maxFileSize: maxFileSize || null, @@ -54,6 +53,7 @@ export const FigureModalUploadFileSource: FC = () => { }) // use the basic XHR uploader .use(XHRUpload, { + endpoint: `/project/${projectId}/upload?folder_id=${rootFile.id}`, headers: { 'X-CSRF-TOKEN': window.csrfToken, }, @@ -91,7 +91,7 @@ export const FigureModalUploadFileSource: FC = () => { useEffect(() => { // broadcast doc metadata after each successful upload - const onUploadSuccess = (_file: UppyFile, response: any) => { + const onUploadSuccess = (_file: UppyFile | undefined, response: any) => { setUploading(false) if (response.body.entity_type === 'doc') { window.setTimeout(() => { @@ -139,7 +139,11 @@ export const FigureModalUploadFileSource: FC = () => { } // handle upload errors - const onError = (_file: UppyFile, error: any, response: any) => { + const onError = ( + _file: UppyFile | undefined, + error: any, + response: any + ) => { setUploading(false) setUploadError(error) switch (response?.status) { diff --git a/services/web/frontend/stylesheets/app/editor/figure-modal.less b/services/web/frontend/stylesheets/app/editor/figure-modal.less index 3e9743fc2c..8561d0aa18 100644 --- a/services/web/frontend/stylesheets/app/editor/figure-modal.less +++ b/services/web/frontend/stylesheets/app/editor/figure-modal.less @@ -145,3 +145,7 @@ .figure-modal .select-wrapper:not(:first-child) { margin-top: 16px; } + +.figure-modal-upload .uppy-Dashboard-AddFiles-list { + display: none; +} diff --git a/services/web/package.json b/services/web/package.json index ebc7224fd9..45e5f18ed1 100644 --- a/services/web/package.json +++ b/services/web/package.json @@ -13,7 +13,7 @@ "test:unit:all": "npm run test:unit:run_dir -- test/unit/src modules/*/test/unit/src", "test:unit:all:silent": "npm run test:unit:all -- --reporter dot", "test:unit:app": "npm run test:unit:run_dir -- test/unit/src", - "test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,jsx,mjs,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,jsx,ts,tsx}' test/frontend modules/*/test/frontend", + "test:frontend": "NODE_ENV=test TZ=GMT mocha --recursive --timeout 5000 --exit --extension js,jsx,mjs,ts,tsx --grep=$MOCHA_GREP --require test/frontend/bootstrap.js --ignore '**/*.spec.{js,jsx,ts,tsx}' --ignore '**/helpers/**/*.{js,jsx,ts,tsx}' test/frontend modules/*/test/frontend", "test:frontend:coverage": "c8 --all --include 'frontend/js' --include 'modules/*/frontend/js' --exclude 'frontend/js/vendor' --reporter=lcov --reporter=text-summary npm run test:frontend", "start": "node app.js", "nodemon": "node --watch app.js --watch-locales", @@ -243,11 +243,11 @@ "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", - "@uppy/core": "^1.15.0", - "@uppy/dashboard": "^1.11.0", - "@uppy/react": "^1.11.0", - "@uppy/utils": "^4.0.7", - "@uppy/xhr-upload": "^1.6.8", + "@uppy/core": "^3.8.0", + "@uppy/dashboard": "^3.7.1", + "@uppy/react": "^3.2.1", + "@uppy/utils": "^5.7.0", + "@uppy/xhr-upload": "^3.6.0", "abort-controller": "^3.0.0", "acorn": "^7.1.1", "acorn-walk": "^7.1.1", diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.spec.tsx new file mode 100644 index 0000000000..8a7b41b167 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.spec.tsx @@ -0,0 +1,69 @@ +import FileTreeCreateNameInput from '../../../../../../frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input' +import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name' + +describe('', function () { + it('renders an empty input', function () { + cy.mount( + + + + ) + + cy.findByLabelText('File Name') + cy.findByPlaceholderText('File Name') + }) + + it('renders a custom label and placeholder', function () { + cy.mount( + + + + ) + + cy.findByLabelText('File name in this project') + cy.findByPlaceholderText('Enter a file name…') + }) + + it('uses an initial name', function () { + cy.mount( + + + + ) + + cy.findByLabelText('File Name').should('have.value', 'test.tex') + }) + + it('focuses the name', function () { + cy.spy(window, 'requestAnimationFrame').as('requestAnimationFrame') + + cy.mount( + + + + ) + + cy.findByLabelText('File Name').as('input') + + cy.get('@input').should('have.value', 'test.tex') + + cy.get('@requestAnimationFrame').should('have.been.calledOnce') + + // https://github.com/jsdom/jsdom/issues/2995 + // "window.getSelection doesn't work with selection of element" + // const selection = window.getSelection().toString() + // expect(selection).to.equal('test') + + // wait for the selection to update + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(100) + + cy.get('@input').then(element => { + expect(element.get(0).selectionStart).to.equal(0) + expect(element.get(0).selectionEnd).to.equal(4) + }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.jsx deleted file mode 100644 index 81cc4a0437..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-create-name-input.test.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { expect } from 'chai' -import { screen, waitFor, cleanup } from '@testing-library/react' -import sinon from 'sinon' - -import renderWithContext from '../../helpers/render-with-context' - -import FileTreeCreateNameInput from '../../../../../../frontend/js/features/file-tree/components/file-tree-create/file-tree-create-name-input' -import FileTreeCreateNameProvider from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-create-name' - -describe('', function () { - const sandbox = sinon.createSandbox() - - beforeEach(function () { - sandbox.spy(window, 'requestAnimationFrame') - }) - - afterEach(function () { - sandbox.restore() - cleanup() - }) - - it('renders an empty input', async function () { - renderWithContext( - - - - ) - - await screen.getByLabelText('File Name') - await screen.getByPlaceholderText('File Name') - }) - - it('renders a custom label and placeholder', async function () { - renderWithContext( - - - - ) - - await screen.getByLabelText('File name in this project') - await screen.getByPlaceholderText('Enter a file name…') - }) - - it('uses an initial name', async function () { - renderWithContext( - - - - ) - - const input = await screen.getByLabelText('File Name') - expect(input.value).to.equal('test.tex') - }) - - it('focuses the name', async function () { - renderWithContext( - - - - ) - - const input = await screen.getByLabelText('File Name') - expect(input.value).to.equal('test.tex') - - await waitFor( - () => expect(window.requestAnimationFrame).to.have.been.calledOnce - ) - - // https://github.com/jsdom/jsdom/issues/2995 - // "window.getSelection doesn't work with selection of element" - // const selection = window.getSelection().toString() - // expect(selection).to.equal('test') - - // wait for the selection to update - await new Promise(resolve => window.setTimeout(resolve, 100)) - - expect(input.selectionStart).to.equal(0) - expect(input.selectionEnd).to.equal(4) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.spec.tsx new file mode 100644 index 0000000000..7eae4bf602 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.spec.tsx @@ -0,0 +1,508 @@ +import { useEffect } from 'react' +import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file' +import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable' +import { useFileTreeData } from '../../../../../../frontend/js/shared/context/file-tree-data-context' +import { EditorProviders } from '../../../../helpers/editor-providers' +import { FileTreeProvider } from '../../helpers/file-tree-provider' + +describe('', function () { + it('handles invalid file names', function () { + cy.mount( + + + + + + ) + + cy.findByLabelText('File Name').as('input') + cy.findByRole('button', { name: 'Create' }).as('submit') + + cy.get('@input').should('have.value', 'name.tex') + cy.get('@submit').should('not.be.disabled') + cy.findByRole('alert').should('not.exist') + + cy.get('@input').clear() + cy.get('@submit').should('be.disabled') + cy.findByRole('alert').should('contain.text', 'File name is empty') + + cy.get('@input').type('test.tex') + cy.get('@submit').should('not.be.disabled') + cy.findByRole('alert').should('not.exist') + + cy.get('@input').type('oops/i/did/it/again') + cy.get('@submit').should('be.disabled') + cy.findByRole('alert').should('contain.text', 'contains invalid characters') + }) + + it('displays an error when the file limit is reached', function () { + cy.window().then(win => { + win.ExposedSettings.maxEntitiesPerProject = 10 + }) + + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: Array.from({ length: 10 }, (_, index) => ({ + _id: `entity-${index}`, + })), + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByRole('alert') + .invoke('text') + .should('match', /This project has reached the \d+ file limit/) + }) + + it('displays a warning when the file limit is nearly reached', function () { + cy.window().then(win => { + win.ExposedSettings.maxEntitiesPerProject = 10 + }) + + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: Array.from({ length: 9 }, (_, index) => ({ + _id: `entity-${index}`, + })), + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/) + }) + + it('counts files in nested folders', function () { + cy.window().then(win => { + win.ExposedSettings.maxEntitiesPerProject = 10 + }) + + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: 'doc-1' }], + fileRefs: [], + folders: [ + { + docs: [{ _id: 'doc-2' }], + fileRefs: [], + folders: [ + { + docs: [ + { _id: 'doc-3' }, + { _id: 'doc-4' }, + { _id: 'doc-5' }, + { _id: 'doc-6' }, + { _id: 'doc-7' }, + ], + fileRefs: [], + folders: [], + }, + ], + }, + ], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/) + }) + + it('counts folders toward the limit', function () { + cy.window().then(win => { + win.ExposedSettings.maxEntitiesPerProject = 10 + }) + + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: 'doc-1' }], + fileRefs: [], + folders: [ + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + { docs: [], fileRefs: [], folders: [] }, + ], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByText(/This project is approaching the file limit \(\d+\/\d+\)/) + }) + + it('creates a new file when the form is submitted', function () { + cy.intercept('post', '/project/*/doc', { + statusCode: 204, + }).as('createDoc') + + cy.mount( + + + + + + ) + + cy.findByLabelText('File Name').type('test') + cy.findByRole('button', { name: 'Create' }).click() + + cy.wait('@createDoc') + + cy.get('@createDoc').its('request.body').should('deep.equal', { + parent_folder_id: 'root-folder-id', + name: 'test.tex', + }) + }) + + it('imports a new file from a project', function () { + cy.window().then(win => { + win.ExposedSettings.hasLinkedProjectFileFeature = true + win.ExposedSettings.hasLinkedProjectOutputFileFeature = true + }) + + cy.intercept('/user/projects', { + body: { + projects: [ + { + _id: 'test-project', + name: 'This Project', + }, + { + _id: 'project-1', + name: 'Project One', + }, + { + _id: 'project-2', + name: 'Project Two', + }, + ], + }, + }) + + cy.intercept('/project/*/entities', { + body: { + entities: [ + { + path: '/foo.tex', + }, + { + path: '/bar.tex', + }, + ], + }, + }) + + cy.intercept('post', '/project/*/compile', { + body: { + status: 'success', + outputFiles: [ + { + build: 'test', + path: 'baz.jpg', + }, + { + build: 'test', + path: 'ball.jpg', + }, + ], + }, + }) + + cy.intercept('post', '/project/*/linked_file', { + statusCode: 204, + }).as('createLinkedFile') + + cy.mount( + + + + + + ) + + // initial state, no project selected + cy.findByLabelText('Select a Project').should('not.be.disabled') + + // the submit button should be disabled + cy.findByRole('button', { name: 'Create' }).should('be.disabled') + + // the source file selector should be disabled + cy.findByLabelText('Select a File').should('be.disabled') + cy.findByLabelText('Select an Output File').should('not.exist') + // TODO: check for options length, excluding current project + + // select a project + cy.findByLabelText('Select a Project').select('project-2') + + // wait for the source file selector to be enabled + cy.findByLabelText('Select a File').should('not.be.disabled') + cy.findByLabelText('Select an Output File').should('not.exist') + cy.findByRole('button', { name: 'Create' }).should('be.disabled') + + // TODO: check for fileInput options length, excluding current project + + // click on the button to toggle between source and output files + cy.findByRole('button', { + // NOTE: When changing the label, update the other tests with this label as well. + name: 'select from output files', + }).click() + + // wait for the output file selector to be enabled + cy.findByLabelText('Select an Output File').should('not.be.disabled') + cy.findByLabelText('Select a File').should('not.exist') + cy.findByRole('button', { name: 'Create' }).should('be.disabled') + + // TODO: check for entityInput options length, excluding current project + cy.findByLabelText('Select an Output File').select('ball.jpg') + cy.findByRole('button', { name: 'Create' }).should('not.be.disabled') + cy.findByRole('button', { name: 'Create' }).click() + + cy.get('@createLinkedFile') + .its('request.body') + .should('deep.equal', { + name: 'ball.jpg', + provider: 'project_output_file', + parent_folder_id: 'root-folder-id', + data: { + source_project_id: 'project-2', + source_output_file_path: 'ball.jpg', + build_id: 'test', + }, + }) + }) + + describe('when the output files feature is not available', function () { + beforeEach(function () { + cy.window().then(win => { + win.ExposedSettings.hasLinkedProjectFileFeature = true + win.ExposedSettings.hasLinkedProjectOutputFileFeature = false + }) + }) + + it('should not show the import from output file mode', function () { + cy.intercept('/user/projects', { + body: { + projects: [ + { + _id: 'test-project', + name: 'This Project', + }, + { + _id: 'project-1', + name: 'Project One', + }, + { + _id: 'project-2', + name: 'Project Two', + }, + ], + }, + }) + + cy.mount( + + + + + + ) + + cy.findByLabelText('Select a File') + + cy.findByRole('button', { + name: 'select from output files', + }).should('not.exist') + }) + }) + + it('import from a URL when the form is submitted', function () { + cy.intercept('/project/*/linked_file', { + statusCode: 204, + }).as('createLinkedFile') + + cy.mount( + + + + + + ) + + cy.findByLabelText('URL to fetch the file from').type( + 'https://example.com/example.tex' + ) + cy.findByLabelText('File Name In This Project').should( + 'have.value', + 'example.tex' + ) + + // check that the name can still be edited manually + cy.findByLabelText('File Name In This Project').clear() + cy.findByLabelText('File Name In This Project').type('test.tex') + cy.findByLabelText('File Name In This Project').should( + 'have.value', + 'test.tex' + ) + + cy.findByRole('button', { name: 'Create' }).click() + + cy.get('@createLinkedFile') + .its('request.body') + .should('deep.equal', { + name: 'test.tex', + provider: 'url', + parent_folder_id: 'root-folder-id', + data: { url: 'https://example.com/example.tex' }, + }) + }) + + it('uploads a dropped file', function () { + cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', { + statusCode: 204, + }).as('uploadFile') + + cy.mount( + + + + + + ) + + // the submit button should not be present + cy.findByRole('button', { name: 'Create' }).should('not.exist') + + cy.get('input[type=file]') + .eq(0) + .selectFile( + { + contents: Cypress.Buffer.from('test'), + fileName: 'test.tex', + mimeType: 'text/plain', + lastModified: Date.now(), + }, + { + action: 'drag-drop', + force: true, // invisible element + } + ) + + cy.wait('@uploadFile') + }) + + it('uploads a pasted file', function () { + cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', { + statusCode: 204, + }).as('uploadFile') + + cy.mount( + + + + + + ) + + // the submit button should not be present + cy.findByRole('button', { name: 'Create' }).should('not.exist') + + cy.wrap(null).then(() => { + const clipboardData = new DataTransfer() + clipboardData.items.add( + new File(['test'], 'test.tex', { type: 'text/plain' }) + ) + cy.findByLabelText('Uppy Dashboard').trigger('paste', { clipboardData }) + }) + + cy.wait('@uploadFile') + }) + + it('displays upload errors', function () { + cy.intercept('post', '/project/*/upload?folder_id=root-folder-id', { + statusCode: 422, + body: { success: false, error: 'invalid_filename' }, + }).as('uploadFile') + + cy.mount( + + + + + + ) + + // the submit button should not be present + cy.findByRole('button', { name: 'Create' }).should('not.exist') + + cy.wrap(null).then(() => { + const clipboardData = new DataTransfer() + clipboardData.items.add( + new File(['test'], 'tes!t.tex', { type: 'text/plain' }) + ) + cy.findByLabelText('Uppy Dashboard').trigger('paste', { clipboardData }) + }) + + cy.wait('@uploadFile') + + cy.findByText( + `Upload failed: check that the file name doesn’t contain special characters, trailing/leading whitespace or more than 150 characters` + ) + }) +}) + +function OpenWithMode({ mode }: { mode: string }) { + const { newFileCreateMode, startCreatingFile } = useFileTreeActionable() + + const { fileCount } = useFileTreeData() + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => startCreatingFile(mode), []) + + if (!fileCount || !newFileCreateMode) { + return null + } + + return +} diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.jsx deleted file mode 100644 index 99bfcbabde..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-create/file-tree-modal-create-file.test.jsx +++ /dev/null @@ -1,505 +0,0 @@ -import { expect } from 'chai' -import * as sinon from 'sinon' -import { useEffect } from 'react' -import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react' -import fetchMock from 'fetch-mock' -import PropTypes from 'prop-types' - -import renderWithContext from '../../helpers/render-with-context' -import FileTreeModalCreateFile from '../../../../../../frontend/js/features/file-tree/components/modals/file-tree-modal-create-file' -import { useFileTreeActionable } from '../../../../../../frontend/js/features/file-tree/contexts/file-tree-actionable' -import { useFileTreeData } from '../../../../../../frontend/js/shared/context/file-tree-data-context' - -describe('', function () { - beforeEach(function () { - window.csrfToken = 'token' - }) - - afterEach(function () { - delete window.csrfToken - fetchMock.restore() - cleanup() - }) - - it('handles invalid file names', async function () { - renderWithContext() - - const submitButton = screen.getByRole('button', { name: 'Create' }) - - const input = screen.getByLabelText('File Name') - expect(input.value).to.equal('name.tex') - expect(submitButton.disabled).to.be.false - expect(screen.queryAllByRole('alert')).to.be.empty - - fireEvent.change(input, { target: { value: '' } }) - expect(submitButton.disabled).to.be.true - screen.getByRole( - (role, element) => - role === 'alert' && element.textContent.match(/File name is empty/) - ) - - await fireEvent.change(input, { target: { value: 'test.tex' } }) - expect(submitButton.disabled).to.be.false - expect(screen.queryAllByRole('alert')).to.be.empty - - await fireEvent.change(input, { target: { value: 'oops/i/did/it/again' } }) - expect(submitButton.disabled).to.be.true - screen.getByRole( - (role, element) => - role === 'alert' && - element.textContent.match(/contains invalid characters/) - ) - }) - - it('displays an error when the file limit is reached', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: Array.from({ length: 10 }, (_, index) => ({ - _id: `entity-${index}`, - })), - fileRefs: [], - folders: [], - }, - ] - - renderWithContext(, { - contextProps: { rootFolder }, - }) - - screen.getByRole( - (role, element) => - role === 'alert' && - element.textContent.match(/This project has reached the \d+ file limit/) - ) - }) - - it('displays a warning when the file limit is nearly reached', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: Array.from({ length: 9 }, (_, index) => ({ - _id: `entity-${index}`, - })), - fileRefs: [], - folders: [], - }, - ] - - renderWithContext(, { - contextProps: { rootFolder }, - }) - - screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) - }) - - it('counts files in nested folders', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: 'doc-1' }], - fileRefs: [], - folders: [ - { - docs: [{ _id: 'doc-2' }], - fileRefs: [], - folders: [ - { - docs: [ - { _id: 'doc-3' }, - { _id: 'doc-4' }, - { _id: 'doc-5' }, - { _id: 'doc-6' }, - { _id: 'doc-7' }, - ], - fileRefs: [], - folders: [], - }, - ], - }, - ], - }, - ] - - renderWithContext(, { - contextProps: { rootFolder }, - }) - - screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) - }) - - it('counts folders toward the limit', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: 'doc-1' }], - fileRefs: [], - folders: [ - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - { docs: [], fileRefs: [], folders: [] }, - ], - }, - ] - - renderWithContext(, { - contextProps: { rootFolder }, - }) - - screen.getByText(/This project is approaching the file limit \(\d+\/\d+\)/) - }) - - it('creates a new file when the form is submitted', async function () { - fetchMock.post('express:/project/:projectId/doc', () => 204) - - renderWithContext() - - const input = screen.getByLabelText('File Name') - await fireEvent.change(input, { target: { value: 'test.tex' } }) - - const submitButton = screen.getByRole('button', { name: 'Create' }) - - await fireEvent.click(submitButton) - - expect( - fetchMock.called('express:/project/:projectId/doc', { - body: { - parent_folder_id: 'root-folder-id', - name: 'test.tex', - }, - }) - ).to.be.true - }) - - it('imports a new file from a project', async function () { - fetchMock - .get('path:/user/projects', { - projects: [ - { - _id: 'test-project', - name: 'This Project', - }, - { - _id: 'project-1', - name: 'Project One', - }, - { - _id: 'project-2', - name: 'Project Two', - }, - ], - }) - .get('express:/project/:projectId/entities', { - entities: [ - { - path: '/foo.tex', - }, - { - path: '/bar.tex', - }, - ], - }) - .post('express:/project/:projectId/compile', { - status: 'success', - outputFiles: [ - { - build: 'test', - path: 'baz.jpg', - }, - { - build: 'test', - path: 'ball.jpg', - }, - ], - }) - .post('express:/project/:projectId/linked_file', () => 204) - - renderWithContext() - - // initial state, no project selected - const projectInput = screen.getByLabelText('Select a Project') - expect(projectInput.disabled).to.be.true - await waitFor(() => { - expect(projectInput.disabled).to.be.false - }) - - // the submit button should be disabled - const submitButton = screen.getByRole('button', { name: 'Create' }) - expect(submitButton.disabled).to.be.true - - // the source file selector should be disabled - const fileInput = screen.getByLabelText('Select a File') - expect(fileInput.disabled).to.be.true - // TODO: check for options length, excluding current project - - // select a project - await fireEvent.change(projectInput, { target: { value: 'project-2' } }) // TODO: getByRole('option')? - - // wait for the source file selector to be enabled - await waitFor(() => { - expect(fileInput.disabled).to.be.false - }) - expect(screen.queryByLabelText('Select a File')).not.to.be.null - expect(screen.queryByLabelText('Select an Output File')).to.be.null - expect(submitButton.disabled).to.be.true - - // TODO: check for fileInput options length, excluding current project - - // click on the button to toggle between source and output files - const sourceTypeButton = screen.getByRole('button', { - // NOTE: When changing the label, update the other tests with this label as well. - name: 'select from output files', - }) - await fireEvent.click(sourceTypeButton) - - // wait for the output file selector to be enabled - const entityInput = screen.getByLabelText('Select an Output File') - await waitFor(() => { - expect(entityInput.disabled).to.be.false - }) - expect(screen.queryByLabelText('Select a File')).to.be.null - expect(screen.queryByLabelText('Select an Output File')).not.to.be.null - expect(submitButton.disabled).to.be.true - - // TODO: check for entityInput options length, excluding current project - await fireEvent.change(entityInput, { target: { value: 'ball.jpg' } }) // TODO: getByRole('option')? - - await waitFor(() => { - expect(submitButton.disabled).to.be.false - }) - await fireEvent.click(submitButton) - - expect( - fetchMock.called('express:/project/:projectId/linked_file', { - body: { - name: 'ball.jpg', - provider: 'project_output_file', - parent_folder_id: 'root-folder-id', - data: { - source_project_id: 'project-2', - source_output_file_path: 'ball.jpg', - build_id: 'test', - }, - }, - }) - ).to.be.true - }) - - describe('when the output files feature is not available', function () { - const flagBefore = window.ExposedSettings.hasLinkedProjectOutputFileFeature - before(function () { - window.ExposedSettings.hasLinkedProjectOutputFileFeature = false - }) - after(function () { - window.ExposedSettings.hasLinkedProjectOutputFileFeature = flagBefore - }) - - it('should not show the import from output file mode', async function () { - fetchMock.get('path:/user/projects', { - projects: [ - { - _id: 'test-project', - name: 'This Project', - }, - { - _id: 'project-1', - name: 'Project One', - }, - { - _id: 'project-2', - name: 'Project Two', - }, - ], - }) - - renderWithContext() - - // should not show the toggle - expect( - screen.queryByRole('button', { - name: 'select from output files', - }) - ).to.be.null - }) - }) - - it('import from a URL when the form is submitted', async function () { - fetchMock.post('express:/project/:projectId/linked_file', () => 204) - - renderWithContext() - - const urlInput = screen.getByLabelText('URL to fetch the file from') - const nameInput = screen.getByLabelText('File Name In This Project') - - await fireEvent.change(urlInput, { - target: { value: 'https://example.com/example.tex' }, - }) - - // check that the name has updated automatically - expect(nameInput.value).to.equal('example.tex') - - await fireEvent.change(nameInput, { - target: { value: 'test.tex' }, - }) - - // check that the name can still be edited manually - expect(nameInput.value).to.equal('test.tex') - - const submitButton = screen.getByRole('button', { name: 'Create' }) - - await fireEvent.click(submitButton) - - expect( - fetchMock.called('express:/project/:projectId/linked_file', { - body: { - name: 'test.tex', - provider: 'url', - parent_folder_id: 'root-folder-id', - data: { url: 'https://example.com/example.tex' }, - }, - }) - ).to.be.true - }) - - it('uploads a dropped file', async function () { - const xhr = sinon.useFakeXMLHttpRequest() - const requests = [] - xhr.onCreate = request => { - requests.push(request) - } - - renderWithContext() - - // the submit button should not be present - expect(screen.queryByRole('button', { name: 'Create' })).to.be.null - - await waitFor(() => { - const dropzone = screen.getByLabelText('File Uploader') - - expect(dropzone).not.to.be.null - - fireEvent.drop(dropzone, { - dataTransfer: { - files: [new File(['test'], 'test.tex', { type: 'text/plain' })], - }, - }) - }) - - await waitFor(() => expect(requests).to.have.length(1)) - - const [request] = requests - expect(request.url).to.equal( - '/project/123abc/upload?folder_id=root-folder-id' - ) - expect(request.method).to.equal('POST') - - xhr.restore() - }) - - it('uploads a pasted file', async function () { - const xhr = sinon.useFakeXMLHttpRequest() - const requests = [] - xhr.onCreate = request => { - requests.push(request) - } - - renderWithContext() - - // the submit button should not be present - expect(screen.queryByRole('button', { name: 'Create' })).to.be.null - - await waitFor(() => { - const dropzone = screen.getByLabelText('File Uploader') - - expect(dropzone).not.to.be.null - - fireEvent.paste(dropzone, { - clipboardData: { - files: [new File(['test'], 'test.tex', { type: 'text/plain' })], - }, - }) - }) - - await waitFor(() => expect(requests).to.have.length(1)) - - const [request] = requests - expect(request.url).to.equal( - '/project/123abc/upload?folder_id=root-folder-id' - ) - expect(request.method).to.equal('POST') - - xhr.restore() - }) - - it('displays upload errors', async function () { - const xhr = sinon.useFakeXMLHttpRequest() - const requests = [] - xhr.onCreate = request => { - requests.push(request) - } - - renderWithContext() - - // the submit button should not be present - expect(screen.queryByRole('button', { name: 'Create' })).to.be.null - - await waitFor(() => { - const dropzone = screen.getByLabelText('File Uploader') - - expect(dropzone).not.to.be.null - - fireEvent.paste(dropzone, { - clipboardData: { - files: [new File(['test'], 'tes!t.tex', { type: 'text/plain' })], - }, - }) - }) - - await waitFor(() => expect(requests).to.have.length(1)) - - const [request] = requests - expect(request.url).to.equal( - '/project/123abc/upload?folder_id=root-folder-id' - ) - expect(request.method).to.equal('POST') - - request.respond( - 422, - { 'Content-Type': 'application/json' }, - '{ "success": false, "error": "invalid_filename" }' - ) - - await screen.findByText( - `Upload failed: check that the file name doesn’t contain special characters, trailing/leading whitespace or more than 150 characters` - ) - - xhr.restore() - }) -}) - -function OpenWithMode({ mode }) { - const { newFileCreateMode, startCreatingFile } = useFileTreeActionable() - - const { fileCount } = useFileTreeData() - - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => startCreatingFile(mode), []) - - if (!fileCount || !newFileCreateMode) { - return null - } - - return -} -OpenWithMode.propTypes = { - mode: PropTypes.string.isRequired, -} diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-doc.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-doc.spec.tsx new file mode 100644 index 0000000000..38a8734826 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-doc.spec.tsx @@ -0,0 +1,77 @@ +import FileTreeDoc from '../../../../../frontend/js/features/file-tree/components/file-tree-doc' +import { EditorProviders } from '../../../helpers/editor-providers' +import { FileTreeProvider } from '../helpers/file-tree-provider' + +describe('', function () { + it('renders unselected', function () { + cy.mount( + + + + + + ) + + cy.findByRole('treeitem', { selected: false }) + cy.get('i.linked-file-highlight').should('not.exist') + }) + + it('renders selected', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + , + + + ) + + cy.findByRole('treeitem', { selected: false }).click() + cy.findByRole('treeitem', { selected: true }) + }) + + it('renders as linked file', function () { + cy.mount( + + + + + + ) + + cy.findByRole('treeitem') + cy.get('i.linked-file-highlight') + }) + + it('multi-selects', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + , + + + ) + + cy.findByRole('treeitem').click({ ctrlKey: true, cmdKey: true }) + cy.findByRole('treeitem', { selected: true }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.jsx deleted file mode 100644 index 068235404c..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-doc.test.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import { expect } from 'chai' -import { screen, fireEvent } from '@testing-library/react' -import renderWithContext from '../helpers/render-with-context' - -import FileTreeDoc from '../../../../../frontend/js/features/file-tree/components/file-tree-doc' - -describe('', function () { - it('renders unselected', function () { - const { container } = renderWithContext( - - ) - - screen.getByRole('treeitem', { selected: false }) - expect(container.querySelector('i.linked-file-highlight')).to.not.exist - }) - - it('renders selected', function () { - renderWithContext( - , - { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc' }], - fileRefs: [], - folders: [], - }, - ], - }, - } - ) - - const treeitem = screen.getByRole('treeitem', { selected: false }) - fireEvent.click(treeitem) - - screen.getByRole('treeitem', { selected: true }) - }) - - it('renders as linked file', function () { - const { container } = renderWithContext( - - ) - - screen.getByRole('treeitem') - expect(container.querySelector('i.linked-file-highlight')).to.exist - }) - - it('selects', function () { - renderWithContext(, { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc' }], - fileRefs: [], - folders: [], - }, - ], - }, - }) - - const treeitem = screen.getByRole('treeitem', { selected: false }) - fireEvent.click(treeitem) - - screen.getByRole('treeitem', { selected: true }) - }) - - it('multi-selects', function () { - renderWithContext(, { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc' }], - fileRefs: [], - folders: [], - }, - ], - }, - }) - - const treeitem = screen.getByRole('treeitem') - - fireEvent.click(treeitem, { ctrlKey: true }) - screen.getByRole('treeitem', { selected: true }) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx new file mode 100644 index 0000000000..f2c70b1955 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.spec.tsx @@ -0,0 +1,190 @@ +import FileTreeFolderList from '../../../../../frontend/js/features/file-tree/components/file-tree-folder-list' +import { EditorProviders } from '../../../helpers/editor-providers' +import { FileTreeProvider } from '../helpers/file-tree-provider' + +describe('', function () { + it('renders empty', function () { + cy.mount( + + + + + + ) + + cy.findByRole('tree') + cy.findByRole('treeitem').should('not.exist') + }) + + it('renders docs, files and folders', function () { + cy.mount( + + + + + + ) + + cy.findByRole('tree') + cy.findByRole('treeitem', { name: 'A Folder' }) + cy.findByRole('treeitem', { name: 'doc.tex' }) + cy.findByRole('treeitem', { name: 'file.bib' }) + }) + + describe('selection and multi-selection', function () { + it('without write permissions', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '1' }, { _id: '2' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + // click on item 1: it gets selected + cy.findByRole('treeitem', { name: '1.tex' }).click() + cy.findByRole('treeitem', { name: '1.tex', selected: true }) + cy.findByRole('treeitem', { name: '2.tex', selected: false }) + + // meta-click on item 2: no changes + cy.findByRole('treeitem', { name: '2.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: true }) + cy.findByRole('treeitem', { name: '2.tex', selected: false }) + }) + + it('with write permissions', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + // click item 1: it gets selected + cy.findByRole('treeitem', { name: '1.tex' }).click() + cy.findByRole('treeitem', { name: '1.tex', selected: true }) + cy.findByRole('treeitem', { name: '2.tex', selected: false }) + cy.findByRole('treeitem', { name: '3.tex', selected: false }) + + // click on item 2: it gets selected and item 1 is not selected anymore + cy.findByRole('treeitem', { name: '2.tex' }).click() + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: false }) + + // meta-click on item 3: it gets selected and item 2 as well + cy.findByRole('treeitem', { name: '3.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: true }) + + // meta-click on item 1: add to selection + cy.findByRole('treeitem', { name: '1.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: true }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: true }) + + // meta-click on item 1: remove from selection + cy.findByRole('treeitem', { name: '1.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: true }) + + // meta-click on item 3: remove from selection + cy.findByRole('treeitem', { name: '3.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: false }) + + // meta-click on item 2: cannot unselect + cy.findByRole('treeitem', { name: '2.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: false }) + + // meta-click on item 3: add back to selection + cy.findByRole('treeitem', { name: '3.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: true }) + cy.findByRole('treeitem', { name: '3.tex', selected: true }) + + // click on item 3: unselect other items + cy.findByRole('treeitem', { name: '3.tex' }).click() + cy.findByRole('treeitem', { name: '1.tex', selected: false }) + cy.findByRole('treeitem', { name: '2.tex', selected: false }) + cy.findByRole('treeitem', { name: '3.tex', selected: true }) + }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.jsx deleted file mode 100644 index 3c14c7bc89..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-folder-list.test.jsx +++ /dev/null @@ -1,155 +0,0 @@ -import { expect } from 'chai' -import { screen, fireEvent } from '@testing-library/react' -import renderWithContext from '../helpers/render-with-context' - -import FileTreeFolderList from '../../../../../frontend/js/features/file-tree/components/file-tree-folder-list' - -describe('', function () { - it('renders empty', function () { - renderWithContext() - - screen.queryByRole('tree') - expect(screen.queryByRole('treeitem')).to.not.exist - }) - - it('renders docs, files and folders', function () { - const aFolder = { - _id: '456def', - name: 'A Folder', - folders: [], - docs: [], - fileRefs: [], - } - const aDoc = { _id: '789ghi', name: 'doc.tex', linkedFileData: {} } - const aFile = { _id: '987jkl', name: 'file.bib', linkedFileData: {} } - renderWithContext( - - ) - - screen.queryByRole('tree') - screen.queryByRole('treeitem', { name: 'A Folder' }) - screen.queryByRole('treeitem', { name: 'doc.tex' }) - screen.queryByRole('treeitem', { name: 'file.bib' }) - }) - - describe('selection and multi-selection', function () { - it('without write permissions', function () { - const docs = [ - { _id: '1', name: '1.tex' }, - { _id: '2', name: '2.tex' }, - ] - renderWithContext( - , - { - contextProps: { - permissionsLevel: 'readOnly', - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '1' }, { _id: '2' }], - fileRefs: [], - folders: [], - }, - ], - }, - } - ) - - const treeitem1 = screen.getByRole('treeitem', { name: '1.tex' }) - const treeitem2 = screen.getByRole('treeitem', { name: '2.tex' }) - - // click on item 1: it gets selected - fireEvent.click(treeitem1) - screen.getByRole('treeitem', { name: '1.tex', selected: true }) - screen.getByRole('treeitem', { name: '2.tex', selected: false }) - - // meta-click on item 2: no changes - fireEvent.click(treeitem2, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: true }) - screen.getByRole('treeitem', { name: '2.tex', selected: false }) - }) - - it('with write permissions', function () { - const docs = [ - { _id: '1', name: '1.tex' }, - { _id: '2', name: '2.tex' }, - { _id: '3', name: '3.tex' }, - ] - renderWithContext( - , - { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '1' }, { _id: '2' }, { _id: '3' }], - fileRefs: [], - folders: [], - }, - ], - }, - } - ) - - const treeitem1 = screen.getByRole('treeitem', { name: '1.tex' }) - const treeitem2 = screen.getByRole('treeitem', { name: '2.tex' }) - const treeitem3 = screen.getByRole('treeitem', { name: '3.tex' }) - - // click item 1: it gets selected - fireEvent.click(treeitem1) - screen.getByRole('treeitem', { name: '1.tex', selected: true }) - screen.getByRole('treeitem', { name: '2.tex', selected: false }) - screen.getByRole('treeitem', { name: '3.tex', selected: false }) - - // click on item 2: it gets selected and item 1 is not selected anymore - fireEvent.click(treeitem2) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: false }) - - // meta-click on item 3: it gets selected and item 2 as well - fireEvent.click(treeitem3, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: true }) - - // meta-click on item 1: add to selection - fireEvent.click(treeitem1, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: true }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: true }) - - // meta-click on item 1: remove from selection - fireEvent.click(treeitem1, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: true }) - - // meta-click on item 3: remove from selection - fireEvent.click(treeitem3, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: false }) - - // meta-click on item 2: cannot unselect - fireEvent.click(treeitem2, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: false }) - - // meta-click on item 3: add back to selection - fireEvent.click(treeitem3, { ctrlKey: true }) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: true }) - screen.getByRole('treeitem', { name: '3.tex', selected: true }) - - // click on item 3: unselect other items - fireEvent.click(treeitem3) - screen.getByRole('treeitem', { name: '1.tex', selected: false }) - screen.getByRole('treeitem', { name: '2.tex', selected: false }) - screen.getByRole('treeitem', { name: '3.tex', selected: true }) - }) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-folder.spec.tsx new file mode 100644 index 0000000000..24dc139de7 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-folder.spec.tsx @@ -0,0 +1,134 @@ +import FileTreeFolder from '../../../../../frontend/js/features/file-tree/components/file-tree-folder' +import { EditorProviders } from '../../../helpers/editor-providers' +import { FileTreeProvider } from '../helpers/file-tree-provider' +import { getContainerEl } from 'cypress/react' +import ReactDom from 'react-dom' + +describe('', function () { + it('renders unselected', function () { + cy.mount( + + + + + + ) + + cy.findByRole('treeitem', { selected: false }) + cy.findByRole('tree').should('not.exist') + }) + + it('renders selected', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByRole('treeitem', { selected: false }).click() + cy.findByRole('treeitem', { selected: true }) + cy.findByRole('tree').should('not.exist') + }) + + it('expands', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByRole('treeitem') + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByRole('tree') + }) + + it('saves the expanded state for the next render', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc' }], + fileRefs: [], + folders: [], + }, + ] + + cy.mount( + + + + + + ) + + cy.findByRole('tree').should('not.exist') + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByRole('tree') + + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) + + cy.mount( + + + + + + ) + + cy.findByRole('tree') + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.jsx deleted file mode 100644 index a16acc9604..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-folder.test.jsx +++ /dev/null @@ -1,133 +0,0 @@ -import { expect } from 'chai' -import { screen, fireEvent } from '@testing-library/react' -import renderWithContext from '../helpers/render-with-context' - -import FileTreeFolder from '../../../../../frontend/js/features/file-tree/components/file-tree-folder' - -describe('', function () { - beforeEach(function () { - global.localStorage.clear() - }) - - it('renders unselected', function () { - renderWithContext( - - ) - - screen.getByRole('treeitem', { selected: false }) - expect(screen.queryByRole('tree')).to.not.exist - }) - - it('renders selected', function () { - renderWithContext( - , - { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc' }], - fileRefs: [], - folders: [], - }, - ], - }, - } - ) - - const treeitem = screen.getByRole('treeitem', { selected: false }) - fireEvent.click(treeitem) - - screen.getByRole('treeitem', { selected: true }) - expect(screen.queryByRole('tree')).to.not.exist - }) - - it('expands', function () { - renderWithContext( - , - { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc' }], - fileRefs: [], - folders: [], - }, - ], - }, - } - ) - - screen.getByRole('treeitem') - const expandButton = screen.getByRole('button', { name: 'Expand' }) - - fireEvent.click(expandButton) - screen.getByRole('tree') - }) - - it('saves the expanded state for the next render', function () { - const { unmount } = renderWithContext( - , - { - contextProps: { - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc' }], - fileRefs: [], - folders: [], - }, - ], - }, - } - ) - - expect(screen.queryByRole('tree')).to.not.exist - - const expandButton = screen.getByRole('button', { name: 'Expand' }) - fireEvent.click(expandButton) - screen.getByRole('tree') - - unmount() - - renderWithContext( - - ) - - screen.getByRole('tree') - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx new file mode 100644 index 0000000000..2780c31183 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.spec.tsx @@ -0,0 +1,98 @@ +import FileTreeitemInner from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner' +import FileTreeContextMenu from '../../../../../../frontend/js/features/file-tree/components/file-tree-context-menu' +import { EditorProviders } from '../../../../helpers/editor-providers' +import { FileTreeProvider } from '../../helpers/file-tree-provider' + +describe('', function () { + describe('menu', function () { + it('does not display if file is not selected', function () { + cy.mount( + + + , + + + ) + + cy.findByRole('menu', { hidden: true }).should('not.exist') + }) + }) + + describe('context menu', function () { + it('does not display without write permissions', function () { + cy.mount( + + + + + + + ) + + cy.get('div.entity').trigger('contextmenu') + cy.findByRole('menu', { hidden: true }).should('not.exist') + }) + + it('open / close', function () { + cy.mount( + + + + + + + ) + + cy.findByRole('menu', { hidden: true }).should('not.exist') + + // open the context menu + cy.get('div.entity').trigger('contextmenu') + cy.findByRole('menu') + + // close the context menu + cy.get('div.entity').click() + cy.findByRole('menu').should('not.exist') + }) + }) + + describe('name', function () { + it('renders name', function () { + cy.mount( + + + + + + ) + + cy.findByRole('button', { name: 'bar.tex' }) + cy.findByRole('textbox').should('not.exist') + }) + + it('starts rename on menu item click', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc', name: 'bar.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + + + + ) + + cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('menuitem', { name: 'Rename' }).click() + cy.findByRole('button', { name: 'bar.tex' }).should('not.exist') + cy.findByRole('textbox') + }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.jsx deleted file mode 100644 index 334d755154..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-inner.test.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent } from '@testing-library/react' -import renderWithContext from '../../helpers/render-with-context' - -import FileTreeitemInner from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-inner' -import FileTreeContextMenu from '../../../../../../frontend/js/features/file-tree/components/file-tree-context-menu' - -describe('', function () { - const setContextMenuCoords = sinon.stub() - - afterEach(function () { - setContextMenuCoords.reset() - }) - - describe('menu', function () { - it('does not display if file is not selected', function () { - renderWithContext( - , - {} - ) - - expect(screen.queryByRole('menu', { visible: false })).to.not.exist - }) - }) - - describe('context menu', function () { - it('does not display without write permissions', function () { - const { container } = renderWithContext( - <> - - - , - { - contextProps: { permissionsLevel: 'readOnly' }, - } - ) - - const entityElement = container.querySelector('div.entity') - fireEvent.contextMenu(entityElement) - expect(screen.queryByRole('menu')).to.not.exist - }) - - it('open / close', function () { - const { container } = renderWithContext( - <> - - - - ) - - expect(screen.queryByRole('menu')).to.be.null - - // open the context menu - const entityElement = container.querySelector('div.entity') - fireEvent.contextMenu(entityElement) - screen.getByRole('menu', { visible: true }) - - // close the context menu - fireEvent.click(entityElement) - expect(screen.queryByRole('menu')).to.be.null - }) - }) - - describe('name', function () { - it('renders name', function () { - renderWithContext( - - ) - - screen.getByRole('button', { name: 'bar.tex' }) - expect(screen.queryByRole('textbox')).to.not.exist - }) - - it('starts rename on menu item click', function () { - renderWithContext( - <> - - - , - { - contextProps: { - rootDocId: '123abc', - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc', name: 'bar.tex' }], - folders: [], - fileRefs: [], - }, - ], - }, - } - ) - const toggleButton = screen.getByRole('button', { name: 'Menu' }) - fireEvent.click(toggleButton) - const renameButton = screen.getByRole('menuitem', { name: 'Rename' }) - fireEvent.click(renameButton) - expect(screen.queryByRole('button', { name: 'bar.tex' })).to.not.exist - screen.getByRole('textbox') - }) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.spec.tsx new file mode 100644 index 0000000000..c699e07468 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.spec.tsx @@ -0,0 +1,108 @@ +import FileTreeItemName from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name' +import { EditorProviders } from '../../../../helpers/editor-providers' +import { FileTreeProvider } from '../../helpers/file-tree-provider' + +describe('', function () { + it('renders name as button', function () { + cy.mount( + + + + + + ) + + cy.findByRole('button', { name: 'foo.tex' }) + cy.findByRole('textbox').should('not.exist') + }) + + it("doesn't start renaming on unselected component", function () { + cy.mount( + + + + + + ) + + cy.findByRole('button').click() + cy.findByRole('button').click() + cy.findByRole('button').dblclick() + cy.findByRole('textbox').should('not.exist') + }) + + it('start renaming on double-click', function () { + cy.mount( + + + + + + ) + + cy.findByRole('button').click() + cy.findByRole('button').click() + cy.findByRole('button').dblclick() + cy.findByRole('textbox') + cy.findByRole('button').should('not.exist') + cy.get('@setIsDraggable').should('have.been.calledWith', false) + }) + + it('cannot start renaming in read-only', function () { + cy.mount( + + + + + + ) + + cy.findByRole('button').click() + cy.findByRole('button').click() + cy.findByRole('button').dblclick() + + cy.findByRole('textbox').should('not.exist') + }) + + describe('stop renaming', function () { + it('on Escape', function () { + cy.mount( + + + + + + ) + + cy.findByRole('button').click() + cy.findByRole('button').click() + cy.findByRole('button').dblclick() + + cy.findByRole('textbox').clear() + cy.findByRole('textbox').type('bar.tex{esc}') + + cy.findByRole('button', { name: 'foo.tex' }) + cy.get('@setIsDraggable').should('have.been.calledWith', true) + }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.jsx deleted file mode 100644 index fb7c10f767..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-item/file-tree-item-name.test.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent, cleanup } from '@testing-library/react' -import renderWithContext from '../../helpers/render-with-context' - -import FileTreeItemName from '../../../../../../frontend/js/features/file-tree/components/file-tree-item/file-tree-item-name' - -describe('', function () { - const sandbox = sinon.createSandbox() - const setIsDraggable = sinon.stub() - - beforeEach(function () { - sandbox.spy(window, 'requestAnimationFrame') - }) - - afterEach(function () { - sandbox.restore() - setIsDraggable.reset() - cleanup() - }) - - it('renders name as button', function () { - renderWithContext( - - ) - - screen.getByRole('button', { name: 'foo.tex' }) - expect(screen.queryByRole('textbox')).to.not.exist - }) - - it("doesn't start renaming on unselected component", function () { - renderWithContext( - - ) - - const button = screen.queryByRole('button') - fireEvent.click(button) - fireEvent.click(button) - fireEvent.doubleClick(button) - expect(screen.queryByRole('textbox')).to.not.exist - }) - - it('start renaming on double-click', function () { - renderWithContext( - - ) - - const button = screen.queryByRole('button') - fireEvent.click(button) - fireEvent.click(button) - fireEvent.doubleClick(button) - screen.getByRole('textbox') - expect(screen.queryByRole('button')).to.not.exist - expect(window.requestAnimationFrame).to.be.calledOnce - expect(setIsDraggable).to.be.calledWith(false) - }) - - it('cannot start renaming in read-only', function () { - renderWithContext( - , - { - contextProps: { permissionsLevel: 'readOnly' }, - } - ) - - const button = screen.queryByRole('button') - fireEvent.click(button) - fireEvent.click(button) - fireEvent.doubleClick(button) - - expect(screen.queryByRole('textbox')).to.not.exist - }) - - describe('stop renaming', function () { - beforeEach(function () { - renderWithContext( - - ) - - const button = screen.getByRole('button') - fireEvent.click(button) - fireEvent.click(button) - fireEvent.doubleClick(button) - - const input = screen.getByRole('textbox') - fireEvent.change(input, { target: { value: 'bar.tex' } }) - }) - - it('on Escape', function () { - const input = screen.getByRole('textbox') - fireEvent.keyDown(input, { key: 'Escape' }) - - screen.getByRole('button', { name: 'foo.tex' }) - expect(setIsDraggable).to.be.calledWith(true) - }) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx new file mode 100644 index 0000000000..66d631e0f1 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-root.spec.tsx @@ -0,0 +1,394 @@ +// @ts-ignore +import MockedSocket from 'socket.io-mock' +import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' +import { EditorProviders } from '../../../helpers/editor-providers' + +describe('', function () { + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-user', { id: 'user1' }) + }) + }) + + it('renders', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('tree') + cy.findByRole('treeitem') + cy.findByRole('treeitem', { name: 'main.tex', selected: true }) + cy.get('.disconnected-overlay').should('not.exist') + }) + + it('renders with invalid selected doc in local storage', function () { + global.localStorage.setItem( + 'doc.open_id.123abc', + JSON.stringify('not-a-valid-id') + ) + + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + // as a proxy to check that the invalid entity has not been select we start + // a delete and ensure the modal is displayed (the cancel button can be + // selected) This is needed to make sure the test fail. + cy.findByRole('treeitem', { name: 'main.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('menuitem', { name: 'Delete' }).click() + cy.findByRole('button', { name: 'Cancel' }) + }) + + it('renders disconnected overlay', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.get('.disconnected-overlay') + }) + + it('fire onSelect', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [ + { _id: '456def', name: 'main.tex' }, + { _id: '789ghi', name: 'other.tex' }, + ], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.get('@onSelect').should('have.been.calledOnceWith', [ + Cypress.sinon.match({ + entity: Cypress.sinon.match({ _id: '456def', name: 'main.tex' }), + }), + ]) + cy.findByRole('tree') + cy.findByRole('treeitem', { name: 'other.tex' }).click() + cy.get('@onSelect').should('have.been.calledWith', [ + Cypress.sinon.match({ + entity: Cypress.sinon.match({ _id: '789ghi', name: 'other.tex' }), + }), + ]) + }) + + it('listen to editor.openDoc', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [ + { _id: '456def', name: 'main.tex' }, + { _id: '789ghi', name: 'other.tex' }, + ], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('treeitem', { name: 'main.tex', selected: true }) + + // entities not found should be ignored + cy.document().trigger('editor.openDoc', { detail: 'not-an-id' }) + cy.findByRole('treeitem', { name: 'main.tex', selected: true }) + + cy.document().trigger('editor.openDoc', { detail: '789ghi' }) + cy.findByRole('treeitem', { name: 'main.tex', selected: false }) + cy.findByRole('treeitem', { name: 'other.tex', selected: true }) + }) + + it('only shows a menu button when a single item is selected', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [ + { _id: '456def', name: 'main.tex' }, + { _id: '789ghi', name: 'other.tex' }, + ], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('treeitem', { name: 'main.tex', selected: true }) + cy.findByRole('treeitem', { name: 'other.tex', selected: false }) + + // single item selected: menu button is visible + cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 1) + + // select the other item + cy.findByRole('treeitem', { name: 'other.tex' }).click() + + cy.findByRole('treeitem', { name: 'main.tex', selected: false }) + cy.findByRole('treeitem', { name: 'other.tex', selected: true }) + + // single item selected: menu button is visible + cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 1) + + // multi-select the main item + cy.findByRole('treeitem', { name: 'main.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + + cy.findByRole('treeitem', { name: 'main.tex', selected: true }) + cy.findByRole('treeitem', { name: 'other.tex', selected: true }) + + // multiple items selected: no menu button is visible + cy.findAllByRole('button', { name: 'Menu' }).should('have.length', 0) + }) + + describe('when deselecting files', function () { + beforeEach(function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '123abc', name: 'main.tex' }], + folders: [ + { + _id: '789ghi', + name: 'thefolder', + docs: [{ _id: '456def', name: 'sub.tex' }], + fileRefs: [], + folders: [], + }, + ], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + // select the sub file + cy.findByRole('treeitem', { name: 'sub.tex' }).click() + cy.findByRole('treeitem', { name: 'sub.tex' }).should( + 'have.attr', + 'aria-selected', + 'true' + ) + + // click on empty area (after giving it extra height below the tree) + cy.findByTestId('file-tree-inner') + .invoke('attr', 'style', 'height: 400px') + .click() + }) + + it('removes the selected indicator', function () { + cy.findByRole('treeitem', { selected: true }).should('not.exist') + }) + + it('disables the "rename" and "delete" buttons', function () { + cy.findByRole('button', { name: 'Rename' }).should('not.exist') + cy.findByRole('button', { name: 'Delete' }).should('not.exist') + }) + + it('creates new file in the root folder', function () { + cy.intercept('project/*/doc', { statusCode: 200 }) + + cy.findByRole('button', { name: /new file/i }).click() + cy.findByRole('button', { name: /create/i }).click() + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('reciveNewDoc', 'root-folder-id', { + _id: '12345', + name: 'abcdef.tex', + docs: [], + fileRefs: [], + folders: [], + }) + }) + + cy.findByRole('treeitem', { name: 'abcdef.tex' }).then($itemEl => { + cy.findByTestId('file-tree-list-root').then($rootEl => { + expect($itemEl.get(0).parentNode).to.equal($rootEl.get(0)) + }) + }) + }) + + it('starts a new selection', function () { + cy.findByRole('treeitem', { name: 'sub.tex' }).should( + 'have.attr', + 'aria-selected', + 'false' + ) + + cy.findByRole('treeitem', { name: 'main.tex' }).click({ + ctrlKey: true, + cmdKey: true, + }) + + cy.findByRole('treeitem', { name: 'main.tex' }).should( + 'have.attr', + 'aria-selected', + 'true' + ) + }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-root.test.jsx deleted file mode 100644 index 2935ad4de0..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-root.test.jsx +++ /dev/null @@ -1,409 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent, waitFor } from '@testing-library/react' -import fetchMock from 'fetch-mock' -import MockedSocket from 'socket.io-mock' - -import { - renderWithEditorContext, - cleanUpContext, -} from '../../../helpers/render-with-context' -import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' - -describe('', function () { - const onSelect = sinon.stub() - const onInit = sinon.stub() - - beforeEach(function () { - window.metaAttributesCache = new Map() - window.metaAttributesCache.set('ol-user', { id: 'user1' }) - }) - - afterEach(function () { - fetchMock.restore() - onSelect.reset() - onInit.reset() - cleanUpContext() - global.localStorage.clear() - window.metaAttributesCache = new Map() - }) - - it('renders', function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - const { container } = renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'owner', - } - ) - - screen.queryByRole('tree') - screen.getByRole('treeitem') - screen.getByRole('treeitem', { name: 'main.tex', selected: true }) - expect(container.querySelector('.disconnected-overlay')).to.not.exist - }) - - it('renders with invalid selected doc in local storage', async function () { - global.localStorage.setItem( - 'doc.open_id.123abc', - JSON.stringify('not-a-valid-id') - ) - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'owner', - } - ) - - // as a proxy to check that the invalid entity ha not been select we start - // a delete and ensure the modal is displayed (the cancel button can be - // selected) This is needed to make sure the test fail. - const treeitemFile = screen.getByRole('treeitem', { name: 'main.tex' }) - fireEvent.click(treeitemFile, { ctrlKey: true }) - const toggleButton = screen.getByRole('button', { name: 'Menu' }) - fireEvent.click(toggleButton) - const deleteButton = screen.getByRole('menuitem', { name: 'Delete' }) - fireEvent.click(deleteButton) - await waitFor(() => screen.getByRole('button', { name: 'Cancel' })) - }) - - it('renders disconnected overlay', function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - - const { container } = renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'owner', - } - ) - - expect(container.querySelector('.disconnected-overlay')).to.exist - }) - - it('fire onSelect', function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [ - { _id: '456def', name: 'main.tex' }, - { _id: '789ghi', name: 'other.tex' }, - ], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'readOnly', - } - ) - sinon.assert.calledOnce(onSelect) - sinon.assert.calledWithMatch(onSelect, [ - sinon.match({ - entity: { - _id: '456def', - name: 'main.tex', - }, - }), - ]) - onSelect.reset() - - screen.queryByRole('tree') - const treeitem = screen.getByRole('treeitem', { name: 'other.tex' }) - fireEvent.click(treeitem) - sinon.assert.calledOnce(onSelect) - sinon.assert.calledWithMatch(onSelect, [ - sinon.match({ - entity: { - _id: '789ghi', - name: 'other.tex', - }, - }), - ]) - }) - - it('listen to editor.openDoc', function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [ - { _id: '456def', name: 'main.tex' }, - { _id: '789ghi', name: 'other.tex' }, - ], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'owner', - } - ) - - screen.getByRole('treeitem', { name: 'main.tex', selected: true }) - - // entities not found should be ignored - window.dispatchEvent( - new CustomEvent('editor.openDoc', { detail: 'not-an-id' }) - ) - screen.getByRole('treeitem', { name: 'main.tex', selected: true }) - - window.dispatchEvent( - new CustomEvent('editor.openDoc', { detail: '789ghi' }) - ) - screen.getByRole('treeitem', { name: 'main.tex', selected: false }) - screen.getByRole('treeitem', { name: 'other.tex', selected: true }) - }) - - it('only shows a menu button when a single item is selected', function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [ - { _id: '456def', name: 'main.tex' }, - { _id: '789ghi', name: 'other.tex' }, - ], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'owner', - } - ) - - const main = screen.getByRole('treeitem', { - name: 'main.tex', - selected: true, - }) - const other = screen.getByRole('treeitem', { - name: 'other.tex', - selected: false, - }) - - // single item selected: menu button is visible - expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(1) - - // select the other item - fireEvent.click(other) - - screen.getByRole('treeitem', { name: 'main.tex', selected: false }) - screen.getByRole('treeitem', { name: 'other.tex', selected: true }) - - // single item selected: menu button is visible - expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(1) - - // multi-select the main item - fireEvent.click(main, { ctrlKey: true }) - - screen.getByRole('treeitem', { name: 'main.tex', selected: true }) - screen.getByRole('treeitem', { name: 'other.tex', selected: true }) - - // multiple items selected: no menu button is visible - expect(screen.queryAllByRole('button', { name: 'Menu' })).to.have.length(0) - }) - - describe('when deselecting files', function () { - beforeEach(function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '123abc', name: 'main.tex' }], - folders: [ - { - _id: '789ghi', - name: 'thefolder', - docs: [{ _id: '456def', name: 'sub.tex' }], - fileRefs: [], - folders: [], - }, - ], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - features: {}, - permissionsLevel: 'owner', - socket: new MockedSocket(), - } - ) - - // select the sub file - const subDoc = screen.getByRole('treeitem', { name: 'sub.tex' }) - fireEvent.click(subDoc) - expect(subDoc.getAttribute('aria-selected')).to.equal('true') - - // click on empty area - fireEvent.click(screen.getByTestId('file-tree-inner')) - }) - - afterEach(function () { - fetchMock.reset() - }) - - it('removes the selected indicator', function () { - expect(screen.queryByRole('treeitem', { selected: true })).to.be.null - }) - - it('disables the "rename" and "delete" buttons', function () { - expect(screen.queryByRole('button', { name: 'Rename' })).to.be.null - expect(screen.queryByRole('button', { name: 'Delete' })).to.be.null - }) - - it('creates new file in the root folder', async function () { - fetchMock.post('express:/project/:projectId/doc', () => 200) - - fireEvent.click(screen.getByRole('button', { name: /new file/i })) - fireEvent.click(screen.getByRole('button', { name: /create/i })) - - const socketData = { - _id: '12345', - name: 'abcdef.tex', - docs: [], - fileRefs: [], - folders: [], - } - window._ide.socket.socketClient.emit( - 'reciveNewDoc', - 'root-folder-id', - socketData - ) - - await fetchMock.flush(true) - - const newItem = screen.getByRole('treeitem', { name: socketData.name }) - const rootEl = screen.getByTestId('file-tree-list-root') - - expect(newItem.parentNode).to.equal(rootEl) - }) - - it('starts a new selection', function () { - const subDoc = screen.getByRole('treeitem', { name: 'sub.tex' }) - expect(subDoc.getAttribute('aria-selected')).to.equal('false') - - const mainDoc = screen.getByRole('treeitem', { name: 'main.tex' }) - fireEvent.click(mainDoc, { ctrlKey: true }) - expect(mainDoc.getAttribute('aria-selected')).to.equal('true') - - expect(subDoc.getAttribute('aria-selected')).to.equal('false') - }) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx new file mode 100644 index 0000000000..79876aae80 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.spec.tsx @@ -0,0 +1,59 @@ +import FileTreeToolbar from '../../../../../frontend/js/features/file-tree/components/file-tree-toolbar' +import { EditorProviders } from '../../../helpers/editor-providers' +import { FileTreeProvider } from '../helpers/file-tree-provider' + +describe('', function () { + it('without selected files', function () { + cy.mount( + + + + + + ) + + cy.findAllByRole('button', { name: 'New File' }) + cy.findAllByRole('button', { name: 'New Folder' }) + cy.findAllByRole('button', { name: 'Upload' }) + cy.findAllByRole('button', { name: 'Rename' }).should('not.exist') + cy.findAllByRole('button', { name: 'Delete' }).should('not.exist') + }) + + it('read-only', function () { + cy.mount( + + + + + + ) + + cy.findAllByRole('button').should('not.exist') + }) + + it('with one selected file', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + + + ) + + cy.findAllByRole('button', { name: 'New File' }) + cy.findAllByRole('button', { name: 'New Folder' }) + cy.findAllByRole('button', { name: 'Upload' }) + cy.findAllByRole('button', { name: 'Rename' }) + cy.findAllByRole('button', { name: 'Delete' }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.jsx b/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.jsx deleted file mode 100644 index 8812c8a3c7..0000000000 --- a/services/web/test/frontend/features/file-tree/components/file-tree-toolbar.test.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import { expect } from 'chai' -import { screen } from '@testing-library/react' -import renderWithContext from '../helpers/render-with-context' - -import FileTreeToolbar from '../../../../../frontend/js/features/file-tree/components/file-tree-toolbar' - -describe('', function () { - beforeEach(function () { - global.localStorage.clear() - }) - - it('without selected files', function () { - renderWithContext() - - screen.getByRole('button', { name: 'New File' }) - screen.getByRole('button', { name: 'New Folder' }) - screen.getByRole('button', { name: 'Upload' }) - expect(screen.queryByRole('button', { name: 'Rename' })).to.not.exist - expect(screen.queryByRole('button', { name: 'Delete' })).to.not.exist - }) - - it('read-only', function () { - renderWithContext(, { - contextProps: { permissionsLevel: 'readOnly' }, - }) - - expect(screen.queryByRole('button')).to.not.exist - }) - - it('with one selected file', function () { - renderWithContext(, { - contextProps: { - rootDocId: '456def', - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ], - }, - }) - - screen.getByRole('button', { name: 'New File' }) - screen.getByRole('button', { name: 'New Folder' }) - screen.getByRole('button', { name: 'Upload' }) - screen.getByRole('button', { name: 'Rename' }) - screen.getByRole('button', { name: 'Delete' }) - }) -}) diff --git a/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx b/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx new file mode 100644 index 0000000000..312738b627 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/flows/context-menu.spec.tsx @@ -0,0 +1,117 @@ +import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' +import { EditorProviders } from '../../../helpers/editor-providers' + +describe('FileTree Context Menu Flow', function () { + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-user', { id: 'user1' }) + }) + }) + + it('opens on contextMenu event', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('menu').should('not.exist') + cy.findByRole('button', { name: 'main.tex' }).trigger('contextmenu') + cy.findByRole('menu') + }) + + it('closes when a new selection is started', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [ + { _id: '456def', name: 'main.tex' }, + { _id: '456def', name: 'foo.tex' }, + ], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('menu').should('not.exist') + cy.findByRole('button', { name: 'main.tex' }).trigger('contextmenu') + cy.findByRole('menu') + cy.findAllByRole('button', { name: 'foo.tex' }).click() + cy.findByRole('menu').should('not.exist') + }) + + it("doesn't open in read only mode", function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findAllByRole('button', { name: 'main.tex' }).trigger('contextmenu') + cy.findByRole('menu').should('not.exist') + }) +}) diff --git a/services/web/test/frontend/features/file-tree/flows/context-menu.test.jsx b/services/web/test/frontend/features/file-tree/flows/context-menu.test.jsx deleted file mode 100644 index 3e424210fb..0000000000 --- a/services/web/test/frontend/features/file-tree/flows/context-menu.test.jsx +++ /dev/null @@ -1,134 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent } from '@testing-library/react' - -import { - renderWithEditorContext, - cleanUpContext, -} from '../../../helpers/render-with-context' -import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' - -describe('FileTree Context Menu Flow', function () { - const onSelect = sinon.stub() - const onInit = sinon.stub() - - beforeEach(function () { - window.metaAttributesCache = new Map() - window.metaAttributesCache.set('ol-user', { id: 'user1' }) - }) - - afterEach(function () { - onSelect.reset() - onInit.reset() - cleanUpContext() - window.metaAttributesCache = new Map() - }) - - it('opens on contextMenu event', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - } - ) - const treeitem = screen.getByRole('button', { name: 'main.tex' }) - - expect(screen.queryByRole('menu')).to.be.null - - fireEvent.contextMenu(treeitem) - - screen.getByRole('menu') - }) - - it('closes when a new selection is started', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [ - { _id: '456def', name: 'main.tex' }, - { _id: '456def', name: 'foo.tex' }, - ], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - } - ) - const treeitem = screen.getByRole('button', { name: 'main.tex' }) - expect(screen.queryByRole('menu')).to.be.null - - fireEvent.contextMenu(treeitem) - screen.getByRole('menu') - - screen.getByRole('button', { name: 'foo.tex' }).click() - expect(screen.queryByRole('menu')).to.be.null - }) - - it("doesn't open in read only mode", async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - rootFolder, - projectId: '123abc', - rootDocId: '456def', - permissionsLevel: 'readOnly', - } - ) - const treeitem = screen.getByRole('button', { name: 'main.tex' }) - - fireEvent.contextMenu(treeitem) - - expect(screen.queryByRole('menu')).to.not.exist - }) -}) diff --git a/services/web/test/frontend/features/file-tree/flows/create-folder.spec.tsx b/services/web/test/frontend/features/file-tree/flows/create-folder.spec.tsx new file mode 100644 index 0000000000..545bcd6fb9 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/flows/create-folder.spec.tsx @@ -0,0 +1,287 @@ +// @ts-ignore +import MockedSocket from 'socket.io-mock' +import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' +import { EditorProviders } from '../../../helpers/editor-providers' + +describe('FileTree Create Folder Flow', function () { + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-user', { id: 'user1' }) + }) + }) + + it('add to root when no files are selected', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + const name = 'Foo Bar In Root' + + cy.intercept('post', '/project/*/folder', { + body: { + folders: [], + fileRefs: [], + docs: [], + _id: fakeId(), + name, + }, + }).as('createFolder') + + createFolder(name) + + cy.get('@createFolder').its('request.body').should('deep.equal', { + parent_folder_id: 'root-folder-id', + name, + }) + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('reciveNewFolder', 'root-folder-id', { + _id: fakeId(), + name, + docs: [], + fileRefs: [], + folders: [], + }) + }) + + cy.findByRole('treeitem', { name }) + }) + + it('add to folder from folder', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [], + folders: [ + { + _id: '789ghi', + name: 'thefolder', + docs: [], + fileRefs: [], + folders: [], + }, + ], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('button', { name: 'Expand' }).click() + + const name = 'Foo Bar In thefolder' + + cy.intercept('post', '/project/*/folder', { + body: { + folders: [], + fileRefs: [], + docs: [], + _id: fakeId(), + name, + }, + }).as('createFolder') + + createFolder(name) + + cy.get('@createFolder').its('request.body').should('deep.equal', { + parent_folder_id: '789ghi', + name, + }) + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', { + _id: fakeId(), + name, + docs: [], + fileRefs: [], + folders: [], + }) + }) + + // find the created folder + cy.findByRole('treeitem', { name }) + + // collapse the parent folder; created folder should not be rendered anymore + cy.findByRole('button', { name: 'Collapse' }).click() + cy.findByRole('treeitem', { name }).should('not.exist') + }) + + it('add to folder from child', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [], + folders: [ + { + _id: '789ghi', + name: 'thefolder', + docs: [], + fileRefs: [{ _id: '456def', name: 'sub.tex' }], + folders: [], + }, + ], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + const name = 'Foo Bar In thefolder' + + cy.intercept('post', '/project/*/folder', { + body: { + folders: [], + fileRefs: [], + docs: [], + _id: fakeId(), + name, + }, + }).as('createFolder') + + createFolder(name) + + cy.get('@createFolder').its('request.body').should('deep.equal', { + parent_folder_id: '789ghi', + name, + }) + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', { + _id: fakeId(), + name, + docs: [], + fileRefs: [], + folders: [], + }) + }) + + // find the created folder + cy.findByRole('treeitem', { name }) + + // collapse the parent folder; created folder should not be rendered anymore + cy.findByRole('button', { name: 'Collapse' }).click() + cy.findByRole('treeitem', { name }).should('not.exist') + }) + + it('prevents adding duplicate or invalid names', function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'existingFile' }], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + const name = 'existingFile' + + cy.intercept('post', '/project/*/folder', cy.spy().as('createFolder')) + + createFolder(name) + + cy.get('@createFolder').should('not.have.been.called') + + cy.findByRole('alert', { + name: 'A file or folder with this name already exists', + }) + + cy.findByRole('textbox').type('in/valid ') + + cy.findByRole('alert', { + name: 'File name is empty or contains invalid characters', + }) + }) + + function createFolder(name: string) { + cy.findByRole('button', { name: 'New Folder' }).click() + cy.findByRole('textbox').type(name) + cy.findByRole('button', { name: 'Create' }).click() + } + + function fakeId() { + return Math.random().toString(16).replace(/0\./, 'random-test-id-') + } +}) diff --git a/services/web/test/frontend/features/file-tree/flows/create-folder.test.jsx b/services/web/test/frontend/features/file-tree/flows/create-folder.test.jsx deleted file mode 100644 index 1cc5f0eb19..0000000000 --- a/services/web/test/frontend/features/file-tree/flows/create-folder.test.jsx +++ /dev/null @@ -1,296 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent, waitFor } from '@testing-library/react' -import fetchMock from 'fetch-mock' -import MockedSocket from 'socket.io-mock' - -import { - renderWithEditorContext, - cleanUpContext, -} from '../../../helpers/render-with-context' -import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' - -describe('FileTree Create Folder Flow', function () { - const onSelect = sinon.stub() - const onInit = sinon.stub() - - beforeEach(function () { - window.metaAttributesCache = new Map() - window.metaAttributesCache.set('ol-user', { id: 'user1' }) - }) - - afterEach(function () { - fetchMock.restore() - onSelect.reset() - onInit.reset() - cleanUpContext() - window.metaAttributesCache = new Map() - }) - - it('add to root when no files are selected', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - } - ) - - const newFolderName = 'Foo Bar In Root' - const matcher = /\/project\/\w+\/folder/ - const response = { - folders: [], - fileRefs: [], - docs: [], - _id: fakeId(), - name: newFolderName, - } - fetchMock.post(matcher, response) - - await fireCreateFolder(newFolderName) - - const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body) - expect(lastCallBody.name).to.equal(newFolderName) - expect(lastCallBody.parent_folder_id).to.equal('root-folder-id') - - window._ide.socket.socketClient.emit('reciveNewFolder', 'root-folder-id', { - _id: fakeId(), - name: newFolderName, - docs: [], - fileRefs: [], - folders: [], - }) - await screen.findByRole('treeitem', { name: newFolderName }) - }) - - it('add to folder from folder', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [], - folders: [ - { - _id: '789ghi', - name: 'thefolder', - docs: [], - fileRefs: [], - folders: [], - }, - ], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - rootDocId: '789ghi', - } - ) - - const expandButton = screen.getByRole('button', { name: 'Expand' }) - fireEvent.click(expandButton) - - const newFolderName = 'Foo Bar In thefolder' - const matcher = /\/project\/\w+\/folder/ - const response = { - folders: [], - fileRefs: [], - docs: [], - _id: fakeId(), - name: newFolderName, - } - fetchMock.post(matcher, response) - - await fireCreateFolder(newFolderName) - - const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body) - expect(lastCallBody.name).to.equal(newFolderName) - expect(lastCallBody.parent_folder_id).to.equal('789ghi') - - window._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', { - _id: fakeId(), - name: newFolderName, - docs: [], - fileRefs: [], - folders: [], - }) - - // find the created folder - await screen.findByRole('treeitem', { name: newFolderName }) - - // collapse the parent folder; created folder should not be rendered anymore - fireEvent.click(expandButton) - expect(screen.queryByRole('treeitem', { name: newFolderName })).to.not.exist - }) - - it('add to folder from child', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [], - folders: [ - { - _id: '789ghi', - name: 'thefolder', - docs: [], - fileRefs: [{ _id: '456def', name: 'sub.tex' }], - folders: [], - }, - ], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - rootDocId: '456def', - } - ) - - const newFolderName = 'Foo Bar In thefolder' - const matcher = /\/project\/\w+\/folder/ - const response = { - folders: [], - fileRefs: [], - docs: [], - _id: fakeId(), - name: newFolderName, - } - fetchMock.post(matcher, response) - - await fireCreateFolder(newFolderName) - - const lastCallBody = JSON.parse(fetchMock.lastCall(matcher)[1].body) - expect(lastCallBody.name).to.equal(newFolderName) - expect(lastCallBody.parent_folder_id).to.equal('789ghi') - - window._ide.socket.socketClient.emit('reciveNewFolder', '789ghi', { - _id: fakeId(), - name: newFolderName, - docs: [], - fileRefs: [], - folders: [], - }) - - // find the created folder - await screen.findByRole('treeitem', { name: newFolderName }) - - // collapse the parent folder; created folder should not be rendered anymore - fireEvent.click(screen.getByRole('button', { name: 'Collapse' })) - expect(screen.queryByRole('treeitem', { name: newFolderName })).to.not.exist - }) - - it('prevents adding duplicate or invalid names', async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'existingFile' }], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - rootDocId: '456def', - } - ) - - let newFolderName = 'existingFile' - - await fireCreateFolder(newFolderName) - - expect(fetchMock.called()).to.be.false - - await screen.findByRole('alert', { - name: 'A file or folder with this name already exists', - hidden: true, - }) - - newFolderName = 'in/valid ' - setFolderName(newFolderName) - await screen.findByRole('alert', { - name: 'File name is empty or contains invalid characters', - hidden: true, - }) - }) - - async function fireCreateFolder(name) { - const createFolderButton = screen.getByRole('button', { - name: 'New Folder', - }) - fireEvent.click(createFolderButton) - - setFolderName(name) - - const modalCreateButton = await getModalCreateButton() - fireEvent.click(modalCreateButton) - } - - function setFolderName(name) { - const input = screen.getByRole('textbox') - fireEvent.change(input, { target: { value: name } }) - } - - function fakeId() { - return Math.random().toString(16).replace(/0\./, 'random-test-id-') - } - - async function getModalCreateButton() { - return waitFor(() => screen.getByRole('button', { name: 'Create' })) - } -}) diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx b/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx new file mode 100644 index 0000000000..8de18eaef8 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/flows/delete-entity.spec.tsx @@ -0,0 +1,282 @@ +// @ts-ignore +import MockedSocket from 'socket.io-mock' +import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' +import { EditorProviders } from '../../../helpers/editor-providers' + +describe('FileTree Delete Entity Flow', function () { + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-user', { id: 'user1' }) + }) + }) + + describe('single entity', function () { + beforeEach(function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [ + { _id: '123abc', name: 'foo.tex' }, + { _id: '456def', name: 'main.tex' }, + ], + folders: [], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('treeitem', { name: 'main.tex' }).click() + cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('menuitem', { name: 'Delete' }).click() + }) + + it('removes item', function () { + cy.intercept('delete', '/project/*/doc/*', { statusCode: 204 }).as( + 'deleteDoc' + ) + + // check that the confirmation modal is open + cy.findByText( + 'Are you sure you want to permanently delete the following files?' + ) + + cy.findByRole('button', { name: 'Delete' }).click() + + cy.wait('@deleteDoc') + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('removeEntity', '456def') + }) + + cy.findByRole('treeitem', { + name: 'main.tex', + hidden: true, // treeitem might be hidden behind the modal + }).should('not.exist') + + cy.findByRole('treeitem', { + name: 'main.tex', + }).should('not.exist') + + // check that the confirmation modal is closed + cy.findByText( + 'Are you sure you want to permanently delete the following files?' + ).should('not.exist') + + cy.get('@deleteDoc.all').should('have.length', 1) + cy.get('@reindexReferences').should('not.have.been.called') + }) + + it('continues delete on 404s', function () { + cy.intercept('delete', '/project/*/doc/*', { statusCode: 404 }).as( + 'deleteDoc' + ) + + // check that the confirmation modal is open + cy.findByText( + 'Are you sure you want to permanently delete the following files?' + ) + + cy.findByRole('button', { name: 'Delete' }).click() + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('removeEntity', '456def') + }) + + cy.findByRole('treeitem', { + name: 'main.tex', + hidden: true, // treeitem might be hidden behind the modal + }).should('not.exist') + + cy.findByRole('treeitem', { + name: 'main.tex', + }).should('not.exist') + + // check that the confirmation modal is closed + // is not, the 404 probably triggered a bug + cy.findByText( + 'Are you sure you want to permanently delete the following files?' + ).should('not.exist') + }) + + it('aborts delete on error', function () { + cy.intercept('delete', '/project/*/doc/*', { statusCode: 500 }).as( + 'deleteDoc' + ) + + cy.findByRole('button', { name: 'Delete' }).click() + + // The modal should still be open, but the file should not be deleted + cy.findByRole('treeitem', { name: 'main.tex', hidden: true }) + }) + }) + + describe('folders', function () { + beforeEach(function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [ + { + _id: '123abc', + name: 'folder', + docs: [], + folders: [], + fileRefs: [{ _id: '789ghi', name: 'my.bib' }], + }, + ], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + + cy.findByRole('button', { name: 'Expand' }).click() + cy.findByRole('treeitem', { name: 'main.tex' }).click() + cy.findByRole('treeitem', { name: 'my.bib' }).click({ + ctrlKey: true, + cmdKey: true, + }) + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('removeEntity', '123abc') + }) + }) + + it('removes the folder', function () { + cy.findByRole('treeitem', { name: 'folder' }).should('not.exist') + }) + + it('leaves the main file selected', function () { + cy.findByRole('treeitem', { name: 'main.tex', selected: true }) + }) + + it('unselect the child entity', function () { + // as a proxy to check that the child entity has been unselect we start + // a delete and ensure the modal is displayed (the cancel button can be + // selected) This is needed to make sure the test fail. + cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('menuitem', { name: 'Delete' }).click() + cy.findByRole('button', { name: 'Cancel' }) + }) + }) + + describe('multiple entities', function () { + beforeEach(function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'main.tex' }], + folders: [], + fileRefs: [{ _id: '789ghi', name: 'my.bib' }], + }, + ] + + cy.mount( + + + + ) + + // select two files + cy.findByRole('treeitem', { name: 'main.tex' }).click() + cy.findByRole('treeitem', { name: 'my.bib' }).click({ + ctrlKey: true, + cmdKey: true, + }) + + // open the context menu + cy.findByRole('button', { name: 'my.bib' }).trigger('contextmenu') + + // make sure the menu has opened, with only a "Delete" item (as multiple files are selected) + cy.findByRole('menu') + cy.findAllByRole('menuitem').should('have.length', 1) + + // select the Delete menu item + cy.findByRole('menuitem', { name: 'Delete' }).click() + }) + + it('removes all items and reindexes references after deleting .bib file', function () { + cy.intercept('delete', '/project/123abc/doc/456def', { + statusCode: 204, + }).as('deleteDoc') + + cy.intercept('delete', '/project/123abc/file/789ghi', { + statusCode: 204, + }).as('deleteFile') + + cy.findByRole('button', { name: 'Delete' }).click() + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit('removeEntity', '456def') + // @ts-ignore + win._ide.socket.socketClient.emit('removeEntity', '789ghi') + }) + + for (const name of ['main.tex', 'my.bib']) { + for (const hidden of [true, false]) { + cy.findByRole('treeitem', { name, hidden }).should('not.exist') + } + } + + // check that the confirmation modal is closed + cy.findByText('Are you sure').should('not.exist') + + cy.get('@deleteDoc.all').should('have.length', 1) + cy.get('@deleteFile.all').should('have.length', 1) + cy.get('@reindexReferences').should('have.been.calledOnce') + }) + }) +}) diff --git a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.jsx b/services/web/test/frontend/features/file-tree/flows/delete-entity.test.jsx deleted file mode 100644 index 2d378385ef..0000000000 --- a/services/web/test/frontend/features/file-tree/flows/delete-entity.test.jsx +++ /dev/null @@ -1,302 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent, waitFor } from '@testing-library/react' -import fetchMock from 'fetch-mock' -import MockedSocket from 'socket.io-mock' - -import { - renderWithEditorContext, - cleanUpContext, -} from '../../../helpers/render-with-context' -import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' - -describe('FileTree Delete Entity Flow', function () { - const onSelect = sinon.stub() - const onInit = sinon.stub() - const reindexReferences = sinon.stub() - - beforeEach(function () { - window.metaAttributesCache = new Map() - window.metaAttributesCache.set('ol-user', { id: 'user1' }) - }) - - afterEach(function () { - fetchMock.restore() - onSelect.reset() - onInit.reset() - reindexReferences.reset() - cleanUpContext() - window.metaAttributesCache = new Map() - }) - - describe('single entity', function () { - beforeEach(function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - } - ) - - const treeitem = screen.getByRole('treeitem', { name: 'main.tex' }) - fireEvent.click(treeitem) - - const toggleButton = screen.getByRole('button', { name: 'Menu' }) - fireEvent.click(toggleButton) - - const deleteButton = screen.getByRole('menuitem', { name: 'Delete' }) - fireEvent.click(deleteButton) - }) - - it('removes item', async function () { - const fetchMatcher = /\/project\/\w+\/doc\/\w+/ - fetchMock.delete(fetchMatcher, 204) - - const modalDeleteButton = await getModalDeleteButton() - fireEvent.click(modalDeleteButton) - - window._ide.socket.socketClient.emit('removeEntity', '456def') - - await waitFor(() => { - expect( - screen.queryByRole('treeitem', { - name: 'main.tex', - hidden: true, // treeitem might be hidden behind the modal - }) - ).to.not.exist - - expect( - screen.queryByRole('treeitem', { - name: 'main.tex', - }) - ).to.not.exist - - // check that the confirmation modal is closed - expect(screen.queryByText(/Are you sure/)).to.not.exist - }) - - const [lastFetchPath] = fetchMock.lastCall(fetchMatcher) - expect(lastFetchPath).to.equal('/project/123abc/doc/456def') - expect(reindexReferences).not.to.have.been.called - }) - - it('continues delete on 404s', async function () { - fetchMock.delete(/\/project\/\w+\/doc\/\w+/, 404) - - const modalDeleteButton = await getModalDeleteButton() - fireEvent.click(modalDeleteButton) - - window._ide.socket.socketClient.emit('removeEntity', '456def') - - // check that the confirmation modal is open - screen.getByText(/Are you sure/) - - await waitFor(() => { - expect( - screen.queryByRole('treeitem', { - name: 'main.tex', - hidden: true, // treeitem might be hidden behind the modal - }) - ).to.not.exist - - expect( - screen.queryByRole('treeitem', { - name: 'main.tex', - }) - ).to.not.exist - - // check that the confirmation modal is closed - // is not, the 404 probably triggered a bug - expect(screen.queryByText(/Are you sure/)).to.not.exist - }) - }) - - it('aborts delete on error', async function () { - const fetchMatcher = /\/project\/\w+\/doc\/\w+/ - fetchMock.delete(fetchMatcher, 500) - - const modalDeleteButton = await getModalDeleteButton() - fireEvent.click(modalDeleteButton) - - // The modal should still be open, but the file should not be deleted - await screen.findByRole('treeitem', { name: 'main.tex', hidden: true }) - }) - }) - - describe('folders', function () { - beforeEach(function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [ - { - _id: '123abc', - name: 'folder', - docs: [], - folders: [], - fileRefs: [{ _id: '789ghi', name: 'my.bib' }], - }, - ], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - } - ) - - const expandButton = screen.queryByRole('button', { name: 'Expand' }) - if (expandButton) fireEvent.click(expandButton) - const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' }) - fireEvent.click(treeitemDoc) - const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' }) - fireEvent.click(treeitemFile, { ctrlKey: true }) - - window._ide.socket.socketClient.emit('removeEntity', '123abc') - }) - - it('removes the folder', function () { - expect(screen.queryByRole('treeitem', { name: 'folder' })).to.not.exist - }) - - it('leaves the main file selected', function () { - screen.getByRole('treeitem', { name: 'main.tex', selected: true }) - }) - - it('unselect the child entity', async function () { - // as a proxy to check that the child entity has been unselect we start - // a delete and ensure the modal is displayed (the cancel button can be - // selected) This is needed to make sure the test fail. - const toggleButton = screen.getByRole('button', { name: 'Menu' }) - fireEvent.click(toggleButton) - const deleteButton = screen.getByRole('menuitem', { name: 'Delete' }) - fireEvent.click(deleteButton) - await waitFor(() => screen.getByRole('button', { name: 'Cancel' })) - }) - }) - - describe('multiple entities', function () { - beforeEach(async function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'main.tex' }], - folders: [], - fileRefs: [{ _id: '789ghi', name: 'my.bib' }], - }, - ] - - renderWithEditorContext( - null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - } - ) - - // select two files - const treeitemDoc = screen.getByRole('treeitem', { name: 'main.tex' }) - fireEvent.click(treeitemDoc) - const treeitemFile = screen.getByRole('treeitem', { name: 'my.bib' }) - fireEvent.click(treeitemFile, { ctrlKey: true }) - - // open the context menu - const treeitemButton = screen.getByRole('button', { name: 'my.bib' }) - fireEvent.contextMenu(treeitemButton) - - // make sure the menu has opened, with only a "Delete" item (as multiple files are selected) - screen.getByRole('menu') - const menuItems = await screen.findAllByRole('menuitem') - expect(menuItems.length).to.equal(1) - - // select the Delete menu item - const deleteButton = screen.getByRole('menuitem', { name: 'Delete' }) - fireEvent.click(deleteButton) - }) - - it('removes all items and reindexes references after deleting .bib file', async function () { - const fetchMatcher = /\/project\/\w+\/(doc|file)\/\w+/ - fetchMock.delete(fetchMatcher, 204) - - const modalDeleteButton = await getModalDeleteButton() - fireEvent.click(modalDeleteButton) - - window._ide.socket.socketClient.emit('removeEntity', '456def') - window._ide.socket.socketClient.emit('removeEntity', '789ghi') - - await waitFor(() => { - for (const name of ['main.tex', 'my.bib']) { - expect( - screen.queryByRole('treeitem', { - name, - hidden: true, // treeitem might be hidden behind the modal - }) - ).to.not.exist - - expect( - screen.queryByRole('treeitem', { - name, - }) - ).to.not.exist - - // check that the confirmation modal is closed - expect(screen.queryByText(/Are you sure/)).to.not.exist - } - }) - - const [firstFetchPath, secondFetchPath] = fetchMock - .calls() - .map(([url]) => url) - expect(firstFetchPath).to.equal('/project/123abc/doc/456def') - expect(secondFetchPath).to.equal('/project/123abc/file/789ghi') - expect(reindexReferences).to.have.been.called - }) - }) - - async function getModalDeleteButton() { - return waitFor(() => screen.getByRole('button', { name: 'Delete' })) - } -}) diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx b/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx new file mode 100644 index 0000000000..757074437f --- /dev/null +++ b/services/web/test/frontend/features/file-tree/flows/rename-entity.spec.tsx @@ -0,0 +1,167 @@ +// @ts-ignore +import MockedSocket from 'socket.io-mock' +import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' +import { EditorProviders } from '../../../helpers/editor-providers' + +describe('FileTree Rename Entity Flow', function () { + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-user', { id: 'user1' }) + }) + }) + + beforeEach(function () { + const rootFolder = [ + { + _id: 'root-folder-id', + name: 'rootFolder', + docs: [{ _id: '456def', name: 'a.tex' }], + folders: [ + { + _id: '987jkl', + name: 'folder', + docs: [], + fileRefs: [ + { _id: '789ghi', name: 'c.tex' }, + { _id: '981gkp', name: 'e.tex' }, + ], + folders: [], + }, + ], + fileRefs: [], + }, + ] + + cy.mount( + + + + ) + }) + + it('renames doc', function () { + cy.intercept('/project/*/doc/*/rename', { statusCode: 204 }).as('renameDoc') + + renameItem('a.tex', 'b.tex') + + cy.findByRole('treeitem', { name: 'b.tex' }) + + cy.get('@renameDoc').its('request.body').should('deep.equal', { + name: 'b.tex', + }) + }) + + it('renames folder', function () { + cy.intercept('/project/*/folder/*/rename', { statusCode: 204 }).as( + 'renameFolder' + ) + + renameItem('folder', 'new folder name') + + cy.findByRole('treeitem', { name: 'new folder name' }) + + cy.get('@renameFolder').its('request.body').should('deep.equal', { + name: 'new folder name', + }) + }) + + it('renames file in subfolder', function () { + cy.intercept('/project/*/file/*/rename', { statusCode: 204 }).as( + 'renameFile' + ) + + cy.findByRole('button', { name: 'Expand' }).click() + + renameItem('c.tex', 'd.tex') + + cy.findByRole('treeitem', { name: 'folder' }) + cy.findByRole('treeitem', { name: 'd.tex' }) + + cy.get('@renameFile').its('request.body').should('deep.equal', { + name: 'd.tex', + }) + }) + + it('reverts rename on error', function () { + cy.intercept('/project/*/doc/*/rename', { statusCode: 500 }) + + renameItem('a.tex', 'b.tex') + + cy.findByRole('treeitem', { name: 'a.tex' }) + }) + + it('shows error modal on invalid filename', function () { + renameItem('a.tex', '///') + + cy.findByRole('alert', { + name: 'File name is empty or contains invalid characters', + hidden: true, + }) + }) + + it('shows error modal on duplicate filename', function () { + renameItem('a.tex', 'folder') + + cy.findByRole('alert', { + name: 'A file or folder with this name already exists', + hidden: true, + }) + }) + + it('shows error modal on duplicate filename in subfolder', function () { + cy.findByRole('button', { name: 'Expand' }).click() + + renameItem('c.tex', 'e.tex') + + cy.findByRole('alert', { + name: 'A file or folder with this name already exists', + hidden: true, + }) + }) + + it('shows error modal on blocked filename', function () { + renameItem('a.tex', 'prototype') + + cy.findByRole('alert', { + name: 'This file name is blocked.', + hidden: true, + }) + }) + + describe('via socket event', function () { + it('renames doc', function () { + cy.findByRole('treeitem', { name: 'a.tex' }) + + cy.window().then(win => { + // @ts-ignore + win._ide.socket.socketClient.emit( + 'reciveEntityRename', + '456def', + 'socket.tex' + ) + }) + + cy.findByRole('treeitem', { name: 'socket.tex' }) + }) + }) + + function renameItem(from: string, to: string) { + cy.findByRole('treeitem', { name: from }).click() + cy.findByRole('button', { name: 'Menu' }).click() + cy.findByRole('menuitem', { name: 'Rename' }).click() + cy.findByRole('textbox').clear() + cy.findByRole('textbox').type(to + '{enter}') + } +}) diff --git a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.jsx b/services/web/test/frontend/features/file-tree/flows/rename-entity.test.jsx deleted file mode 100644 index 431b1efeef..0000000000 --- a/services/web/test/frontend/features/file-tree/flows/rename-entity.test.jsx +++ /dev/null @@ -1,208 +0,0 @@ -import { expect } from 'chai' -import sinon from 'sinon' -import { screen, fireEvent } from '@testing-library/react' -import fetchMock from 'fetch-mock' -import MockedSocket from 'socket.io-mock' - -import { - renderWithEditorContext, - cleanUpContext, -} from '../../../helpers/render-with-context' -import FileTreeRoot from '../../../../../frontend/js/features/file-tree/components/file-tree-root' - -describe('FileTree Rename Entity Flow', function () { - const onSelect = sinon.stub() - const onInit = sinon.stub() - - beforeEach(function () { - window.metaAttributesCache = new Map() - window.metaAttributesCache.set('ol-user', { id: 'user1' }) - }) - - afterEach(function () { - fetchMock.restore() - onSelect.reset() - onInit.reset() - cleanUpContext() - window.metaAttributesCache = new Map() - }) - - beforeEach(function () { - const rootFolder = [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [{ _id: '456def', name: 'a.tex' }], - folders: [ - { - _id: '987jkl', - name: 'folder', - docs: [], - fileRefs: [ - { _id: '789ghi', name: 'c.tex' }, - { _id: '981gkp', name: 'e.tex' }, - ], - folders: [], - }, - ], - fileRefs: [], - }, - ] - renderWithEditorContext( - null} - setRefProviderEnabled={() => null} - setStartedFreeTrial={() => null} - onSelect={onSelect} - onInit={onInit} - isConnected - />, - { - socket: new MockedSocket(), - rootFolder, - projectId: '123abc', - } - ) - onSelect.reset() - }) - - it('renames doc', function () { - const fetchMatcher = /\/project\/\w+\/doc\/\w+\/rename/ - fetchMock.post(fetchMatcher, 204) - - const input = initItemRename('a.tex') - fireEvent.change(input, { target: { value: 'b.tex' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - screen.getByRole('treeitem', { name: 'b.tex' }) - - const lastFetchBody = getLastFetchBody(fetchMatcher) - expect(lastFetchBody.name).to.equal('b.tex') - - // onSelect should have been called once only: when the doc was selected for - // rename - sinon.assert.calledOnce(onSelect) - }) - - it('renames folder', function () { - const fetchMatcher = /\/project\/\w+\/folder\/\w+\/rename/ - fetchMock.post(fetchMatcher, 204) - - const input = initItemRename('folder') - fireEvent.change(input, { target: { value: 'new folder name' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - screen.getByRole('treeitem', { name: 'new folder name' }) - - const lastFetchBody = getLastFetchBody(fetchMatcher) - expect(lastFetchBody.name).to.equal('new folder name') - }) - - it('renames file in subfolder', function () { - const fetchMatcher = /\/project\/\w+\/file\/\w+\/rename/ - fetchMock.post(fetchMatcher, 204) - - const expandButton = screen.queryByRole('button', { name: 'Expand' }) - if (expandButton) fireEvent.click(expandButton) - - const input = initItemRename('c.tex') - fireEvent.change(input, { target: { value: 'd.tex' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - screen.getByRole('treeitem', { name: 'folder' }) - screen.getByRole('treeitem', { name: 'd.tex' }) - - const lastFetchBody = getLastFetchBody(fetchMatcher) - expect(lastFetchBody.name).to.equal('d.tex') - }) - - it('reverts rename on error', async function () { - const fetchMatcher = /\/project\/\w+\/doc\/\w+\/rename/ - fetchMock.post(fetchMatcher, 500) - - const input = initItemRename('a.tex') - fireEvent.change(input, { target: { value: 'b.tex' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - screen.getByRole('treeitem', { name: 'b.tex' }) - }) - - it('shows error modal on invalid filename', async function () { - const input = initItemRename('a.tex') - fireEvent.change(input, { target: { value: '///' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - await screen.findByRole('alert', { - name: 'File name is empty or contains invalid characters', - hidden: true, - }) - }) - - it('shows error modal on duplicate filename', async function () { - const input = initItemRename('a.tex') - fireEvent.change(input, { target: { value: 'folder' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - await screen.findByRole('alert', { - name: 'A file or folder with this name already exists', - hidden: true, - }) - }) - - it('shows error modal on duplicate filename in subfolder', async function () { - const expandButton = screen.queryByRole('button', { name: 'Expand' }) - if (expandButton) fireEvent.click(expandButton) - - const input = initItemRename('c.tex') - fireEvent.change(input, { target: { value: 'e.tex' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - await screen.findByRole('alert', { - name: 'A file or folder with this name already exists', - hidden: true, - }) - }) - - it('shows error modal on blocked filename', async function () { - const input = initItemRename('a.tex') - fireEvent.change(input, { target: { value: 'prototype' } }) - fireEvent.keyDown(input, { key: 'Enter' }) - - await screen.findByRole('alert', { - name: 'This file name is blocked.', - hidden: true, - }) - }) - - describe('via socket event', function () { - it('renames doc', function () { - screen.getByRole('treeitem', { name: 'a.tex' }) - - window._ide.socket.socketClient.emit( - 'reciveEntityRename', - '456def', - 'socket.tex' - ) - - screen.getByRole('treeitem', { name: 'socket.tex' }) - }) - }) - - function initItemRename(treeitemName) { - const treeitem = screen.getByRole('treeitem', { name: treeitemName }) - fireEvent.click(treeitem) - - const toggleButton = screen.getByRole('button', { name: 'Menu' }) - fireEvent.click(toggleButton) - - const renameButton = screen.getByRole('menuitem', { name: 'Rename' }) - fireEvent.click(renameButton) - - return screen.getByRole('textbox') - } - function getLastFetchBody(matcher) { - const [, { body }] = fetchMock.lastCall(matcher) - return JSON.parse(body) - } -}) diff --git a/services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx b/services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx new file mode 100644 index 0000000000..5996b71145 --- /dev/null +++ b/services/web/test/frontend/features/file-tree/helpers/file-tree-provider.tsx @@ -0,0 +1,18 @@ +import { FC } from 'react' +import FileTreeContext from '@/features/file-tree/components/file-tree-context' + +export const FileTreeProvider: FC<{ + refProviders?: Record +}> = ({ children, refProviders = {} }) => { + return ( + {}} + > + <>{children} + + ) +} diff --git a/services/web/test/frontend/features/file-tree/helpers/render-with-context.jsx b/services/web/test/frontend/features/file-tree/helpers/render-with-context.jsx deleted file mode 100644 index 5ae9bd0061..0000000000 --- a/services/web/test/frontend/features/file-tree/helpers/render-with-context.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import FileTreeContext from '../../../../../frontend/js/features/file-tree/components/file-tree-context' -import { renderWithEditorContext } from '../../../helpers/render-with-context' -import { debugConsole } from '@/utils/debugging' - -export default (children, options = {}) => { - let { contextProps = {}, ...renderOptions } = options - contextProps = { - projectId: '123abc', - rootFolder: [ - { - _id: 'root-folder-id', - name: 'rootFolder', - docs: [], - fileRefs: [], - folders: [], - }, - ], - refProviders: {}, - reindexReferences: () => { - debugConsole.warn('reindex references') - }, - setRefProviderEnabled: provider => { - debugConsole.warn(`ref provider ${provider} enabled`) - }, - setStartedFreeTrial: () => { - debugConsole.warn('started free trial') - }, - onSelect: () => {}, - ...contextProps, - } - const { - refProviders, - reindexReferences, - setRefProviderEnabled, - setStartedFreeTrial, - onSelect, - ...editorContextProps - } = contextProps - return renderWithEditorContext( - - {children} - , - editorContextProps, - renderOptions - ) -} diff --git a/services/web/test/frontend/features/project-list/components/new-project-button/upload-project-modal.spec.tsx b/services/web/test/frontend/features/project-list/components/new-project-button/upload-project-modal.spec.tsx new file mode 100644 index 0000000000..9b44a374ea --- /dev/null +++ b/services/web/test/frontend/features/project-list/components/new-project-button/upload-project-modal.spec.tsx @@ -0,0 +1,105 @@ +import UploadProjectModal from '../../../../../../frontend/js/features/project-list/components/new-project-button/upload-project-modal' + +describe('', function () { + const maxUploadSize = 10 * 1024 * 1024 // 10 MB + + beforeEach(function () { + cy.window().then(win => { + win.metaAttributesCache.set('ol-ExposedSettings', { maxUploadSize }) + }) + }) + + it('uploads a dropped file', function () { + cy.intercept('post', '/project/new/upload', { + body: { success: true, project_id: '123abc' }, + }).as('uploadProject') + + cy.mount( + + ) + + cy.findByRole('button', { + name: 'Select a .zip file', + }).trigger('drop', { + dataTransfer: { + files: [new File(['test'], 'test.zip', { type: 'application/zip' })], + }, + }) + + cy.wait('@uploadProject') + cy.get('@openProject').should('have.been.calledOnceWith', '123abc') + }) + + it('shows error on file type other than zip', function () { + cy.mount( + + ) + + cy.findByRole('button', { + name: 'Select a .zip file', + }).trigger('drop', { + dataTransfer: { + files: [new File(['test'], 'test.png', { type: 'image/png' })], + }, + }) + + cy.findByText('You can only upload: .zip') + cy.get('@openProject').should('not.have.been.called') + }) + + it('shows error for files bigger than maxUploadSize', function () { + cy.mount( + + ) + + const file = new File(['test'], 'test.zip', { type: 'application/zip' }) + Object.defineProperty(file, 'size', { value: maxUploadSize + 1 }) + + cy.findByRole('button', { + name: 'Select a .zip file', + }).trigger('drop', { + dataTransfer: { + files: [file], + }, + }) + + cy.findByText('test.zip exceeds maximum allowed size of 10 MB') + cy.get('@openProject').should('not.have.been.called') + }) + + it('handles server error', function () { + cy.intercept('post', '/project/new/upload', { + statusCode: 422, + body: { success: false }, + }).as('uploadProject') + + cy.mount( + + ) + + cy.findByRole('button', { + name: 'Select a .zip file', + }).trigger('drop', { + dataTransfer: { + files: [new File(['test'], 'test.zip', { type: 'application/zip' })], + }, + }) + + cy.wait('@uploadProject') + + cy.findByText('Upload failed') + cy.get('@openProject').should('not.have.been.called') + }) +}) diff --git a/services/web/test/frontend/features/project-list/components/new-project-button/upload-project-modal.test.tsx b/services/web/test/frontend/features/project-list/components/new-project-button/upload-project-modal.test.tsx deleted file mode 100644 index 7c19a19a53..0000000000 --- a/services/web/test/frontend/features/project-list/components/new-project-button/upload-project-modal.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import sinon from 'sinon' -import { render, screen, fireEvent, waitFor } from '@testing-library/react' -import UploadProjectModal from '../../../../../../frontend/js/features/project-list/components/new-project-button/upload-project-modal' -import { expect } from 'chai' -import * as useLocationModule from '../../../../../../frontend/js/shared/hooks/use-location' - -describe('', function () { - const originalWindowCSRFToken = window.csrfToken - const maxUploadSize = 10 * 1024 * 1024 // 10 MB - - let assignStub: sinon.SinonStub - - beforeEach(function () { - assignStub = sinon.stub() - this.locationStub = sinon.stub(useLocationModule, 'useLocation').returns({ - assign: assignStub, - reload: sinon.stub(), - }) - window.metaAttributesCache.set('ol-ExposedSettings', { - maxUploadSize, - }) - window.csrfToken = 'token' - }) - - afterEach(function () { - this.locationStub.restore() - window.metaAttributesCache = new Map() - window.csrfToken = originalWindowCSRFToken - }) - - it('uploads a dropped file', async function () { - const xhr = sinon.useFakeXMLHttpRequest() - const requests: sinon.SinonFakeXMLHttpRequest[] = [] - xhr.onCreate = request => { - requests.push(request) - } - - render( {}} />) - - const uploadButton = screen.getByRole('button', { - name: 'Select a .zip file', - }) - - expect(uploadButton).not.to.be.null - - fireEvent.drop(uploadButton, { - dataTransfer: { - files: [new File(['test'], 'test.zip', { type: 'application/zip' })], - }, - }) - - await waitFor(() => expect(requests).to.have.length(1)) - - const [request] = requests - expect(request.url).to.equal('/project/new/upload') - expect(request.method).to.equal('POST') - - const projectId = '123abc' - request.respond( - 200, - { 'Content-Type': 'application/json' }, - JSON.stringify({ success: true, project_id: projectId }) - ) - - await waitFor(() => { - sinon.assert.calledOnce(assignStub) - sinon.assert.calledWith(assignStub, `/project/${projectId}`) - }) - - xhr.restore() - }) - - it('shows error on file type other than zip', async function () { - render( {}} />) - - const uploadButton = screen.getByRole('button', { - name: 'Select a .zip file', - }) - - expect(uploadButton).not.to.be.null - - fireEvent.drop(uploadButton, { - dataTransfer: { - files: [new File(['test'], 'test.png', { type: 'image/png' })], - }, - }) - - await waitFor(() => screen.getByText('You can only upload: .zip')) - }) - - it('shows error for files bigger than maxUploadSize', async function () { - render( {}} />) - - const uploadButton = screen.getByRole('button', { - name: 'Select a .zip file', - }) - expect(uploadButton).not.to.be.null - - const filename = 'test.zip' - const file = new File(['test'], filename, { type: 'application/zip' }) - Object.defineProperty(file, 'size', { value: maxUploadSize + 1 }) - - fireEvent.drop(uploadButton, { - dataTransfer: { - files: [file], - }, - }) - - await waitFor(() => - screen.getByText(`${filename} exceeds maximum allowed size of 10 MB`) - ) - }) - - it('handles server error', async function () { - const xhr = sinon.useFakeXMLHttpRequest() - const requests: sinon.SinonFakeXMLHttpRequest[] = [] - xhr.onCreate = request => { - requests.push(request) - } - - render( {}} />) - - const uploadButton = screen.getByRole('button', { - name: 'Select a .zip file', - }) - expect(uploadButton).not.to.be.null - - fireEvent.drop(uploadButton, { - dataTransfer: { - files: [new File(['test'], 'test.zip', { type: 'application/zip' })], - }, - }) - - await waitFor(() => expect(requests).to.have.length(1)) - - const [request] = requests - expect(request.url).to.equal('/project/new/upload') - expect(request.method).to.equal('POST') - request.respond( - 422, - { 'Content-Type': 'application/json' }, - JSON.stringify({ success: false }) - ) - - await waitFor(() => { - sinon.assert.notCalled(assignStub) - screen.getByText('Upload failed') - }) - - xhr.restore() - }) -}) diff --git a/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx b/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx index 9603329279..88aff2d5c3 100644 --- a/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx @@ -83,7 +83,7 @@ describe('', function () { cy.findByRole('menu').within(() => { cy.findByText('Upload from computer').click() }) - cy.findByLabelText('File Uploader') + cy.findByLabelText('Uppy Dashboard') .get('.uppy-Dashboard-input:first') .as('file-input') })