Compare commits

...
Author SHA1 Message Date
StandardNotes CI 062e50e901 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].8
2023-09-20 15:50:55 +00:00
Aman Harwara ec61901aa5 fix: Fixed issue where using table or image option in Super mobile toolbar would make uninteractive 2023-09-20 20:52:01 +05:30
Karol Sójko 0d15858b5f chore: fix notification type on shared vault user removal (#2521) 2023-09-20 17:01:07 +02:00
StandardNotes CI 09d139846e chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
2023-09-20 13:40:09 +00:00
Karol Sójko da234f960e chore: skip still flaky history tests (#2520) 2023-09-20 15:10:23 +02:00
StandardNotes CI f7c2434141 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
2023-09-20 12:25:08 +00:00
Aman Harwara e2901d9535 chore: only invalidate vault user cache when required (#2519) 2023-09-20 17:20:30 +05:30
StandardNotes CI 3a42925ca2 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2023-09-20 10:25:37 +00:00
Karol Sójko 3e1557ab36 chore: fix history test with additional await if needed (#2517) 2023-09-20 11:50:15 +02:00
StandardNotes CI 9e1583f798 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2023-09-19 19:23:41 +00:00
Aman Harwara 35d92be63c chore: update vaults preferences 2023-09-20 00:22:34 +05:30
Karol Sójko 3b61fa5911 chore: disabling history tests as they are still flaky 2023-09-19 15:13:19 +02:00
StandardNotes CI 0f046cbe83 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2023-09-19 06:04:39 +00:00
Karol Sójko 102d9c8e2d chore: re-enable flaky history tests (#2510) 2023-09-19 07:33:58 +02:00
StandardNotes CI ca01b2dc95 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2023-09-18 17:39:29 +00:00
Aman Harwara dbf918523c chore: show account update prompt in vaults preferences if account protocol is older than 004 2023-09-18 22:39:24 +05:30
StandardNotes CI fe8521eff7 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2023-09-18 14:51:53 +00:00
Aman Harwara 48e7820100 chore: vault member permissions (#2509) 2023-09-18 19:53:34 +05:30
StandardNotes CI 2af610c7bf chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2023-09-16 10:17:30 +00:00
Aman Harwara 896372f04d feat: Added table cell menu for adding/removing rows and columns to Super note tables and merging cells (#2508) 2023-09-16 15:14:20 +05:30
75 changed files with 1594 additions and 292 deletions
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.26.57](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/api
## [1.26.56](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-13)
**Note:** Version bump only for package @standardnotes/api
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.26.56",
"version": "1.26.57",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -36,7 +36,7 @@
},
"dependencies": {
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/utils": "workspace:*",
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.234](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.233](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.232](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.231](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.230](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.229](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.228](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.227](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.226](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-16)
**Note:** Version bump only for package @standardnotes/clipper
## [1.1.225](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/clipper
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/clipper",
"description": "Web clipper browser extension for Standard Notes",
"version": "1.1.225",
"version": "1.1.234",
"private": true,
"scripts": {
"build-mv2": "yarn clean && webpack --config ./webpack.config.prod.js",
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.108.166](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.165](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.164](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.163](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.162](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.161](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.160](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.159](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.158](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-16)
**Note:** Version bump only for package @standardnotes/desktop
## [3.108.157](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/desktop
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.108.157",
"version": "3.108.166",
"license": "CC BY-NC-SA 4.0",
"author": "Standard Notes.",
"private": true,
@@ -35,7 +35,7 @@
},
"dependencies": {
"@electron/remote": "^2.0.9",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/electron-clear-data": "1.1.1",
"@standardnotes/web": "workspace:*",
"axios": "^1.1.3",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.21.79](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/encryption
## [1.21.78](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-13)
**Note:** Version bump only for package @standardnotes/encryption
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.21.78",
"version": "1.21.79",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -29,7 +29,7 @@
},
"dependencies": {
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/sncrypto-common": "workspace:*",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.59.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/features
## [1.59.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-07)
**Note:** Version bump only for package @standardnotes/features
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/features",
"version": "1.59.18",
"version": "1.59.19",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -26,7 +26,7 @@
},
"dependencies": {
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"reflect-metadata": "^0.1.13"
},
"devDependencies": {
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.90](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.89](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-13)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.89",
"version": "1.28.90",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.17.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/files
## [1.17.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-13)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.17.8",
"version": "1.17.9",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.56.145](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.144](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.143](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.142](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.141](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.140](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.139](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.138](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.137](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-16)
**Note:** Version bump only for package @standardnotes/mobile
## [3.56.136](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.56.136",
"version": "3.56.145",
"author": "Standard Notes.",
"private": true,
"license": "CC BY-NC-SA 4.0",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.47.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/models
## [1.47.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-13)
**Note:** Version bump only for package @standardnotes/models
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/models",
"version": "1.47.14",
"version": "1.47.15",
"publishConfig": {
"access": "public"
},
@@ -20,7 +20,7 @@
},
"dependencies": {
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/features": "workspace:*",
"@standardnotes/responses": "workspace:*",
"@standardnotes/sncrypto-common": "workspace:^",
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.518](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.517](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.516](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.515](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.514](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.513](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.512](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.511](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.510](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-16)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.509](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.509",
"version": "1.4.518",
"license": "CC BY-NC-SA 4.0",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.13.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/responses
## [1.13.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-07)
**Note:** Version bump only for package @standardnotes/responses
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/responses",
"version": "1.13.42",
"version": "1.13.43",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.64.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/services
## [1.64.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/services
## [1.64.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/services
## [1.64.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/services
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.64.19",
"version": "1.64.22",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -18,7 +18,7 @@
"dependencies": {
"@standardnotes/api": "workspace:^",
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/encryption": "workspace:^",
"@standardnotes/features": "workspace:^",
"@standardnotes/files": "workspace:^",
@@ -101,7 +101,7 @@ export class SharedVaultService
private async handleNotification(event: NotificationServiceEventPayload): Promise<void> {
switch (event.eventPayload.props.type.value) {
case NotificationType.TYPES.RemovedFromSharedVault: {
case NotificationType.TYPES.SelfRemovedFromSharedVault: {
const vault = this._getVault.execute<SharedVaultListingInterface>({
sharedVaultUuid: event.eventPayload.props.sharedVaultUuid.value,
})
@@ -179,7 +179,8 @@ export class VaultInviteService
this.removePendingInvite(pendingInvite.invite.uuid)
void this.sync.sync()
this.sync.sync().catch(console.error)
this.vaultUsers.invalidateVaultUsersCache(pendingInvite.invite.shared_vault_uuid).catch(console.error)
await this._decryptErroredPayloads.execute()
@@ -191,7 +192,7 @@ export class VaultInviteService
public async getInvitableContactsForSharedVault(
sharedVault: SharedVaultListingInterface,
): Promise<TrustedContactInterface[]> {
const users = await this.vaultUsers.getSharedVaultUsers(sharedVault)
const users = await this.vaultUsers.getSharedVaultUsersFromServer(sharedVault)
if (!users) {
return []
}
@@ -0,0 +1,27 @@
import { UserServiceInterface } from '../../User/UserServiceInterface'
import { Result, SharedVaultUserPermission, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { VaultUserCache } from '../VaultUserCache'
export class IsReadonlyVaultMember implements SyncUseCaseInterface<boolean> {
constructor(
private users: UserServiceInterface,
private cache: VaultUserCache,
) {}
execute(vault: SharedVaultListingInterface): Result<boolean> {
if (!vault.sharing) {
return Result.ok(true)
}
if (!vault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${vault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
const user = this.users.sureUser
const vaultUsers = this.cache.get(vault.sharing.sharedVaultUuid)
const userPermission = vaultUsers?.find((vaultUser) => vaultUser.user_uuid === user.uuid)?.permission
return Result.ok(userPermission === SharedVaultUserPermission.PERMISSIONS.Read)
}
}
@@ -0,0 +1,29 @@
import { UserServiceInterface } from '../../User/UserServiceInterface'
import { Result, SharedVaultUserPermission, SyncUseCaseInterface } from '@standardnotes/domain-core'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { VaultUserCache } from '../VaultUserCache'
export class IsVaultAdmin implements SyncUseCaseInterface<boolean> {
constructor(
private users: UserServiceInterface,
private cache: VaultUserCache,
) {}
execute(vault: SharedVaultListingInterface): Result<boolean> {
if (!vault.sharing) {
return Result.ok(true)
}
if (!vault.sharing.ownerUserUuid) {
throw new Error(`Shared vault ${vault.sharing.sharedVaultUuid} does not have an owner user uuid`)
}
const user = this.users.sureUser
const vaultUsers = this.cache.get(vault.sharing.sharedVaultUuid)
const userPermission = vaultUsers?.find((vaultUser) => vaultUser.user_uuid === user.uuid)?.permission
return Result.ok(
userPermission === SharedVaultUserPermission.PERMISSIONS.Admin || vault.sharing.ownerUserUuid === user.uuid,
)
}
}
@@ -5,13 +5,15 @@ import { InternalEventBusInterface } from './../Internal/InternalEventBusInterfa
import { RemoveVaultMember } from './UseCase/RemoveSharedVaultMember'
import { VaultServiceInterface } from '../Vault/VaultServiceInterface'
import { GetVaultUsers } from './UseCase/GetVaultUsers'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { SharedVaultListingInterface, VaultListingInterface } from '@standardnotes/models'
import { VaultUserServiceInterface } from './VaultUserServiceInterface'
import { ClientDisplayableError, SharedVaultUserServerHash, isClientDisplayableError } from '@standardnotes/responses'
import { AbstractService } from './../Service/AbstractService'
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
import { Result } from '@standardnotes/domain-core'
import { IsVaultOwner } from './UseCase/IsVaultOwner'
import { IsReadonlyVaultMember } from './UseCase/IsReadonlyVaultMember'
import { IsVaultAdmin } from './UseCase/IsVaultAdmin'
export class VaultUserService extends AbstractService<VaultUserServiceEvent> implements VaultUserServiceInterface {
constructor(
@@ -20,6 +22,8 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
private _getVaultUsers: GetVaultUsers,
private _removeVaultMember: RemoveVaultMember,
private _isVaultOwner: IsVaultOwner,
private _isVaultAdmin: IsVaultAdmin,
private _isReadonlyVaultMember: IsReadonlyVaultMember,
private _getVault: GetVault,
private _leaveVault: LeaveVault,
eventBus: InternalEventBusInterface,
@@ -37,7 +41,31 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
;(this._leaveVault as unknown) = undefined
}
public async getSharedVaultUsers(
async invalidateVaultUsersCache(sharedVaultUuid?: string) {
if (sharedVaultUuid) {
await this._getVaultUsers.execute({
sharedVaultUuid: sharedVaultUuid,
readFromCache: false,
})
void this.notifyEvent(VaultUserServiceEvent.InvalidatedUserCacheForVault, sharedVaultUuid)
return
}
await Promise.all(
this.vaults.getVaults().map(async (vault) => {
if (!vault.isSharedVaultListing()) {
return
}
await this._getVaultUsers.execute({
sharedVaultUuid: vault.sharing.sharedVaultUuid,
readFromCache: false,
})
void this.notifyEvent(VaultUserServiceEvent.InvalidatedUserCacheForVault, vault.sharing.sharedVaultUuid)
}),
)
void this.notifyEvent(VaultUserServiceEvent.InvalidatedAllUserCache)
}
public async getSharedVaultUsersFromServer(
sharedVault: SharedVaultListingInterface,
): Promise<SharedVaultUserServerHash[] | undefined> {
const result = await this._getVaultUsers.execute({
@@ -55,6 +83,17 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
return this._isVaultOwner.execute(sharedVault).getValue()
}
public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean {
return this._isVaultAdmin.execute(sharedVault).getValue()
}
public isCurrentUserReadonlyVaultMember(vault: VaultListingInterface): boolean {
if (!vault.isSharedVaultListing()) {
return false
}
return this._isReadonlyVaultMember.execute(vault).getValue()
}
async removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>> {
if (!this.isCurrentUserSharedVaultOwner(sharedVault)) {
throw new Error('Only vault admins can remove users')
@@ -101,4 +140,17 @@ export class VaultUserService extends AbstractService<VaultUserServiceEvent> imp
void this.notifyEvent(VaultUserServiceEvent.UsersChanged)
}
getFormattedMemberPermission(permission: string): string {
switch (permission) {
case 'admin':
return 'Admin'
case 'write':
return 'Read / Write'
case 'read':
return 'Read-only'
default:
return 'Unknown'
}
}
}
@@ -1,3 +1,5 @@
export enum VaultUserServiceEvent {
UsersChanged = 'VaultUserServiceEvent.UsersChanged',
InvalidatedAllUserCache = 'VaultUserServiceEvent.InvalidatedUserCache',
InvalidatedUserCacheForVault = 'VaultUserServiceEvent.InvalidatedUserCacheForVault',
}
@@ -1,13 +1,19 @@
import { ApplicationServiceInterface } from './../Service/ApplicationServiceInterface'
import { SharedVaultListingInterface } from '@standardnotes/models'
import { SharedVaultListingInterface, VaultListingInterface } from '@standardnotes/models'
import { ClientDisplayableError, SharedVaultUserServerHash } from '@standardnotes/responses'
import { VaultUserServiceEvent } from './VaultUserServiceEvent'
import { Result } from '@standardnotes/domain-core'
export interface VaultUserServiceInterface extends ApplicationServiceInterface<VaultUserServiceEvent, unknown> {
getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise<SharedVaultUserServerHash[] | undefined>
getSharedVaultUsersFromServer(
sharedVault: SharedVaultListingInterface,
): Promise<SharedVaultUserServerHash[] | undefined>
isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean
isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean
isCurrentUserReadonlyVaultMember(vault: VaultListingInterface): boolean
removeUserFromSharedVault(sharedVault: SharedVaultListingInterface, userUuid: string): Promise<Result<void>>
leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise<ClientDisplayableError | void>
isVaultUserOwner(user: SharedVaultUserServerHash): boolean
getFormattedMemberPermission(permission: string): string
invalidateVaultUsersCache(sharedVaultUuid?: string): Promise<void>
}
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.202.82](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.81](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.80](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.79](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.78](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.77](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.76](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/snjs
## [2.202.75](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/snjs
@@ -469,6 +469,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
source: SyncSource.External,
sourceDescription: 'Application Launch',
})
this.vaultUsers.invalidateVaultUsersCache().catch(console.error)
})
.catch((error) => {
void this.notifyEvent(ApplicationEvent.LocalDatabaseReadError, error)
@@ -172,6 +172,8 @@ import { EncryptionOperators } from '@standardnotes/encryption'
import { AsymmetricMessagePayload, AsymmetricMessageSharedVaultInvite } from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AuthorizeVaultDeletion } from '@standardnotes/services/src/Domain/Vault/UseCase/AuthorizeVaultDeletion'
import { IsVaultAdmin } from '@standardnotes/services/src/Domain/VaultUser/UseCase/IsVaultAdmin'
import { IsReadonlyVaultMember } from '@standardnotes/services/src/Domain/VaultUser/UseCase/IsReadonlyVaultMember'
export class Dependencies {
private factory = new Map<symbol, () => unknown>()
@@ -335,6 +337,17 @@ export class Dependencies {
return new IsVaultOwner(this.get<UserService>(TYPES.UserService))
})
this.factory.set(TYPES.IsVaultAdmin, () => {
return new IsVaultAdmin(this.get<UserService>(TYPES.UserService), this.get<VaultUserCache>(TYPES.VaultUserCache))
})
this.factory.set(TYPES.IsReadonlyVaultMember, () => {
return new IsReadonlyVaultMember(
this.get<UserService>(TYPES.UserService),
this.get<VaultUserCache>(TYPES.VaultUserCache),
)
})
this.factory.set(TYPES.DecryptBackupFile, () => {
return new DecryptBackupFile(
this.get<EncryptionService>(TYPES.EncryptionService),
@@ -844,6 +857,8 @@ export class Dependencies {
this.get<GetVaultUsers>(TYPES.GetVaultUsers),
this.get<RemoveVaultMember>(TYPES.RemoveVaultMember),
this.get<IsVaultOwner>(TYPES.IsVaultOwner),
this.get<IsVaultAdmin>(TYPES.IsVaultAdmin),
this.get<IsReadonlyVaultMember>(TYPES.IsReadonlyVaultMember),
this.get<GetVault>(TYPES.GetVault),
this.get<LeaveVault>(TYPES.LeaveVault),
this.get<InternalEventBus>(TYPES.InternalEventBus),
@@ -152,6 +152,8 @@ export const TYPES = {
EncryptTypeAPayloadWithKeyLookup: Symbol.for('EncryptTypeAPayloadWithKeyLookup'),
DecryptBackupFile: Symbol.for('DecryptBackupFile'),
IsVaultOwner: Symbol.for('IsVaultOwner'),
IsVaultAdmin: Symbol.for('IsVaultAdmin'),
IsReadonlyVaultMember: Symbol.for('IsReadonlyVaultMember'),
RemoveItemsFromMemory: Symbol.for('RemoveItemsFromMemory'),
ReencryptTypeAItems: Symbol.for('ReencryptTypeAItems'),
DecryptErroredPayloads: Symbol.for('DecryptErroredPayloads'),
+125 -88
View File
@@ -7,23 +7,17 @@ const expect = chai.expect
describe.skip('history manager', () => {
const largeCharacterChange = 25
let application, history, email, password
const syncOptions = {
checkIntegrity: true,
awaitAll: true,
}
beforeEach(function () {
localStorage.clear()
})
afterEach(function () {
localStorage.clear()
})
describe('session', function () {
let application, history
beforeEach(async function () {
localStorage.clear()
application = await Factory.createInitAppWithFakeCrypto()
history = application.dependencies.get(TYPES.HistoryManager)
@@ -33,6 +27,7 @@ describe.skip('history manager', () => {
afterEach(async function () {
await Factory.safeDeinit(application)
localStorage.clear()
})
async function setTextAndSync(application, item, text) {
@@ -234,96 +229,102 @@ describe.skip('history manager', () => {
describe('remote', function () {
this.timeout(Factory.TwentySecondTimeout)
let context
beforeEach(async function () {
localStorage.clear()
application = await Factory.createInitAppWithFakeCrypto()
history = application.dependencies.get(TYPES.HistoryManager)
email = UuidGenerator.GenerateUuid()
password = UuidGenerator.GenerateUuid()
await Factory.registerUserToApplication({
application: application,
email: email,
password: password,
})
context = await Factory.createVaultsContextWithRealCrypto()
await context.launch()
await context.register()
})
afterEach(async function () {
await Factory.safeDeinit(application)
await context.deinit()
localStorage.clear()
sinon.restore()
context = undefined
})
it('response from server should be failed if not signed in', async function () {
await application.user.signOut()
application = await Factory.createInitAppWithFakeCrypto()
history = application.dependencies.get(TYPES.HistoryManager)
const note = await context.createSyncedNote('test note')
const item = await Factory.createSyncedNote(application)
await application.sync.sync(syncOptions)
const itemHistoryOrError = await application.listRevisions.execute({ itemUuid: item.uuid })
await context.signout()
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(true)
})
it('should save initial revisions on server', async () => {
const item = await Factory.createSyncedNote(application)
expect(item).to.be.ok
const note = await context.createSyncedNote('test note')
expect(note).to.be.ok
await Factory.sleep(Factory.ServerRevisionCreationDelay)
const itemHistoryOrError = await application.listRevisions.execute({ itemUuid: item.uuid })
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
const itemHistory = itemHistoryOrError.getValue()
let itemHistory = itemHistoryOrError.getValue()
if (itemHistory.length === 0) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'No revisions found on the server. This is likely a delay issue. Retrying...')
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
itemHistory = itemHistoryOrError.getValue()
}
expect(itemHistory.length >= 1).to.be.true
})
it('should not create new revisions within the revision frequency window', async () => {
const item = await Factory.createSyncedNote(application)
const note = await context.createSyncedNote('test note')
await application.changeAndSaveItem.execute(
item,
(mutator) => {
mutator.title = Math.random()
},
undefined,
undefined,
syncOptions,
)
await context.changeNoteTitleAndSync(note, 'new title 1')
await Factory.sleep(Factory.ServerRevisionCreationDelay)
const itemHistoryOrError = await application.listRevisions.execute({ itemUuid: item.uuid })
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
const itemHistory = itemHistoryOrError.getValue()
let itemHistory = itemHistoryOrError.getValue()
if (itemHistory.length === 0) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'No revisions found on the server. This is likely a delay issue. Retrying...')
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
itemHistory = itemHistoryOrError.getValue()
}
expect(itemHistory.length >= 1).to.be.true
})
it('should create new revisions outside the revision frequency window', async function () {
let item = await Factory.createSyncedNote(application)
const note = await context.createSyncedNote('test note')
await Factory.sleep(Factory.ServerRevisionFrequency)
/** Sync with different contents, should create new entry */
const newTitleAfterFirstChange = `The title should be: ${Math.random()}`
await application.changeAndSaveItem.execute(
item,
(mutator) => {
mutator.title = newTitleAfterFirstChange
},
undefined,
undefined,
syncOptions,
)
await context.changeNoteTitleAndSync(note, `The title should be: ${Math.random()}`)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
const itemHistoryOrError = await application.listRevisions.execute({ itemUuid: item.uuid })
const itemHistory = itemHistoryOrError.getValue()
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
let itemHistory = itemHistoryOrError.getValue()
if (itemHistory.length !== 2) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'Not enough revisions found on the server. This is likely a delay issue. Retrying...')
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
itemHistory = itemHistoryOrError.getValue()
}
expect(itemHistory.length).to.equal(2)
const oldestEntry = lastElement(itemHistory)
let revisionFromServerOrError = await application.getRevision.execute({
itemUuid: item.uuid,
let revisionFromServerOrError = await context.application.getRevision.execute({
itemUuid: note.uuid,
revisionUuid: oldestEntry.uuid,
})
const revisionFromServer = revisionFromServerOrError.getValue()
@@ -331,25 +332,36 @@ describe.skip('history manager', () => {
let payloadFromServer = revisionFromServer.payload
expect(payloadFromServer.errorDecrypting).to.be.undefined
expect(payloadFromServer.uuid).to.eq(item.payload.uuid)
expect(payloadFromServer.content).to.eql(item.payload.content)
expect(payloadFromServer.uuid).to.eq(note.payload.uuid)
expect(payloadFromServer.content).to.eql(note.payload.content)
item = application.items.findItem(item.uuid)
const item = context.application.items.findItem(note.uuid)
expect(payloadFromServer.content).to.not.eql(item.payload.content)
})
it('duplicate revisions should not have the originals uuid', async function () {
const note = await Factory.createSyncedNote(application)
await Factory.markDirtyAndSyncItem(application, note)
const dupe = await application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(application, dupe)
const note = await context.createSyncedNote('test note')
await Factory.markDirtyAndSyncItem(context.application, note)
const dupe = await context.application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(context.application, dupe)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
const dupeHistoryOrError = await application.listRevisions.execute({ itemUuid: dupe.uuid })
const dupeHistory = dupeHistoryOrError.getValue()
const dupeHistoryOrError = await context.application.listRevisions.execute({ itemUuid: dupe.uuid })
expect(dupeHistoryOrError.isFailed()).to.equal(false)
let dupeHistory = dupeHistoryOrError.getValue()
if (dupeHistory.length === 0) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'No revisions found on the server. This is likely a delay issue. Retrying...')
const dupeRevisionOrError = await application.getRevision.execute({
const dupeHistoryOrError = await context.application.listRevisions.execute({ itemUuid: dupe.uuid })
expect(dupeHistoryOrError.isFailed()).to.equal(false)
dupeHistory = dupeHistoryOrError.getValue()
}
expect(dupeHistory.length >= 1).to.be.true
const dupeRevisionOrError = await context.application.getRevision.execute({
itemUuid: dupe.uuid,
revisionUuid: dupeHistory[0].uuid,
})
@@ -358,54 +370,79 @@ describe.skip('history manager', () => {
})
it('revisions count matches original for duplicated items', async function () {
const note = await Factory.createSyncedNote(application)
const note = await context.createSyncedNote('test note')
await Factory.sleep(Factory.ServerRevisionFrequency)
await Factory.markDirtyAndSyncItem(application, note)
await Factory.markDirtyAndSyncItem(context.application, note)
await Factory.sleep(Factory.ServerRevisionFrequency)
await Factory.markDirtyAndSyncItem(application, note)
await Factory.markDirtyAndSyncItem(context.application, note)
await Factory.sleep(Factory.ServerRevisionFrequency)
await Factory.markDirtyAndSyncItem(application, note)
await Factory.markDirtyAndSyncItem(context.application, note)
const dupe = await application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(application, dupe)
const dupe = await context.application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(context.application, dupe)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
const expectedRevisions = 4
const noteHistoryOrError = await application.listRevisions.execute({ itemUuid: note.uuid })
const noteHistory = noteHistoryOrError.getValue()
const noteHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(noteHistoryOrError.isFailed()).to.equal(false)
let noteHistory = noteHistoryOrError.getValue()
if (noteHistory.length !== expectedRevisions) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'No revisions found on the server. This is likely a delay issue. Retrying...')
const dupeHistoryOrError = await application.listRevisions.execute({ itemUuid: dupe.uuid })
const dupeHistory = dupeHistoryOrError.getValue()
const noteHistoryOrError = await context.application.listRevisions.execute({ itemUuid: note.uuid })
expect(noteHistoryOrError.isFailed()).to.equal(false)
noteHistory = noteHistoryOrError.getValue()
}
expect(noteHistory.length).to.equal(expectedRevisions)
const dupeHistoryOrError = await context.application.listRevisions.execute({ itemUuid: dupe.uuid })
expect(dupeHistoryOrError.isFailed()).to.equal(false)
let dupeHistory = dupeHistoryOrError.getValue()
if (dupeHistory.length !== expectedRevisions + 1) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'No revisions found on the server. This is likely a delay issue. Retrying...')
const dupeHistoryOrError = await context.application.listRevisions.execute({ itemUuid: dupe.uuid })
expect(dupeHistoryOrError.isFailed()).to.equal(false)
dupeHistory = dupeHistoryOrError.getValue()
}
expect(dupeHistory.length).to.equal(expectedRevisions + 1)
}).timeout(Factory.SixtySecondTimeout)
it('can decrypt revisions for duplicate_of items', async function () {
const note = await Factory.createSyncedNote(application)
const note = await context.createSyncedNote('test note')
await Factory.sleep(Factory.ServerRevisionFrequency)
const changedText = `${Math.random()}`
await application.changeAndSaveItem.execute(note, (mutator) => {
mutator.title = changedText
})
await Factory.markDirtyAndSyncItem(application, note)
await context.changeNoteTitleAndSync(note, changedText)
await Factory.markDirtyAndSyncItem(context.application, note)
const dupe = await application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(application, dupe)
const dupe = await context.application.mutator.duplicateItem(note, true)
await Factory.markDirtyAndSyncItem(context.application, dupe)
await Factory.sleep(Factory.ServerRevisionCreationDelay)
const itemHistoryOrError = await application.listRevisions.execute({ itemUuid: dupe.uuid })
const itemHistory = itemHistoryOrError.getValue()
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: dupe.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
let itemHistory = itemHistoryOrError.getValue()
if (itemHistory.length <= 1) {
await Factory.sleep(Factory.ServerRevisionCreationDelay, 'No revisions found on the server. This is likely a delay issue. Retrying...')
const itemHistoryOrError = await context.application.listRevisions.execute({ itemUuid: dupe.uuid })
expect(itemHistoryOrError.isFailed()).to.equal(false)
itemHistory = itemHistoryOrError.getValue()
}
expect(itemHistory.length).to.be.above(1)
const newestRevision = itemHistory[0]
const fetchedOrError = await application.getRevision.execute({
const fetchedOrError = await context.application.getRevision.execute({
itemUuid: dupe.uuid,
revisionUuid: newestRevision.uuid,
})
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.202.75",
"version": "2.202.82",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -37,7 +37,7 @@
"@babel/preset-env": "*",
"@standardnotes/api": "workspace:*",
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/domain-events": "^2.122.0",
"@standardnotes/encryption": "workspace:*",
"@standardnotes/features": "workspace:*",
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.31.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.31.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.31.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.31.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/ui-services
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.31.14",
"version": "1.31.17",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -16,7 +16,7 @@
},
"dependencies": {
"@standardnotes/common": "^1.50.0",
"@standardnotes/domain-core": "^1.28.0",
"@standardnotes/domain-core": "^1.32.0",
"@standardnotes/features": "workspace:^",
"@standardnotes/filepicker": "workspace:^",
"@standardnotes/models": "workspace:^",
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.173.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
### Bug Fixes
* Fixed issue where using table or image option in Super mobile toolbar would make uninteractive ([ec61901](https://github.com/standardnotes/app/commit/ec61901aa5f0c08b7b77029e225e81c19decbd60))
## [3.173.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/web
## [3.173.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/web
## [3.173.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)
**Note:** Version bump only for package @standardnotes/web
## [3.173.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/web
## [3.173.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)
**Note:** Version bump only for package @standardnotes/web
## [3.173.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/web
## [3.173.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)
**Note:** Version bump only for package @standardnotes/web
# [3.173.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-16)
### Features
* Added table cell menu for adding/removing rows and columns to Super note tables and merging cells ([#2508](https://github.com/standardnotes/app/issues/2508)) ([896372f](https://github.com/standardnotes/app/commit/896372f04dfd2f302395077433e3ece32b251242))
## [3.172.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)
**Note:** Version bump only for package @standardnotes/web
+105
View File
@@ -1,5 +1,110 @@
{
"versions": [
{
"version": "3.173.8",
"title": "[3.173.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)",
"date": null,
"body": "### Bug Fixes\n\n* Fixed issue where using table or image option in Super mobile toolbar would make uninteractive ([ec61901](https://github.com/standardnotes/app/commit/ec61901aa5f0c08b7b77029e225e81c19decbd60))",
"parsed": {
"_": [
"Fixed issue where using table or image option in Super mobile toolbar would make uninteractive (ec61901)"
],
"Bug Fixes": [
"Fixed issue where using table or image option in Super mobile toolbar would make uninteractive (ec61901)"
]
}
},
{
"version": "3.173.7",
"title": "[3.173.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.6",
"title": "[3.173.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.5",
"title": "[3.173.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-20)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.4",
"title": "[3.173.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.3",
"title": "[3.173.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-19)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.2",
"title": "[3.173.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.1",
"title": "[3.173.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-18)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.173.0",
"title": "[3.173.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-16)",
"date": null,
"body": "### Features\n\n* Added table cell menu for adding/removing rows and columns to Super note tables and merging cells ([#2508](https://github.com/standardnotes/app/issues/2508)) ([896372f](https://github.com/standardnotes/app/commit/896372f04dfd2f302395077433e3ece32b251242))",
"parsed": {
"_": [
"Added table cell menu for adding/removing rows and columns to Super note tables and merging cells (#2508) (896372f)"
],
"Features": [
"Added table cell menu for adding/removing rows and columns to Super note tables and merging cells (#2508) (896372f)"
]
}
},
{
"version": "3.172.28",
"title": "[3.172.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-09-15)",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.172.28",
"version": "3.173.8",
"license": "CC BY-NC-SA 4.0",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -5,22 +5,23 @@ import { classNames } from '@standardnotes/utils'
import StyledTooltip from '../StyledTooltip/StyledTooltip'
type Props = {
onClick: () => void
onClick: MouseEventHandler
className?: string
icon: IconType
iconClassName?: string
iconProps?: Partial<Parameters<typeof Icon>[0]>
label: string
id?: string
} & ComponentPropsWithoutRef<'button'>
const RoundIconButton = forwardRef(
(
{ onClick, className, icon: iconType, iconClassName, id, label, ...props }: Props,
{ onClick, className, icon: iconType, iconClassName, iconProps, id, label, ...props }: Props,
ref: ForwardedRef<HTMLButtonElement>,
) => {
const click: MouseEventHandler = (e) => {
e.preventDefault()
onClick()
onClick(e)
}
return (
<StyledTooltip label={label}>
@@ -37,7 +38,7 @@ const RoundIconButton = forwardRef(
aria-label={label}
{...props}
>
<Icon type={iconType} className={iconClassName} />
<Icon {...iconProps} type={iconType} className={iconClassName} />
</button>
</StyledTooltip>
)
@@ -20,6 +20,7 @@ interface Props {
componentViewer: ComponentViewerInterface
requestReload?: (viewer: ComponentViewerInterface, force?: boolean) => void
onLoad?: () => void
readonly?: boolean
}
/**
@@ -30,7 +31,7 @@ const MaxLoadThreshold = 4000
const VisibilityChangeKey = 'visibilitychange'
const MSToWaitAfterIframeLoadToAvoidFlicker = 35
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload }) => {
const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer, requestReload, readonly = false }) => {
const application = useApplication()
const iframeRef = useRef<HTMLIFrameElement | null>(null)
@@ -50,7 +51,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
const reloadValidityStatus = useCallback(() => {
setFeatureStatus(componentViewer.getFeatureStatus())
if (!componentViewer.lockReadonly) {
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled)
componentViewer.setReadonly(featureStatus !== FeatureStatus.Entitled || readonly)
}
setIsComponentValid(componentViewer.shouldRender())
@@ -60,7 +61,7 @@ const IframeFeatureView: FunctionComponent<Props> = ({ onLoad, componentViewer,
setError(componentViewer.getError())
setDeprecationMessage(uiFeature.deprecationMessage)
}, [componentViewer, uiFeature, featureStatus, isComponentValid, isLoading])
}, [componentViewer, isLoading, isComponentValid, uiFeature.deprecationMessage, featureStatus, readonly])
useEffect(() => {
reloadValidityStatus()
@@ -330,9 +330,12 @@ const ContentTableView = ({ application, items }: Props) => {
setContextMenuItem(file)
},
rowActions: (item) => {
const vault = application.vaults.getItemVault(item)
const isReadonly = vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
return (
<div className="flex items-center gap-2">
<ItemLinksCell item={item} />
{!isReadonly && <ItemLinksCell item={item} />}
<ContextMenuCell items={[item]} />
</div>
)
@@ -69,6 +69,18 @@ const FileMenuOptions: FunctionComponent<Props> = ({
closeMenu()
}, [closeMenu, toggleAppPane])
const areSomeFilesInReadonlySharedVault = selectedFiles.some((file) => {
const vault = application.vaults.getItemVault(file)
return vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
})
const hasAdminPermissionForAllSharedFiles = selectedFiles.every((file) => {
const vault = application.vaults.getItemVault(file)
if (!vault?.isSharedVaultListing()) {
return true
}
return application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
})
if (selectedFiles.length === 0) {
return <div className="text-center">No files selected</div>
}
@@ -91,19 +103,25 @@ const FileMenuOptions: FunctionComponent<Props> = ({
</>
)}
{application.featuresController.isEntitledToVaults() && (
<AddToVaultMenuOption iconClassName={iconClass} items={selectedFiles} />
<AddToVaultMenuOption
iconClassName={iconClass}
items={selectedFiles}
disabled={!hasAdminPermissionForAllSharedFiles}
/>
)}
<AddTagOption
navigationController={application.navigationController}
linkingController={application.linkingController}
selectedItems={selectedFiles}
iconClassName={`text-neutral mr-2 ${MenuItemIconSize}`}
disabled={areSomeFilesInReadonlySharedVault}
/>
<MenuSwitchButtonItem
checked={hasProtectedFiles}
onChange={(hasProtectedFiles) => {
void application.filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles)
}}
disabled={areSomeFilesInReadonlySharedVault}
>
<Icon type="lock" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
Password protect
@@ -123,6 +141,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
onClick={() => {
renameToggleCallback?.(true)
}}
disabled={areSomeFilesInReadonlySharedVault}
>
<Icon type="pencil" className={`mr-2 text-neutral ${MenuItemIconSize}`} />
Rename
@@ -133,6 +152,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
closeMenuAndToggleFilesList()
void application.filesController.deleteFilesPermanently(selectedFiles)
}}
disabled={areSomeFilesInReadonlySharedVault}
>
<Icon type="trash" className={`mr-2 text-danger ${MenuItemIconSize}`} />
<span className="text-danger">Delete permanently</span>
@@ -113,6 +113,9 @@ const FilePreviewModal = observer(({ application }: Props) => {
return null
}
const vault = application.vaults.getItemVault(currentFile)
const isReadonly = vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
return (
<Modal
title={currentFile.name}
@@ -181,15 +184,17 @@ const FilePreviewModal = observer(({ application }: Props) => {
)}
</div>
<div className="flex items-center">
<StyledTooltip label="Rename file" className="!z-modal">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
aria-label="Rename file"
>
<Icon type="pencil-filled" className="text-neutral" />
</button>
</StyledTooltip>
{!isReadonly && (
<StyledTooltip label="Rename file" className="!z-modal">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
aria-label="Rename file"
>
<Icon type="pencil-filled" className="text-neutral" />
</button>
</StyledTooltip>
)}
<StyledTooltip label="Show linked items" className="!z-modal">
<button
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
@@ -249,7 +254,11 @@ const FilePreviewModal = observer(({ application }: Props) => {
</div>
{showLinkedBubblesContainer && (
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border px-3.5 py-1.5">
<LinkedItemBubblesContainer linkingController={application.linkingController} item={currentFile} />
<LinkedItemBubblesContainer
linkingController={application.linkingController}
item={currentFile}
readonly={isReadonly}
/>
</div>
)}
<div className="flex min-h-0 flex-grow flex-col-reverse md:flex-row">
@@ -11,11 +11,35 @@ import Popover from '../Popover/Popover'
import FilePreviewInfoPanel from '../FilePreview/FilePreviewInfoPanel'
import { useFileDragNDrop } from '../FileDragNDropProvider'
import RoundIconButton from '../Button/RoundIconButton'
import { useItemVaultInfo } from '@/Hooks/useItemVaultInfo'
import Icon from '../Icon/Icon'
import { VaultUserServiceEvent } from '@standardnotes/snjs'
const SyncTimeoutNoDebounceMs = 100
const SyncTimeoutDebounceMs = 350
const FileViewWithoutProtection = ({ application, file }: FileViewProps) => {
const { vault } = useItemVaultInfo(file)
const [isReadonly, setIsReadonly] = useState(false)
useEffect(() => {
if (!vault) {
return
}
setIsReadonly(application.vaultUsers.isCurrentUserReadonlyVaultMember(vault))
}, [application.vaultUsers, vault])
useEffect(() => {
return application.vaultUsers.addEventObserver((event, data) => {
if (event === VaultUserServiceEvent.InvalidatedUserCacheForVault) {
if ((data as string) !== vault?.sharing?.sharedVaultUuid) {
return
}
setIsReadonly(vault ? application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) : false)
}
})
}, [application.vaultUsers, vault])
const syncTimeoutRef = useRef<number>()
const fileInfoButtonRef = useRef<HTMLButtonElement>(null)
@@ -67,6 +91,12 @@ const FileViewWithoutProtection = ({ application, file }: FileViewProps) => {
return (
<div className="sn-component section editor" aria-label="File" ref={fileDragTargetRef}>
<div className="flex flex-col">
{isReadonly && (
<div className="bg-warning-faded relative flex items-center px-3.5 py-2 text-sm text-accessory-tint-3">
<Icon type="pencil-off" className="mr-3" />
This file is readonly
</div>
)}
<div
className="content-title-bar section-title-bar section-title-bar z-editor-title-bar w-full"
id="file-title-bar"
@@ -85,11 +115,12 @@ const FileViewWithoutProtection = ({ application, file }: FileViewProps) => {
spellCheck={false}
defaultValue={file.name}
autoComplete="off"
disabled={isReadonly}
/>
</div>
</div>
<div className="flex items-center gap-3">
<LinkedItemsButton linkingController={application.linkingController} />
{!isReadonly && <LinkedItemsButton linkingController={application.linkingController} />}
<RoundIconButton
label="File information panel"
onClick={toggleFileInfoPanel}
@@ -110,7 +141,11 @@ const FileViewWithoutProtection = ({ application, file }: FileViewProps) => {
</div>
</div>
<div className="hidden md:flex">
<LinkedItemBubblesContainer item={file} linkingController={application.linkingController} />
<LinkedItemBubblesContainer
item={file}
linkingController={application.linkingController}
readonly={isReadonly}
/>
</div>
</div>
</div>
@@ -166,7 +166,7 @@ const LinkedItemBubblesContainer = ({
const { vault, lastEditedByContact } = useItemVaultInfo(item)
if (readonly && itemsToDisplay.length === 0) {
if (readonly && itemsToDisplay.length === 0 && !vault) {
return null
}
@@ -41,7 +41,7 @@ const MenuItem = forwardRef(
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
className={classNames(
'flex w-full cursor-pointer select-none border-0 bg-transparent px-3 py-2 text-left md:py-1.5',
'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground',
'text-mobile-menu-item text-text enabled:hover:bg-contrast enabled:hover:text-foreground',
'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item',
'disabled:cursor-not-allowed disabled:opacity-60',
className,
@@ -37,8 +37,9 @@ const MenuSwitchButtonItem = forwardRef(
ref={ref}
className={classNames(
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5',
'text-left text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none',
'text-left text-text focus:bg-info-backdrop focus:shadow-none enabled:hover:bg-contrast enabled:hover:text-foreground',
'text-mobile-menu-item md:text-tablet-menu-item lg:text-menu-item',
'disabled:cursor-not-allowed disabled:opacity-60',
className,
)}
onClick={() => {
@@ -10,6 +10,7 @@ import {
SNNote,
NoteType,
PayloadEmitSource,
VaultServiceInterface,
} from '@standardnotes/snjs'
import NoteView from './NoteView'
import { NoteViewController } from './Controller/NoteViewController'
@@ -19,6 +20,7 @@ describe('NoteView', () => {
let application: WebApplication
let notesController: NotesController
let vaults: VaultServiceInterface
const createNoteView = () =>
new NoteView({
@@ -36,9 +38,13 @@ describe('NoteView', () => {
notesController.getSpellcheckStateForNote = jest.fn()
notesController.getEditorWidthForNote = jest.fn()
vaults = {} as jest.Mocked<VaultServiceInterface>
vaults.getItemVault = jest.fn().mockReturnValue(undefined)
application = {
notesController,
noteViewController,
vaults,
} as unknown as jest.Mocked<WebApplication>
application.hasProtectionSources = jest.fn().mockReturnValue(true)
@@ -26,6 +26,7 @@ import {
PrefKey,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
SNNote,
VaultUserServiceEvent,
} from '@standardnotes/snjs'
import { confirmDialog, DELETE_NOTE_KEYBOARD_COMMAND, KeyboardKey } from '@standardnotes/ui-services'
import { ChangeEventHandler, createRef, CSSProperties, KeyboardEventHandler, RefObject } from 'react'
@@ -68,6 +69,7 @@ type State = {
isDesktop?: boolean
editorLineWidth: EditorLineWidth
noteLocked: boolean
readonly: boolean
noteStatus?: NoteStatus
saveError?: boolean
showProtectedWarning: boolean
@@ -96,6 +98,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
private removeNoteStreamObserver?: () => void
private removeComponentManagerObserver?: () => void
private removeInnerNoteObserver?: () => void
private removeVaultUsersEventHandler?: () => void
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null
private noteViewElementRef: RefObject<HTMLDivElement>
@@ -116,6 +119,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.debounceReloadEditorComponent = debounce(this.debounceReloadEditorComponent.bind(this), 25)
const itemVault = this.application.vaults.getItemVault(this.controller.item)
this.state = {
availableStackComponents: [],
editorStateDidLoad: false,
@@ -124,6 +129,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
isDesktop: isDesktopApplication(),
noteStatus: undefined,
noteLocked: this.controller.item.locked,
readonly: itemVault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(itemVault) : false,
showProtectedWarning: false,
spellcheck: true,
stackComponentViewers: [],
@@ -154,6 +160,9 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
this.removeTrashKeyObserver?.()
this.removeTrashKeyObserver = undefined
this.removeVaultUsersEventHandler?.()
this.removeVaultUsersEventHandler = undefined
this.clearNoteProtectionInactivityTimer()
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
@@ -207,6 +216,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
override componentDidMount(): void {
super.componentDidMount()
this.removeVaultUsersEventHandler = this.application.vaultUsers.addEventObserver((event, data) => {
if (event === VaultUserServiceEvent.InvalidatedUserCacheForVault) {
const vault = this.application.vaults.getItemVault(this.note)
if ((data as string) !== vault?.sharing?.sharedVaultUuid) {
return
}
this.setState({
readonly: vault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) : false,
})
}
})
this.registerKeyboardShortcuts()
this.removeInnerNoteObserver = this.controller.addNoteInnerValueChangeObserver((note, source) => {
@@ -855,6 +876,13 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
/>
)}
{this.state.readonly && (
<div className="bg-warning-faded flex items-center px-3.5 py-2 text-sm text-accessory-tint-3">
<Icon type="pencil-off" className="mr-3" />
You don't have permission to edit this note
</div>
)}
{this.state.noteLocked && (
<EditingDisabledBanner
onClick={() => this.application.notesController.setLockSelectedNotes(!this.state.noteLocked)}
@@ -878,7 +906,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<div className="title flex-grow overflow-auto">
<input
className="input text-lg"
disabled={this.state.noteLocked}
disabled={this.state.noteLocked || this.state.readonly}
id={ElementIds.NoteTitleEditor}
onChange={this.onTitleChange}
onFocus={(event) => {
@@ -911,18 +939,22 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
)}
{renderHeaderOptions && (
<div className="note-view-options-buttons flex items-center gap-3">
<LinkedItemsButton
linkingController={this.application.linkingController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<ChangeEditorButton
noteViewController={this.controller}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<PinNoteButton
notesController={this.application.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
{!this.state.readonly && (
<>
<LinkedItemsButton
linkingController={this.application.linkingController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<ChangeEditorButton
noteViewController={this.controller}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
<PinNoteButton
notesController={this.application.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/>
</>
)}
<NotesOptionsPanel
notesController={this.application.notesController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
@@ -934,7 +966,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<CollaborationInfoHUD item={this.note} />
</div>
<div className="hidden md:block">
<LinkedItemBubblesContainer item={this.note} linkingController={this.application.linkingController} />
<LinkedItemBubblesContainer
item={this.note}
linkingController={this.application.linkingController}
readonly={this.state.readonly}
/>
</div>
</div>
)}
@@ -961,6 +997,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
componentViewer={this.state.editorComponentViewer}
onLoad={this.onEditorComponentLoad}
requestReload={this.editorComponentViewerRequestsReload}
readonly={this.state.readonly}
/>
</div>
)}
@@ -971,7 +1008,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
spellcheck={this.state.spellcheck}
ref={this.setPlainEditorRef}
controller={this.controller}
locked={this.state.noteLocked}
locked={this.state.noteLocked || this.state.readonly}
onFocus={this.onPlainFocus}
onBlur={this.onPlainBlur}
/>
@@ -986,6 +1023,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
filesController={this.application.filesController}
spellcheck={this.state.spellcheck}
controller={this.controller}
readonly={this.state.readonly}
/>
</div>
)}
@@ -16,6 +16,7 @@ type Props = {
linkingController: LinkingController
selectedItems: DecryptedItemInterface[]
iconClassName: string
disabled?: boolean
}
const AddTagOption: FunctionComponent<Props> = ({
@@ -23,6 +24,7 @@ const AddTagOption: FunctionComponent<Props> = ({
linkingController,
selectedItems,
iconClassName,
disabled,
}) => {
const application = useApplication()
const menuContainerRef = useRef<HTMLDivElement>(null)
@@ -57,6 +59,7 @@ const AddTagOption: FunctionComponent<Props> = ({
}
}}
ref={buttonRef}
disabled={disabled}
>
<div className="flex items-center">
<Icon type="hashtag" className={iconClassName} />
@@ -12,9 +12,15 @@ type ChangeEditorOptionProps = {
application: WebApplication
note: SNNote
iconClassName: string
disabled?: boolean
}
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ application, note, iconClassName }) => {
const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({
application,
note,
iconClassName,
disabled,
}) => {
const [isOpen, setIsOpen] = useState(false)
const menuContainerRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -38,6 +44,7 @@ const ChangeEditorOption: FunctionComponent<ChangeEditorOptionProps> = ({ applic
setIsOpen(false)
}
}}
disabled={disabled}
ref={buttonRef}
>
<div className="flex items-center">
@@ -1,13 +0,0 @@
import Icon from '@/Components/Icon/Icon'
import MenuItem from '../Menu/MenuItem'
type DeletePermanentlyButtonProps = {
onClick: () => void
}
export const DeletePermanentlyButton = ({ onClick }: DeletePermanentlyButtonProps) => (
<MenuItem onClick={onClick}>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
)
@@ -30,7 +30,6 @@ import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/Keyboard
import { NoteAttributes } from './NoteAttributes'
import { SpellcheckOptions } from './SpellcheckOptions'
import { NoteSizeWarning } from './NoteSizeWarning'
import { DeletePermanentlyButton } from './DeletePermanentlyButton'
import { useCommandService } from '../CommandProvider'
import { iconClass } from './ClassNames'
import SuperNoteOptions from './SuperNoteOptions'
@@ -209,6 +208,17 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
}
const areSomeNotesInSharedVault = notes.some((note) => application.vaults.getItemVault(note)?.isSharedVaultListing())
const areSomeNotesInReadonlySharedVault = notes.some((note) => {
const vault = application.vaults.getItemVault(note)
return vault?.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)
})
const hasAdminPermissionForAllSharedNotes = notes.every((note) => {
const vault = application.vaults.getItemVault(note)
if (!vault?.isSharedVaultListing()) {
return true
}
return application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
})
if (notes.length === 0) {
return null
@@ -220,13 +230,13 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
<>
{notes.length === 1 && (
<>
<MenuItem onClick={openRevisionHistoryModal}>
<MenuItem onClick={openRevisionHistoryModal} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="history" className={iconClass} />
Note history
{historyShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={historyShortcut} />}
</MenuItem>
<HorizontalSeparator classes="my-2" />
<MenuItem onClick={toggleLineWidthModal}>
<MenuItem onClick={toggleLineWidthModal} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="line-width" className={iconClass} />
Editor width
{editorWidthShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={editorWidthShortcut} />}
@@ -238,6 +248,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onChange={(locked) => {
application.notesController.setLockSelectedNotes(locked)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pencil-off" className={iconClass} />
Prevent editing
@@ -247,6 +258,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onChange={(hidePreviews) => {
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="rich-text" className={iconClass} />
Show preview
@@ -256,6 +268,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onChange={(protect) => {
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="lock" className={iconClass} />
Password protect
@@ -263,13 +276,18 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
{notes.length === 1 && (
<>
<HorizontalSeparator classes="my-2" />
<ChangeEditorOption iconClassName={iconClass} application={application} note={notes[0]} />
<ChangeEditorOption
iconClassName={iconClass}
application={application}
note={notes[0]}
disabled={areSomeNotesInReadonlySharedVault}
/>
</>
)}
<HorizontalSeparator classes="my-2" />
{application.featuresController.isEntitledToVaults() && (
<AddToVaultMenuOption iconClassName={iconClass} items={notes} />
<AddToVaultMenuOption iconClassName={iconClass} items={notes} disabled={!hasAdminPermissionForAllSharedNotes} />
)}
{application.navigationController.tagsCount > 0 && (
@@ -278,12 +296,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
navigationController={application.navigationController}
selectedItems={notes}
linkingController={application.linkingController}
disabled={areSomeNotesInReadonlySharedVault}
/>
)}
<MenuItem
onClick={() => {
application.notesController.setStarSelectedNotes(!starred)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="star" className={iconClass} />
{starred ? 'Unstar' : 'Star'}
@@ -295,6 +315,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onClick={() => {
application.notesController.setPinSelectedNotes(true)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="pin" className={iconClass} />
Pin to top
@@ -306,6 +327,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
onClick={() => {
application.notesController.setPinSelectedNotes(false)
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unpin" className={iconClass} />
Unpin
@@ -382,7 +404,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
)}
</>
)}
<MenuItem onClick={duplicateSelectedItems}>
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
<Icon type="copy" className={iconClass} />
Duplicate
</MenuItem>
@@ -392,6 +414,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="archive" className={iconClassWarning} />
<span className="text-warning">Archive</span>
@@ -403,6 +426,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="unarchive" className={iconClassWarning} />
<span className="text-warning">Unarchive</span>
@@ -410,18 +434,23 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
)}
{notTrashed &&
(altKeyDown ? (
<DeletePermanentlyButton
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
/>
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
) : (
<MenuItem
onClick={async () => {
await application.notesController.setTrashSelectedNotes(true)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="trash" className={iconClassDanger} />
<span className="text-danger">Move to trash</span>
@@ -434,21 +463,27 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
await application.notesController.setTrashSelectedNotes(false)
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<Icon type="restore" className={iconClassSuccess} />
<span className="text-success">Restore</span>
</MenuItem>
<DeletePermanentlyButton
<MenuItem
disabled={areSomeNotesInReadonlySharedVault}
onClick={async () => {
await application.notesController.deleteNotesPermanently()
closeMenuAndToggleNotesList()
}}
/>
>
<Icon type="close" className="mr-2 text-danger" />
<span className="text-danger">Delete permanently</span>
</MenuItem>
<MenuItem
onClick={async () => {
await application.notesController.emptyTrash()
closeMenuAndToggleNotesList()
}}
disabled={areSomeNotesInReadonlySharedVault}
>
<div className="flex items-start">
<Icon type="trash-sweep" className="mr-2 text-danger" />
@@ -485,16 +520,19 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
editorForNote={editorForNote}
notesController={application.notesController}
note={notes[0]}
disabled={areSomeNotesInReadonlySharedVault}
/>
)}
<HorizontalSeparator classes="my-2" />
<NoteAttributes application={application} note={notes[0]} />
<NoteAttributes className="mb-2" application={application} note={notes[0]} />
<NoteSizeWarning note={notes[0]} />
</>
) : null}
) : (
<div className="h-2" />
)}
<ModalOverlay isOpen={showExportSuperModal} close={closeSuperExportModal}>
<SuperExportModal exportNotes={downloadSelectedItems} close={closeSuperExportModal} />
@@ -9,7 +9,8 @@ export const SpellcheckOptions: FunctionComponent<{
editorForNote: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
notesController: NotesController
note: SNNote
}> = ({ editorForNote, notesController, note }) => {
disabled?: boolean
}> = ({ editorForNote, notesController, note, disabled }) => {
const spellcheckControllable = editorForNote.featureDescription.spellcheckControl
const noteSpellcheck = !spellcheckControllable
? true
@@ -24,7 +25,7 @@ export const SpellcheckOptions: FunctionComponent<{
onChange={() => {
notesController.toggleGlobalSpellcheckForNote(note).catch(console.error)
}}
disabled={!spellcheckControllable}
disabled={disabled || !spellcheckControllable}
>
<Icon type="notes" className={iconClass} />
Spellcheck
@@ -130,7 +130,7 @@ const ContactInviteModal: FunctionComponent<Props> = ({ vault, onCloseDialog })
wrapper: 'col-start-2',
}}
items={Object.keys(SharedVaultUserPermission.PERMISSIONS).map((key) => ({
label: key === 'Write' ? 'Read / Write' : key,
label: application.vaultUsers.getFormattedMemberPermission(key.toLowerCase()),
value: key,
}))}
value={selectedContact.permission}
@@ -2,11 +2,10 @@ import { useApplication } from '@/Components/ApplicationProvider'
import Button from '@/Components/Button/Button'
import Icon from '@/Components/Icon/Icon'
import ModalOverlay from '@/Components/Modal/ModalOverlay'
import { InviteRecord, SharedVaultUserPermission } from '@standardnotes/snjs'
import { InviteRecord } from '@standardnotes/snjs'
import { useCallback, useState } from 'react'
import EditContactModal from '../Contacts/EditContactModal'
import { CheckmarkCircle } from '../../../../UIElements/CheckmarkCircle'
import { capitalizeString } from '@/Utils'
type Props = {
inviteRecord: InviteRecord
@@ -36,10 +35,7 @@ const InviteItem = ({ inviteRecord }: Props) => {
const trustedContact = application.contacts.findSenderContactForInvite(inviteRecord.invite)
const permission =
inviteRecord.invite.permission === SharedVaultUserPermission.PERMISSIONS.Write
? 'Read / Write'
: capitalizeString(inviteRecord.invite.permission)
const permission = application.vaultUsers.getFormattedMemberPermission(inviteRecord.invite.permission)
return (
<>
@@ -15,6 +15,8 @@ import {
SharedVaultServiceEvent,
VaultUserServiceEvent,
RoleName,
ProtocolVersion,
compareVersions,
} from '@standardnotes/snjs'
import VaultItem from './Vaults/VaultItem'
import Button from '@/Components/Button/Button'
@@ -24,7 +26,7 @@ import PreferencesPane from '../../PreferencesComponents/PreferencesPane'
import { ToastType, addToast } from '@standardnotes/toast'
import NoProSubscription from '../Account/NoProSubscription'
const Vaults = () => {
const Vaults = observer(() => {
const application = useApplication()
const hasAccount = application.hasAccount()
@@ -136,13 +138,11 @@ const Vaults = () => {
<ModalOverlay isOpen={isAddContactModalOpen} close={closeAddContactModal}>
<EditContactModal onCloseDialog={closeAddContactModal} />
</ModalOverlay>
<EditVaultModal
isVaultModalOpen={isVaultModalOpen}
creatingSharedVault={isCreatingSharedVault}
closeVaultModal={closeVaultModal}
/>
{invites.length > 0 && (
<PreferencesGroup>
<PreferencesSegment>
@@ -155,7 +155,6 @@ const Vaults = () => {
</PreferencesSegment>
</PreferencesGroup>
)}
{hasAccount && (
<PreferencesGroup>
<PreferencesSegment>
@@ -173,33 +172,6 @@ const Vaults = () => {
</PreferencesSegment>
</PreferencesGroup>
)}
<PreferencesGroup>
<PreferencesSegment>
<Title>Vaults</Title>
{vaults.length > 0 && (
<div className="my-2 flex flex-col gap-3.5">
{vaults.map((vault) => {
return <VaultItem vault={vault} key={vault.uuid} />
})}
</div>
)}
{canCreateMoreVaults ? (
<div className="mt-2.5 flex gap-3">
<Button label="Create Vault" onClick={createNewVault} />
<Button label="Create Shared Vault" onClick={createNewSharedVault} />
</div>
) : (
<div className="mt-3.5">
<NoProSubscription
application={application}
text={<span>Please upgrade in order to increase your shared vault limit.</span>}
/>
</div>
)}
</PreferencesSegment>
</PreferencesGroup>
{hasAccount && (
<PreferencesGroup>
<PreferencesSegment>
@@ -242,8 +214,59 @@ const Vaults = () => {
</PreferencesSegment>
</PreferencesGroup>
)}
<PreferencesGroup>
<PreferencesSegment>
<Title>Vaults</Title>
{vaults.length > 0 && (
<div className="my-2 flex flex-col gap-3.5">
{vaults.map((vault) => {
return <VaultItem vault={vault} key={vault.uuid} />
})}
</div>
)}
{canCreateMoreVaults ? (
<div className="mt-2.5 flex gap-3">
<Button label="Create Vault" onClick={createNewVault} />
{hasAccount && <Button label="Create Shared Vault" onClick={createNewSharedVault} />}
</div>
) : (
<div className="mt-3.5">
<NoProSubscription
application={application}
text={<span>Please upgrade in order to increase your shared vault limit.</span>}
/>
</div>
)}
</PreferencesSegment>
</PreferencesGroup>
</PreferencesPane>
)
})
const VaultsWrapper = () => {
const application = useApplication()
const hasAccount = application.hasAccount()
const accountProtocolVersion = application.getUserVersion()
const isAccountProtocolNotSupported =
accountProtocolVersion && compareVersions(accountProtocolVersion, ProtocolVersion.V004) < 0
if (hasAccount && isAccountProtocolNotSupported) {
return (
<PreferencesPane>
<PreferencesGroup>
<Title>Account update required</Title>
<Subtitle>
In order to use Vaults, you must update your account to use the latest data encryption version.
</Subtitle>
<Button primary className="mt-3" onClick={() => application.upgradeProtocolVersion().catch(console.error)}>
Update Account
</Button>
</PreferencesGroup>
</PreferencesPane>
)
}
return <Vaults />
}
export default observer(Vaults)
export default observer(VaultsWrapper)
@@ -23,7 +23,8 @@ const VaultItem = ({ vault }: Props) => {
const [isVaultModalOpen, setIsVaultModalOpen] = useState(false)
const closeVaultModal = () => setIsVaultModalOpen(false)
const { isCurrentUserAdmin, isLocked, canShowLockOption, toggleLock, ensureVaultIsUnlocked } = useVault(vault)
const { isCurrentUserAdmin, isCurrentUserOwner, isLocked, canShowLockOption, toggleLock, ensureVaultIsUnlocked } =
useVault(vault)
const deleteVault = useCallback(async () => {
const confirm = await application.alerts.confirm(
@@ -122,8 +123,8 @@ const VaultItem = ({ vault }: Props) => {
<div className="mt-2 flex w-full flex-wrap gap-3">
<Button label="Edit" onClick={openEditModal} />
{canShowLockOption && <Button label={isLocked ? 'Unlock' : 'Lock'} onClick={toggleLock} />}
{isCurrentUserAdmin && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
{!isCurrentUserAdmin && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
{isCurrentUserOwner && <Button colorStyle="danger" label="Delete" onClick={deleteVault} />}
{!isCurrentUserOwner && vault.isSharedVaultListing() && <Button label="Leave Vault" onClick={leaveVault} />}
{isCurrentUserAdmin ? (
vault.isSharedVaultListing() ? (
<Button colorStyle="info" label="Invite Contacts" onClick={openInviteModal} />
@@ -66,11 +66,11 @@ const EditVaultModalContent: FunctionComponent<{
if (existingVault.isSharedVaultListing()) {
setIsAdmin(
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultOwner(existingVault),
existingVault.isSharedVaultListing() && application.vaultUsers.isCurrentUserSharedVaultAdmin(existingVault),
)
setIsLoadingCollaborationInfo(true)
const users = await application.vaultUsers.getSharedVaultUsers(existingVault)
const users = await application.vaultUsers.getSharedVaultUsersFromServer(existingVault)
if (users) {
setMembers(users)
}
@@ -1,9 +1,8 @@
import { useCallback } from 'react'
import { useApplication } from '@/Components/ApplicationProvider'
import { SharedVaultInviteServerHash, SharedVaultUserPermission } from '@standardnotes/snjs'
import { SharedVaultInviteServerHash } from '@standardnotes/snjs'
import Icon from '@/Components/Icon/Icon'
import Button from '@/Components/Button/Button'
import { capitalizeString } from '@/Utils'
export const VaultModalInvites = ({
invites,
@@ -30,10 +29,7 @@ export const VaultModalInvites = ({
<div className="space-y-3.5">
{invites.map((invite) => {
const contact = application.contacts.findContactForInvite(invite)
const permission =
invite.permission === SharedVaultUserPermission.PERMISSIONS.Write
? 'Read / Write'
: capitalizeString(invite.permission)
const permission = application.vaultUsers.getFormattedMemberPermission(invite.permission)
return (
<div
@@ -1,9 +1,8 @@
import { useCallback } from 'react'
import { useApplication } from '@/Components/ApplicationProvider'
import { SharedVaultUserPermission, SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs'
import { SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs'
import Icon from '@/Components/Icon/Icon'
import Button from '@/Components/Button/Button'
import { capitalizeString } from '@/Utils'
export const VaultModalMembers = ({
members,
@@ -35,10 +34,7 @@ export const VaultModalMembers = ({
{members.map((member) => {
const isMemberVaultOwner = application.vaultUsers.isVaultUserOwner(member)
const contact = application.contacts.findContactForServerUser(member)
const permission =
member.permission === SharedVaultUserPermission.PERMISSIONS.Write
? 'Read / Write'
: capitalizeString(member.permission)
const permission = application.vaultUsers.getFormattedMemberPermission(member.permission)
return (
<div
@@ -26,6 +26,7 @@ import { SuperEditorContentId } from './Constants'
import { classNames } from '@standardnotes/utils'
import { MarkdownTransformers } from './MarkdownTransformers'
import { RemoveBrokenTablesPlugin } from './Plugins/TablePlugin'
import TableActionMenuPlugin from './Plugins/TableCellActionMenuPlugin'
type BlocksEditorProps = {
onChange?: (value: string, preview: string) => void
@@ -92,7 +93,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
/>
<ListPlugin />
<MarkdownShortcutPlugin transformers={MarkdownTransformers} />
<TablePlugin />
<TablePlugin hasCellMerge />
<OnChangePlugin onChange={handleChange} ignoreSelectionChange={true} />
<HistoryPlugin />
<HorizontalRulePlugin />
@@ -111,6 +112,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
<TableActionMenuPlugin anchorElem={floatingAnchorElem} cellMerge />
</>
)}
{children}
@@ -291,7 +291,9 @@ body {
z-index: 10;
display: block;
position: fixed;
box-shadow: 0 12px 28px 0 rgba(0, 0, 0, 0.2), 0 2px 4px 0 rgba(0, 0, 0, 0.1),
box-shadow:
0 12px 28px 0 rgba(0, 0, 0, 0.2),
0 2px 4px 0 rgba(0, 0, 0, 0.1),
inset 0 0 0 1px rgba(255, 255, 255, 0.5);
border-radius: 8px;
min-height: 40px;
@@ -657,15 +659,6 @@ body {
cursor: pointer;
}
i.chevron-down {
background-color: transparent;
background-size: contain;
display: inline-block;
height: 8px;
width: 8px;
background-image: url(#{$blocks-editor-icons-path}/chevron-down.svg);
}
.action-button {
background-color: #eee;
border: 0;
@@ -860,24 +853,6 @@ body {
background-size: contain;
}
.toolbar i.chevron-down {
margin-top: 3px;
width: 16px;
height: 16px;
display: flex;
user-select: none;
}
.toolbar i.chevron-down.inside {
width: 16px;
height: 16px;
display: flex;
margin-left: -25px;
margin-top: 11px;
margin-right: 10px;
pointer-events: none;
}
.toolbar .divider {
width: 1px;
background-color: #eee;
@@ -25,6 +25,11 @@ const CodeOptionsPlugin = () => {
const [selectedElementKey, setSelectedElementKey] = useState<NodeKey | null>(null)
const updateToolbar = useCallback(() => {
if (!editor.isEditable()) {
setIsCode(false)
return
}
const selection = $getSelection()
if (!$isRangeSelection(selection)) {
return
@@ -0,0 +1,600 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import {
DEPRECATED_GridCellNode,
ElementNode,
$createParagraphNode,
$getRoot,
$getSelection,
$isElementNode,
$isParagraphNode,
$isRangeSelection,
$isTextNode,
DEPRECATED_$getNodeTriplet,
DEPRECATED_$isGridCellNode,
DEPRECATED_$isGridSelection,
GridSelection,
} from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import useLexicalEditable from '@lexical/react/useLexicalEditable'
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$getTableCellNodeFromLexicalNode,
$getTableColumnIndexFromTableCellNode,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$isTableCellNode,
$isTableRowNode,
$unmergeCell,
getTableSelectionFromTableElement,
HTMLTableElementWithWithTableSelectionState,
TableCellHeaderStates,
TableCellNode,
} from '@lexical/table'
import { ReactPortal, useCallback, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import invariant from '../../Lexical/Shared/invariant'
import Popover from '@/Components/Popover/Popover'
import RoundIconButton from '@/Components/Button/RoundIconButton'
import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem'
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
function computeSelectionCount(selection: GridSelection): {
columns: number
rows: number
} {
const selectionShape = selection.getShape()
return {
columns: selectionShape.toX - selectionShape.fromX + 1,
rows: selectionShape.toY - selectionShape.fromY + 1,
}
}
// This is important when merging cells as there is no good way to re-merge weird shapes (a result
// of selecting merged cells and non-merged)
function isGridSelectionRectangular(selection: GridSelection): boolean {
const nodes = selection.getNodes()
const currentRows: Array<number> = []
let currentRow = null
let expectedColumns = null
let currentColumns = 0
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if ($isTableCellNode(node)) {
const row = node.getParentOrThrow()
invariant($isTableRowNode(row), 'Expected CellNode to have a RowNode parent')
if (currentRow !== row) {
if (expectedColumns !== null && currentColumns !== expectedColumns) {
return false
}
if (currentRow !== null) {
expectedColumns = currentColumns
}
currentRow = row
currentColumns = 0
}
const colSpan = node.__colSpan
for (let j = 0; j < colSpan; j++) {
if (currentRows[currentColumns + j] === undefined) {
currentRows[currentColumns + j] = 0
}
currentRows[currentColumns + j] += node.__rowSpan
}
currentColumns += colSpan
}
}
return (
(expectedColumns === null || currentColumns === expectedColumns) && currentRows.every((v) => v === currentRows[0])
)
}
function $canUnmerge(): boolean {
const selection = $getSelection()
if (
($isRangeSelection(selection) && !selection.isCollapsed()) ||
(DEPRECATED_$isGridSelection(selection) && !selection.anchor.is(selection.focus)) ||
(!$isRangeSelection(selection) && !DEPRECATED_$isGridSelection(selection))
) {
return false
}
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor)
return cell.__colSpan > 1 || cell.__rowSpan > 1
}
function $cellContainsEmptyParagraph(cell: DEPRECATED_GridCellNode): boolean {
if (cell.getChildrenSize() !== 1) {
return false
}
const firstChild = cell.getFirstChildOrThrow()
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
return false
}
return true
}
function $selectLastDescendant(node: ElementNode): void {
const lastDescendant = node.getLastDescendant()
if ($isTextNode(lastDescendant)) {
lastDescendant.select()
} else if ($isElementNode(lastDescendant)) {
lastDescendant.selectEnd()
} else if (lastDescendant !== null) {
lastDescendant.selectNext()
}
}
type TableCellActionMenuProps = Readonly<{
onClose: () => void
tableCellNode: TableCellNode
cellMerge: boolean
}>
function TableActionMenu({ onClose, tableCellNode: _tableCellNode, cellMerge }: TableCellActionMenuProps) {
const [editor] = useLexicalComposerContext()
const dropDownRef = useRef<HTMLDivElement | null>(null)
const [tableCellNode, updateTableCellNode] = useState(_tableCellNode)
const [selectionCounts, updateSelectionCounts] = useState({
columns: 1,
rows: 1,
})
const [canMergeCells, setCanMergeCells] = useState(false)
const [canUnmergeCell, setCanUnmergeCell] = useState(false)
useEffect(() => {
return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
const nodeUpdated = nodeMutations.get(tableCellNode.getKey()) === 'updated'
if (nodeUpdated) {
editor.getEditorState().read(() => {
updateTableCellNode(tableCellNode.getLatest())
})
}
})
}, [editor, tableCellNode])
useEffect(() => {
editor.getEditorState().read(() => {
const selection = $getSelection()
// Merge cells
if (DEPRECATED_$isGridSelection(selection)) {
const currentSelectionCounts = computeSelectionCount(selection)
updateSelectionCounts(computeSelectionCount(selection))
setCanMergeCells(
isGridSelectionRectangular(selection) &&
(currentSelectionCounts.columns > 1 || currentSelectionCounts.rows > 1),
)
}
// Unmerge cell
setCanUnmergeCell($canUnmerge())
})
}, [editor])
const clearTableSelection = useCallback(() => {
editor.update(() => {
if (tableCellNode.isAttached()) {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode)
const tableElement = editor.getElementByKey(tableNode.getKey()) as HTMLTableElementWithWithTableSelectionState
if (!tableElement) {
throw new Error('Expected to find tableElement in DOM')
}
const tableSelection = getTableSelectionFromTableElement(tableElement)
if (tableSelection !== null) {
tableSelection.clearHighlight()
}
tableNode.markDirty()
updateTableCellNode(tableCellNode.getLatest())
}
const rootNode = $getRoot()
rootNode.selectStart()
})
}, [editor, tableCellNode])
const mergeTableCellsAtSelection = () => {
editor.update(() => {
const selection = $getSelection()
if (DEPRECATED_$isGridSelection(selection)) {
const { columns, rows } = computeSelectionCount(selection)
const nodes = selection.getNodes()
let firstCell: null | DEPRECATED_GridCellNode = null
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (DEPRECATED_$isGridCellNode(node)) {
if (firstCell === null) {
node.setColSpan(columns).setRowSpan(rows)
firstCell = node
const isEmpty = $cellContainsEmptyParagraph(node)
let firstChild
if (isEmpty && $isParagraphNode((firstChild = node.getFirstChild()))) {
firstChild.remove()
}
} else if (DEPRECATED_$isGridCellNode(firstCell)) {
const isEmpty = $cellContainsEmptyParagraph(node)
if (!isEmpty) {
firstCell.append(...node.getChildren())
}
node.remove()
}
}
}
if (firstCell !== null) {
if (firstCell.getChildrenSize() === 0) {
firstCell.append($createParagraphNode())
}
$selectLastDescendant(firstCell)
}
onClose()
}
})
}
const unmergeTableCellsAtSelection = () => {
editor.update(() => {
$unmergeCell()
})
}
const insertTableRowAtSelection = useCallback(
(shouldInsertAfter: boolean) => {
editor.update(() => {
$insertTableRow__EXPERIMENTAL(shouldInsertAfter)
onClose()
})
},
[editor, onClose],
)
const insertTableColumnAtSelection = useCallback(
(shouldInsertAfter: boolean) => {
editor.update(() => {
for (let i = 0; i < selectionCounts.columns; i++) {
$insertTableColumn__EXPERIMENTAL(shouldInsertAfter)
}
onClose()
})
},
[editor, onClose, selectionCounts.columns],
)
const deleteTableRowAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableRow__EXPERIMENTAL()
onClose()
})
}, [editor, onClose])
const deleteTableAtSelection = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode)
tableNode.remove()
clearTableSelection()
onClose()
})
}, [editor, tableCellNode, clearTableSelection, onClose])
const deleteTableColumnAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableColumn__EXPERIMENTAL()
onClose()
})
}, [editor, onClose])
const toggleTableRowIsHeader = useCallback(
(headerState?: number) => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode)
const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode)
const tableRows = tableNode.getChildren()
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) {
throw new Error('Expected table cell to be inside of table row.')
}
const tableRow = tableRows[tableRowIndex]
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row')
}
tableRow.getChildren().forEach((tableCell) => {
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell')
}
if (headerState === undefined) {
tableCell.toggleHeaderStyle(TableCellHeaderStates.ROW)
return
}
tableCell.setHeaderStyles(headerState)
})
clearTableSelection()
onClose()
})
},
[editor, tableCellNode, clearTableSelection, onClose],
)
const toggleTableColumnIsHeader = useCallback(
(headerState?: number) => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode)
const tableColumnIndex = $getTableColumnIndexFromTableCellNode(tableCellNode)
const tableRows = tableNode.getChildren()
for (let r = 0; r < tableRows.length; r++) {
const tableRow = tableRows[r]
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row')
}
const tableCells = tableRow.getChildren()
if (tableColumnIndex >= tableCells.length || tableColumnIndex < 0) {
throw new Error('Expected table cell to be inside of table row.')
}
const tableCell = tableCells[tableColumnIndex]
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell')
}
if (headerState === undefined) {
tableCell.toggleHeaderStyle(TableCellHeaderStates.COLUMN)
continue
}
tableCell.setHeaderStyles(headerState)
}
clearTableSelection()
onClose()
})
},
[editor, tableCellNode, clearTableSelection, onClose],
)
let mergeCellButton: null | JSX.Element = null
if (cellMerge) {
if (canMergeCells) {
mergeCellButton = <MenuItem onClick={mergeTableCellsAtSelection}>Merge cells</MenuItem>
} else if (canUnmergeCell) {
mergeCellButton = <MenuItem onClick={unmergeTableCellsAtSelection}>Unmerge cells</MenuItem>
}
}
const isCurrentCellRowHeader = (tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW
const isCurrentCellColumnHeader =
(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
return (
<Menu className="dropdown" ref={dropDownRef} a11yLabel="Table actions menu" isOpen>
{mergeCellButton}
{!!mergeCellButton && <MenuItemSeparator />}
<MenuItem onClick={() => insertTableRowAtSelection(false)}>
Insert {selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`} above
</MenuItem>
<MenuItem onClick={() => insertTableRowAtSelection(true)}>
Insert {selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`} below
</MenuItem>
<MenuItemSeparator />
<MenuItem onClick={() => insertTableColumnAtSelection(false)}>
Insert {selectionCounts.columns === 1 ? 'column' : `${selectionCounts.columns} columns`} left
</MenuItem>
<MenuItem onClick={() => insertTableColumnAtSelection(true)}>
Insert {selectionCounts.columns === 1 ? 'column' : `${selectionCounts.columns} columns`} right
</MenuItem>
<MenuItemSeparator />
<MenuItem onClick={deleteTableColumnAtSelection}>Delete column</MenuItem>
<MenuItem onClick={deleteTableRowAtSelection}>Delete row</MenuItem>
<MenuItem onClick={deleteTableAtSelection}>Delete table</MenuItem>
<MenuItemSeparator />
<MenuItem
onClick={() => {
toggleTableRowIsHeader(isCurrentCellRowHeader ? TableCellHeaderStates.NO_STATUS : TableCellHeaderStates.ROW)
}}
>
{isCurrentCellRowHeader ? 'Remove' : 'Add'} row header
</MenuItem>
<MenuItem
onClick={() => {
toggleTableColumnIsHeader(
isCurrentCellColumnHeader ? TableCellHeaderStates.NO_STATUS : TableCellHeaderStates.COLUMN,
)
}}
>
{isCurrentCellColumnHeader ? 'Remove' : 'Add'} column header
</MenuItem>
</Menu>
)
}
function TableCellActionMenuContainer({
anchorElem,
cellMerge,
}: {
anchorElem: HTMLElement
cellMerge: boolean
}): JSX.Element {
const [editor] = useLexicalComposerContext()
const menuButtonRef = useRef(null)
const menuRootRef = useRef(null)
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [tableCellNode, setTableMenuCellNode] = useState<TableCellNode | null>(null)
const moveMenu = useCallback(() => {
const menu = menuButtonRef.current
const selection = $getSelection()
const nativeSelection = window.getSelection()
const activeElement = document.activeElement
if (selection == null || menu == null) {
setTableMenuCellNode(null)
return
}
const rootElement = editor.getRootElement()
if (
$isRangeSelection(selection) &&
rootElement !== null &&
nativeSelection !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const tableCellNodeFromSelection = $getTableCellNodeFromLexicalNode(selection.anchor.getNode())
if (tableCellNodeFromSelection == null) {
setTableMenuCellNode(null)
return
}
const tableCellParentNodeDOM = editor.getElementByKey(tableCellNodeFromSelection.getKey())
if (tableCellParentNodeDOM == null) {
setTableMenuCellNode(null)
return
}
setTableMenuCellNode(tableCellNodeFromSelection)
} else if (!activeElement) {
setTableMenuCellNode(null)
}
}, [editor])
useEffect(() => {
return editor.registerUpdateListener(() => {
editor.getEditorState().read(() => {
moveMenu()
})
})
})
const setMenuButtonPosition = useCallback(() => {
const menuButtonDOM = menuButtonRef.current as HTMLButtonElement | null
if (menuButtonDOM != null && tableCellNode != null) {
const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey())
if (tableCellNodeDOM != null) {
const tableCellRect = tableCellNodeDOM.getBoundingClientRect()
const menuButtonRect = menuButtonDOM.getBoundingClientRect()
const anchorRect = anchorElem.getBoundingClientRect()
const top = tableCellRect.top - anchorRect.top + menuButtonRect.height / 2 - 2
const left = tableCellRect.right - menuButtonRect.width - 8 - anchorRect.left
menuButtonDOM.style.opacity = '1'
menuButtonDOM.style.transform = `translate(${left}px, ${top}px)`
} else {
menuButtonDOM.style.opacity = '0'
menuButtonDOM.style.transform = 'translate(-10000px, -10000px)'
}
}
}, [menuButtonRef, tableCellNode, editor, anchorElem])
useEffect(() => {
setMenuButtonPosition()
}, [setMenuButtonPosition])
useEffect(() => {
const scrollerElem = editor.getRootElement()
const update = () => {
editor.getEditorState().read(() => {
setMenuButtonPosition()
})
setIsMenuOpen(false)
}
window.addEventListener('resize', update)
if (scrollerElem) {
scrollerElem.addEventListener('scroll', update)
}
return () => {
window.removeEventListener('resize', update)
if (scrollerElem) {
scrollerElem.removeEventListener('scroll', update)
}
}
}, [editor, anchorElem, setMenuButtonPosition])
const prevTableCellDOM = useRef(tableCellNode)
useEffect(() => {
if (prevTableCellDOM.current !== tableCellNode) {
setIsMenuOpen(false)
}
prevTableCellDOM.current = tableCellNode
}, [prevTableCellDOM, tableCellNode])
return (
<div className="table-cell-action-button-container" ref={menuButtonRef}>
{tableCellNode != null && (
<>
<RoundIconButton
label="Open table actions menu"
icon="chevron-down"
iconProps={{
size: 'small',
}}
className="!h-6 !min-w-6 bg-default md:!h-5 md:!min-w-5"
onClick={(e) => {
e.stopPropagation()
setIsMenuOpen(!isMenuOpen)
}}
ref={menuRootRef}
/>
<Popover
open={isMenuOpen}
title="Table actions"
className="py-1"
anchorElement={menuRootRef}
disableMobileFullscreenTakeover
>
<TableActionMenu onClose={() => setIsMenuOpen(false)} tableCellNode={tableCellNode} cellMerge={cellMerge} />
</Popover>
</>
)}
</div>
)
}
export default function TableActionMenuPlugin({
anchorElem = document.body,
cellMerge = false,
}: {
anchorElem?: HTMLElement
cellMerge?: boolean
}): null | ReactPortal {
const isEditable = useLexicalEditable()
return createPortal(
isEditable ? <TableCellActionMenuContainer anchorElem={anchorElem} cellMerge={cellMerge} /> : null,
anchorElem,
)
}
@@ -372,17 +372,17 @@ const MobileToolbarPlugin = () => {
}, [editor, updateEditorSelection])
const isFocusInEditorOrToolbar = isInEditor || isInToolbar || isInLinkEditor
if (!isMobile || !isFocusInEditorOrToolbar) {
return null
}
return (
<>
{modal}
<div className="bg-contrast" id="super-mobile-toolbar">
<div
className={classNames('bg-contrast', !isMobile || !isFocusInEditorOrToolbar ? 'hidden' : '')}
id="super-mobile-toolbar"
>
{isSelectionLink && (
<div
className="border-t border-border px-2 focus:outline-none focus:shadow-none"
className="border-t border-border px-2 focus:shadow-none focus:outline-none"
ref={linkEditorRef}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
>
@@ -406,7 +406,7 @@ const MobileToolbarPlugin = () => {
return (
<StyledTooltip showOnMobile showOnHover label={item.name} key={item.name}>
<button
className="flex items-center justify-center rounded p-0.5 disabled:opacity-50 select-none hover:bg-default"
className="flex select-none items-center justify-center rounded p-0.5 hover:bg-default disabled:opacity-50"
aria-label={item.name}
onMouseDown={(event) => {
event.preventDefault()
@@ -420,7 +420,7 @@ const MobileToolbarPlugin = () => {
>
<div
className={classNames(
'flex items-center justify-center p-2 rounded transition-colors duration-75',
'flex items-center justify-center rounded p-2 transition-colors duration-75',
item.active && 'bg-info text-info-contrast',
)}
>
@@ -58,6 +58,7 @@ type Props = {
linkingController: LinkingController
filesController: FilesController
spellcheck: boolean
readonly?: boolean
}
export const SuperEditor: FunctionComponent<Props> = ({
@@ -66,6 +67,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
filesController,
spellcheck,
controller,
readonly,
}) => {
const note = useRef(controller.item)
const changeEditorFunction = useRef<ChangeEditorFunction>()
@@ -217,7 +219,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<ErrorBoundary>
<LinkingControllerProvider controller={linkingController}>
<FilesControllerProvider controller={filesController}>
<BlocksEditorComposer readonly={note.current.locked} initialValue={note.current.text}>
<BlocksEditorComposer readonly={note.current.locked || readonly} initialValue={note.current.text}>
<BlocksEditor
onChange={handleChange}
className={classNames(
@@ -227,6 +229,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
)}
previewLength={SuperNotePreviewCharLimit}
spellcheck={spellcheck}
readonly={note.current.locked || readonly}
>
<ItemSelectionPlugin currentNote={note.current} />
<FilePlugin currentNote={note.current} />
@@ -242,7 +245,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
<NodeObserverPlugin nodeType={BubbleNode} onRemove={handleBubbleRemove} />
<NodeObserverPlugin nodeType={FileNode} onRemove={handleBubbleRemove} />
<ExportPlugin />
<ReadonlyPlugin note={note.current} />
{readonly === undefined && <ReadonlyPlugin note={note.current} />}
<AutoFocusPlugin isEnabled={controller.isTemplateNote} />
<SuperSearchContextProvider>
<SearchPlugin />
@@ -78,6 +78,7 @@ const VaultMenu = observer(({ items }: { items: DecryptedItemInterface[] }) => {
doesVaultContainItems(vault) ? void removeItemsFromVault() : void addItemsToVault(vault)
}}
className={doesVaultContainItems(vault) ? 'font-bold' : ''}
disabled={vault.isSharedVaultListing() && application.vaultUsers.isCurrentUserReadonlyVaultMember(vault)}
>
<Icon
type={vault.iconString}
@@ -98,7 +99,15 @@ const VaultMenu = observer(({ items }: { items: DecryptedItemInterface[] }) => {
)
})
const AddToVaultMenuOption = ({ iconClassName, items }: { iconClassName: string; items: DecryptedItemInterface[] }) => {
const AddToVaultMenuOption = ({
iconClassName,
items,
disabled,
}: {
iconClassName: string
items: DecryptedItemInterface[]
disabled?: boolean
}) => {
const application = useApplication()
const buttonRef = useRef<HTMLButtonElement>(null)
@@ -123,6 +132,7 @@ const AddToVaultMenuOption = ({ iconClassName, items }: { iconClassName: string;
}
}}
ref={buttonRef}
disabled={disabled}
>
<div className="flex items-center">
<Icon type="safe-square" className={iconClassName} />
@@ -37,6 +37,9 @@ export const useVault = (vault: VaultListingInterface) => {
}, [application.vaultDisplayService, application.vaultLocks, canShowLockOption, isLocked, vault])
const isCurrentUserAdmin = !vault.isSharedVaultListing()
? true
: application.vaultUsers.isCurrentUserSharedVaultAdmin(vault)
const isCurrentUserOwner = !vault.isSharedVaultListing()
? true
: application.vaultUsers.isCurrentUserSharedVaultOwner(vault)
@@ -54,5 +57,6 @@ export const useVault = (vault: VaultListingInterface) => {
toggleLock,
ensureVaultIsUnlocked,
isCurrentUserAdmin,
isCurrentUserOwner,
}
}
+12 -12
View File
@@ -4138,7 +4138,7 @@ __metadata:
resolution: "@standardnotes/api@workspace:packages/api"
dependencies:
"@standardnotes/common": ^1.50.0
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/models": "workspace:*"
"@standardnotes/responses": "workspace:*"
"@standardnotes/utils": "workspace:*"
@@ -4288,7 +4288,7 @@ __metadata:
"@babel/core": "*"
"@babel/preset-env": "*"
"@electron/remote": ^2.0.9
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/electron-clear-data": 1.1.1
"@standardnotes/web": "workspace:*"
"@types/fs-extra": ^11.0.1
@@ -4340,12 +4340,12 @@ __metadata:
languageName: node
linkType: hard
"@standardnotes/domain-core@npm:^1.28.0":
version: 1.28.0
resolution: "@standardnotes/domain-core@npm:1.28.0"
"@standardnotes/domain-core@npm:^1.32.0":
version: 1.32.0
resolution: "@standardnotes/domain-core@npm:1.32.0"
dependencies:
uuid: ^9.0.0
checksum: 9ee58eea35ed5d988513c5a24f751b9c3a2be2a324c30684db54ecbd16f0c8e19a80b8e464bde46e86905590085923c7e1abbe1b792a4ef0a19c1171e8f3b6c2
checksum: 7d1263782caae6e7aefb245243f3ddc830efec707e2903d3dcb52bd5404d2c9464d3a24c2d4e04671fd9577b9c02bb8e9270b2571fe853b9469889c4b76c67a4
languageName: node
linkType: hard
@@ -4392,7 +4392,7 @@ __metadata:
dependencies:
"@standardnotes/common": ^1.50.0
"@standardnotes/config": 2.4.3
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/models": "workspace:*"
"@standardnotes/responses": "workspace:*"
"@standardnotes/sncrypto-common": "workspace:*"
@@ -4427,7 +4427,7 @@ __metadata:
resolution: "@standardnotes/features@workspace:packages/features"
dependencies:
"@standardnotes/common": ^1.50.0
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@types/jest": ^29.2.3
"@typescript-eslint/eslint-plugin": "*"
eslint: "*"
@@ -4633,7 +4633,7 @@ __metadata:
resolution: "@standardnotes/models@workspace:packages/models"
dependencies:
"@standardnotes/common": ^1.50.0
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/features": "workspace:*"
"@standardnotes/responses": "workspace:*"
"@standardnotes/sncrypto-common": "workspace:^"
@@ -4729,7 +4729,7 @@ __metadata:
dependencies:
"@standardnotes/api": "workspace:^"
"@standardnotes/common": ^1.50.0
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/encryption": "workspace:^"
"@standardnotes/features": "workspace:^"
"@standardnotes/files": "workspace:^"
@@ -4830,7 +4830,7 @@ __metadata:
"@babel/preset-env": "*"
"@standardnotes/api": "workspace:*"
"@standardnotes/common": ^1.50.0
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/domain-events": ^2.122.0
"@standardnotes/encryption": "workspace:*"
"@standardnotes/features": "workspace:*"
@@ -4955,7 +4955,7 @@ __metadata:
resolution: "@standardnotes/ui-services@workspace:packages/ui-services"
dependencies:
"@standardnotes/common": ^1.50.0
"@standardnotes/domain-core": ^1.28.0
"@standardnotes/domain-core": ^1.32.0
"@standardnotes/features": "workspace:^"
"@standardnotes/filepicker": "workspace:^"
"@standardnotes/models": "workspace:^"