mirror of
https://github.com/standardnotes/app
synced 2026-09-13 18:46:08 -04:00
Compare commits
195
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef6e971592 | ||
|
|
1c48dd135c | ||
|
|
495786514e | ||
|
|
cfd6cf5932 | ||
|
|
8c5944f233 | ||
|
|
2b63484758 | ||
|
|
ccb15faff5 | ||
|
|
dc33aef660 | ||
|
|
771758fcd4 | ||
|
|
3fbe28e068 | ||
|
|
e67240821c | ||
|
|
d7c6ef6134 | ||
|
|
8311271a49 | ||
|
|
4e61d9b3e3 | ||
|
|
dd41082e71 | ||
|
|
adc6e9a353 | ||
|
|
bfd9f37331 | ||
|
|
044a4fc36b | ||
|
|
fe4841c631 | ||
|
|
14a98b5736 | ||
|
|
326e1a455b | ||
|
|
c3c8c2b9ce | ||
|
|
5e79cf90f4 | ||
|
|
e798b71b9f | ||
|
|
afd89c387f | ||
|
|
6bf6924a8b | ||
|
|
0f7b1539e0 | ||
|
|
e1c21d5fbd | ||
|
|
52d0fed437 | ||
|
|
caee8f1c6f | ||
|
|
40194d010f | ||
|
|
0373bf9a2d | ||
|
|
d7eb43401a | ||
|
|
a5a866a62a | ||
|
|
6090962b2c | ||
|
|
a9445dfcca | ||
|
|
b3fe769a60 | ||
|
|
fc6d28d166 | ||
|
|
03eac4353d | ||
|
|
58d84c47c1 | ||
|
|
577318e208 | ||
|
|
b8da877804 | ||
|
|
17f4d20fb5 | ||
|
|
4f07b915f0 | ||
|
|
f2f61b18e2 | ||
|
|
9fb506e532 | ||
|
|
3b64acc290 | ||
|
|
e6824a3414 | ||
|
|
08985bb5c8 | ||
|
|
77cb6e7891 | ||
|
|
7e7c5e14ab | ||
|
|
631be2e494 | ||
|
|
fe36009482 | ||
|
|
e77e852801 | ||
|
|
c59d824240 | ||
|
|
2f3bc9b85b | ||
|
|
1ec173dcd6 | ||
|
|
35b00737bf | ||
|
|
f3e147f4c9 | ||
|
|
63fdb328cb | ||
|
|
94c280256e | ||
|
|
cf0dc19f63 | ||
|
|
2696b07759 | ||
|
|
2cc5afb00a | ||
|
|
1828f80ba5 | ||
|
|
9c260c02f1 | ||
|
|
e4b3028d67 | ||
|
|
eceb46bed7 | ||
|
|
b9e0d0d154 | ||
|
|
a7add80633 | ||
|
|
0941183c8b | ||
|
|
c94035c1d6 | ||
|
|
343c39e873 | ||
|
|
889f245ab9 | ||
|
|
e8ada146e1 | ||
|
|
c9c5e915ce | ||
|
|
a1ec13d0b0 | ||
|
|
31bb03943d | ||
|
|
2b84c242f7 | ||
|
|
b8bf8ca112 | ||
|
|
c914b88e2d | ||
|
|
0169224bf2 | ||
|
|
4049b19170 | ||
|
|
df4148f824 | ||
|
|
91f3295cff | ||
|
|
c9078f374b | ||
|
|
bb4f1ff099 | ||
|
|
abdaec89b7 | ||
|
|
f559442a67 | ||
|
|
51aed0a518 | ||
|
|
35bb671e31 | ||
|
|
d76b190aba | ||
|
|
d6fd85e451 | ||
|
|
c90087462f | ||
|
|
58838a44cc | ||
|
|
0223800fdf | ||
|
|
318b1fdb0f | ||
|
|
fb517a91b0 | ||
|
|
c7b158eb64 | ||
|
|
d4b63e4ea6 | ||
|
|
cd8596b14e | ||
|
|
6c11766051 | ||
|
|
7f41564328 | ||
|
|
4e576d7526 | ||
|
|
317c6555d2 | ||
|
|
220f62665e | ||
|
|
1dc9086257 | ||
|
|
d149b500ee | ||
|
|
ad70bea368 | ||
|
|
3bcd969f83 | ||
|
|
d4af612fea | ||
|
|
b068b814f2 | ||
|
|
51de012236 | ||
|
|
2694c2f1a9 | ||
|
|
d3609853c6 | ||
|
|
a7ac715499 | ||
|
|
5ac93a6bd8 | ||
|
|
be4cc4e605 | ||
|
|
2fa3fec9ab | ||
|
|
059ee9ea36 | ||
|
|
66d08895fe | ||
|
|
518643dc71 | ||
|
|
5a0c005e8c | ||
|
|
45cdab6f6c | ||
|
|
46245b5569 | ||
|
|
8b103307cb | ||
|
|
a70d1009a3 | ||
|
|
a5b1ff9595 | ||
|
|
d691d7a3b3 | ||
|
|
71675dc6a2 | ||
|
|
6a1b6cef1b | ||
|
|
3d8c12a789 | ||
|
|
370aaecfb8 | ||
|
|
68bfb2ff68 | ||
|
|
a66bb08c3b | ||
|
|
ccde207de8 | ||
|
|
d3262b0f01 | ||
|
|
ffbc984c58 | ||
|
|
979163bae7 | ||
|
|
c928f8a281 | ||
|
|
0e10b347b6 | ||
|
|
caf2c4a876 | ||
|
|
1d22365086 | ||
|
|
bd5ccbfab6 | ||
|
|
46b19d8caf | ||
|
|
a6a0d615c5 | ||
|
|
f3203e2e80 | ||
|
|
e0edabb4b9 | ||
|
|
e68c708628 | ||
|
|
e4f3f26937 | ||
|
|
8575b3e714 | ||
|
|
7c261af230 | ||
|
|
71e20f2a2f | ||
|
|
3855b9f767 | ||
|
|
5c29e22ea4 | ||
|
|
07cb00a815 | ||
|
|
4f2ee4b56f | ||
|
|
90e10c76f7 | ||
|
|
2175c9cdcb | ||
|
|
8c41f0ecc3 | ||
|
|
c8c166d2a0 | ||
|
|
6cacdd8c58 | ||
|
|
b5b6192b60 | ||
|
|
635e2d9150 | ||
|
|
0c23e4d36c | ||
|
|
1e8ce4bee6 | ||
|
|
29368c51b8 | ||
|
|
621bf1b810 | ||
|
|
28e43d37c0 | ||
|
|
e07fed267f | ||
|
|
1726302615 | ||
|
|
73ca12275f | ||
|
|
1ee6be4e5c | ||
|
|
42fc1d073d | ||
|
|
c32541c6e1 | ||
|
|
34d9100af6 | ||
|
|
7f2074a6ec | ||
|
|
0e95b451d6 | ||
|
|
0e886bf0f1 | ||
|
|
9b3f11d85b | ||
|
|
349fd7204e | ||
|
|
afa9aafa3e | ||
|
|
b57786eeeb | ||
|
|
f30d0898ec | ||
|
|
1729bc4292 | ||
|
|
abe2b17cb4 | ||
|
|
d51de34daa | ||
|
|
ec0f0a2b18 | ||
|
|
a417e5ba8d | ||
|
|
02eb969e07 | ||
|
|
767d354780 | ||
|
|
052e7d2f90 | ||
|
|
e3ec8503a5 | ||
|
|
cc2762a29d | ||
|
|
96e8dfdd31 |
@@ -160,7 +160,6 @@ jobs:
|
||||
run:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- run: echo APP_VERSION=$(node -p "require('./../web/package.json').version") >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
@@ -169,6 +168,8 @@ jobs:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'yarn'
|
||||
|
||||
- run: echo APP_VERSION=$(node -p "require('./../web/package.json').version") >> $GITHUB_ENV
|
||||
|
||||
- run: yarn install --immutable
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
@@ -64,14 +64,14 @@ jobs:
|
||||
|
||||
- name: Run E2E test suite
|
||||
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.1
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
repo: self-hosted
|
||||
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
workflow_file_name: testing-with-stable-server.yml
|
||||
workflow_file_name: testing-with-updating-client-and-server.yml
|
||||
wait_interval: 30
|
||||
client_payload: '{"image_tag": "${{ github.sha }}"}'
|
||||
client_payload: '{"snjs_image_tag": "${{ github.sha }}"}'
|
||||
propagate_failure: true
|
||||
trigger_workflow: true
|
||||
wait_workflow: true
|
||||
|
||||
@@ -40,14 +40,14 @@ jobs:
|
||||
docker tag standardnotes/snjs:${{ github.sha }} standardnotes/snjs:test
|
||||
docker push standardnotes/snjs:test
|
||||
- name: Run E2E test suite
|
||||
uses: convictional/trigger-workflow-and-wait@v1.6.1
|
||||
uses: convictional/trigger-workflow-and-wait@master
|
||||
with:
|
||||
owner: standardnotes
|
||||
repo: e2e
|
||||
repo: self-hosted
|
||||
github_token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
workflow_file_name: testing-with-stable-server.yml
|
||||
workflow_file_name: testing-with-updating-client-and-server.yml
|
||||
wait_interval: 30
|
||||
client_payload: '{"image_tag": "${{ github.sha }}"}'
|
||||
client_payload: '{"snjs_image_tag": "${{ github.sha }}"}'
|
||||
propagate_failure: true
|
||||
trigger_workflow: true
|
||||
wait_workflow: true
|
||||
|
||||
@@ -45,10 +45,18 @@ jobs:
|
||||
aws-region: us-east-1
|
||||
- name: Deploy static site to S3 bucket
|
||||
run: aws s3 sync packages/web/dist/ s3://app.standardnotes.com --delete
|
||||
- name: Invalidate CloudFront Cache
|
||||
- name: Invalidate CloudFront Cache on .COM
|
||||
uses: chetan/invalidate-cloudfront-action@master
|
||||
env:
|
||||
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_DISTRIBUTION_ID }}
|
||||
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_COM_DISTRIBUTION_ID }}
|
||||
PATHS: '/*'
|
||||
AWS_REGION: 'us-east-1'
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
- name: Invalidate CloudFront Cache on .ORG
|
||||
uses: chetan/invalidate-cloudfront-action@master
|
||||
env:
|
||||
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_ORG_DISTRIBUTION_ID }}
|
||||
PATHS: '/*'
|
||||
AWS_REGION: 'us-east-1'
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,58 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.20.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-27)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** handling expired token responses ([#2113](https://github.com/standardnotes/app/issues/2113)) ([2696b07](https://github.com/standardnotes/app/commit/2696b07759104fb1d40781897f69e63133ad824a))
|
||||
|
||||
## [1.20.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** refreshing sessions ([#2106](https://github.com/standardnotes/app/issues/2106)) ([bb4f1ff](https://github.com/standardnotes/app/commit/bb4f1ff0990eb5a6f64e76a699fb95540cffdbb4))
|
||||
|
||||
## [1.20.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-02)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-01)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.20.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -6,12 +6,4 @@ module.exports = {
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||
},
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 22,
|
||||
functions: 69,
|
||||
lines: 67,
|
||||
statements: 67
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.20.12",
|
||||
"version": "1.20.24",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -23,7 +23,7 @@
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"lint:fix": "eslint src --ext .ts --fix",
|
||||
"test": "jest spec --coverage"
|
||||
"test": "jest spec --coverage --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.45.0",
|
||||
"@standardnotes/domain-core": "^1.11.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
import { type Invitation } from '@standardnotes/models'
|
||||
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
|
||||
import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface'
|
||||
|
||||
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
|
||||
import { SubscriptionApiService } from './SubscriptionApiService'
|
||||
|
||||
describe('SubscriptionApiService', () => {
|
||||
let subscriptionServer: SubscriptionServerInterface
|
||||
|
||||
const createService = () => new SubscriptionApiService(subscriptionServer)
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptionServer = {} as jest.Mocked<SubscriptionServerInterface>
|
||||
subscriptionServer.invite = jest.fn().mockReturnValue({
|
||||
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
|
||||
} as jest.Mocked<SubscriptionInviteResponse>)
|
||||
subscriptionServer.cancelInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteCancelResponse>)
|
||||
subscriptionServer.listInvites = jest.fn().mockReturnValue({
|
||||
data: { invitations: [{} as jest.Mocked<Invitation>] },
|
||||
} as jest.Mocked<SubscriptionInviteListResponse>)
|
||||
subscriptionServer.acceptInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
|
||||
})
|
||||
|
||||
it('should invite a user', async () => {
|
||||
const response = await createService().invite('[email protected]')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.invite).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invite a user if it is already inviting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.Inviting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.invite('[email protected]')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not invite a user if the server fails', async () => {
|
||||
subscriptionServer.invite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().invite('[email protected]')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should cancel an invite', async () => {
|
||||
const response = await createService().cancelInvite('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.cancelInvite).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not cancel an invite if it is already canceling', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.CancelingInvite, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.cancelInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not cancel an invite if the server fails', async () => {
|
||||
subscriptionServer.cancelInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().cancelInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list invites', async () => {
|
||||
const response = await createService().listInvites()
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
invitations: [{} as jest.Mocked<Invitation>],
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.listInvites).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not list invitations if it is already listing', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.ListingInvites, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listInvites()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list invites if the server fails', async () => {
|
||||
subscriptionServer.listInvites = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listInvites()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should accept an invite', async () => {
|
||||
const response = await createService().acceptInvite('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(subscriptionServer.acceptInvite).toHaveBeenCalledWith({
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept an invite if it is already accepting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[SubscriptionApiOperations.AcceptingInvite, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.acceptInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not accept an invite if the server fails', async () => {
|
||||
subscriptionServer.acceptInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().acceptInvite('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,197 +0,0 @@
|
||||
import { ProtocolVersion, UserRequestType } from '@standardnotes/common'
|
||||
import { type RootKeyParamsInterface } from '@standardnotes/models'
|
||||
import { UserDeletionResponse } from '../../Response/User/UserDeletionResponse'
|
||||
|
||||
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
|
||||
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
|
||||
import { UserServerInterface } from '../../Server'
|
||||
import { UserRequestServerInterface } from '../../Server/UserRequest/UserRequestServerInterface'
|
||||
|
||||
import { UserApiOperations } from './UserApiOperations'
|
||||
import { UserApiService } from './UserApiService'
|
||||
|
||||
describe('UserApiService', () => {
|
||||
let userServer: UserServerInterface
|
||||
let userRequestServer: UserRequestServerInterface
|
||||
let keyParams: RootKeyParamsInterface
|
||||
|
||||
const createService = () => new UserApiService(userServer, userRequestServer)
|
||||
|
||||
beforeEach(() => {
|
||||
userServer = {} as jest.Mocked<UserServerInterface>
|
||||
userServer.register = jest.fn().mockReturnValue({
|
||||
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
|
||||
} as jest.Mocked<UserRegistrationResponse>)
|
||||
userServer.deleteAccount = jest.fn().mockReturnValue({
|
||||
data: { message: 'Success' },
|
||||
} as jest.Mocked<UserDeletionResponse>)
|
||||
|
||||
userRequestServer = {} as jest.Mocked<UserRequestServerInterface>
|
||||
userRequestServer.submitUserRequest = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<UserRequestResponse>)
|
||||
|
||||
keyParams = {} as jest.Mocked<RootKeyParamsInterface>
|
||||
keyParams.getPortableValue = jest.fn().mockReturnValue({
|
||||
identifier: '[email protected]',
|
||||
version: ProtocolVersion.V004,
|
||||
})
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
user: {
|
||||
email: '[email protected]',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(userServer.register).toHaveBeenCalledWith({
|
||||
api: '20200115',
|
||||
email: '[email protected]',
|
||||
ephemeral: false,
|
||||
identifier: '[email protected]',
|
||||
password: 'testpasswd',
|
||||
version: '004',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not register a user if it is already registering', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.Registering, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.register({ email: '[email protected]', serverPassword: 'testpasswd', keyParams, ephemeral: false })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not register a user if the server fails', async () => {
|
||||
userServer.register = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().register({
|
||||
email: '[email protected]',
|
||||
serverPassword: 'testpasswd',
|
||||
keyParams,
|
||||
ephemeral: false,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should submit a user request', async () => {
|
||||
const response = await createService().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(userRequestServer.submitUserRequest).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
})
|
||||
|
||||
it('should not submit a user request if it is already submitting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.SubmittingRequest, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.submitUserRequest({ userUuid: '1-2-3', requestType: UserRequestType.ExitDiscount })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not submit a user request if the server fails', async () => {
|
||||
userRequestServer.submitUserRequest = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await createService().deleteAccount('1-2-3')
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
message: 'Success',
|
||||
},
|
||||
})
|
||||
expect(userServer.deleteAccount).toHaveBeenCalledWith({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not delete a user if it is already deleting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[UserApiOperations.DeletingAccount, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.deleteAccount('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not delete a user if the server fails', async () => {
|
||||
userServer.deleteAccount = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().deleteAccount('1-2-3')
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,61 +0,0 @@
|
||||
import { WebSocketConnectionTokenResponse } from '../../Response'
|
||||
|
||||
import { WebSocketServerInterface } from '../../Server/WebSocket/WebSocketServerInterface'
|
||||
import { WebSocketApiOperations } from './WebSocketApiOperations'
|
||||
|
||||
import { WebSocketApiService } from './WebSocketApiService'
|
||||
|
||||
describe('WebSocketApiService', () => {
|
||||
let webSocketServer: WebSocketServerInterface
|
||||
|
||||
const createService = () => new WebSocketApiService(webSocketServer)
|
||||
|
||||
beforeEach(() => {
|
||||
webSocketServer = {} as jest.Mocked<WebSocketServerInterface>
|
||||
webSocketServer.createConnectionToken = jest.fn().mockReturnValue({
|
||||
data: { token: 'foobar' },
|
||||
} as jest.Mocked<WebSocketConnectionTokenResponse>)
|
||||
})
|
||||
|
||||
it('should create a websocket connection token', async () => {
|
||||
const response = await createService().createConnectionToken()
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
token: 'foobar',
|
||||
},
|
||||
})
|
||||
expect(webSocketServer.createConnectionToken).toHaveBeenCalledWith({})
|
||||
})
|
||||
|
||||
it('should not create a token if it is already creating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WebSocketApiOperations.CreatingConnectionToken, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.createConnectionToken()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not create a token if the server fails', async () => {
|
||||
webSocketServer.createConnectionToken = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().createConnectionToken()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,368 +0,0 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import { HttpStatusCode } from '../../Http'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
|
||||
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
|
||||
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
|
||||
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
|
||||
import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface'
|
||||
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
|
||||
|
||||
import { WorkspaceApiOperations } from './WorkspaceApiOperations'
|
||||
import { WorkspaceApiService } from './WorkspaceApiService'
|
||||
|
||||
describe('WorkspaceApiService', () => {
|
||||
let workspaceServer: WorkspaceServerInterface
|
||||
|
||||
const createService = () => new WorkspaceApiService(workspaceServer)
|
||||
|
||||
beforeEach(() => {
|
||||
workspaceServer = {} as jest.Mocked<WorkspaceServerInterface>
|
||||
workspaceServer.createWorkspace = jest.fn().mockReturnValue({
|
||||
data: { uuid: '1-2-3' },
|
||||
} as jest.Mocked<WorkspaceCreationResponse>)
|
||||
workspaceServer.inviteToWorkspace = jest.fn().mockReturnValue({
|
||||
data: { uuid: 'i-1-2-3' },
|
||||
} as jest.Mocked<WorkspaceInvitationResponse>)
|
||||
workspaceServer.acceptInvite = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
|
||||
workspaceServer.listWorkspaces = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
} as jest.Mocked<WorkspaceListResponse>)
|
||||
workspaceServer.listWorkspaceUsers = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: [] },
|
||||
} as jest.Mocked<WorkspaceUserListResponse>)
|
||||
workspaceServer.initiateKeyshare = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const response = await createService().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.createWorkspace).toHaveBeenCalledWith({
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
workspaceType: 'private',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not create a workspace if it is already creating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Creating, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not create a workspace if the server fails', async () => {
|
||||
workspaceServer.createWorkspace = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should invite to a workspace', async () => {
|
||||
const response = await createService().inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: 'i-1-2-3',
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.inviteToWorkspace).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: 'write-and-read',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not invite to a workspace if it is already inviting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Inviting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not invite to a workspace if the server fails', async () => {
|
||||
workspaceServer.inviteToWorkspace = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().inviteToWorkspace({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
inviteeEmail: '[email protected]',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should accept invite to a workspace', async () => {
|
||||
const response = await createService().acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.acceptInvite).toHaveBeenCalledWith({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not accept invite to a workspace if it is already accepting', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.Accepting, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not accept invite to a workspace if the server fails', async () => {
|
||||
workspaceServer.acceptInvite = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().acceptInvite({
|
||||
userUuid: 'u-1-2-3',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'foo',
|
||||
encryptedPrivateKey: 'bar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
const response = await createService().listWorkspaces()
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
ownedWorkspaces: [],
|
||||
joinedWorkspaces: [],
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.listWorkspaces).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not list workspaces if it is already listing them', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaces, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listWorkspaces()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list workspaces if the server fails', async () => {
|
||||
workspaceServer.listWorkspaces = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listWorkspaces()
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
const response = await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
users: [],
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.listWorkspaceUsers).toHaveBeenCalledWith({ workspaceUuid: 'w-1-2-3' })
|
||||
})
|
||||
|
||||
it('should not list workspace users if it is already listing them', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaceUsers, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not list workspace users if the server fails', async () => {
|
||||
workspaceServer.listWorkspaceUsers = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should initiate keyshare in workspace for user', async () => {
|
||||
const response = await createService().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
expect(workspaceServer.initiateKeyshare).toHaveBeenCalledWith({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
})
|
||||
|
||||
it('should not initiate keyshare in workspace if it is already initiating', async () => {
|
||||
const service = createService()
|
||||
Object.defineProperty(service, 'operationsInProgress', {
|
||||
get: () => new Map([[WorkspaceApiOperations.InitiatingKeyshare, true]]),
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await service.initiateKeyshare({ workspaceUuid: 'w-1-2-3', userUuid: 'u-1-2-3', encryptedWorkspaceKey: 'foobar' })
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should not initiate keyshare in workspace if the server fails', async () => {
|
||||
workspaceServer.initiateKeyshare = jest.fn().mockImplementation(() => {
|
||||
throw new Error('Oops')
|
||||
})
|
||||
|
||||
let error = null
|
||||
try {
|
||||
await createService().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
} catch (caughtError) {
|
||||
error = caughtError
|
||||
}
|
||||
|
||||
expect(error).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Environment } from '@standardnotes/models'
|
||||
|
||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||
import { HttpService } from './HttpService'
|
||||
|
||||
describe('HttpService', () => {
|
||||
const environment = Environment.Web
|
||||
const appVersion = '1.2.3'
|
||||
const snjsVersion = '2.3.4'
|
||||
const host = 'http://bar'
|
||||
let updateMetaCallback: (meta: HttpResponseMeta) => void
|
||||
|
||||
const createService = () => {
|
||||
const service = new HttpService(environment, appVersion, snjsVersion, updateMetaCallback)
|
||||
service.setHost(host)
|
||||
return service
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
updateMetaCallback = jest.fn()
|
||||
})
|
||||
|
||||
it('should set host', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service['host']).toEqual('http://bar')
|
||||
|
||||
service.setHost('http://foo')
|
||||
|
||||
expect(service['host']).toEqual('http://foo')
|
||||
})
|
||||
|
||||
it('should set and use the authorization token', () => {
|
||||
const service = createService()
|
||||
|
||||
expect(service['authorizationToken']).toBeUndefined()
|
||||
|
||||
service.setAuthorizationToken('foo-bar')
|
||||
|
||||
expect(service['authorizationToken']).toEqual('foo-bar')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import { isString, joinPaths, sleep } from '@standardnotes/utils'
|
||||
import { Environment } from '@standardnotes/models'
|
||||
import { Session, SessionToken } from '@standardnotes/domain-core'
|
||||
|
||||
import { HttpRequestParams } from './HttpRequestParams'
|
||||
import { HttpVerb } from './HttpVerb'
|
||||
import { HttpRequest } from './HttpRequest'
|
||||
@@ -10,21 +12,26 @@ import { XMLHttpRequestState } from './XMLHttpRequestState'
|
||||
import { ErrorMessage } from '../Error/ErrorMessage'
|
||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
|
||||
import { Paths } from '../Server/Auth/Paths'
|
||||
import { SessionRefreshResponse } from '../Response/Auth/SessionRefreshResponse'
|
||||
|
||||
export class HttpService implements HttpServiceInterface {
|
||||
private authorizationToken?: string
|
||||
private session: Session | null
|
||||
private __latencySimulatorMs?: number
|
||||
private host!: string
|
||||
private declare host: string
|
||||
|
||||
constructor(
|
||||
private environment: Environment,
|
||||
private appVersion: string,
|
||||
private snjsVersion: string,
|
||||
private updateMetaCallback: (meta: HttpResponseMeta) => void,
|
||||
) {}
|
||||
private refreshSessionCallback: (session: Session) => void,
|
||||
) {
|
||||
this.session = null
|
||||
}
|
||||
|
||||
setAuthorizationToken(authorizationToken: string): void {
|
||||
this.authorizationToken = authorizationToken
|
||||
setSession(session: Session): void {
|
||||
this.session = session
|
||||
}
|
||||
|
||||
setHost(host: string): void {
|
||||
@@ -36,7 +43,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Get,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,7 +52,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Post,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,7 +61,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Put,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +70,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Patch,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +79,7 @@ export class HttpService implements HttpServiceInterface {
|
||||
url: joinPaths(this.host, path),
|
||||
params,
|
||||
verb: HttpVerb.Delete,
|
||||
authentication: authentication ?? this.authorizationToken,
|
||||
authentication: authentication ?? this.session?.accessToken.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,9 +96,68 @@ export class HttpService implements HttpServiceInterface {
|
||||
this.updateMetaCallback(response.meta)
|
||||
}
|
||||
|
||||
if (response.status === HttpStatusCode.ExpiredAccessToken) {
|
||||
const isSessionRefreshed = await this.refreshSession()
|
||||
if (!isSessionRefreshed) {
|
||||
return response
|
||||
}
|
||||
|
||||
httpRequest.authentication = this.session?.accessToken.value
|
||||
|
||||
return this.runHttp(httpRequest)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
private async refreshSession(): Promise<boolean> {
|
||||
if (this.session === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const response = (await this.post(Paths.v1.refreshSession, {
|
||||
access_token: this.session.accessToken.value,
|
||||
refresh_token: this.session.refreshToken.value,
|
||||
})) as SessionRefreshResponse
|
||||
|
||||
if (response.data.error) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (response.meta) {
|
||||
this.updateMetaCallback(response.meta)
|
||||
}
|
||||
|
||||
const accessTokenOrError = SessionToken.create(
|
||||
response.data.session.access_token,
|
||||
response.data.session.access_expiration,
|
||||
)
|
||||
if (accessTokenOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const accessToken = accessTokenOrError.getValue()
|
||||
|
||||
const refreshTokenOrError = SessionToken.create(
|
||||
response.data.session.refresh_token,
|
||||
response.data.session.refresh_expiration,
|
||||
)
|
||||
if (refreshTokenOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const refreshToken = refreshTokenOrError.getValue()
|
||||
|
||||
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
|
||||
if (sessionOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.setSession(sessionOrError.getValue())
|
||||
|
||||
this.refreshSessionCallback(this.session)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
|
||||
if (
|
||||
httpRequest.params !== undefined &&
|
||||
@@ -199,13 +265,12 @@ export class HttpService implements HttpServiceInterface {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.MultipleChoices) {
|
||||
resolve(response)
|
||||
} else {
|
||||
if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.InternalServerError) {
|
||||
if (httpStatus === HttpStatusCode.Forbidden && response.data && response.data.error !== undefined) {
|
||||
;(response.data as HttpErrorResponseBody).error.message = ErrorMessage.RateLimited
|
||||
}
|
||||
|
||||
resolve(response)
|
||||
} else {
|
||||
reject(response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Session } from '@standardnotes/domain-core'
|
||||
|
||||
import { HttpRequestParams } from './HttpRequestParams'
|
||||
import { HttpResponse } from './HttpResponse'
|
||||
|
||||
export interface HttpServiceInterface {
|
||||
setHost(host: string): void
|
||||
setAuthorizationToken(authorizationToken: string): void
|
||||
setSession(session: Session): void
|
||||
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
post(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
put(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||
|
||||
@@ -7,4 +7,5 @@ export enum HttpStatusCode {
|
||||
Forbidden = 403,
|
||||
Gone = 410,
|
||||
ExpiredAccessToken = 498,
|
||||
InternalServerError = 500,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Either } from '@standardnotes/common'
|
||||
|
||||
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
|
||||
import { HttpResponse } from '../../Http/HttpResponse'
|
||||
import { SessionRefreshResponseBody } from './SessionRefreshResponseBody'
|
||||
|
||||
export interface SessionRefreshResponse extends HttpResponse {
|
||||
data: Either<SessionRefreshResponseBody, HttpErrorResponseBody>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export type SessionRefreshResponseBody = {
|
||||
session: {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
access_expiration: number
|
||||
refresh_expiration: number
|
||||
readonly_access: boolean
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const SessionPaths = {
|
||||
refreshSession: '/v1/sessions/refresh',
|
||||
}
|
||||
|
||||
export const Paths = {
|
||||
v1: {
|
||||
...SessionPaths,
|
||||
},
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
import { type Invitation } from '@standardnotes/models'
|
||||
|
||||
import { ApiVersion } from '../../Api'
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { SubscriptionInviteResponse } from '../../Response'
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
|
||||
import { SubscriptionServer } from './SubscriptionServer'
|
||||
|
||||
describe('SubscriptionServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new SubscriptionServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
})
|
||||
|
||||
it('should invite a user to a shared subscription', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
|
||||
} as jest.Mocked<SubscriptionInviteResponse>)
|
||||
|
||||
const response = await createServer().invite({
|
||||
api: ApiVersion.v0,
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept an invite to a shared subscription', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
|
||||
|
||||
const response = await createServer().acceptInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should decline an invite to a shared subscription', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteDeclineResponse>)
|
||||
|
||||
const response = await createServer().declineInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should cancel an invite to a shared subscription', async () => {
|
||||
httpService.delete = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<SubscriptionInviteCancelResponse>)
|
||||
|
||||
const response = await createServer().cancelInvite({
|
||||
api: ApiVersion.v0,
|
||||
inviteUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should list invitations to a shared subscription', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
data: { invitations: [{} as jest.Mocked<Invitation>] },
|
||||
} as jest.Mocked<SubscriptionInviteListResponse>)
|
||||
|
||||
const response = await createServer().listInvites({
|
||||
api: ApiVersion.v0,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
invitations: [{} as jest.Mocked<Invitation>],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,54 +0,0 @@
|
||||
import { ProtocolVersion } from '@standardnotes/common'
|
||||
import { ApiVersion } from '../../Api'
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { UserDeletionResponse, UserRegistrationResponse } from '../../Response'
|
||||
import { UserServer } from './UserServer'
|
||||
|
||||
describe('UserServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new UserServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
|
||||
} as jest.Mocked<UserRegistrationResponse>)
|
||||
httpService.delete = jest.fn().mockReturnValue({
|
||||
data: { message: 'Success' },
|
||||
} as jest.Mocked<UserDeletionResponse>)
|
||||
})
|
||||
|
||||
it('should register a user', async () => {
|
||||
const response = await createServer().register({
|
||||
password: 'test',
|
||||
api: ApiVersion.v0,
|
||||
email: '[email protected]',
|
||||
ephemeral: false,
|
||||
version: ProtocolVersion.V004,
|
||||
pw_nonce: 'test',
|
||||
identifier: '[email protected]',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
user: {
|
||||
email: '[email protected]',
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should delete a user', async () => {
|
||||
const response = await createServer().deleteAccount({
|
||||
userUuid: '1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
message: 'Success',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { UserRequestType } from '@standardnotes/common'
|
||||
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
|
||||
|
||||
import { UserRequestServer } from './UserRequestServer'
|
||||
|
||||
describe('UserRequestServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new UserRequestServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<UserRequestResponse>)
|
||||
})
|
||||
|
||||
it('should submit a user request', async () => {
|
||||
const response = await createServer().submitUserRequest({
|
||||
userUuid: '1-2-3',
|
||||
requestType: UserRequestType.ExitDiscount,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import { HttpServiceInterface } from '../../Http'
|
||||
import { WebSocketConnectionTokenResponse } from '../../Response'
|
||||
|
||||
import { WebSocketServer } from './WebSocketServer'
|
||||
|
||||
describe('WebSocketServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new WebSocketServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { token: 'foobar' },
|
||||
} as jest.Mocked<WebSocketConnectionTokenResponse>)
|
||||
})
|
||||
|
||||
it('should create a websocket connection token', async () => {
|
||||
const response = await createServer().createConnectionToken({})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
token: 'foobar',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,124 +0,0 @@
|
||||
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
|
||||
|
||||
import { HttpServiceInterface, HttpStatusCode } from '../../Http'
|
||||
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
|
||||
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
|
||||
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
|
||||
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
|
||||
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
|
||||
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
|
||||
|
||||
import { WorkspaceServer } from './WorkspaceServer'
|
||||
|
||||
describe('WorkspaceServer', () => {
|
||||
let httpService: HttpServiceInterface
|
||||
|
||||
const createServer = () => new WorkspaceServer(httpService)
|
||||
|
||||
beforeEach(() => {
|
||||
httpService = {} as jest.Mocked<HttpServiceInterface>
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { uuid: '1-2-3' },
|
||||
} as jest.Mocked<WorkspaceCreationResponse>)
|
||||
})
|
||||
|
||||
it('should create a workspace', async () => {
|
||||
const response = await createServer().createWorkspace({
|
||||
workspaceType: WorkspaceType.Private,
|
||||
encryptedPrivateKey: 'foo',
|
||||
encryptedWorkspaceKey: 'bar',
|
||||
publicKey: 'buzz',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: '1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should inivte to a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { uuid: 'i-1-2-3' },
|
||||
} as jest.Mocked<WorkspaceInvitationResponse>)
|
||||
|
||||
const response = await createServer().inviteToWorkspace({
|
||||
inviteeEmail: '[email protected]',
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
uuid: 'i-1-2-3',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should accept invitation to a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
|
||||
|
||||
const response = await createServer().acceptInvite({
|
||||
encryptedPrivateKey: 'foo',
|
||||
inviteUuid: 'i-1-2-3',
|
||||
publicKey: 'bar',
|
||||
userUuid: 'u-1-2-3',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspaces', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
} as jest.Mocked<WorkspaceListResponse>)
|
||||
|
||||
const response = await createServer().listWorkspaces({})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should list workspace users', async () => {
|
||||
httpService.get = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { users: [] },
|
||||
} as jest.Mocked<WorkspaceUserListResponse>)
|
||||
|
||||
const response = await createServer().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: { users: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should initiate keyshare for user in a workspace', async () => {
|
||||
httpService.post = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCode.Success,
|
||||
data: { success: true },
|
||||
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
|
||||
|
||||
const response = await createServer().initiateKeyshare({
|
||||
workspaceUuid: 'w-1-2-3',
|
||||
userUuid: 'u-1-2-3',
|
||||
encryptedWorkspaceKey: 'foobar',
|
||||
})
|
||||
|
||||
expect(response).toEqual({
|
||||
status: 200,
|
||||
data: {
|
||||
success: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,81 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
# [1.12.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-26)
|
||||
|
||||
### Features
|
||||
|
||||
* Clicking the link option in Super notes will automatically add the selected text as the link ([326e1a4](https://github.com/standardnotes/app/commit/326e1a455bb510d795f357123f617618267848cc))
|
||||
|
||||
## [1.11.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
## [1.11.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
# [1.11.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-24)
|
||||
|
||||
### Features
|
||||
|
||||
* Persist collapsible block state in Super notes ([#2119](https://github.com/standardnotes/app/issues/2119)) ([577318e](https://github.com/standardnotes/app/commit/577318e208367dc52caf2d76fd87419eaead7ed4))
|
||||
|
||||
## [1.10.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
## [1.10.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
# [1.10.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Features
|
||||
|
||||
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
|
||||
|
||||
## [1.9.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve variance between Super header font sizes and weights ([8b10330](https://github.com/standardnotes/app/commit/8b103307cba41855713b8627cf5d66a6fef6ff52))
|
||||
|
||||
## [1.9.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Disable all-caps for H2/H3 headings in Super notes ([9b3f11d](https://github.com/standardnotes/app/commit/9b3f11d85b10914a03aab93803513766670c47fe))
|
||||
* Super note block drag-n-drop on mobile ([#2072](https://github.com/standardnotes/app/issues/2072)) ([349fd72](https://github.com/standardnotes/app/commit/349fd7204e209d56b720a3b0cc6b2bfc0fc50f47))
|
||||
|
||||
## [1.9.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
## [1.9.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-30)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
## [1.9.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Make super note styling consistent with plaintext notes ([#2068](https://github.com/standardnotes/app/issues/2068)) ([a417e5b](https://github.com/standardnotes/app/commit/a417e5ba8d3fc224b1527d0a9cfb0f25d197f9de))
|
||||
|
||||
## [1.9.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Only autofocus super notes when creating new note ([#2063](https://github.com/standardnotes/app/issues/2063)) ([96e8dfd](https://github.com/standardnotes/app/commit/96e8dfdd310fad88a360c4ac984b376b51a618d1))
|
||||
|
||||
## [1.9.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-28)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@standardnotes/blocks-editor",
|
||||
"version": "1.9.1",
|
||||
"version": "1.12.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"tsc": "tsc -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.6.0",
|
||||
"@lexical/react": "0.7.5",
|
||||
"@standardnotes/icons": "workspace:*",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"lexical": "^0.6.0",
|
||||
"lexical": "0.7.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import {FunctionComponent, useCallback, useState} from 'react';
|
||||
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
||||
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
||||
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
|
||||
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
|
||||
import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
|
||||
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
|
||||
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
||||
@@ -22,10 +21,10 @@ import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin';
|
||||
import CodeHighlightPlugin from '../Lexical/Plugins/CodeHighlightPlugin';
|
||||
import FloatingTextFormatToolbarPlugin from '../Lexical/Plugins/FloatingTextFormatToolbarPlugin';
|
||||
import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugin';
|
||||
import {TabIndentationPlugin} from '../Lexical/Plugins/TabIndentationPlugin';
|
||||
import {truncateString} from './Utils';
|
||||
import {SuperEditorContentId} from './Constants';
|
||||
import {classNames} from '@standardnotes/utils';
|
||||
import {EditorLineHeight} from '@standardnotes/snjs';
|
||||
import {MarkdownTransformers} from './MarkdownTransformers';
|
||||
|
||||
type BlocksEditorProps = {
|
||||
@@ -35,7 +34,6 @@ type BlocksEditorProps = {
|
||||
previewLength?: number;
|
||||
spellcheck?: boolean;
|
||||
ignoreFirstChange?: boolean;
|
||||
lineHeight?: EditorLineHeight;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
@@ -46,7 +44,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
previewLength,
|
||||
spellcheck,
|
||||
ignoreFirstChange = false,
|
||||
lineHeight,
|
||||
readonly,
|
||||
}) => {
|
||||
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
|
||||
@@ -104,7 +101,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
id={SuperEditorContentId}
|
||||
className={classNames(
|
||||
'ContentEditable__root overflow-y-auto',
|
||||
lineHeight && `leading-${lineHeight.toLowerCase()}`,
|
||||
className,
|
||||
)}
|
||||
spellCheck={spellcheck}
|
||||
@@ -112,7 +108,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
placeholder=""
|
||||
placeholder={null}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
<ListPlugin />
|
||||
@@ -121,7 +117,6 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
<OnChangePlugin onChange={handleChange} ignoreSelectionChange={true} />
|
||||
<HistoryPlugin />
|
||||
<HorizontalRulePlugin />
|
||||
<AutoFocusPlugin />
|
||||
<ClearEditorPlugin />
|
||||
<CheckListPlugin />
|
||||
<CodeHighlightPlugin />
|
||||
@@ -131,6 +126,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
<TwitterPlugin />
|
||||
<YouTubePlugin />
|
||||
<CollapsiblePlugin />
|
||||
<TabIndentationPlugin />
|
||||
{!readonly && floatingAnchorElem && (
|
||||
<>
|
||||
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
||||
|
||||
@@ -192,9 +192,10 @@ export function AutoEmbedDialog({
|
||||
const embedResult =
|
||||
text != null && urlMatch != null ? embedConfig.parseUrl(text) : null;
|
||||
|
||||
const onClick = () => {
|
||||
if (embedResult != null) {
|
||||
embedConfig.insertNode(editor, embedResult);
|
||||
const onClick = async () => {
|
||||
const result = await embedResult;
|
||||
if (result != null) {
|
||||
embedConfig.insertNode(editor, result);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
+16
-10
@@ -20,6 +20,7 @@ type SerializedCollapsibleContainerNode = Spread<
|
||||
{
|
||||
type: 'collapsible-container';
|
||||
version: 1;
|
||||
open: boolean;
|
||||
},
|
||||
SerializedElementNode
|
||||
>;
|
||||
@@ -29,25 +30,27 @@ export class CollapsibleContainerNode extends ElementNode {
|
||||
|
||||
constructor(open: boolean, key?: NodeKey) {
|
||||
super(key);
|
||||
this.__open = open;
|
||||
this.__open = open ?? false;
|
||||
}
|
||||
|
||||
static getType(): string {
|
||||
static override getType(): string {
|
||||
return 'collapsible-container';
|
||||
}
|
||||
|
||||
static clone(node: CollapsibleContainerNode): CollapsibleContainerNode {
|
||||
static override clone(
|
||||
node: CollapsibleContainerNode,
|
||||
): CollapsibleContainerNode {
|
||||
return new CollapsibleContainerNode(node.__open, node.__key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
override createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = document.createElement('details');
|
||||
dom.classList.add('Collapsible__container');
|
||||
dom.open = this.__open;
|
||||
return dom;
|
||||
}
|
||||
|
||||
updateDOM(
|
||||
override updateDOM(
|
||||
prevNode: CollapsibleContainerNode,
|
||||
dom: HTMLDetailsElement,
|
||||
): boolean {
|
||||
@@ -62,18 +65,19 @@ export class CollapsibleContainerNode extends ElementNode {
|
||||
return {};
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
static override importJSON(
|
||||
serializedNode: SerializedCollapsibleContainerNode,
|
||||
): CollapsibleContainerNode {
|
||||
const node = $createCollapsibleContainerNode();
|
||||
const node = $createCollapsibleContainerNode(serializedNode.open);
|
||||
return node;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCollapsibleContainerNode {
|
||||
override exportJSON(): SerializedCollapsibleContainerNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'collapsible-container',
|
||||
version: 1,
|
||||
open: this.__open,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -91,8 +95,10 @@ export class CollapsibleContainerNode extends ElementNode {
|
||||
}
|
||||
}
|
||||
|
||||
export function $createCollapsibleContainerNode(): CollapsibleContainerNode {
|
||||
return new CollapsibleContainerNode(true);
|
||||
export function $createCollapsibleContainerNode(
|
||||
open: boolean,
|
||||
): CollapsibleContainerNode {
|
||||
return new CollapsibleContainerNode(open);
|
||||
}
|
||||
|
||||
export function $isCollapsibleContainerNode(
|
||||
|
||||
+10
-7
@@ -24,21 +24,24 @@ type SerializedCollapsibleContentNode = Spread<
|
||||
>;
|
||||
|
||||
export class CollapsibleContentNode extends ElementNode {
|
||||
static getType(): string {
|
||||
static override getType(): string {
|
||||
return 'collapsible-content';
|
||||
}
|
||||
|
||||
static clone(node: CollapsibleContentNode): CollapsibleContentNode {
|
||||
static override clone(node: CollapsibleContentNode): CollapsibleContentNode {
|
||||
return new CollapsibleContentNode(node.__key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig): HTMLElement {
|
||||
override createDOM(config: EditorConfig): HTMLElement {
|
||||
const dom = document.createElement('div');
|
||||
dom.classList.add('Collapsible__content');
|
||||
return dom;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: CollapsibleContentNode, dom: HTMLElement): boolean {
|
||||
override updateDOM(
|
||||
prevNode: CollapsibleContentNode,
|
||||
dom: HTMLElement,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -46,17 +49,17 @@ export class CollapsibleContentNode extends ElementNode {
|
||||
return {};
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
static override importJSON(
|
||||
serializedNode: SerializedCollapsibleContentNode,
|
||||
): CollapsibleContentNode {
|
||||
return $createCollapsibleContentNode();
|
||||
}
|
||||
|
||||
isShadowRoot(): boolean {
|
||||
override isShadowRoot(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCollapsibleContentNode {
|
||||
override exportJSON(): SerializedCollapsibleContentNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'collapsible-content',
|
||||
|
||||
+21
-8
@@ -31,21 +31,34 @@ type SerializedCollapsibleTitleNode = Spread<
|
||||
>;
|
||||
|
||||
export class CollapsibleTitleNode extends ElementNode {
|
||||
static getType(): string {
|
||||
static override getType(): string {
|
||||
return 'collapsible-title';
|
||||
}
|
||||
|
||||
static clone(node: CollapsibleTitleNode): CollapsibleTitleNode {
|
||||
static override clone(node: CollapsibleTitleNode): CollapsibleTitleNode {
|
||||
return new CollapsibleTitleNode(node.__key);
|
||||
}
|
||||
|
||||
createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
|
||||
override createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
|
||||
const dom = document.createElement('summary');
|
||||
dom.classList.add('Collapsible__title');
|
||||
dom.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
editor.update(() => {
|
||||
const containerNode = this.getParentOrThrow();
|
||||
if ($isCollapsibleContainerNode(containerNode)) {
|
||||
containerNode.toggleOpen();
|
||||
}
|
||||
});
|
||||
};
|
||||
return dom;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: CollapsibleTitleNode, dom: HTMLElement): boolean {
|
||||
override updateDOM(
|
||||
prevNode: CollapsibleTitleNode,
|
||||
dom: HTMLElement,
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -53,13 +66,13 @@ export class CollapsibleTitleNode extends ElementNode {
|
||||
return {};
|
||||
}
|
||||
|
||||
static importJSON(
|
||||
static override importJSON(
|
||||
serializedNode: SerializedCollapsibleTitleNode,
|
||||
): CollapsibleTitleNode {
|
||||
return $createCollapsibleTitleNode();
|
||||
}
|
||||
|
||||
exportJSON(): SerializedCollapsibleTitleNode {
|
||||
override exportJSON(): SerializedCollapsibleTitleNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'collapsible-title',
|
||||
@@ -67,12 +80,12 @@ export class CollapsibleTitleNode extends ElementNode {
|
||||
};
|
||||
}
|
||||
|
||||
collapseAtStart(_selection: RangeSelection): boolean {
|
||||
override collapseAtStart(_selection: RangeSelection): boolean {
|
||||
this.getParentOrThrow().insertBefore(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
insertNewAfter(): ElementNode {
|
||||
override insertNewAfter(): ElementNode {
|
||||
const containerNode = this.getParentOrThrow();
|
||||
|
||||
if (!$isCollapsibleContainerNode(containerNode)) {
|
||||
|
||||
@@ -204,7 +204,7 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
||||
const content = $createCollapsibleContentNode().append(
|
||||
$createParagraphNode(),
|
||||
);
|
||||
const container = $createCollapsibleContainerNode().append(
|
||||
const container = $createCollapsibleContainerNode(true).append(
|
||||
title,
|
||||
content,
|
||||
);
|
||||
|
||||
@@ -12,13 +12,22 @@ import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$getNearestNodeFromDOMNode,
|
||||
$getNodeByKey,
|
||||
$getRoot,
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
DRAGOVER_COMMAND,
|
||||
DROP_COMMAND,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
} from 'lexical';
|
||||
import {DragEvent as ReactDragEvent, useEffect, useRef, useState} from 'react';
|
||||
import {
|
||||
DragEvent as ReactDragEvent,
|
||||
TouchEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
import {BlockIcon} from '@standardnotes/icons';
|
||||
|
||||
@@ -26,7 +35,7 @@ import {isHTMLElement} from '../../Utils/guard';
|
||||
import {Point} from '../../Utils/point';
|
||||
import {ContainsPointReturn, Rect} from '../../Utils/rect';
|
||||
|
||||
const SPACE = 4;
|
||||
const DRAGGABLE_BLOCK_MENU_LEFT_SPACE = -2;
|
||||
const TARGET_LINE_HALF_HEIGHT = 2;
|
||||
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
|
||||
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
|
||||
@@ -50,18 +59,16 @@ function getCurrentIndex(keysLength: number): number {
|
||||
}
|
||||
|
||||
function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
|
||||
const root = editor.getEditorState()._nodeMap.get('root');
|
||||
return root ? root.__children : [];
|
||||
return editor.getEditorState().read(() => $getRoot().getChildrenKeys());
|
||||
}
|
||||
|
||||
function elementContainingEventLocation(
|
||||
anchorElem: HTMLElement,
|
||||
element: HTMLElement,
|
||||
event: MouseEvent,
|
||||
eventLocation: Point,
|
||||
): {contains: ContainsPointReturn; element: HTMLElement} {
|
||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
||||
|
||||
const eventLocation = new Point(event.x, event.y);
|
||||
const elementDomRect = Rect.fromDOM(element);
|
||||
const {marginTop, marginBottom} = window.getComputedStyle(element);
|
||||
|
||||
@@ -87,7 +94,7 @@ function elementContainingEventLocation(
|
||||
const childResult = elementContainingEventLocation(
|
||||
anchorElem,
|
||||
child as HTMLElement,
|
||||
event,
|
||||
eventLocation,
|
||||
);
|
||||
|
||||
if (childResult.contains.result) {
|
||||
@@ -102,7 +109,7 @@ function elementContainingEventLocation(
|
||||
function getBlockElement(
|
||||
anchorElem: HTMLElement,
|
||||
editor: LexicalEditor,
|
||||
event: MouseEvent,
|
||||
eventLocation: Point,
|
||||
): HTMLElement | null {
|
||||
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
|
||||
|
||||
@@ -121,7 +128,7 @@ function getBlockElement(
|
||||
const {contains, element} = elementContainingEventLocation(
|
||||
anchorElem,
|
||||
elem,
|
||||
event,
|
||||
eventLocation,
|
||||
);
|
||||
|
||||
if (contains.result) {
|
||||
@@ -172,7 +179,7 @@ function setMenuPosition(
|
||||
(parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 -
|
||||
anchorElementRect.top;
|
||||
|
||||
const left = SPACE;
|
||||
const left = DRAGGABLE_BLOCK_MENU_LEFT_SPACE;
|
||||
|
||||
floatingElem.style.opacity = '1';
|
||||
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
||||
@@ -214,11 +221,12 @@ function setTargetLine(
|
||||
}
|
||||
|
||||
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
|
||||
const left = TEXT_BOX_HORIZONTAL_PADDING - SPACE;
|
||||
const left = TEXT_BOX_HORIZONTAL_PADDING - DRAGGABLE_BLOCK_MENU_LEFT_SPACE;
|
||||
|
||||
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
|
||||
targetLineElem.style.width = `${
|
||||
anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - SPACE) * 2
|
||||
anchorWidth -
|
||||
(TEXT_BOX_HORIZONTAL_PADDING - DRAGGABLE_BLOCK_MENU_LEFT_SPACE) * 2
|
||||
}px`;
|
||||
targetLineElem.style.opacity = '.6';
|
||||
}
|
||||
@@ -240,6 +248,7 @@ function useDraggableBlockMenu(
|
||||
const targetLineRef = useRef<HTMLDivElement>(null);
|
||||
const [draggableBlockElem, setDraggableBlockElem] =
|
||||
useState<HTMLElement | null>(null);
|
||||
const dragDataRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
@@ -253,7 +262,11 @@ function useDraggableBlockMenu(
|
||||
return;
|
||||
}
|
||||
|
||||
const _draggableBlockElem = getBlockElement(anchorElem, editor, event);
|
||||
const _draggableBlockElem = getBlockElement(
|
||||
anchorElem,
|
||||
editor,
|
||||
new Point(event.clientX, event.clientY),
|
||||
);
|
||||
|
||||
setDraggableBlockElem(_draggableBlockElem);
|
||||
}
|
||||
@@ -277,54 +290,13 @@ function useDraggableBlockMenu(
|
||||
}
|
||||
}, [anchorElem, draggableBlockElem]);
|
||||
|
||||
useEffect(() => {
|
||||
function onDragover(event: DragEvent): boolean {
|
||||
const [isFileTransfer] = eventFiles(event);
|
||||
if (isFileTransfer) {
|
||||
return false;
|
||||
}
|
||||
const {pageY, target} = event;
|
||||
if (!isHTMLElement(target)) {
|
||||
return false;
|
||||
}
|
||||
const targetBlockElem = getBlockElement(anchorElem, editor, event);
|
||||
const targetLineElem = targetLineRef.current;
|
||||
if (targetBlockElem === null || targetLineElem === null) {
|
||||
return false;
|
||||
}
|
||||
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
|
||||
// Prevent default event to be able to trigger onDrop events
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onDrop(event: DragEvent): boolean {
|
||||
const [isFileTransfer] = eventFiles(event);
|
||||
if (isFileTransfer) {
|
||||
return false;
|
||||
}
|
||||
const {target, dataTransfer, pageY} = event;
|
||||
const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || '';
|
||||
const draggedNode = $getNodeByKey(dragData);
|
||||
if (!draggedNode) {
|
||||
return false;
|
||||
}
|
||||
if (!isHTMLElement(target)) {
|
||||
return false;
|
||||
}
|
||||
const targetBlockElem = getBlockElement(anchorElem, editor, event);
|
||||
if (!targetBlockElem) {
|
||||
return false;
|
||||
}
|
||||
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
||||
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
if (targetNode === draggedNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const insertDraggedNode = useCallback(
|
||||
(
|
||||
draggedNode: LexicalNode,
|
||||
targetNode: LexicalNode,
|
||||
targetBlockElem: HTMLElement,
|
||||
pageY: number,
|
||||
) => {
|
||||
let nodeToInsert = draggedNode;
|
||||
const targetParent = targetNode.getParent();
|
||||
const sourceParent = draggedNode.getParent();
|
||||
@@ -342,6 +314,71 @@ function useDraggableBlockMenu(
|
||||
} else {
|
||||
targetNode.insertBefore(nodeToInsert);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function onDragover(event: DragEvent): boolean {
|
||||
const [isFileTransfer] = eventFiles(event);
|
||||
if (isFileTransfer) {
|
||||
return false;
|
||||
}
|
||||
const {pageY, target} = event;
|
||||
if (!isHTMLElement(target)) {
|
||||
return false;
|
||||
}
|
||||
const targetBlockElem = getBlockElement(
|
||||
anchorElem,
|
||||
editor,
|
||||
new Point(event.pageX, pageY),
|
||||
);
|
||||
const targetLineElem = targetLineRef.current;
|
||||
if (targetBlockElem === null || targetLineElem === null) {
|
||||
return false;
|
||||
}
|
||||
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
|
||||
// Prevent default event to be able to trigger onDrop events
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
function onDrop(event: DragEvent): boolean {
|
||||
const [isFileTransfer] = eventFiles(event);
|
||||
if (isFileTransfer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {target, dataTransfer, pageY} = event;
|
||||
if (!isHTMLElement(target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || '';
|
||||
const draggedNode = $getNodeByKey(dragData);
|
||||
if (!draggedNode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetBlockElem = getBlockElement(
|
||||
anchorElem,
|
||||
editor,
|
||||
new Point(event.pageX, pageY),
|
||||
);
|
||||
if (!targetBlockElem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
||||
if (!targetNode) {
|
||||
return false;
|
||||
}
|
||||
if (targetNode === draggedNode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
insertDraggedNode(draggedNode, targetNode, targetBlockElem, event.pageY);
|
||||
|
||||
setDraggableBlockElem(null);
|
||||
|
||||
return true;
|
||||
@@ -363,7 +400,7 @@ function useDraggableBlockMenu(
|
||||
COMMAND_PRIORITY_HIGH,
|
||||
),
|
||||
);
|
||||
}, [anchorElem, editor]);
|
||||
}, [anchorElem, editor, insertDraggedNode]);
|
||||
|
||||
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
|
||||
const dataTransfer = event.dataTransfer;
|
||||
@@ -385,6 +422,79 @@ function useDraggableBlockMenu(
|
||||
hideTargetLine(targetLineRef.current);
|
||||
}
|
||||
|
||||
function onTouchStart(): void {
|
||||
if (!draggableBlockElem) {
|
||||
return;
|
||||
}
|
||||
editor.update(() => {
|
||||
const node = $getNearestNodeFromDOMNode(draggableBlockElem);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const nodeKey = node.getKey();
|
||||
dragDataRef.current = nodeKey;
|
||||
});
|
||||
}
|
||||
|
||||
function onTouchMove(event: TouchEvent) {
|
||||
const {pageX, pageY} = event.targetTouches[0];
|
||||
const rootElement = editor.getRootElement();
|
||||
if (rootElement) {
|
||||
const {top, bottom} = rootElement.getBoundingClientRect();
|
||||
const scrollOffset = 20;
|
||||
if (pageY - top < scrollOffset) {
|
||||
rootElement.scrollTop -= scrollOffset;
|
||||
} else if (bottom - pageY < scrollOffset) {
|
||||
rootElement.scrollTop += scrollOffset;
|
||||
}
|
||||
}
|
||||
const targetBlockElem = getBlockElement(
|
||||
anchorElem,
|
||||
editor,
|
||||
new Point(pageX, pageY),
|
||||
);
|
||||
const targetLineElem = targetLineRef.current;
|
||||
if (targetBlockElem === null || targetLineElem === null) {
|
||||
return;
|
||||
}
|
||||
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
|
||||
}
|
||||
|
||||
function onTouchEnd(event: TouchEvent): void {
|
||||
hideTargetLine(targetLineRef.current);
|
||||
|
||||
editor.update(() => {
|
||||
const {pageX, pageY} = event.changedTouches[0];
|
||||
|
||||
const dragData = dragDataRef.current || '';
|
||||
const draggedNode = $getNodeByKey(dragData);
|
||||
if (!draggedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetBlockElem = getBlockElement(
|
||||
anchorElem,
|
||||
editor,
|
||||
new Point(pageX, pageY),
|
||||
);
|
||||
if (!targetBlockElem) {
|
||||
return;
|
||||
}
|
||||
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
||||
|
||||
if (!targetNode) {
|
||||
return;
|
||||
}
|
||||
if (targetNode === draggedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertDraggedNode(draggedNode, targetNode, targetBlockElem, pageY);
|
||||
});
|
||||
|
||||
setDraggableBlockElem(null);
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
<div
|
||||
@@ -392,7 +502,10 @@ function useDraggableBlockMenu(
|
||||
ref={menuRef}
|
||||
draggable={true}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}>
|
||||
onDragEnd={onDragEnd}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchMove={onTouchMove}
|
||||
onTouchEnd={onTouchEnd}>
|
||||
<div className={isEditable ? 'icon' : ''}>
|
||||
<BlockIcon className="text-text pointer-events-none" />
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user