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')
})