mirror of
https://github.com/overleaf/overleaf.git
synced 2025-12-05 01:10:29 +00:00
Compare commits
13 Commits
e861e28296
...
d4992914c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4992914c2 | ||
|
|
e2cb424695 | ||
|
|
abff6fccd4 | ||
|
|
016778295a | ||
|
|
68c9d1931d | ||
|
|
00f6a1e0f9 | ||
|
|
f2a05b1a2e | ||
|
|
4a4b82cec1 | ||
|
|
9d2f5b3cde | ||
|
|
f954796709 | ||
|
|
49e8d0c551 | ||
|
|
1f356754de | ||
|
|
e822ac0ee0 |
202
package-lock.json
generated
202
package-lock.json
generated
@@ -13191,7 +13191,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.4.0.tgz",
|
||||
"integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@lit/reactive-element": {
|
||||
@@ -15599,9 +15598,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@paralleldrive/cuid2": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz",
|
||||
"integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.1.5"
|
||||
@@ -15620,6 +15619,55 @@
|
||||
"react-dom": ">= 16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/webcomponents": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@phosphor-icons/webcomponents/-/webcomponents-2.1.5.tgz",
|
||||
"integrity": "sha512-JcvQkZxvcX2jK+QCclm8+e8HXqtdFW9xV4/kk2aL9Y3dJA2oQVt+pzbv1orkumz3rfx4K9mn9fDoMr1He1yr7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lit": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/webcomponents/node_modules/@lit/reactive-element": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.1.tgz",
|
||||
"integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/webcomponents/node_modules/lit": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.3.1.tgz",
|
||||
"integrity": "sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-element": "^4.2.0",
|
||||
"lit-html": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/webcomponents/node_modules/lit-element": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.1.tgz",
|
||||
"integrity": "sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@lit-labs/ssr-dom-shim": "^1.4.0",
|
||||
"@lit/reactive-element": "^2.1.0",
|
||||
"lit-html": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@phosphor-icons/webcomponents/node_modules/lit-html": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz",
|
||||
"integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -20579,8 +20627,7 @@
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
@@ -20595,9 +20642,10 @@
|
||||
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.15.tgz",
|
||||
"integrity": "sha512-yeinDVQunb03AEP8luErFcyf/7Lf7AzKCD0NXfgVoGCCQDNpZET8Jgq74oBgqKld3hafLbfzt/3inUdQvaFeXQ=="
|
||||
"version": "13.15.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
@@ -28216,7 +28264,8 @@
|
||||
"node_modules/dottie": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
|
||||
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
|
||||
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/downshift": {
|
||||
"version": "9.0.9",
|
||||
@@ -34416,7 +34465,8 @@
|
||||
"integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
|
||||
"engines": [
|
||||
"node >= 0.4.0"
|
||||
]
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
@@ -36076,9 +36126,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/json-refs/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
@@ -39307,11 +39357,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.40",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.40.tgz",
|
||||
"integrity": "sha512-tWfmNkRYmBkPJz5mr9GVDn9vRlVZOTe6yqY92rFxiOdWXbjaR0+9LwQnZGGuNR63X456NqmEkbskte8tWL5ePg==",
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": ">= 2.9.0"
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
@@ -41235,9 +41286,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-loader/node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -41267,9 +41318,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-loader/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -45552,9 +45603,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/retry-as-promised": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz",
|
||||
"integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA=="
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
|
||||
"integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/retry-request": {
|
||||
"version": "4.2.2",
|
||||
@@ -46182,31 +46234,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sequelize": {
|
||||
"version": "6.31.0",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.31.0.tgz",
|
||||
"integrity": "sha512-nCPVtv+QydBmb3Us2jCNAr1Dx3gST83VZxxrUQn/JAVFCOrmYOgUaPUz5bevummyNf30zfHsZhIKYAOD3ULfTA==",
|
||||
"version": "6.37.7",
|
||||
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
|
||||
"integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/sequelize"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/validator": "^13.7.1",
|
||||
"debug": "^4.3.3",
|
||||
"dottie": "^2.0.2",
|
||||
"inflection": "^1.13.2",
|
||||
"@types/debug": "^4.1.8",
|
||||
"@types/validator": "^13.7.17",
|
||||
"debug": "^4.3.4",
|
||||
"dottie": "^2.0.6",
|
||||
"inflection": "^1.13.4",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"pg-connection-string": "^2.5.0",
|
||||
"retry-as-promised": "^7.0.3",
|
||||
"semver": "^7.3.5",
|
||||
"moment": "^2.29.4",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"pg-connection-string": "^2.6.1",
|
||||
"retry-as-promised": "^7.0.4",
|
||||
"semver": "^7.5.4",
|
||||
"sequelize-pool": "^7.1.0",
|
||||
"toposort-class": "^1.0.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.7.0",
|
||||
"validator": "^13.9.0",
|
||||
"wkx": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -46301,28 +46354,39 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
|
||||
"integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sequelize/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"node_modules/sequelize/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/sequelize/node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sequelize/node_modules/semver": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -46334,15 +46398,11 @@
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/sequelize/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
@@ -48923,9 +48983,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-tools/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
@@ -49983,7 +50043,8 @@
|
||||
"node_modules/toposort-class": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
|
||||
"integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg="
|
||||
"integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.0",
|
||||
@@ -51212,9 +51273,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
|
||||
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
|
||||
"version": "13.15.23",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz",
|
||||
"integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
@@ -52991,6 +53053,7 @@
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
|
||||
"integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -53411,9 +53474,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/z-schema/node_modules/validator": {
|
||||
"version": "10.11.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz",
|
||||
"integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==",
|
||||
"version": "13.15.20",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.20.tgz",
|
||||
"integrity": "sha512-KxPOq3V2LmfQPP4eqf3Mq/zrT0Dqp2Vmx2Bn285LwVahLc+CsxOM0crBHczm8ijlcjZ0Q5Xd6LW3z3odTPnlrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
@@ -53562,7 +53625,7 @@
|
||||
"pg-copy-streams": "^2.2.2",
|
||||
"promptly": "^3.0.3",
|
||||
"recurly": "^4.0.1",
|
||||
"sequelize": "^6.31.0",
|
||||
"sequelize": "^6.37.7",
|
||||
"yargs": "^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -55649,6 +55712,7 @@
|
||||
"@overleaf/stream-utils": "*",
|
||||
"@overleaf/validation-tools": "*",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@phosphor-icons/webcomponents": "^2.1.5",
|
||||
"@slack/webhook": "^7.0.2",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/stripe-js": "^7.7.0",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"swagger-tools@0.10.4": {
|
||||
"path-to-regexp": "3.3.0",
|
||||
"body-parser": "1.20.3",
|
||||
"multer": "2.0.2"
|
||||
"multer": "2.0.2",
|
||||
"validator": "13.15.20"
|
||||
},
|
||||
"request@2.88.2": {
|
||||
"tough-cookie": "5.1.2",
|
||||
|
||||
@@ -8,7 +8,7 @@ import express from 'express'
|
||||
import logger from '@overleaf/logger'
|
||||
import Metrics from '@overleaf/metrics'
|
||||
import { expressify } from '@overleaf/promise-utils'
|
||||
import { drainQueue, healthCheck } from './storage/scripts/backup_worker.mjs'
|
||||
import { healthCheck } from './storage/scripts/backup_worker.mjs'
|
||||
const app = express()
|
||||
|
||||
logger.initialize('history-v1-backup-worker')
|
||||
@@ -39,7 +39,6 @@ app.use((err, req, res, next) => {
|
||||
|
||||
async function triggerGracefulShutdown(server, signal) {
|
||||
logger.info({ signal }, 'graceful shutdown: started shutdown sequence')
|
||||
await drainQueue()
|
||||
server.close(function () {
|
||||
logger.info({ signal }, 'graceful shutdown: closed server')
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -93,8 +93,10 @@ process.on('SIGINT', handleSignal)
|
||||
process.on('SIGTERM', handleSignal)
|
||||
|
||||
function handleSignal() {
|
||||
gracefulShutdownInitiated = true
|
||||
logger.info({}, 'graceful shutdown initiated, draining queue')
|
||||
if (!gracefulShutdownInitiated) {
|
||||
gracefulShutdownInitiated = true
|
||||
logger.info({}, 'graceful shutdown: waiting for backups to complete')
|
||||
}
|
||||
}
|
||||
|
||||
async function retry(fn, times, delayMs) {
|
||||
@@ -1071,6 +1073,45 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all database connections gracefully
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function closeConnections() {
|
||||
/** @type {Error[]} */
|
||||
const errors = []
|
||||
|
||||
try {
|
||||
await knex.destroy()
|
||||
console.log('Postgres connection closed')
|
||||
} catch (err) {
|
||||
console.error('Error closing Postgres connection:', err)
|
||||
errors.push(/** @type {Error} */ (err))
|
||||
}
|
||||
|
||||
try {
|
||||
await client.close()
|
||||
console.log('MongoDB connection closed')
|
||||
} catch (err) {
|
||||
console.error('Error closing MongoDB connection:', err)
|
||||
errors.push(/** @type {Error} */ (err))
|
||||
}
|
||||
|
||||
try {
|
||||
await redis.disconnect()
|
||||
console.log('Redis connection closed')
|
||||
} catch (err) {
|
||||
console.error('Error closing Redis connection:', err)
|
||||
errors.push(/** @type {Error} */ (err))
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(
|
||||
`Failed to close ${errors.length} connection(s): ${errors.map(e => e.message).join(', ')}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Only run command-line interface when script is run directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main()
|
||||
@@ -1083,30 +1124,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
console.error('Error backing up project:', err)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(() => {
|
||||
knex
|
||||
.destroy()
|
||||
.then(() => {
|
||||
console.log('Postgres connection closed')
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error closing Postgres connection:', err)
|
||||
})
|
||||
client
|
||||
.close()
|
||||
.then(() => {
|
||||
console.log('MongoDB connection closed')
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error closing MongoDB connection:', err)
|
||||
})
|
||||
redis
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
console.log('Redis connection closed')
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error closing Redis connection:', err)
|
||||
})
|
||||
.finally(async () => {
|
||||
await closeConnections()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
backupProject,
|
||||
initializeProjects,
|
||||
configureBackup,
|
||||
closeConnections,
|
||||
} from './backup.mjs'
|
||||
|
||||
const CONCURRENCY = 15
|
||||
const JOB_CONCURRENCY = parseInt(process.env.JOB_CONCURRENCY, 10) || 15
|
||||
const UPLOAD_CONCURRENCY = parseInt(process.env.UPLOAD_CONCURRENCY, 10) || 50
|
||||
const WARN_THRESHOLD = 2 * 60 * 60 * 1000 // warn if projects are older than this
|
||||
const redisOptions = config.get('redis.queue')
|
||||
const JOB_TIME_BUCKETS = [10, 100, 500, 1000, 5000, 10000, 30000, 60000] // milliseconds
|
||||
@@ -17,7 +19,20 @@ const LAG_TIME_BUCKETS_HRS = [
|
||||
] // hours
|
||||
|
||||
// Configure backup settings to match worker concurrency
|
||||
configureBackup({ concurrency: 50, useSecondary: true })
|
||||
configureBackup({ concurrency: UPLOAD_CONCURRENCY, useSecondary: true })
|
||||
|
||||
let gracefulShutdownInitiated = false
|
||||
|
||||
process.on('SIGINT', handleSignal)
|
||||
process.on('SIGTERM', handleSignal)
|
||||
|
||||
async function handleSignal() {
|
||||
if (!gracefulShutdownInitiated) {
|
||||
gracefulShutdownInitiated = true
|
||||
logger.info({}, 'graceful shutdown: stopping backup worker')
|
||||
await drainQueue()
|
||||
}
|
||||
}
|
||||
|
||||
// Create a Bull queue named 'backup'
|
||||
const backupQueue = new Queue('backup', {
|
||||
@@ -61,15 +76,15 @@ backupQueue.on('lock-extension-failed', (job, err) => {
|
||||
})
|
||||
|
||||
backupQueue.on('paused', () => {
|
||||
logger.info('queue paused')
|
||||
logger.info({}, 'queue paused')
|
||||
})
|
||||
|
||||
backupQueue.on('resumed', () => {
|
||||
logger.info('queue resumed')
|
||||
logger.info({}, 'queue resumed')
|
||||
})
|
||||
|
||||
// Process jobs
|
||||
backupQueue.process(CONCURRENCY, async job => {
|
||||
backupQueue.process(JOB_CONCURRENCY, async job => {
|
||||
const { projectId, startDate, endDate } = job.data
|
||||
|
||||
if (projectId) {
|
||||
@@ -137,10 +152,10 @@ async function runInit(startDate, endDate) {
|
||||
}
|
||||
|
||||
export async function drainQueue() {
|
||||
logger.info({ queue: backupQueue.name }, 'pausing queue')
|
||||
await backupQueue.pause(true) // pause this worker and wait for jobs to finish
|
||||
logger.info({ queue: backupQueue.name }, 'closing queue')
|
||||
await backupQueue.close()
|
||||
logger.info({ queue: backupQueue.name }, 'closing database connections')
|
||||
await closeConnections()
|
||||
}
|
||||
|
||||
export async function healthCheck() {
|
||||
|
||||
@@ -18,6 +18,9 @@ RUN mkdir -p /overleaf/services/web/data/dumpFolder \
|
||||
&& chmod -R 0755 /overleaf/services/web/data \
|
||||
&& chown -R node:node /overleaf/services/web/data
|
||||
|
||||
# Add intermediate certificate for prism.optica.org
|
||||
COPY services/web/certs/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt.pem /usr/local/share/ca-certificates/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt
|
||||
RUN update-ca-certificates
|
||||
|
||||
# the deps image is used for caching npm ci
|
||||
FROM base AS deps-prod
|
||||
@@ -127,6 +130,7 @@ CMD ["node", "--expose-gc", "app.mjs"]
|
||||
FROM pug AS app
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/DigiCertGlobalG2TLSRSASHA2562020CA1-1.crt
|
||||
COPY --from=webpack-no-sourcemaps /overleaf/services/web/public /overleaf/services/web/public
|
||||
USER node
|
||||
CMD ["node", "--expose-gc", "app.mjs"]
|
||||
|
||||
@@ -12,6 +12,7 @@ import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
|
||||
import OError from '@overleaf/o-error'
|
||||
import TagsHandler from '../Tags/TagsHandler.mjs'
|
||||
import { promiseMapWithLimit } from '@overleaf/promise-utils'
|
||||
import LimitationsManager from '../Subscription/LimitationsManager.mjs'
|
||||
|
||||
export default {
|
||||
promises: {
|
||||
@@ -124,16 +125,20 @@ async function transferOwnership(projectId, newOwnerId, options = {}) {
|
||||
{ previousOwnerId, newOwnerId }
|
||||
)
|
||||
|
||||
// Determine which permissions to give old owner based on
|
||||
// new owner's existing permissions
|
||||
const newPermissions =
|
||||
_getUserPermissions(newOwner, project) || PrivilegeLevels.READ_ONLY
|
||||
// Transfer ownership
|
||||
await _transferOwnership(projectId, previousOwnerId, newOwnerId)
|
||||
|
||||
await _transferOwnership(
|
||||
// Determine which permissions to give old owner (after transfer, so we use new owner's limits)
|
||||
const { privilegeLevel, pendingPrivilegeLevel } =
|
||||
await _determinePrivilegeLevelForPreviousOwner(projectId)
|
||||
|
||||
// Add the previous owner back to the project with determined permissions
|
||||
await CollaboratorsHandler.promises.addUserIdToProject(
|
||||
projectId,
|
||||
previousOwnerId,
|
||||
newOwnerId,
|
||||
newPermissions
|
||||
previousOwnerId,
|
||||
privilegeLevel,
|
||||
pendingPrivilegeLevel
|
||||
)
|
||||
|
||||
// Flush project to TPDS
|
||||
@@ -189,26 +194,34 @@ function _userIsCollaborator(user, project) {
|
||||
return Boolean(_getUserPermissions(user, project))
|
||||
}
|
||||
|
||||
async function _transferOwnership(
|
||||
projectId,
|
||||
previousOwnerId,
|
||||
newOwnerId,
|
||||
newPermissions
|
||||
) {
|
||||
async function _determinePrivilegeLevelForPreviousOwner(projectId) {
|
||||
// Try to give READ_AND_WRITE if space available based on new owner's limits
|
||||
const canAddEditor =
|
||||
await LimitationsManager.promises.canAddXEditCollaborators(projectId, 1)
|
||||
|
||||
if (canAddEditor) {
|
||||
return { privilegeLevel: PrivilegeLevels.READ_AND_WRITE }
|
||||
}
|
||||
|
||||
// Collaborator limit is reached for editor and reviewer so fall back to read-only
|
||||
// Add pending editor status so they are automatically upgraded when possible
|
||||
return {
|
||||
privilegeLevel: PrivilegeLevels.READ_ONLY,
|
||||
pendingPrivilegeLevel: { pendingEditor: true },
|
||||
}
|
||||
}
|
||||
|
||||
async function _transferOwnership(projectId, previousOwnerId, newOwnerId) {
|
||||
// Remove new owner from collaborators list
|
||||
await CollaboratorsHandler.promises.removeUserFromProject(
|
||||
projectId,
|
||||
newOwnerId
|
||||
)
|
||||
// Update project ownership
|
||||
await Project.updateOne(
|
||||
{ _id: projectId },
|
||||
{ $set: { owner_ref: newOwnerId } }
|
||||
).exec()
|
||||
await CollaboratorsHandler.promises.addUserIdToProject(
|
||||
projectId,
|
||||
newOwnerId,
|
||||
previousOwnerId,
|
||||
newPermissions
|
||||
)
|
||||
}
|
||||
|
||||
async function _sendEmails(project, previousOwner, newOwner) {
|
||||
|
||||
@@ -455,6 +455,7 @@ const _ProjectController = {
|
||||
'compile-timeout-remove-info',
|
||||
'ai-workbench',
|
||||
'compile-timeout-target-plans',
|
||||
'writefull-figure-generator',
|
||||
].filter(Boolean)
|
||||
|
||||
const getUserValues = async userId =>
|
||||
|
||||
@@ -6,6 +6,7 @@ import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs'
|
||||
import TpdsProjectFlusher from '../ThirdPartyDataStore/TpdsProjectFlusher.mjs'
|
||||
import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
|
||||
import SystemMessageManager from '../SystemMessages/SystemMessageManager.mjs'
|
||||
import Modules from '../../infrastructure/Modules.js'
|
||||
|
||||
const AdminController = {
|
||||
_sendDisconnectAllUsersMessage: delay => {
|
||||
@@ -30,16 +31,23 @@ const AdminController = {
|
||||
)
|
||||
}
|
||||
|
||||
SystemMessageManager.getMessagesFromDB(function (error, systemMessages) {
|
||||
if (error) {
|
||||
return next(error)
|
||||
SystemMessageManager.getMessagesFromDB(
|
||||
async function (error, systemMessages) {
|
||||
if (error) {
|
||||
return next(error)
|
||||
}
|
||||
const privilegesMatrixResults = await Modules.promises.hooks.fire(
|
||||
'getPrivilegesMatrix'
|
||||
)
|
||||
const privilegesMatrix = privilegesMatrixResults[0] || null
|
||||
res.render('admin/index', {
|
||||
title: 'System Admin',
|
||||
openSockets,
|
||||
systemMessages,
|
||||
privilegesMatrix,
|
||||
})
|
||||
}
|
||||
res.render('admin/index', {
|
||||
title: 'System Admin',
|
||||
openSockets,
|
||||
systemMessages,
|
||||
})
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
disconnectAllUsers: (req, res) => {
|
||||
|
||||
@@ -91,8 +91,8 @@ const rateLimiters = {
|
||||
duration: 60,
|
||||
}),
|
||||
compileProjectHttp: new RateLimiter('compile-project-http', {
|
||||
points: 800,
|
||||
duration: 60 * 60,
|
||||
points: 200,
|
||||
duration: 10 * 60,
|
||||
}),
|
||||
confirmEmail: new RateLimiter('confirm-email', {
|
||||
points: 10,
|
||||
|
||||
@@ -2,13 +2,15 @@ include terms_of_service
|
||||
include recaptcha
|
||||
|
||||
mixin ciamLogo
|
||||
a.brand.overleaf-ds-logo(href='/' aria-label='Overleaf')
|
||||
header.ciam-logo
|
||||
a.brand.overleaf-ds-logo(href='/')
|
||||
span.visually-hidden Overleaf
|
||||
|
||||
mixin ciamCardSeparator
|
||||
hr.ciam-card-separator
|
||||
|
||||
mixin ciamCardFooter
|
||||
.ciam-card-footer
|
||||
section.ciam-card-footer
|
||||
+ciamCardSeparator
|
||||
.ciam-footer-ds-logo
|
||||
img(
|
||||
@@ -25,3 +27,16 @@ mixin ciamTermsOfServiceAgreement
|
||||
mixin ciamRecaptchaConditions
|
||||
p
|
||||
+recaptchaConditionsContent
|
||||
|
||||
mixin ciamCustomFormDangerMessage(key)
|
||||
div(
|
||||
class='notification ciam-notification notification-type-error'
|
||||
hidden
|
||||
data-ol-custom-form-message=key
|
||||
role='alert'
|
||||
aria-live='polite'
|
||||
)
|
||||
.notification-icon
|
||||
ph-warning-circle(aria-hidden='true')
|
||||
.notification-content.text-left
|
||||
block
|
||||
|
||||
@@ -16,6 +16,7 @@ block content
|
||||
+bookmarkable-tabset-header('system-messages', 'System Messages', true)
|
||||
+bookmarkable-tabset-header('open-sockets', 'Open Sockets')
|
||||
+bookmarkable-tabset-header('open-close-editor', 'Open/Close Editor')
|
||||
+bookmarkable-tabset-header('privileges-matrix', 'Privileges Matrix')
|
||||
if hasFeature('saas')
|
||||
+bookmarkable-tabset-header('tpds', 'TPDS/Dropbox Management')
|
||||
|
||||
@@ -71,6 +72,41 @@ block content
|
||||
button.btn.btn-danger(type='submit') Reopen Editor
|
||||
p.small Will reopen the editor after closing.
|
||||
|
||||
.tab-pane(role='tabpanel' id='privileges-matrix')
|
||||
p This matrix shows the access levels for different admin roles across various user management privileges. The matrix is automatically generated from the code-defined roles and capabilities.
|
||||
if hasFeature('saas') && privilegesMatrix
|
||||
.privileges-matrix-container
|
||||
table.table.table-bordered.table-striped.privileges-matrix-table
|
||||
thead
|
||||
tr
|
||||
th Webpage and privilege
|
||||
th(colspan=privilegesMatrix.roles.length) Access Level Needed For Team
|
||||
tr
|
||||
th
|
||||
each role in privilegesMatrix.roles
|
||||
th= role.displayName
|
||||
tbody
|
||||
- let currentSection = null
|
||||
each row in privilegesMatrix.privileges
|
||||
- const privilege = row.privilege
|
||||
- if (currentSection !== privilege.section)
|
||||
- currentSection = privilege.section
|
||||
tr.section-header
|
||||
td(colspan=privilegesMatrix.roles.length + 1)
|
||||
strong= privilege.section
|
||||
tr
|
||||
td.privilege-label= privilege.label
|
||||
each role in privilegesMatrix.roles
|
||||
- const accessLevel = row.access[role.id]
|
||||
td.access-cell(class=`access-${accessLevel}`)
|
||||
span.access-badge(class=`badge-${accessLevel}`)
|
||||
if accessLevel === 'yes'
|
||||
| Yes
|
||||
else
|
||||
| No
|
||||
else
|
||||
p The privileges matrix is not available in this environment.
|
||||
|
||||
if hasFeature('saas')
|
||||
.tab-pane(role='tabpanel' id='tpds')
|
||||
h3 Flush project to TPDS
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
|
||||
MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT
|
||||
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh
|
||||
bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD
|
||||
ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV
|
||||
cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy
|
||||
FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc
|
||||
3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8
|
||||
osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT
|
||||
zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud
|
||||
EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G
|
||||
A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd
|
||||
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG
|
||||
CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG
|
||||
NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH
|
||||
Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
|
||||
L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC
|
||||
ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG
|
||||
9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t
|
||||
wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS
|
||||
slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R
|
||||
bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4
|
||||
chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN
|
||||
JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -665,6 +665,7 @@
|
||||
"full_doc_history": "",
|
||||
"full_width": "",
|
||||
"future_payments": "",
|
||||
"generate_from_text": "",
|
||||
"generate_from_text_or_image": "",
|
||||
"generate_tables_and_equations": "",
|
||||
"generate_token": "",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// These are used in the CIAM registration form
|
||||
import '@phosphor-icons/webcomponents/PhBank'
|
||||
import '@phosphor-icons/webcomponents/PhEye'
|
||||
import '@phosphor-icons/webcomponents/PhEyeSlash'
|
||||
@@ -1,24 +1,42 @@
|
||||
import { materialIcon } from '@/features/utils/material-icon'
|
||||
import classNames from 'classnames'
|
||||
import '@phosphor-icons/webcomponents/PhWarningCircle'
|
||||
|
||||
function dsErrorIcon() {
|
||||
const icon = document.createElement('ph-warning-circle')
|
||||
icon.className = 'ciam-form-text-icon'
|
||||
icon.ariaHidden = 'true'
|
||||
return icon
|
||||
}
|
||||
|
||||
export default function inputValidator(
|
||||
inputEl: HTMLInputElement | HTMLTextAreaElement
|
||||
) {
|
||||
const isDsBranded = inputEl.classList.contains('form-control-ds')
|
||||
const messageEl = document.createElement('div')
|
||||
messageEl.className =
|
||||
inputEl.getAttribute('data-ol-validation-message-classes') ||
|
||||
'small text-danger mt-2 form-text'
|
||||
classNames(
|
||||
'small text-danger mt-2 form-text',
|
||||
|
||||
{ 'form-text-ds': isDsBranded }
|
||||
)
|
||||
messageEl.hidden = true
|
||||
|
||||
const messageInnerEl = messageEl.appendChild(document.createElement('span'))
|
||||
messageInnerEl.className = 'form-text-inner'
|
||||
messageInnerEl.className = classNames('form-text-inner', {
|
||||
'form-text-inner-ds': isDsBranded,
|
||||
})
|
||||
|
||||
const messageTextNode = document.createTextNode('')
|
||||
|
||||
const iconEl = materialIcon('error')
|
||||
const iconEl = isDsBranded ? dsErrorIcon() : materialIcon('error')
|
||||
messageInnerEl.append(iconEl)
|
||||
messageInnerEl.append(messageTextNode)
|
||||
|
||||
inputEl.insertAdjacentElement('afterend', messageEl)
|
||||
const inputContainerEl =
|
||||
inputEl.closest('.form-complex-input-container') || inputEl
|
||||
inputContainerEl.insertAdjacentElement('afterend', messageEl)
|
||||
|
||||
// Hide messages until the user leaves the input field or submits the form.
|
||||
let canDisplayErrorMessages = false
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DropdownMenu } from '@/shared/components/dropdown/dropdown-menu'
|
||||
import { RailTabKey } from '../../contexts/rail-context'
|
||||
import { RailElement } from '../../utils/rail-types'
|
||||
import RailTab from './rail-tab'
|
||||
import { shouldIncludeRailTab } from '../../utils/rail-utils'
|
||||
|
||||
export default function RailOverflowDropdown({
|
||||
tabs,
|
||||
@@ -15,7 +16,7 @@ export default function RailOverflowDropdown({
|
||||
return (
|
||||
<DropdownMenu className="ide-rail-overflow-dropdown">
|
||||
{tabs
|
||||
.filter(({ hide }) => !hide)
|
||||
.filter(shouldIncludeRailTab)
|
||||
.map(({ icon, key, indicator, title, disabled }) => (
|
||||
<RailTab
|
||||
open={isOpen && selectedTab === key}
|
||||
|
||||
@@ -6,6 +6,7 @@ import usePreviousValue from '@/shared/hooks/use-previous-value'
|
||||
import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
|
||||
import { Tab } from 'react-bootstrap'
|
||||
import { RailElement } from '../../utils/rail-types'
|
||||
import { shouldIncludeRailTab } from '../../utils/rail-utils'
|
||||
|
||||
export default function RailPanel({
|
||||
isReviewPanelOpen,
|
||||
@@ -52,9 +53,7 @@ export default function RailPanel({
|
||||
>
|
||||
<Tab.Content className="ide-rail-tab-content">
|
||||
{railTabs
|
||||
.filter(({ hide }) => {
|
||||
return typeof hide === 'function' ? !hide() : !hide
|
||||
})
|
||||
.filter(shouldIncludeRailTab)
|
||||
.map(({ key, component, mountOnFirstLoad }) => (
|
||||
<Tab.Pane
|
||||
eventKey={key}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import OLTooltip from '@/shared/components/ol/ol-tooltip'
|
||||
import MaterialIcon, {
|
||||
AvailableUnfilledIcon,
|
||||
} from '@/shared/components/material-icon'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import classNames from 'classnames'
|
||||
import { forwardRef, ReactElement } from 'react'
|
||||
import { NavLink } from 'react-bootstrap'
|
||||
import { RailElement } from '../../utils/rail-types'
|
||||
|
||||
const RailTab = forwardRef<
|
||||
HTMLAnchorElement,
|
||||
{
|
||||
icon: AvailableUnfilledIcon
|
||||
icon: RailElement['icon']
|
||||
eventKey: string
|
||||
open: boolean
|
||||
indicator?: ReactElement
|
||||
@@ -31,26 +30,43 @@ const RailTab = forwardRef<
|
||||
})}
|
||||
disabled={disabled}
|
||||
>
|
||||
{open ? (
|
||||
<MaterialIcon
|
||||
className="ide-rail-tab-link-icon"
|
||||
type={icon}
|
||||
accessibilityLabel={title}
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcon
|
||||
className="ide-rail-tab-link-icon"
|
||||
type={icon}
|
||||
accessibilityLabel={title}
|
||||
unfilled
|
||||
/>
|
||||
)}
|
||||
<RailTabIcon icon={icon} title={title} open={open} />
|
||||
{indicator}
|
||||
</NavLink>
|
||||
</OLTooltip>
|
||||
)
|
||||
})
|
||||
|
||||
const RailTabIcon = ({
|
||||
icon,
|
||||
title,
|
||||
open,
|
||||
}: {
|
||||
icon: RailElement['icon']
|
||||
title: string
|
||||
open: boolean
|
||||
}) => {
|
||||
if (typeof icon === 'string') {
|
||||
return open ? (
|
||||
<MaterialIcon
|
||||
type={icon}
|
||||
className="ide-rail-tab-link-icon"
|
||||
accessibilityLabel={title}
|
||||
/>
|
||||
) : (
|
||||
<MaterialIcon
|
||||
type={icon}
|
||||
className="ide-rail-tab-link-icon"
|
||||
unfilled
|
||||
accessibilityLabel={title}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
const Component = icon
|
||||
return <Component open={open} title={title} />
|
||||
}
|
||||
}
|
||||
|
||||
RailTab.displayName = 'RailTab'
|
||||
|
||||
export default RailTab
|
||||
|
||||
@@ -30,6 +30,7 @@ import EditorTourRailTooltip from '../editor-tour/editor-tour-rail-tooltip'
|
||||
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
|
||||
import EditorTourThemeTooltip from '../editor-tour/editor-tour-theme-tooltip'
|
||||
import EditorTourSwitchBackTooltip from '../editor-tour/editor-tour-switch-back-tooltip'
|
||||
import { shouldIncludeRailTab } from '../../utils/rail-utils'
|
||||
|
||||
const moduleRailEntries = (
|
||||
importOverleafModules('railEntries') as {
|
||||
@@ -174,7 +175,7 @@ export const RailLayout = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const validTabKeys = railTabs
|
||||
.filter(tab => (typeof tab.hide === 'function' ? !tab.hide() : !tab.hide))
|
||||
.filter(shouldIncludeRailTab)
|
||||
.map(tab => tab.key)
|
||||
if (!validTabKeys.includes(selectedTab) && isOpen) {
|
||||
// If the selected tab is no longer valid (e.g. due to permissions changes),
|
||||
@@ -224,9 +225,7 @@ export const RailLayout = () => {
|
||||
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
|
||||
<div className="ide-rail-tabs-wrapper" ref={tabWrapperRef}>
|
||||
{tabsInRail
|
||||
.filter(({ hide }) =>
|
||||
typeof hide === 'function' ? !hide() : !hide
|
||||
)
|
||||
.filter(shouldIncludeRailTab)
|
||||
.map(({ icon, key, indicator, title, disabled, ref }) => (
|
||||
<RailTab
|
||||
open={isOpen && selectedTab === key}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { AvailableUnfilledIcon } from '@/shared/components/material-icon'
|
||||
import { RailTabKey } from '../contexts/rail-context'
|
||||
import { ReactElement } from 'react'
|
||||
import { FC, ReactElement } from 'react'
|
||||
|
||||
export type CustomRailTabIcon = FC<{ open: boolean; title: string }>
|
||||
|
||||
export type RailElement = {
|
||||
icon: AvailableUnfilledIcon
|
||||
icon: AvailableUnfilledIcon | CustomRailTabIcon
|
||||
key: RailTabKey
|
||||
component: ReactElement | null
|
||||
indicator?: ReactElement
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RailElement } from './rail-types'
|
||||
|
||||
export function shouldIncludeRailTab({ hide }: RailElement): boolean {
|
||||
if (typeof hide === 'function') {
|
||||
return !hide()
|
||||
}
|
||||
return !hide
|
||||
}
|
||||
@@ -7,14 +7,20 @@ import { useTranslation } from 'react-i18next'
|
||||
import { emitToolbarEvent } from '../../extensions/toolbar/utils/analytics'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-context'
|
||||
import { insertFigure } from '../../extensions/toolbar/commands'
|
||||
import sparkleWhite from '@/shared/svgs/sparkle-small-white.svg'
|
||||
import sparkle from '@/shared/svgs/ai-sparkle-text.svg'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import { ToolbarButton } from './toolbar-button'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
|
||||
|
||||
export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { writefullInstance } = useEditorContext()
|
||||
const { write } = usePermissionsContext()
|
||||
|
||||
const openFigureModal = useCallback(
|
||||
(source: FigureModalSource, sourceName: string) => {
|
||||
emitToolbarEvent(view, `toolbar-figure-modal-${sourceName}`)
|
||||
@@ -32,6 +38,10 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
|
||||
hasLinkUrlFeature,
|
||||
} = getMeta('ol-ExposedSettings')
|
||||
|
||||
const hasGenerateFromTextFeature =
|
||||
writefullInstance !== null &&
|
||||
isSplitTestEnabled('writefull-figure-generator')
|
||||
|
||||
if (!write) {
|
||||
return (
|
||||
<ToolbarButton
|
||||
@@ -88,6 +98,27 @@ export const InsertFigureDropdown = memo(function InsertFigureDropdown() {
|
||||
{t('from_url')}
|
||||
</OLListGroupItem>
|
||||
)}
|
||||
{hasGenerateFromTextFeature && (
|
||||
<OLListGroupItem
|
||||
onClick={() => {
|
||||
writefullInstance!.openFigureGenerator()
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ol-cm-toolbar-ai-sparkle-gradient"
|
||||
src={sparkle}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img
|
||||
alt="sparkle"
|
||||
className="ol-cm-toolbar-ai-sparkle-white"
|
||||
src={sparkleWhite}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{t('generate_from_text')}</span>
|
||||
</OLListGroupItem>
|
||||
)}
|
||||
</ToolbarButtonMenu>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ const en = {
|
||||
'do-not-know': 'Don’t know',
|
||||
equation: 'equation',
|
||||
table: 'table',
|
||||
figure: 'figure',
|
||||
or: 'or',
|
||||
close: 'Close',
|
||||
submit: 'Submit',
|
||||
@@ -228,7 +229,7 @@ const en = {
|
||||
'create-modal.accept-tos_equation': 'Before you generate an equation',
|
||||
'create-modal.image-picker-placeholder':
|
||||
'Drop an image of the __name__ here',
|
||||
'create-modal.enter-prompt': 'Enter your own prompt/Paste an image here:',
|
||||
'create-modal.enter-prompt': 'Enter your prompt to generate the __name__:',
|
||||
'create-modal.enter-prompt-or-paste-image':
|
||||
'Enter your prompt or paste an image with the __name__:',
|
||||
'create-modal.drop-image': 'Or, drop an image of the __name__ here:',
|
||||
@@ -390,6 +391,7 @@ const es = {
|
||||
'do-not-know': 'No lo sé',
|
||||
equation: 'equación',
|
||||
table: 'tabla',
|
||||
figure: 'figura',
|
||||
or: 'o',
|
||||
close: 'Cerrar',
|
||||
submit: 'Enviar',
|
||||
@@ -616,7 +618,7 @@ const es = {
|
||||
'create-modal.accept-tos_equation': 'Antes de generar una ecuación',
|
||||
'create-modal.image-picker-placeholder': 'Arrastra una imagen aquí',
|
||||
'create-modal.enter-prompt':
|
||||
'Introduce tu propio prompt/pega una imagen aquí:',
|
||||
'Introduce tu propio prompt para generar la __name__:',
|
||||
'create-modal.enter-prompt-or-paste-image':
|
||||
'Introduce tu propio prompt o pega una imagen de la __name__:',
|
||||
'create-modal.drop-image': 'O, arrastre una imagen de __name__ aquí:',
|
||||
|
||||
@@ -2,6 +2,7 @@ import './utils/webpack-public-path'
|
||||
import './infrastructure/error-reporter'
|
||||
import './infrastructure/hotjar'
|
||||
import './features/form-helpers/hydrate-form'
|
||||
import './features/form-helpers/form-phosphor-icons'
|
||||
import './features/form-helpers/password-visibility'
|
||||
import './features/link-helpers/slow-link'
|
||||
import './features/event-tracking'
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import overleafLogo from '@/shared/svgs/overleaf-a-ds-solution-mallard.svg'
|
||||
|
||||
type Props = { children: ReactNode }
|
||||
|
||||
const CiamLayout: FC<Props> = ({ children }: Props) => (
|
||||
<div className="ciam-layout ciam-enabled">
|
||||
<a
|
||||
href="/"
|
||||
aria-label="Overleaf"
|
||||
className="brand"
|
||||
style={{ backgroundImage: `url("${overleafLogo}")` }}
|
||||
/>
|
||||
<header className="ciam-logo">
|
||||
<a href="/" className="brand overleaf-ds-logo">
|
||||
<span className="visually-hidden">Overleaf</span>
|
||||
</a>
|
||||
</header>
|
||||
<div className="ciam-container">
|
||||
<main className="ciam-card" id="main-content">
|
||||
{children}
|
||||
|
||||
@@ -17,4 +17,5 @@ export interface WritefullAPI {
|
||||
): void
|
||||
openTableGenerator(): void
|
||||
openEquationGenerator(): void
|
||||
openFigureGenerator(): void
|
||||
}
|
||||
|
||||
@@ -1,33 +1,59 @@
|
||||
@use 'sass:math';
|
||||
|
||||
.overleaf-ds-logo {
|
||||
background-image: url('../../../frontend/js/shared/svgs/overleaf-a-ds-solution-mallard.svg');
|
||||
}
|
||||
|
||||
.ciam-layout {
|
||||
@include full-height-stacked-page;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ds-spacing-400);
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
gap: var(--ds-spacing-800);
|
||||
}
|
||||
}
|
||||
|
||||
.ciam-enabled {
|
||||
@include ds-body-md-regular;
|
||||
|
||||
font-family: var(--ds-font-family-sans), sans-serif;
|
||||
--password-visibility-toggle-width: calc(24px + 2 * var(--ds-spacing-250));
|
||||
|
||||
&,
|
||||
h1 {
|
||||
font-family: var(--ds-font-family-sans), sans-serif;
|
||||
color: var(--ds-color-text-primary);
|
||||
}
|
||||
|
||||
.ciam-container {
|
||||
flex: 1 1 auto;
|
||||
padding: var(--ds-spacing-350);
|
||||
padding: 0 var(--ds-spacing-300);
|
||||
}
|
||||
|
||||
.brand {
|
||||
.ciam-logo {
|
||||
padding: var(--ds-spacing-800) 0 0 0;
|
||||
text-align: center;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-left: var(--ds-spacing-800);
|
||||
padding-right: var(--ds-spacing-800);
|
||||
}
|
||||
}
|
||||
|
||||
.ciam-logo .brand {
|
||||
flex-shrink: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
height: 64px;
|
||||
width: 130px;
|
||||
margin: var(--ds-spacing-350) auto;
|
||||
height: 49px;
|
||||
width: 107px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin: var(--ds-spacing-350) var(--ds-spacing-800);
|
||||
height: 64px;
|
||||
width: 130px;
|
||||
margin: 9px 0; // Vertical margin isn't an exacting spacing value in the design
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,11 +75,32 @@
|
||||
padding: var(--ds-spacing-800) var(--ds-spacing-400);
|
||||
border-radius: var(--ds-border-radius-400);
|
||||
max-width: 464px;
|
||||
margin: var(--ds-spacing-400) auto;
|
||||
margin: 0 auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding: var(--ds-spacing-1300);
|
||||
}
|
||||
|
||||
.notification {
|
||||
@include ds-body-sm-regular;
|
||||
|
||||
color: var(--ds-color-text-primary);
|
||||
padding: 0 var(--ds-spacing-400);
|
||||
border-width: 0;
|
||||
border-radius: var(--ds-border-radius-200);
|
||||
|
||||
.notification-icon {
|
||||
font-size: math.div(20em, 14);
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
padding: var(--ds-spacing-400) 0;
|
||||
}
|
||||
|
||||
&.notification-type-error {
|
||||
background-color: var(--ds-color-red-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ciam-disclaimers p {
|
||||
@@ -77,6 +124,7 @@
|
||||
p {
|
||||
@include ds-body-sm-regular;
|
||||
|
||||
color: var(--ds-color-text-secondary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -86,25 +134,31 @@
|
||||
padding: var(--ds-spacing-200) 0;
|
||||
}
|
||||
|
||||
.ciam-stepper {
|
||||
margin: 0;
|
||||
height: 4px;
|
||||
border-radius: var(--ds-border-radius-full);
|
||||
|
||||
.step {
|
||||
background: var(--ds-color-neutral-200);
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: var(--ds-spacing-600);
|
||||
justify-content: center;
|
||||
padding: var(--ds-spacing-350) 0;
|
||||
margin: 0 auto var(--spacing-15) auto;
|
||||
display: flex;
|
||||
gap: var(--ds-spacing-600);
|
||||
justify-content: center;
|
||||
padding: var(--ds-spacing-300) 0;
|
||||
margin: 0 auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: var(--ds-spacing-800);
|
||||
margin-right: var(--ds-spacing-800);
|
||||
justify-content: start;
|
||||
}
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: var(--ds-spacing-800);
|
||||
margin-right: var(--ds-spacing-800);
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
@include ds-body-sm-regular;
|
||||
}
|
||||
a {
|
||||
@include ds-body-sm-regular;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
@import 'ds-design-system';
|
||||
|
||||
.ciam-register-container {
|
||||
@include full-viewport-height;
|
||||
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ciam-register-columns {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
@@ -15,10 +7,51 @@
|
||||
|
||||
.ciam-register-login-link {
|
||||
text-align: center;
|
||||
margin: var(--ds-spacing-200) 0;
|
||||
margin: 0;
|
||||
padding: var(--ds-spacing-200) 0;
|
||||
}
|
||||
|
||||
.ciam-work-uni-sso {
|
||||
color: var(--ds-color-text-secondary);
|
||||
padding-top: var(--ds-spacing-200);
|
||||
margin-bottom: var(--ds-spacing-400);
|
||||
font-weight: var(--ds-font-weight-semibold);
|
||||
}
|
||||
|
||||
.ciam-register-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.login-register-or-text-container {
|
||||
@include ds-body-xs-semibold;
|
||||
|
||||
gap: var(--ds-spacing-250);
|
||||
padding: var(--ds-spacing-200) 0 0 0;
|
||||
margin-bottom: var(--ds-spacing-400);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--ds-color-neutral-200);
|
||||
}
|
||||
}
|
||||
|
||||
.login-register-error-container {
|
||||
padding-bottom: 0;
|
||||
|
||||
.notification {
|
||||
margin-bottom: var(--ds-spacing-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ciam-password-group {
|
||||
margin-bottom: var(--ds-spacing-400);
|
||||
}
|
||||
|
||||
.ciam-password-requirements-message {
|
||||
@include ds-body-sm-regular;
|
||||
|
||||
color: var(--ds-color-text-secondary);
|
||||
padding-top: var(--ds-spacing-200);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// TODO: Replace `fuchsia` by the correct colors.
|
||||
|
||||
.ciam-enabled {
|
||||
.ciam-enabled,
|
||||
.website-redesign:not(.application-page) .ciam-enabled .notification {
|
||||
// Links
|
||||
// used in services/web/frontend/stylesheets/base/links.scss
|
||||
--link-color: var(--ds-color-text-secondary);
|
||||
--link-hover-color: fuchsia;
|
||||
--link-color: var(--ds-color-text-primary);
|
||||
--link-hover-color: var(--ds-color-text-secondary);
|
||||
--link-visited-color: var(--ds-color-text-secondary);
|
||||
--link-text-decoration: underline;
|
||||
--link-hover-text-decoration: none;
|
||||
|
||||
// TODO: validate that this is correct
|
||||
--link-visited-color: var(--ds-color-text-secondary);
|
||||
--link-color-dark: fuchsia;
|
||||
--link-hover-color-dark: fuchsia;
|
||||
--link-visited-color-dark: fuchsia;
|
||||
--link-text-decoration: underline;
|
||||
--link-hover-text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,26 @@
|
||||
.form-control-ds,
|
||||
.form-group-ds,
|
||||
.form-text-ds,
|
||||
.form-label-ds {
|
||||
.form-label-ds,
|
||||
.form-group-ds label,
|
||||
.website-redesign .form-group-ds label,
|
||||
.website-redesign .form-label-ds {
|
||||
@include ds-body-sm-semibold;
|
||||
|
||||
--bs-body-font-family: var(--ds-font-family-sans), sans-serif;
|
||||
--bs-success-rgb: 25, 117, 76; // #19754c
|
||||
--bs-danger-rgb: 195, 9, 43; // #c3092b
|
||||
--content-placeholder: var(--ds-color-text-disabled);
|
||||
|
||||
// Without this, it inherits the body's --bs-body-font-family which isn't the DS font
|
||||
font-family: var(--ds-font-family-sans), sans-serif;
|
||||
font-family:
|
||||
var(--ds-font-family-sans), sans-serif; // Without this, it inherits the body's --bs-body-font-family which isn't the DS font
|
||||
|
||||
color: var(--ds-color-text-primary);
|
||||
margin-bottom: var(--ds-spacing-100);
|
||||
}
|
||||
|
||||
.form-group-ds {
|
||||
margin-bottom: var(--ds-spacing-400);
|
||||
}
|
||||
|
||||
input.form-control.form-control-ds {
|
||||
@@ -83,3 +95,11 @@ input.form-control.form-control-ds {
|
||||
font-size: math.div(20em, 14);
|
||||
}
|
||||
}
|
||||
|
||||
.form-complex-input-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ciam-form-input-icon {
|
||||
font-size: var(--ds-font-size-600);
|
||||
}
|
||||
|
||||
@@ -100,3 +100,76 @@
|
||||
.expiration-label {
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.privileges-matrix-container {
|
||||
overflow-x: auto;
|
||||
margin-top: var(--spacing-04);
|
||||
}
|
||||
|
||||
.privileges-matrix-table {
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
|
||||
thead {
|
||||
background-color: var(--bg-accent-01);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
th {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
padding: var(--spacing-03);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
.section-header {
|
||||
background-color: var(--bg-accent-02);
|
||||
font-weight: 600;
|
||||
|
||||
td {
|
||||
padding: var(--spacing-03);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.privilege-label {
|
||||
padding: var(--spacing-03);
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.access-cell {
|
||||
text-align: center;
|
||||
padding: var(--spacing-03);
|
||||
border: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.access-badge {
|
||||
display: inline-block;
|
||||
padding: var(--spacing-01) var(--spacing-02);
|
||||
border-radius: var(--border-radius-large);
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.badge-yes {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-no {
|
||||
background-color: #8b4513;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,12 @@ body {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ide-rail-tab-img-icon {
|
||||
width: 20px;
|
||||
max-height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.open-rail {
|
||||
color: var(--ide-rail-link-active-color);
|
||||
background-color: var(--ide-rail-link-active-background);
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
.login-register-or-text-container {
|
||||
padding: var(--spacing-08) 0 var(--spacing-05) 0;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
font-size: var(--font-size-02);
|
||||
text-align: center;
|
||||
:root {
|
||||
--password-visibility-toggle-width: 35px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
.login-register-or-text-container {
|
||||
display: flex;
|
||||
gap: var(--spacing-05);
|
||||
padding: var(--spacing-08) 0 var(--spacing-05) 0;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-02);
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
height: 1px;
|
||||
background-color: var(--neutral-20);
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: calc(var(--spacing-08) + var(--spacing-08) / 4);
|
||||
}
|
||||
|
||||
.login-register-or-text {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
padding: 0 var(--spacing-05);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +37,7 @@
|
||||
|
||||
.form-group-password-input {
|
||||
input.form-control {
|
||||
padding-right: 35px;
|
||||
padding-right: var(--password-visibility-toggle-width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +45,8 @@
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 35px; // TODO: Should this be calculated ?
|
||||
height: 35px; // TODO: Should this be calculated ?
|
||||
width: var(--password-visibility-toggle-width);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -861,6 +861,7 @@
|
||||
"gallery_page_title": "Gallery - Templates, Examples and Articles written in LaTeX",
|
||||
"gallery_show_more_tags": "Show more",
|
||||
"general": "General",
|
||||
"generate_from_text": "From text",
|
||||
"generate_from_text_or_image": "From text or image",
|
||||
"generate_tables_and_equations": "Generate tables and equations from text and images. Try it for free in the Overleaf toolbar!",
|
||||
"generate_token": "Generate token",
|
||||
@@ -1415,6 +1416,7 @@
|
||||
"more_project_collaborators": "<0>More</0> project <0>collaborators</0>",
|
||||
"more_than_one_kind_of_snippet_was_requested": "The link to open this content on Overleaf included some invalid parameters. If this keeps happening for links on a particular site, please report this to them.",
|
||||
"most_popular_uppercase": "Most popular",
|
||||
"must_be_at_least_n_characters": "Must be at least __n__ characters. Avoid common passwords.",
|
||||
"must_be_email_address": "Must be an email address.",
|
||||
"must_be_purchased_online": "Must be purchased online",
|
||||
"my_library": "My Library",
|
||||
@@ -2533,8 +2535,10 @@
|
||||
"try_to_compile_despite_errors": "Try to compile despite errors",
|
||||
"turn_off": "Turn off",
|
||||
"turn_off_link_sharing": "Turn off link sharing",
|
||||
"turn_off_password_visibility": "Turn off password visibility",
|
||||
"turn_on": "Turn on",
|
||||
"turn_on_link_sharing": "Turn on link sharing",
|
||||
"turn_on_password_visibility": "Turn on password visibility",
|
||||
"tutorials": "Tutorials",
|
||||
"uk": "Ukrainian",
|
||||
"unable_to_extract_the_supplied_zip_file": "Opening this content on Overleaf failed because the zip file could not be extracted. Please ensure that it is a valid zip file. If this keeps happening for links on a particular site, please report this to them.",
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"@overleaf/stream-utils": "*",
|
||||
"@overleaf/validation-tools": "*",
|
||||
"@phosphor-icons/react": "^2.1.7",
|
||||
"@phosphor-icons/webcomponents": "^2.1.5",
|
||||
"@slack/webhook": "^7.0.2",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/stripe-js": "^7.7.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import UserHelper from './helpers/User.mjs'
|
||||
import Features from '../../../app/src/infrastructure/Features.mjs'
|
||||
|
||||
const User = UserHelper.promises
|
||||
|
||||
@@ -53,11 +54,38 @@ describe('Project ownership transfer', function () {
|
||||
})
|
||||
|
||||
it('adds the previous owner as a read/write collaborator', async function () {
|
||||
// Skip this test in SaaS environments as limited collaborators are enforced
|
||||
if (Features.hasFeature('saas')) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
const project = await this.collaboratorSession.getProject(this.projectId)
|
||||
expect(project.collaberator_refs.map(x => x.toString())).to.have.members([
|
||||
this.owner._id.toString(),
|
||||
this.invitedAdmin._id.toString(),
|
||||
])
|
||||
expect(project.owner_ref.toString()).to.equal(
|
||||
this.collaborator._id.toString()
|
||||
)
|
||||
expect(project.readOnly_refs.map(x => x.toString())).to.be.empty
|
||||
})
|
||||
|
||||
it('adds the previous owner as a read only', async function () {
|
||||
// Skip this test in non-SaaS environments as unlimited collaborators are allowed
|
||||
if (!Features.hasFeature('saas')) {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
const project = await this.collaboratorSession.getProject(this.projectId)
|
||||
expect(project.collaberator_refs.map(x => x.toString())).to.have.members([
|
||||
this.invitedAdmin._id.toString(),
|
||||
])
|
||||
expect(project.owner_ref.toString()).to.equal(
|
||||
this.collaborator._id.toString()
|
||||
)
|
||||
expect(project.readOnly_refs.map(x => x.toString())).to.have.members([
|
||||
this.owner._id.toString(),
|
||||
])
|
||||
})
|
||||
|
||||
it('lets the new owner open the project', async function () {
|
||||
|
||||
@@ -80,6 +80,11 @@ describe('OwnershipTransferHandler', function () {
|
||||
addProjectsToTag: sinon.stub().resolves(),
|
||||
},
|
||||
}
|
||||
ctx.LimitationsManager = {
|
||||
promises: {
|
||||
canAddXEditCollaborators: sinon.stub().resolves(true),
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
|
||||
vi.importActual('../../../../app/src/Features/Errors/Errors.js')
|
||||
@@ -136,6 +141,13 @@ describe('OwnershipTransferHandler', function () {
|
||||
})
|
||||
)
|
||||
|
||||
vi.doMock(
|
||||
'../../../../app/src/Features/Subscription/LimitationsManager.mjs',
|
||||
() => ({
|
||||
default: ctx.LimitationsManager,
|
||||
})
|
||||
)
|
||||
|
||||
ctx.handler = (await import(MODULE_PATH)).default
|
||||
})
|
||||
|
||||
@@ -195,30 +207,197 @@ describe('OwnershipTransferHandler', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project to a read-only collaborator', async function (ctx) {
|
||||
it('should check collaborator limits after ownership transfer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
|
||||
{ _id: ctx.project._id },
|
||||
sinon.match({ $set: { owner_ref: ctx.readOnlyCollaborator._id } })
|
||||
|
||||
expect(ctx.ProjectModel.updateOne).to.have.been.calledBefore(
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
||||
)
|
||||
|
||||
expect(
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
||||
).to.have.been.calledBefore(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
)
|
||||
})
|
||||
|
||||
it('gives old owner read-only permissions if new owner was previously a viewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_ONLY
|
||||
)
|
||||
describe('when there are edit collaborator slots available', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(true)
|
||||
})
|
||||
|
||||
it('should give old owner read/write permissions when transferring to a read-only collaborator', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE,
|
||||
undefined
|
||||
)
|
||||
expect(
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
||||
).to.have.been.calledWith(ctx.project._id, 1)
|
||||
})
|
||||
|
||||
it('should give old owner read/write permissions when transferring to an editor', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE,
|
||||
undefined
|
||||
)
|
||||
expect(
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
||||
).to.have.been.calledWith(ctx.project._id, 1)
|
||||
})
|
||||
|
||||
it('should give old owner read/write permissions when transferring to a reviewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE,
|
||||
undefined
|
||||
)
|
||||
expect(
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
||||
).to.have.been.calledWith(ctx.project._id, 1)
|
||||
})
|
||||
|
||||
it('should give old owner read/write permissions when transferring to a non-collaborator', async function (ctx) {
|
||||
ctx.project.collaberator_refs = []
|
||||
ctx.project.readOnly_refs = []
|
||||
ctx.project.reviewer_refs = []
|
||||
const newOwner = {
|
||||
_id: new ObjectId(),
|
||||
email: 'admin@example.com',
|
||||
}
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(newOwner._id)
|
||||
.resolves(newOwner)
|
||||
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
newOwner._id,
|
||||
{ allowTransferToNonCollaborators: true }
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
newOwner._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE,
|
||||
undefined
|
||||
)
|
||||
expect(
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators
|
||||
).to.have.been.calledWith(ctx.project._id, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when there are no edit collaborator slots available', function () {
|
||||
beforeEach(function (ctx) {
|
||||
ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(false)
|
||||
})
|
||||
|
||||
it('should give old owner read-only with pending editor flag when transferring to an editor', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_ONLY,
|
||||
{ pendingEditor: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('should give old owner read-only with pending editor flag when transferring to a reviewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_ONLY,
|
||||
{ pendingEditor: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('should give old owner read-only with pending editor flag when transferring to a read-only collaborator', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.readOnlyCollaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_ONLY,
|
||||
{ pendingEditor: true }
|
||||
)
|
||||
})
|
||||
|
||||
it('should give old owner read-only with pending editor flag when transferring to a non-collaborator', async function (ctx) {
|
||||
ctx.project.collaberator_refs = []
|
||||
ctx.project.readOnly_refs = []
|
||||
ctx.project.reviewer_refs = []
|
||||
const newOwner = {
|
||||
_id: new ObjectId(),
|
||||
email: 'newowner@example.com',
|
||||
}
|
||||
ctx.UserGetter.promises.getUser
|
||||
.withArgs(newOwner._id)
|
||||
.resolves(newOwner)
|
||||
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
newOwner._id,
|
||||
{ allowTransferToNonCollaborators: true }
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
newOwner._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_ONLY,
|
||||
{ pendingEditor: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should do nothing if transferring back to the owner', async function (ctx) {
|
||||
@@ -239,21 +418,6 @@ describe('OwnershipTransferHandler', function () {
|
||||
).to.have.been.calledWith(ctx.project._id, ctx.collaborator._id)
|
||||
})
|
||||
|
||||
it('should add the former project owner as a read/write collaborator', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.collaborator._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.READ_AND_WRITE
|
||||
)
|
||||
})
|
||||
|
||||
it('should transfer ownership of the project to a reviewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
@@ -265,21 +429,6 @@ describe('OwnershipTransferHandler', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('gives old owner reviewer permissions if new owner was previously a reviewer', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id
|
||||
)
|
||||
expect(
|
||||
ctx.CollaboratorsHandler.promises.addUserIdToProject
|
||||
).to.have.been.calledWith(
|
||||
ctx.project._id,
|
||||
ctx.reviewer._id,
|
||||
ctx.user._id,
|
||||
PrivilegeLevels.REVIEW
|
||||
)
|
||||
})
|
||||
|
||||
it('should flush the project to tpds', async function (ctx) {
|
||||
await ctx.handler.promises.transferOwnership(
|
||||
ctx.project._id,
|
||||
|
||||
Reference in New Issue
Block a user