mirror of
https://github.com/standardnotes/app
synced 2026-09-16 00:46:00 -04:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
173bacdfa3 | ||
|
|
c503d781ae | ||
|
|
9fb85cd77a | ||
|
|
bd40a49484 | ||
|
|
4a1612c7dc | ||
|
|
d3ed4391b7 | ||
|
|
856e499d89 | ||
|
|
4f0805d5b0 | ||
|
|
401a35f4db | ||
|
|
83b484002c | ||
|
|
30310dbb45 | ||
|
|
c6e3580678 | ||
|
|
ea285a36d8 | ||
|
|
1addaf4c39 | ||
|
|
92bba942f1 | ||
|
|
f71e6fe703 | ||
|
|
143b528b73 | ||
|
|
5d202a823e | ||
|
|
8d7d577b1c | ||
|
|
76f23e13ec | ||
|
|
4f3b258363 | ||
|
|
9cc3304741 | ||
|
|
4432f1cb4c | ||
|
|
6c47f95748 | ||
|
|
57fab4f670 | ||
|
|
fb7b9350b2 | ||
|
|
fb72c2fdae | ||
|
|
050fa9cabc | ||
|
|
a8535cfa23 | ||
|
|
27bb4ebdae | ||
|
|
60a8e8b92b | ||
|
|
1f07d9fa17 | ||
|
|
d333d4f188 | ||
|
|
7e3d7a8825 | ||
|
|
e99c7b7c51 | ||
|
|
0282a7958a | ||
|
|
4030953b00 | ||
|
|
ba9a9f8d01 | ||
|
|
80e76ee52c | ||
|
|
bbebfb42fd | ||
|
|
9954bdc29f | ||
|
|
e4de9c1be7 | ||
|
|
07ffc01ede | ||
|
|
eec7ae1589 | ||
|
|
361f773f2c | ||
|
|
5ffd37d447 | ||
|
|
2b66ff82ee | ||
|
|
c83dc48d3f | ||
|
|
8dc7eaf3a4 | ||
|
|
e40cd157b6 | ||
|
|
66805fa552 |
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"extends": ["./node_modules/@standardnotes/config/src/.eslintrc"],
|
||||
"rules": {
|
||||
"max-classes-per-file": ["error", 1]
|
||||
},
|
||||
"ignorePatterns": [".eslintrc.js", "*.webpack.*.js", "webpack-defaults.js", "jest.config.js", "__mocks__", "**/**/coverage"]
|
||||
}
|
||||
|
||||
@@ -36,11 +36,23 @@ jobs:
|
||||
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
|
||||
id: extract_branch
|
||||
|
||||
- name: Find open PR
|
||||
uses: juliangruber/find-pull-request-action@v1
|
||||
id: find-pull-request
|
||||
with:
|
||||
branch: ${{ steps.extract_branch.outputs.branch }}
|
||||
- run: echo "Pull Request ${number} (${sha})"
|
||||
env:
|
||||
number: ${{ steps.find-pull-request.outputs.number }}
|
||||
sha: ${{ steps.find-pull-request.outputs.head-sha }}
|
||||
|
||||
- uses: peter-evans/create-pull-request@v4
|
||||
if: startsWith(github.event.head_commit.message, 'chore(deps)') == false && startsWith(github.event.head_commit.message, 'chore(deps-dev)') == false
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
branch: ${{ github.ref }}
|
||||
base: main
|
||||
title: ${{ steps.extract_branch.outputs.branch }}
|
||||
body: "Public PR: https://github.com/standardnotes/app/pull/${{ steps.find-pull-request.outputs.number }}"
|
||||
assignees: ${{ github.actor }}
|
||||
reviewers: ${{ github.actor }}
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@
|
||||
"css-loader": "^6.7.1",
|
||||
"eslint": "^8.17.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.0",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.1",
|
||||
"npm-check-updates": "^14.1.1",
|
||||
"prettier": "^2.6.2",
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.17.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
|
||||
|
||||
# [1.17.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** add accepting invites by the client ([4f0805d](https://github.com/standardnotes/app/commit/4f0805d5b078e5a3524f5d6699b45dfd7e3456f3))
|
||||
|
||||
## [1.16.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.16.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.16.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.16.3",
|
||||
"version": "1.17.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -2,4 +2,5 @@ export enum SubscriptionApiOperations {
|
||||
Inviting,
|
||||
CancelingInvite,
|
||||
ListingInvites,
|
||||
AcceptingInvite,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { 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'
|
||||
@@ -24,6 +25,9 @@ describe('SubscriptionApiService', () => {
|
||||
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 () => {
|
||||
@@ -160,4 +164,48 @@ describe('SubscriptionApiService', () => {
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,9 +6,11 @@ import { SubscriptionServerInterface } from '../../Server/Subscription/Subscript
|
||||
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
|
||||
import { SubscriptionApiServiceInterface } from './SubscriptionApiServiceInterface'
|
||||
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export class SubscriptionApiService implements SubscriptionApiServiceInterface {
|
||||
private operationsInProgress: Map<SubscriptionApiOperations, boolean>
|
||||
@@ -78,4 +80,24 @@ export class SubscriptionApiService implements SubscriptionApiServiceInterface {
|
||||
throw new ApiCallError(ErrorMessage.GenericFail)
|
||||
}
|
||||
}
|
||||
|
||||
async acceptInvite(inviteUuid: Uuid): Promise<SubscriptionInviteAcceptResponse> {
|
||||
if (this.operationsInProgress.get(SubscriptionApiOperations.AcceptingInvite)) {
|
||||
throw new ApiCallError(ErrorMessage.GenericInProgress)
|
||||
}
|
||||
|
||||
this.operationsInProgress.set(SubscriptionApiOperations.AcceptingInvite, true)
|
||||
|
||||
try {
|
||||
const response = await this.subscriptionServer.acceptInvite({
|
||||
inviteUuid,
|
||||
})
|
||||
|
||||
this.operationsInProgress.set(SubscriptionApiOperations.AcceptingInvite, false)
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
throw new ApiCallError(ErrorMessage.GenericFail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
|
||||
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
|
||||
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
|
||||
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
|
||||
@@ -8,4 +9,5 @@ export interface SubscriptionApiServiceInterface {
|
||||
invite(inviteeEmail: string): Promise<SubscriptionInviteResponse>
|
||||
listInvites(): Promise<SubscriptionInviteListResponse>
|
||||
cancelInvite(inviteUuid: Uuid): Promise<SubscriptionInviteCancelResponse>
|
||||
acceptInvite(inviteUuid: Uuid): Promise<SubscriptionInviteAcceptResponse>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
import { ApiEndpointParam } from '../ApiEndpointParam'
|
||||
import { ApiVersion } from '../../Api/ApiVersion'
|
||||
|
||||
export type SubscriptionInviteAcceptRequestParams = {
|
||||
[ApiEndpointParam.ApiVersion]: ApiVersion.v0
|
||||
inviteUuid: Uuid
|
||||
[additionalParam: string]: unknown
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,3 +1 @@
|
||||
export type SubscriptionInviteAcceptResponseBody = {
|
||||
success: boolean
|
||||
}
|
||||
export type SubscriptionInviteAcceptResponseBody = { success: true } | { success: false; message: string }
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.7.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "chore: remove deprecated editors folder" ([fc00b5c](https://github.com/standardnotes/app/commit/fc00b5c4996045750589c3e08c139d5d39bf4140))
|
||||
* Revert "chore: fix code editor build" ([bb8ac49](https://github.com/standardnotes/app/commit/bb8ac49052136b3f688f19df5c71c2c32a85ed51))
|
||||
|
||||
## [2.7.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/components-meta
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/components-meta",
|
||||
"version": "2.7.20",
|
||||
"version": "2.7.21",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
"main": "dist",
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.23.234](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.233](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.232](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.231](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.230](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.229](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.228](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.227](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.226](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.225](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.224](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.223](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.222](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.221](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.23.220](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.23.220",
|
||||
"version": "3.23.234",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.18.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.18.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.18.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.18.2",
|
||||
"version": "1.18.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -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.53.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
## [1.53.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/features",
|
||||
"version": "1.53.1",
|
||||
"version": "1.53.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -5,4 +5,5 @@ export enum NoteType {
|
||||
RichText = 'rich-text',
|
||||
Spreadsheet = 'spreadsheet',
|
||||
Task = 'task',
|
||||
Plain = 'plain-text',
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.24.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.24.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.24.4",
|
||||
"version": "1.24.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.11.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.11.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.11.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.11.4",
|
||||
"version": "1.11.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,64 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.43.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* help & feedback links on mobile ([#1849](https://github.com/standardnotes/app/issues/1849)) ([fb72c2f](https://github.com/standardnotes/app/commit/fb72c2fdae277cb4e348ce5ee550f0545f281dcd))
|
||||
|
||||
## [3.43.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.43.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="mailto"/>
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
<string>mailto</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.43.7",
|
||||
"version": "3.43.21",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -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.29.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue with not being able to unlink a file from a note ([#1836](https://github.com/standardnotes/app/issues/1836)) ([4030953](https://github.com/standardnotes/app/commit/4030953b00ad609dc2b0f5b66cfd971d449f6121))
|
||||
|
||||
# [1.29.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
### Features
|
||||
|
||||
* authorize notes for listed ([#1823](https://github.com/standardnotes/app/issues/1823)) ([9954bdc](https://github.com/standardnotes/app/commit/9954bdc29f9de2a42c4539d55749991d86a72943))
|
||||
|
||||
# [1.28.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.28.0",
|
||||
"version": "1.29.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@ export enum AppDataField {
|
||||
NotAvailableOnMobile = 'notAvailableOnMobile',
|
||||
MobileActive = 'mobileActive',
|
||||
LastSize = 'lastSize',
|
||||
PrefersPlainEditor = 'prefersPlainEditor',
|
||||
LegacyPrefersPlainEditor = 'prefersPlainEditor',
|
||||
ComponentInstallError = 'installError',
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FillItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { SNComponent } from './Component'
|
||||
import { ComponentContent } from './ComponentContent'
|
||||
import { PayloadTimestampDefaults } from '../../Abstract/Payload'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
|
||||
describe('component model', () => {
|
||||
it('valid hosted url should ignore url', () => {
|
||||
@@ -46,4 +47,42 @@ describe('component model', () => {
|
||||
expect(component.hasValidHostedUrl()).toBe(true)
|
||||
expect(component.hosted_url).toBe('http://foo.com')
|
||||
})
|
||||
|
||||
it('should return noteType as specified in package_info', () => {
|
||||
const component = new SNComponent(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Component,
|
||||
content: FillItemContent({
|
||||
package_info: {
|
||||
note_type: NoteType.Authentication,
|
||||
},
|
||||
} as ComponentContent),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
|
||||
expect(component.noteType).toEqual(NoteType.Authentication)
|
||||
})
|
||||
|
||||
it('should return plain as noteType if no note type defined in package_info', () => {
|
||||
const component = new SNComponent(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Component,
|
||||
content: FillItemContent({
|
||||
package_info: {},
|
||||
} as ComponentContent),
|
||||
...PayloadTimestampDefaults(),
|
||||
},
|
||||
PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
|
||||
expect(component.noteType).toEqual(NoteType.Plain)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ComponentFlag,
|
||||
ComponentPermission,
|
||||
FindNativeFeature,
|
||||
NoteType,
|
||||
} from '@standardnotes/features'
|
||||
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
|
||||
import { ComponentContent, ComponentInterface } from './ComponentContent'
|
||||
@@ -177,6 +178,10 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
|
||||
return this.package_info as ThirdPartyFeatureDescription
|
||||
}
|
||||
|
||||
public get noteType(): NoteType {
|
||||
return this.package_info.note_type || NoteType.Plain
|
||||
}
|
||||
|
||||
public get isDeprecated(): boolean {
|
||||
let flags: string[] = this.package_info.flags ?? []
|
||||
flags = flags.map((flag: string) => flag.toLowerCase())
|
||||
|
||||
@@ -4,7 +4,8 @@ import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/Dec
|
||||
import { FileMetadata } from './FileMetadata'
|
||||
import { FileProtocolV1 } from './FileProtocolV1'
|
||||
import { SortableItem } from '../../Runtime/Collection/CollectionSort'
|
||||
import { ConflictStrategy } from '../../Abstract/Item'
|
||||
import { ConflictStrategy, ItemInterface } from '../../Abstract/Item'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
|
||||
type EncryptedBytesLength = number
|
||||
type DecryptedBytesLength = number
|
||||
@@ -31,6 +32,8 @@ export type FileContentSpecialized = FileContentWithoutSize & FileMetadata & Siz
|
||||
|
||||
export type FileContent = FileContentSpecialized & ItemContent
|
||||
|
||||
export const isFile = (x: ItemInterface): x is FileItem => x.content_type === ContentType.File
|
||||
|
||||
export class FileItem
|
||||
extends DecryptedItem<FileContent>
|
||||
implements FileContentWithoutSize, Sizes, FileProtocolV1, FileMetadata, SortableItem
|
||||
|
||||
@@ -29,14 +29,9 @@ describe('SNNote Tests', () => {
|
||||
expect(note.preview_html).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should set mobilePrefersPlainEditor when given a valid choice', () => {
|
||||
const selected = createNote({
|
||||
mobilePrefersPlainEditor: true,
|
||||
})
|
||||
it('should not set default value for note type if none is provided', () => {
|
||||
const note = createNote({})
|
||||
|
||||
const unselected = createNote()
|
||||
|
||||
expect(selected.mobilePrefersPlainEditor).toBeTruthy()
|
||||
expect(unselected.mobilePrefersPlainEditor).toBe(undefined)
|
||||
expect(note.noteType).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AppDataField } from './../../Abstract/Item/Types/AppDataField'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem'
|
||||
import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface'
|
||||
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
|
||||
import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { NoteContent, NoteContentSpecialized } from './NoteContent'
|
||||
|
||||
@@ -10,12 +11,15 @@ export const isNote = (x: ItemInterface): x is SNNote => x.content_type === Cont
|
||||
export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpecialized {
|
||||
public readonly title: string
|
||||
public readonly text: string
|
||||
public readonly mobilePrefersPlainEditor?: boolean
|
||||
public readonly hidePreview: boolean = false
|
||||
public readonly preview_plain: string
|
||||
public readonly preview_html: string
|
||||
public readonly prefersPlainEditor: boolean
|
||||
public readonly spellcheck?: boolean
|
||||
public readonly noteType?: NoteType
|
||||
public readonly authorizedForListed: boolean
|
||||
|
||||
/** The package_info.identifier of the editor (not its uuid), such as org.standardnotes.advanced-markdown */
|
||||
public readonly editorIdentifier?: FeatureIdentifier | string
|
||||
|
||||
constructor(payload: DecryptedPayloadInterface<NoteContent>) {
|
||||
super(payload)
|
||||
@@ -26,9 +30,15 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
|
||||
this.preview_html = String(this.payload.content.preview_html || '')
|
||||
this.hidePreview = Boolean(this.payload.content.hidePreview)
|
||||
this.spellcheck = this.payload.content.spellcheck
|
||||
this.noteType = this.payload.content.noteType
|
||||
this.editorIdentifier = this.payload.content.editorIdentifier
|
||||
this.authorizedForListed = this.payload.content.authorizedForListed || false
|
||||
|
||||
this.prefersPlainEditor = this.getAppDomainValueWithDefault(AppDataField.PrefersPlainEditor, false)
|
||||
|
||||
this.mobilePrefersPlainEditor = this.payload.content.mobilePrefersPlainEditor
|
||||
if (!this.noteType) {
|
||||
const prefersPlain = this.getAppDomainValueWithDefault(AppDataField.LegacyPrefersPlainEditor, false)
|
||||
if (prefersPlain) {
|
||||
this.noteType = NoteType.Plain
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
|
||||
export interface NoteContentSpecialized {
|
||||
title: string
|
||||
text: string
|
||||
mobilePrefersPlainEditor?: boolean
|
||||
hidePreview?: boolean
|
||||
preview_plain?: string
|
||||
preview_html?: string
|
||||
spellcheck?: boolean
|
||||
noteType?: NoteType
|
||||
editorIdentifier?: FeatureIdentifier | string
|
||||
authorizedForListed?: boolean
|
||||
}
|
||||
|
||||
export type NoteContent = NoteContentSpecialized & ItemContent
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NoteMutator } from './NoteMutator'
|
||||
import { createNote } from './../../Utilities/Test/SpecUtils'
|
||||
import { MutationType } from '../../Abstract/Item'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
|
||||
describe('note mutator', () => {
|
||||
it('sets noteType', () => {
|
||||
const note = createNote({})
|
||||
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||
mutator.noteType = NoteType.Authentication
|
||||
const result = mutator.getResult()
|
||||
|
||||
expect(result.content.noteType).toEqual(NoteType.Authentication)
|
||||
})
|
||||
|
||||
it('sets editorIdentifier', () => {
|
||||
const note = createNote({})
|
||||
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||
mutator.editorIdentifier = FeatureIdentifier.MarkdownProEditor
|
||||
const result = mutator.getResult()
|
||||
|
||||
expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
|
||||
import { NoteContent } from './NoteContent'
|
||||
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
|
||||
import { SNNote } from './Note'
|
||||
import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ContentReferenceType } from '../../Abstract/Item'
|
||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||
|
||||
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
||||
set title(title: string) {
|
||||
@@ -27,14 +27,22 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
||||
this.mutableContent.preview_html = preview_html
|
||||
}
|
||||
|
||||
set prefersPlainEditor(prefersPlainEditor: boolean) {
|
||||
this.setAppDataItem(AppDataField.PrefersPlainEditor, prefersPlainEditor)
|
||||
}
|
||||
|
||||
set spellcheck(spellcheck: boolean) {
|
||||
this.mutableContent.spellcheck = spellcheck
|
||||
}
|
||||
|
||||
set noteType(noteType: NoteType) {
|
||||
this.mutableContent.noteType = noteType
|
||||
}
|
||||
|
||||
set editorIdentifier(identifier: FeatureIdentifier | string | undefined) {
|
||||
this.mutableContent.editorIdentifier = identifier
|
||||
}
|
||||
|
||||
set authorizedForListed(authorizedForListed: boolean) {
|
||||
this.mutableContent.authorizedForListed = authorizedForListed
|
||||
}
|
||||
|
||||
toggleSpellcheck(): void {
|
||||
if (this.mutableContent.spellcheck == undefined) {
|
||||
this.mutableContent.spellcheck = false
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { DecryptedPayloadInterface } from './../../Abstract/Payload/Interfaces/DecryptedPayload'
|
||||
import { ComponentContent } from '../../Syncable/Component/ComponentContent'
|
||||
import { ComponentArea } from '@standardnotes/features'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { ComponentMutator, SNComponent } from '../../Syncable/Component'
|
||||
import { CreateDecryptedItemFromPayload } from '../Item/ItemGenerator'
|
||||
import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection'
|
||||
import { MutationType } from '../../Abstract/Item/Types/MutationType'
|
||||
import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes'
|
||||
import { isDecryptedPayload } from '../../Abstract/Payload'
|
||||
import { SyncResolvedPayload } from '../../Runtime/Deltas/Utilities/SyncResolvedPayload'
|
||||
|
||||
export type AffectorFunction = (
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
duplicatePayload: FullyFormedPayloadInterface,
|
||||
baseCollection: ImmutablePayloadCollection<FullyFormedPayloadInterface>,
|
||||
) => SyncResolvedPayload[]
|
||||
|
||||
const NoteDuplicationAffectedPayloads: AffectorFunction = (
|
||||
basePayload: FullyFormedPayloadInterface,
|
||||
duplicatePayload: FullyFormedPayloadInterface,
|
||||
baseCollection: ImmutablePayloadCollection<FullyFormedPayloadInterface>,
|
||||
) => {
|
||||
/** If note has editor, maintain editor relationship in duplicate note */
|
||||
const components = baseCollection
|
||||
.all(ContentType.Component)
|
||||
.filter(isDecryptedPayload)
|
||||
.map((payload) => {
|
||||
return CreateDecryptedItemFromPayload<ComponentContent, SNComponent>(
|
||||
payload as DecryptedPayloadInterface<ComponentContent>,
|
||||
)
|
||||
})
|
||||
|
||||
const editor = components
|
||||
.filter((c) => c.area === ComponentArea.Editor)
|
||||
.find((e) => {
|
||||
return e.isExplicitlyEnabledForItem(basePayload.uuid)
|
||||
})
|
||||
|
||||
if (!editor) {
|
||||
return []
|
||||
}
|
||||
|
||||
/** Modify the editor to include new note */
|
||||
const mutator = new ComponentMutator(editor, MutationType.NoUpdateUserTimestamps)
|
||||
mutator.associateWithItem(duplicatePayload.uuid)
|
||||
|
||||
const result = mutator.getResult() as SyncResolvedPayload
|
||||
|
||||
return [result]
|
||||
}
|
||||
|
||||
export const AffectorMapping = {
|
||||
[ContentType.Note]: NoteDuplicationAffectedPayloads,
|
||||
} as Partial<Record<ContentType, AffectorFunction>>
|
||||
@@ -2,7 +2,6 @@ import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource'
|
||||
import { extendArray, UuidGenerator } from '@standardnotes/utils'
|
||||
import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection'
|
||||
import { ItemContent } from '../../Abstract/Content/ItemContent'
|
||||
import { AffectorMapping } from './AffectorFunction'
|
||||
import { PayloadsByUpdatingReferencingPayloadReferences } from './PayloadsByUpdatingReferencingPayloadReferences'
|
||||
import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck'
|
||||
import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes'
|
||||
@@ -69,13 +68,5 @@ export function PayloadsByDuplicating<C extends ItemContent = ItemContent>(dto:
|
||||
extendArray(results, updatedReferencing)
|
||||
}
|
||||
|
||||
const affector = AffectorMapping[payload.content_type]
|
||||
if (affector) {
|
||||
const affected = affector(payload, copy, baseCollection)
|
||||
if (affected) {
|
||||
extendArray(results, affected)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ export * from './Utilities/Item/FindItem'
|
||||
export * from './Utilities/Item/ItemContentsDiffer'
|
||||
export * from './Utilities/Item/ItemContentsEqual'
|
||||
export * from './Utilities/Item/ItemGenerator'
|
||||
export * from './Utilities/Payload/AffectorFunction'
|
||||
export * from './Utilities/Payload/CopyPayloadWithContentOverride'
|
||||
export * from './Utilities/Payload/CreatePayload'
|
||||
export * from './Utilities/Payload/FindPayload'
|
||||
|
||||
@@ -3,6 +3,62 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.3.163](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.162](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.161](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.160](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.159](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.158](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.157](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.156](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.155](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.154](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.153](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.152](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.151](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.150](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.3.149](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.3.149",
|
||||
"version": "1.3.163",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -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.11.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.11.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-13)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/responses",
|
||||
"version": "1.11.1",
|
||||
"version": "1.11.2",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.34.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.34.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.34.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue with not being able to unlink a file from a note ([#1836](https://github.com/standardnotes/app/issues/1836)) ([4030953](https://github.com/standardnotes/app/commit/4030953b00ad609dc2b0f5b66cfd971d449f6121))
|
||||
|
||||
# [1.34.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
### Features
|
||||
|
||||
* authorize notes for listed ([#1823](https://github.com/standardnotes/app/issues/1823)) ([9954bdc](https://github.com/standardnotes/app/commit/9954bdc29f9de2a42c4539d55749991d86a72943))
|
||||
|
||||
## [1.33.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.33.1",
|
||||
"version": "1.34.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -24,4 +24,5 @@ export enum ChallengeReason {
|
||||
UnprotectFile,
|
||||
UnprotectNote,
|
||||
DeleteAccount,
|
||||
AuthorizeNoteForListed,
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@ export interface ComponentManagerInterface {
|
||||
urlOverride?: string,
|
||||
): ComponentViewerInterface
|
||||
presentPermissionsDialog(_dialog: PermissionDialog): void
|
||||
getDefaultEditor(): SNComponent
|
||||
getDefaultEditor(): SNComponent | undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
export enum ItemRelationshipDirection {
|
||||
AReferencesB,
|
||||
BReferencesA,
|
||||
NoRelationship,
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
import { ContentType, Uuid } from '@standardnotes/common'
|
||||
import {
|
||||
SNNote,
|
||||
@@ -76,9 +78,9 @@ export interface ItemsClientInterface {
|
||||
linkNoteToNote(note: SNNote, otherNote: SNNote): Promise<SNNote>
|
||||
linkFileToFile(file: FileItem, otherFile: FileItem): Promise<FileItem>
|
||||
|
||||
unlinkItem(
|
||||
item: DecryptedItemInterface<ItemContent>,
|
||||
itemToUnlink: DecryptedItemInterface<ItemContent>,
|
||||
unlinkItems(
|
||||
itemOne: DecryptedItemInterface<ItemContent>,
|
||||
itemTwo: DecryptedItemInterface<ItemContent>,
|
||||
): Promise<DecryptedItemInterface<ItemContent>>
|
||||
|
||||
/**
|
||||
@@ -115,12 +117,6 @@ export interface ItemsClientInterface {
|
||||
*/
|
||||
getSortedTagsForItem(item: DecryptedItemInterface<ItemContent>): SNTag[]
|
||||
|
||||
getSortedLinkedFilesForItem(item: DecryptedItemInterface<ItemContent>): FileItem[]
|
||||
getSortedFilesLinkingToItem(item: DecryptedItemInterface<ItemContent>): FileItem[]
|
||||
|
||||
getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): SNNote[]
|
||||
getSortedNotesLinkingToItem(item: DecryptedItemInterface<ItemContent>): SNNote[]
|
||||
|
||||
isSmartViewTitle(title: string): boolean
|
||||
|
||||
getSmartViews(): SmartView[]
|
||||
@@ -152,12 +148,4 @@ export interface ItemsClientInterface {
|
||||
* @returns Whether the item is a template (unmanaged)
|
||||
*/
|
||||
isTemplateItem(item: DecryptedItemInterface): boolean
|
||||
|
||||
/**
|
||||
* @returns `'direct'` if `itemOne` has the reference to `itemTwo`, `'indirect'` if `itemTwo` has the reference to `itemOne`, `'unlinked'` if neither reference each other
|
||||
*/
|
||||
relationshipTypeForItems(
|
||||
itemOne: DecryptedItemInterface,
|
||||
itemTwo: DecryptedItemInterface,
|
||||
): 'direct' | 'indirect' | 'unlinked'
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export * from './Item/ItemCounterInterface'
|
||||
export * from './Item/ItemManagerInterface'
|
||||
export * from './Item/ItemsClientInterface'
|
||||
export * from './Item/ItemsServerInterface'
|
||||
export * from './Item/ItemRelationshipDirection'
|
||||
export * from './Mutator/MutatorClientInterface'
|
||||
export * from './Payloads/PayloadManagerInterface'
|
||||
export * from './Preferences/PreferenceServiceInterface'
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.142.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
|
||||
|
||||
## [2.142.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.142.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue with not being able to unlink a file from a note ([#1836](https://github.com/standardnotes/app/issues/1836)) ([4030953](https://github.com/standardnotes/app/commit/4030953b00ad609dc2b0f5b66cfd971d449f6121))
|
||||
|
||||
# [2.142.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
### Features
|
||||
|
||||
* authorize notes for listed ([#1823](https://github.com/standardnotes/app/issues/1823)) ([9954bdc](https://github.com/standardnotes/app/commit/9954bdc29f9de2a42c4539d55749991d86a72943))
|
||||
|
||||
## [2.141.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -68,7 +68,7 @@ import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
|
||||
import { SnjsVersion } from './../Version'
|
||||
import { SNLog } from '../Log'
|
||||
import { Challenge, ChallengeResponse } from '../Services'
|
||||
import { Challenge, ChallengeResponse, ListedClientInterface } from '../Services'
|
||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||
|
||||
@@ -86,9 +86,7 @@ type ApplicationObserver = {
|
||||
|
||||
type ObserverRemover = () => void
|
||||
|
||||
export class SNApplication
|
||||
implements ApplicationInterface, AppGroupManagedApplication, InternalServices.ListedClientInterface
|
||||
{
|
||||
export class SNApplication implements ApplicationInterface, AppGroupManagedApplication {
|
||||
onDeinit!: ExternalServices.DeinitCallback
|
||||
|
||||
/**
|
||||
@@ -273,6 +271,10 @@ export class SNApplication
|
||||
return this.componentManagerService
|
||||
}
|
||||
|
||||
public get listed(): ListedClientInterface {
|
||||
return this.listedService
|
||||
}
|
||||
|
||||
public computePrivateUsername(username: string): Promise<string | undefined> {
|
||||
return ComputePrivateUsername(this.options.crypto, username)
|
||||
}
|
||||
@@ -682,25 +684,6 @@ export class SNApplication
|
||||
return this.protectionService.authorizeSearchingProtectedNotesText()
|
||||
}
|
||||
|
||||
public canRegisterNewListedAccount(): boolean {
|
||||
return this.listedService.canRegisterNewListedAccount()
|
||||
}
|
||||
|
||||
public async requestNewListedAccount(): Promise<Responses.ListedAccount | undefined> {
|
||||
return this.listedService.requestNewListedAccount()
|
||||
}
|
||||
|
||||
public async getListedAccounts(): Promise<Responses.ListedAccount[]> {
|
||||
return this.listedService.getListedAccounts()
|
||||
}
|
||||
|
||||
public getListedAccountInfo(
|
||||
account: Responses.ListedAccount,
|
||||
inContextOfItem?: UuidString,
|
||||
): Promise<Responses.ListedAccountInfo | undefined> {
|
||||
return this.listedService.getListedAccountInfo(account, inContextOfItem)
|
||||
}
|
||||
|
||||
public async createEncryptedBackupFileForAutomatedDesktopBackups(): Promise<BackupFile | undefined> {
|
||||
return this.protocolService.createEncryptedBackupFile()
|
||||
}
|
||||
@@ -1096,11 +1079,11 @@ export class SNApplication
|
||||
this.createComponentManager()
|
||||
this.createMigrationService()
|
||||
this.createMfaService()
|
||||
this.createListedService()
|
||||
this.createActionsManager()
|
||||
this.createFileService()
|
||||
this.createIntegrityService()
|
||||
this.createMutatorService()
|
||||
this.createListedService()
|
||||
this.createActionsManager()
|
||||
this.createStatusService()
|
||||
|
||||
if (isDesktopDevice(this.deviceInterface)) {
|
||||
@@ -1175,6 +1158,8 @@ export class SNApplication
|
||||
this.itemManager,
|
||||
this.settingsService,
|
||||
this.deprecatedHttpService,
|
||||
this.protectionService,
|
||||
this.mutator,
|
||||
this.internalEventBus,
|
||||
)
|
||||
this.services.push(this.listedService)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { SNApplication } from './../Application/Application'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { MutatorService } from './../Services/Mutator/MutatorService'
|
||||
import { SNComponentManager } from './../Services/ComponentManager/ComponentManager'
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import { NoteViewController } from './NoteViewController'
|
||||
|
||||
describe('note view controller', () => {
|
||||
let application: SNApplication
|
||||
|
||||
beforeEach(() => {
|
||||
application = {} as jest.Mocked<SNApplication>
|
||||
application.streamItems = jest.fn()
|
||||
|
||||
const componentManager = {} as jest.Mocked<SNComponentManager>
|
||||
componentManager.getDefaultEditor = jest.fn()
|
||||
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
||||
|
||||
const mutator = {} as jest.Mocked<MutatorService>
|
||||
mutator.createTemplateItem = jest.fn()
|
||||
Object.defineProperty(application, 'mutator', { value: mutator })
|
||||
})
|
||||
|
||||
it('should create notes with plaintext note type', async () => {
|
||||
const controller = new NoteViewController(application)
|
||||
await controller.initialize(false)
|
||||
|
||||
expect(application.mutator.createTemplateItem).toHaveBeenCalledWith(
|
||||
ContentType.Note,
|
||||
expect.objectContaining({ noteType: NoteType.Plain }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { NoteType } from '@standardnotes/features'
|
||||
import {
|
||||
NoteMutator,
|
||||
SNNote,
|
||||
@@ -66,9 +67,12 @@ export class NoteViewController implements ItemViewControllerInterface {
|
||||
|
||||
async initialize(addTagHierarchy: boolean): Promise<void> {
|
||||
if (!this.item) {
|
||||
const editor = this.application.componentManager.getDefaultEditor()
|
||||
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(ContentType.Note, {
|
||||
text: '',
|
||||
title: this.defaultTitle || '',
|
||||
noteType: editor?.noteType || NoteType.Plain,
|
||||
editorIdentifier: editor?.identifier,
|
||||
references: [],
|
||||
})
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ export const ChallengeStrings = {
|
||||
SelectProtectedNote: 'Authentication is required to select a protected note',
|
||||
DisableMfa: 'Authentication is required to disable two-factor authentication',
|
||||
DeleteAccount: 'Authentication is required to delete your account',
|
||||
ListedAuthorization: 'Authentication is required to approve this note for Listed',
|
||||
}
|
||||
|
||||
export const ErrorAlertStrings = {
|
||||
|
||||
@@ -79,6 +79,8 @@ export class Challenge implements ChallengeInterface {
|
||||
return ChallengeStrings.DisableMfa
|
||||
case ChallengeReason.DeleteAccount:
|
||||
return ChallengeStrings.DeleteAccount
|
||||
case ChallengeReason.AuthorizeNoteForListed:
|
||||
return ChallengeStrings.ListedAuthorization
|
||||
case ChallengeReason.Custom:
|
||||
return ''
|
||||
default:
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
*/
|
||||
|
||||
import { SNPreferencesService } from '../Preferences/PreferencesService'
|
||||
import { createNote } from './../../Spec/SpecUtils'
|
||||
import {
|
||||
ComponentAction,
|
||||
ComponentPermission,
|
||||
FeatureDescription,
|
||||
FindNativeFeature,
|
||||
FeatureIdentifier,
|
||||
NoteType,
|
||||
} from '@standardnotes/features'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { GenericItem, SNComponent, Environment, Platform } from '@standardnotes/models'
|
||||
@@ -306,6 +308,26 @@ describe('featuresService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('editors', () => {
|
||||
it('getEditorForNote should return undefined is note type is plain', () => {
|
||||
const note = createNote({
|
||||
noteType: NoteType.Plain,
|
||||
})
|
||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
||||
|
||||
expect(manager.editorForNote(note)).toBe(undefined)
|
||||
})
|
||||
|
||||
it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => {
|
||||
const note = createNote({})
|
||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
||||
manager['legacyGetEditorForNote'] = jest.fn()
|
||||
manager.editorForNote(note)
|
||||
|
||||
expect(manager['legacyGetEditorForNote']).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('editor change alert', () => {
|
||||
it('should not require alert switching from plain editor', () => {
|
||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
||||
|
||||
@@ -17,7 +17,14 @@ import {
|
||||
import { SNSyncService } from '@Lib/Services/Sync/SyncService'
|
||||
import find from 'lodash/find'
|
||||
import uniq from 'lodash/uniq'
|
||||
import { ComponentArea, ComponentAction, ComponentPermission, FindNativeFeature } from '@standardnotes/features'
|
||||
import {
|
||||
ComponentArea,
|
||||
ComponentAction,
|
||||
ComponentPermission,
|
||||
FindNativeFeature,
|
||||
NoteType,
|
||||
FeatureIdentifier,
|
||||
} from '@standardnotes/features'
|
||||
import { Copy, filterFromArray, removeFromArray, sleep, assert } from '@standardnotes/utils'
|
||||
import { UuidString } from '@Lib/Types/UuidString'
|
||||
import { AllowedBatchContentTypes } from '@Lib/Services/ComponentManager/Types'
|
||||
@@ -112,6 +119,10 @@ export class SNComponentManager
|
||||
})
|
||||
}
|
||||
|
||||
componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined {
|
||||
return this.components.find((component) => component.identifier === identifier)
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
|
||||
@@ -569,13 +580,6 @@ export class SNComponentManager
|
||||
}
|
||||
|
||||
allComponentIframes(): HTMLIFrameElement[] {
|
||||
if (this.isMobile) {
|
||||
/**
|
||||
* Retrieving all iframes is typically related to lifecycle management of
|
||||
* non-editor components. So this function is not useful to mobile.
|
||||
*/
|
||||
return []
|
||||
}
|
||||
return Array.from(document.getElementsByTagName('iframe'))
|
||||
}
|
||||
|
||||
@@ -584,23 +588,29 @@ export class SNComponentManager
|
||||
}
|
||||
|
||||
editorForNote(note: SNNote): SNComponent | undefined {
|
||||
if (note.editorIdentifier) {
|
||||
return this.componentWithIdentifier(note.editorIdentifier)
|
||||
}
|
||||
|
||||
if (note.noteType === NoteType.Plain) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.legacyGetEditorForNote(note)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses legacy approach of note/editor association. New method uses note.editorIdentifier and note.noteType directly.
|
||||
*/
|
||||
private legacyGetEditorForNote(note: SNNote): SNComponent | undefined {
|
||||
const editors = this.componentsForArea(ComponentArea.Editor)
|
||||
for (const editor of editors) {
|
||||
if (editor.isExplicitlyEnabledForItem(note.uuid)) {
|
||||
return editor
|
||||
}
|
||||
}
|
||||
let defaultEditor
|
||||
/* No editor found for note. Use default editor, if note does not prefer system editor */
|
||||
if (this.isMobile) {
|
||||
if (!note.mobilePrefersPlainEditor) {
|
||||
defaultEditor = this.getDefaultEditor()
|
||||
}
|
||||
} else {
|
||||
if (!note.prefersPlainEditor) {
|
||||
defaultEditor = this.getDefaultEditor()
|
||||
}
|
||||
}
|
||||
const defaultEditor = this.getDefaultEditor()
|
||||
|
||||
if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) {
|
||||
return defaultEditor
|
||||
} else {
|
||||
@@ -608,15 +618,9 @@ export class SNComponentManager
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultEditor(): SNComponent {
|
||||
getDefaultEditor(): SNComponent | undefined {
|
||||
const editors = this.componentsForArea(ComponentArea.Editor)
|
||||
if (this.isMobile) {
|
||||
return editors.filter((e) => {
|
||||
return e.isMobileDefault
|
||||
})[0]
|
||||
} else {
|
||||
return editors.filter((e) => e.isDefaultEditor())[0]
|
||||
}
|
||||
return editors.filter((e) => e.isDefaultEditor())[0]
|
||||
}
|
||||
|
||||
permissionsStringForPermissions(permissions: ComponentPermission[], component: SNComponent): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { InternalEventBusInterface } from '@standardnotes/services'
|
||||
import { InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services'
|
||||
import { ItemManager } from './ItemManager'
|
||||
import { PayloadManager } from '../Payloads/PayloadManager'
|
||||
import { UuidGenerator } from '@standardnotes/utils'
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SmartView,
|
||||
SystemViewId,
|
||||
} from '@standardnotes/models'
|
||||
import { createNoteWithTitle } from '../../Spec/SpecUtils'
|
||||
|
||||
const setupRandomUuid = () => {
|
||||
UuidGenerator.SetGenerator(() => String(Math.random()))
|
||||
@@ -83,19 +84,6 @@ describe('itemManager', () => {
|
||||
)
|
||||
}
|
||||
|
||||
const createNote = (title: string) => {
|
||||
return new Models.SNNote(
|
||||
new Models.DecryptedPayload({
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: Models.FillItemContent<Models.NoteContent>({
|
||||
title: title,
|
||||
}),
|
||||
...PayloadTimestampDefaults(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const createFile = (name: string) => {
|
||||
return new Models.FileItem(
|
||||
new Models.DecryptedPayload({
|
||||
@@ -168,7 +156,7 @@ describe('itemManager', () => {
|
||||
it('viewing notes with tag', async () => {
|
||||
itemManager = createService()
|
||||
const tag = createTag('parent')
|
||||
const note = createNote('note')
|
||||
const note = createNoteWithTitle('note')
|
||||
await itemManager.insertItems([tag, note])
|
||||
await itemManager.addTagToNote(note, tag, false)
|
||||
|
||||
@@ -185,9 +173,9 @@ describe('itemManager', () => {
|
||||
it('viewing trashed notes smart view should include archived notes', async () => {
|
||||
itemManager = createService()
|
||||
|
||||
const archivedNote = createNote('archived')
|
||||
const trashedNote = createNote('trashed')
|
||||
const archivedAndTrashedNote = createNote('archived&trashed')
|
||||
const archivedNote = createNoteWithTitle('archived')
|
||||
const trashedNote = createNoteWithTitle('trashed')
|
||||
const archivedAndTrashedNote = createNoteWithTitle('archived&trashed')
|
||||
|
||||
await itemManager.insertItems([archivedNote, trashedNote, archivedAndTrashedNote])
|
||||
|
||||
@@ -389,8 +377,8 @@ describe('itemManager', () => {
|
||||
await itemManager.insertItems([parentTag, childTag])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
|
||||
const parentNote = createNote('parentNote')
|
||||
const childNote = createNote('childNote')
|
||||
const parentNote = createNoteWithTitle('parentNote')
|
||||
const childNote = createNoteWithTitle('childNote')
|
||||
await itemManager.insertItems([parentNote, childNote])
|
||||
|
||||
await itemManager.addTagToNote(parentNote, parentTag, false)
|
||||
@@ -477,7 +465,7 @@ describe('itemManager', () => {
|
||||
const fooDelimiter = await itemManager.createTag('bar.foo')
|
||||
const barFooDelimiter = await itemManager.createTag('baz.bar.foo')
|
||||
const fooAttached = await itemManager.createTag('Foo')
|
||||
const note = createNote('note')
|
||||
const note = createNoteWithTitle('note')
|
||||
await itemManager.insertItems([foo, foobar, bar, barfoo, fooDelimiter, barFooDelimiter, fooAttached, note])
|
||||
await itemManager.addTagToNote(note, fooAttached, false)
|
||||
|
||||
@@ -501,8 +489,8 @@ describe('itemManager', () => {
|
||||
await itemManager.insertItems([parentTag, childTag])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
|
||||
const parentNote = createNote('parentNote')
|
||||
const childNote = createNote('childNote')
|
||||
const parentNote = createNoteWithTitle('parentNote')
|
||||
const childNote = createNoteWithTitle('childNote')
|
||||
await itemManager.insertItems([parentNote, childNote])
|
||||
|
||||
await itemManager.addTagToNote(parentNote, parentTag, false)
|
||||
@@ -519,8 +507,8 @@ describe('itemManager', () => {
|
||||
const tag1 = createTag('tag 1')
|
||||
await itemManager.insertItems([tag1])
|
||||
|
||||
const note1 = createNote('note 1')
|
||||
const note2 = createNote('note 2')
|
||||
const note1 = createNoteWithTitle('note 1')
|
||||
const note2 = createNoteWithTitle('note 2')
|
||||
await itemManager.insertItems([note1, note2])
|
||||
|
||||
await itemManager.addTagToNote(note1, tag1, false)
|
||||
@@ -654,8 +642,8 @@ describe('itemManager', () => {
|
||||
const view = itemManager.untaggedNotesSmartView
|
||||
|
||||
const tag = createTag('tag')
|
||||
const untaggedNote = createNote('note')
|
||||
const taggedNote = createNote('taggedNote')
|
||||
const untaggedNote = createNoteWithTitle('note')
|
||||
const taggedNote = createNoteWithTitle('taggedNote')
|
||||
await itemManager.insertItems([tag, untaggedNote, taggedNote])
|
||||
|
||||
expect(itemManager.notesMatchingSmartView(view)).toHaveLength(2)
|
||||
@@ -704,7 +692,7 @@ describe('itemManager', () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
const note = createNote('note')
|
||||
const note = createNoteWithTitle('note')
|
||||
|
||||
await itemManager.insertItems([parentTag, childTag, note])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
@@ -722,7 +710,7 @@ describe('itemManager', () => {
|
||||
itemManager = createService()
|
||||
const parentTag = createTag('parent')
|
||||
const childTag = createTag('child')
|
||||
const note = createNote('note')
|
||||
const note = createNoteWithTitle('note')
|
||||
|
||||
await itemManager.insertItems([parentTag, childTag, note])
|
||||
await itemManager.setTagParent(parentTag, childTag)
|
||||
@@ -772,7 +760,7 @@ describe('itemManager', () => {
|
||||
|
||||
it('should link file with note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNote('invoices')
|
||||
const note = createNoteWithTitle('invoices')
|
||||
const file = createFile('invoice_1.pdf')
|
||||
await itemManager.insertItems([note, file])
|
||||
|
||||
@@ -785,7 +773,7 @@ describe('itemManager', () => {
|
||||
|
||||
it('should unlink file from note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNote('invoices')
|
||||
const note = createNoteWithTitle('invoices')
|
||||
const file = createFile('invoice_1.pdf')
|
||||
await itemManager.insertItems([note, file])
|
||||
|
||||
@@ -796,25 +784,10 @@ describe('itemManager', () => {
|
||||
expect(references).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should get files linked with note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNote('invoices')
|
||||
const file = createFile('invoice_1.pdf')
|
||||
const secondFile = createFile('unrelated-file.xlsx')
|
||||
await itemManager.insertItems([note, file, secondFile])
|
||||
|
||||
await itemManager.associateFileWithNote(file, note)
|
||||
|
||||
const filesAssociatedWithNote = itemManager.getSortedFilesLinkingToItem(note)
|
||||
|
||||
expect(filesAssociatedWithNote).toHaveLength(1)
|
||||
expect(filesAssociatedWithNote[0].uuid).toBe(file.uuid)
|
||||
})
|
||||
|
||||
it('should link note to note', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNote('research')
|
||||
const note2 = createNote('citation')
|
||||
const note = createNoteWithTitle('research')
|
||||
const note2 = createNoteWithTitle('citation')
|
||||
await itemManager.insertItems([note, note2])
|
||||
|
||||
const resultingNote = await itemManager.linkNoteToNote(note, note2)
|
||||
@@ -839,108 +812,56 @@ describe('itemManager', () => {
|
||||
|
||||
it('should get the relationship type for two items', async () => {
|
||||
itemManager = createService()
|
||||
const firstNote = createNote('First note')
|
||||
const secondNote = createNote('Second note')
|
||||
const unlinkedNote = createNote('Unlinked note')
|
||||
const firstNote = createNoteWithTitle('First note')
|
||||
const secondNote = createNoteWithTitle('Second note')
|
||||
const unlinkedNote = createNoteWithTitle('Unlinked note')
|
||||
await itemManager.insertItems([firstNote, secondNote, unlinkedNote])
|
||||
|
||||
const firstNoteLinkedToSecond = await itemManager.linkNoteToNote(firstNote, secondNote)
|
||||
|
||||
const relationshipOfFirstNoteToSecond = itemManager.relationshipTypeForItems(firstNoteLinkedToSecond, secondNote)
|
||||
const relationshipOfSecondNoteToFirst = itemManager.relationshipTypeForItems(secondNote, firstNoteLinkedToSecond)
|
||||
const relationshipOfFirstNoteToUnlinked = itemManager.relationshipTypeForItems(
|
||||
const relationshipOfFirstNoteToSecond = itemManager.relationshipDirectionBetweenItems(
|
||||
firstNoteLinkedToSecond,
|
||||
secondNote,
|
||||
)
|
||||
const relationshipOfSecondNoteToFirst = itemManager.relationshipDirectionBetweenItems(
|
||||
secondNote,
|
||||
firstNoteLinkedToSecond,
|
||||
)
|
||||
const relationshipOfFirstNoteToUnlinked = itemManager.relationshipDirectionBetweenItems(
|
||||
firstNoteLinkedToSecond,
|
||||
unlinkedNote,
|
||||
)
|
||||
|
||||
expect(relationshipOfFirstNoteToSecond).toBe('direct')
|
||||
expect(relationshipOfSecondNoteToFirst).toBe('indirect')
|
||||
expect(relationshipOfFirstNoteToUnlinked).toBe('unlinked')
|
||||
expect(relationshipOfFirstNoteToSecond).toBe(ItemRelationshipDirection.AReferencesB)
|
||||
expect(relationshipOfSecondNoteToFirst).toBe(ItemRelationshipDirection.BReferencesA)
|
||||
expect(relationshipOfFirstNoteToUnlinked).toBe(ItemRelationshipDirection.NoRelationship)
|
||||
})
|
||||
|
||||
it('should unlink itemToUnlink from item', async () => {
|
||||
it('should unlink itemOne from itemTwo if relation is direct', async () => {
|
||||
itemManager = createService()
|
||||
const note = createNote('Note 1')
|
||||
const note2 = createNote('Note 2')
|
||||
const note = createNoteWithTitle('Note 1')
|
||||
const note2 = createNoteWithTitle('Note 2')
|
||||
await itemManager.insertItems([note, note2])
|
||||
|
||||
const linkedItem = await itemManager.linkNoteToNote(note, note2)
|
||||
const unlinkedItem = await itemManager.unlinkItem(linkedItem, note2)
|
||||
const unlinkedItem = await itemManager.unlinkItems(linkedItem, note2)
|
||||
const references = unlinkedItem.references
|
||||
|
||||
expect(unlinkedItem.uuid).toBe(note.uuid)
|
||||
expect(references).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should get all linked files for item', async () => {
|
||||
it('should unlink itemTwo from itemOne if relation is indirect', async () => {
|
||||
itemManager = createService()
|
||||
const file = createFile('A1')
|
||||
const file2 = createFile('B2')
|
||||
const file3 = createFile('C3')
|
||||
const note = createNoteWithTitle('Note 1')
|
||||
const note2 = createNoteWithTitle('Note 2')
|
||||
await itemManager.insertItems([note, note2])
|
||||
|
||||
await itemManager.insertItems([file, file2, file3])
|
||||
const linkedItem = await itemManager.linkNoteToNote(note, note2)
|
||||
const changedItem = await itemManager.unlinkItems(linkedItem, note2)
|
||||
|
||||
await itemManager.linkFileToFile(file, file3)
|
||||
await itemManager.linkFileToFile(file, file2)
|
||||
|
||||
const sortedFilesForItem = itemManager.getSortedLinkedFilesForItem(file)
|
||||
|
||||
expect(sortedFilesForItem).toHaveLength(2)
|
||||
expect(sortedFilesForItem[0].uuid).toEqual(file2.uuid)
|
||||
expect(sortedFilesForItem[1].uuid).toEqual(file3.uuid)
|
||||
})
|
||||
|
||||
it('should get all files linking to item', async () => {
|
||||
itemManager = createService()
|
||||
const baseFile = createFile('file')
|
||||
const fileToLink1 = createFile('A1')
|
||||
const fileToLink2 = createFile('B2')
|
||||
|
||||
await itemManager.insertItems([baseFile, fileToLink1, fileToLink2])
|
||||
|
||||
await itemManager.linkFileToFile(fileToLink2, baseFile)
|
||||
await itemManager.linkFileToFile(fileToLink1, baseFile)
|
||||
|
||||
const sortedFilesForItem = itemManager.getSortedFilesLinkingToItem(baseFile)
|
||||
|
||||
expect(sortedFilesForItem).toHaveLength(2)
|
||||
expect(sortedFilesForItem[0].uuid).toEqual(fileToLink1.uuid)
|
||||
expect(sortedFilesForItem[1].uuid).toEqual(fileToLink2.uuid)
|
||||
})
|
||||
|
||||
it('should get all linked notes for item', async () => {
|
||||
itemManager = createService()
|
||||
const baseNote = createNote('note')
|
||||
const noteToLink1 = createNote('A1')
|
||||
const noteToLink2 = createNote('B2')
|
||||
|
||||
await itemManager.insertItems([baseNote, noteToLink1, noteToLink2])
|
||||
|
||||
await itemManager.linkNoteToNote(baseNote, noteToLink2)
|
||||
await itemManager.linkNoteToNote(baseNote, noteToLink1)
|
||||
|
||||
const sortedFilesForItem = itemManager.getSortedLinkedNotesForItem(baseNote)
|
||||
|
||||
expect(sortedFilesForItem).toHaveLength(2)
|
||||
expect(sortedFilesForItem[0].uuid).toEqual(noteToLink1.uuid)
|
||||
expect(sortedFilesForItem[1].uuid).toEqual(noteToLink2.uuid)
|
||||
})
|
||||
|
||||
it('should get all notes linking to item', async () => {
|
||||
itemManager = createService()
|
||||
const baseNote = createNote('note')
|
||||
const noteToLink1 = createNote('A1')
|
||||
const noteToLink2 = createNote('B2')
|
||||
|
||||
await itemManager.insertItems([baseNote, noteToLink1, noteToLink2])
|
||||
|
||||
await itemManager.linkNoteToNote(noteToLink2, baseNote)
|
||||
await itemManager.linkNoteToNote(noteToLink1, baseNote)
|
||||
|
||||
const sortedFilesForItem = itemManager.getSortedNotesLinkingToItem(baseNote)
|
||||
|
||||
expect(sortedFilesForItem).toHaveLength(2)
|
||||
expect(sortedFilesForItem[0].uuid).toEqual(noteToLink1.uuid)
|
||||
expect(sortedFilesForItem[1].uuid).toEqual(noteToLink2.uuid)
|
||||
expect(changedItem.uuid).toBe(note.uuid)
|
||||
expect(changedItem.references).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ import { UuidString } from '../../Types/UuidString'
|
||||
import * as Models from '@standardnotes/models'
|
||||
import * as Services from '@standardnotes/services'
|
||||
import { PayloadManagerChangeData } from '../Payloads'
|
||||
import { DiagnosticInfo, ItemsClientInterface } from '@standardnotes/services'
|
||||
import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services'
|
||||
import { ApplicationDisplayOptions } from '@Lib/Application/Options/OptionalOptions'
|
||||
import { CollectionSort, DecryptedItemInterface, ItemContent } from '@standardnotes/models'
|
||||
|
||||
@@ -1169,12 +1169,18 @@ export class ItemManager
|
||||
})
|
||||
}
|
||||
|
||||
public async unlinkItem(
|
||||
item: DecryptedItemInterface<ItemContent>,
|
||||
itemToUnlink: DecryptedItemInterface<ItemContent>,
|
||||
) {
|
||||
return this.changeItem(item, (mutator) => {
|
||||
mutator.removeItemAsRelationship(itemToUnlink)
|
||||
public async unlinkItems(itemA: DecryptedItemInterface<ItemContent>, itemB: DecryptedItemInterface<ItemContent>) {
|
||||
const relationshipDirection = this.relationshipDirectionBetweenItems(itemA, itemB)
|
||||
|
||||
if (relationshipDirection === ItemRelationshipDirection.NoRelationship) {
|
||||
throw new Error('Trying to unlink already unlinked items')
|
||||
}
|
||||
|
||||
const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB
|
||||
const itemToRemove = itemToChange === itemA ? itemB : itemA
|
||||
|
||||
return this.changeItem(itemToChange, (mutator) => {
|
||||
mutator.removeItemAsRelationship(itemToRemove)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1192,54 +1198,6 @@ export class ItemManager
|
||||
)
|
||||
}
|
||||
|
||||
public getSortedLinkedFilesForItem(item: DecryptedItemInterface<ItemContent>): Models.FileItem[] {
|
||||
if (this.isTemplateItem(item)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const filesReferencedByItem = this.referencesForItem(item).filter(
|
||||
(ref) => ref.content_type === ContentType.File,
|
||||
) as Models.FileItem[]
|
||||
|
||||
return naturalSort(filesReferencedByItem, 'title')
|
||||
}
|
||||
|
||||
public getSortedFilesLinkingToItem(item: DecryptedItemInterface<ItemContent>): Models.FileItem[] {
|
||||
if (this.isTemplateItem(item)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const filesReferencingItem = this.itemsReferencingItem(item).filter(
|
||||
(ref) => ref.content_type === ContentType.File,
|
||||
) as Models.FileItem[]
|
||||
|
||||
return naturalSort(filesReferencingItem, 'title')
|
||||
}
|
||||
|
||||
public getSortedLinkedNotesForItem(item: DecryptedItemInterface<ItemContent>): Models.SNNote[] {
|
||||
if (this.isTemplateItem(item)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const notesReferencedByItem = this.referencesForItem(item).filter(
|
||||
(ref) => ref.content_type === ContentType.Note,
|
||||
) as Models.SNNote[]
|
||||
|
||||
return naturalSort(notesReferencedByItem, 'title')
|
||||
}
|
||||
|
||||
public getSortedNotesLinkingToItem(item: Models.DecryptedItemInterface<Models.ItemContent>): Models.SNNote[] {
|
||||
if (this.isTemplateItem(item)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const notesReferencingItem = this.itemsReferencingItem(item).filter(
|
||||
(ref) => ref.content_type === ContentType.Note,
|
||||
) as Models.SNNote[]
|
||||
|
||||
return naturalSort(notesReferencingItem, 'title')
|
||||
}
|
||||
|
||||
public async createTag(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise<Models.SNTag> {
|
||||
const newTag = await this.createItem<Models.SNTag>(
|
||||
ContentType.Tag,
|
||||
@@ -1433,21 +1391,18 @@ export class ItemManager
|
||||
return this.findAnyItems(uuids) as (Models.DecryptedItemInterface | Models.DeletedItemInterface)[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `'direct'` if `itemOne` has the reference to `itemTwo`, `'indirect'` if `itemTwo` has the reference to `itemOne`, `'unlinked'` if neither reference each other
|
||||
*/
|
||||
public relationshipTypeForItems(
|
||||
itemOne: Models.DecryptedItemInterface<Models.ItemContent>,
|
||||
itemTwo: Models.DecryptedItemInterface<Models.ItemContent>,
|
||||
): 'direct' | 'indirect' | 'unlinked' {
|
||||
const itemOneReferencesItemTwo = this.isTemplateItem(itemOne)
|
||||
? false
|
||||
: !!this.referencesForItem(itemOne).find((reference) => reference.uuid === itemTwo.uuid)
|
||||
const itemTwoReferencesItemOne = this.isTemplateItem(itemTwo)
|
||||
? false
|
||||
: !!this.referencesForItem(itemTwo).find((reference) => reference.uuid === itemOne.uuid)
|
||||
public relationshipDirectionBetweenItems(
|
||||
itemA: Models.DecryptedItemInterface<Models.ItemContent>,
|
||||
itemB: Models.DecryptedItemInterface<Models.ItemContent>,
|
||||
): ItemRelationshipDirection {
|
||||
const itemAReferencesItemB = !!itemA.references.find((reference) => reference.uuid === itemB.uuid)
|
||||
const itemBReferencesItemA = !!itemB.references.find((reference) => reference.uuid === itemA.uuid)
|
||||
|
||||
return itemOneReferencesItemTwo ? 'direct' : itemTwoReferencesItemOne ? 'indirect' : 'unlinked'
|
||||
return itemAReferencesItemB
|
||||
? ItemRelationshipDirection.AReferencesB
|
||||
: itemBReferencesItemA
|
||||
? ItemRelationshipDirection.BReferencesA
|
||||
: ItemRelationshipDirection.NoRelationship
|
||||
}
|
||||
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SNNote } from '@standardnotes/models'
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { ListedAccount, ListedAccountInfo } from '@standardnotes/responses'
|
||||
|
||||
@@ -6,4 +7,6 @@ export interface ListedClientInterface {
|
||||
requestNewListedAccount: () => Promise<ListedAccount | undefined>
|
||||
getListedAccounts(): Promise<ListedAccount[]>
|
||||
getListedAccountInfo(account: ListedAccount, inContextOfItem?: Uuid): Promise<ListedAccountInfo | undefined>
|
||||
isNoteAuthorizedForListed(note: SNNote): boolean
|
||||
authorizeNoteForListed(note: SNNote): Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ import { SNSettingsService } from '../Settings/SNSettingsService'
|
||||
import { ListedClientInterface } from './ListedClientInterface'
|
||||
import { SNApiService } from '../Api/ApiService'
|
||||
import { ListedAccount, ListedAccountInfo, ListedAccountInfoResponse } from '@standardnotes/responses'
|
||||
import { SNActionsExtension } from '@standardnotes/models'
|
||||
import { AbstractService, InternalEventBusInterface } from '@standardnotes/services'
|
||||
import { NoteMutator, SNActionsExtension, SNNote } from '@standardnotes/models'
|
||||
import { AbstractService, InternalEventBusInterface, MutatorClientInterface } from '@standardnotes/services'
|
||||
import { SNProtectionService } from '../Protection'
|
||||
|
||||
export class ListedService extends AbstractService implements ListedClientInterface {
|
||||
constructor(
|
||||
@@ -17,6 +18,8 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
private itemManager: ItemManager,
|
||||
private settingsService: SNSettingsService,
|
||||
private httpSerivce: SNHttpService,
|
||||
private protectionService: SNProtectionService,
|
||||
private mutatorService: MutatorClientInterface,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -27,6 +30,8 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
;(this.settingsService as unknown) = undefined
|
||||
;(this.apiService as unknown) = undefined
|
||||
;(this.httpSerivce as unknown) = undefined
|
||||
;(this.protectionService as unknown) = undefined
|
||||
;(this.mutatorService as unknown) = undefined
|
||||
super.deinit()
|
||||
}
|
||||
|
||||
@@ -34,6 +39,23 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
return this.apiService.user != undefined
|
||||
}
|
||||
|
||||
public isNoteAuthorizedForListed(note: SNNote): boolean {
|
||||
return note.authorizedForListed
|
||||
}
|
||||
|
||||
public async authorizeNoteForListed(note: SNNote): Promise<boolean> {
|
||||
const result = await this.protectionService.authorizeListedPublishing()
|
||||
if (result === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
await this.mutatorService.changeAndSaveItem<NoteMutator>(note, (mutator) => {
|
||||
mutator.authorizedForListed = true
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Account creation is asyncronous on the backend due to message-based nature of architecture.
|
||||
* In order to get the newly created account, we poll the server to check for new accounts.
|
||||
@@ -73,8 +95,11 @@ export class ListedService extends AbstractService implements ListedClientInterf
|
||||
if (inContextOfItem) {
|
||||
url += `&item_uuid=${inContextOfItem}`
|
||||
}
|
||||
const response = (await this.httpSerivce.getAbsolute(url)) as ListedAccountInfoResponse
|
||||
if (response.error || !response.data || isString(response.data)) {
|
||||
|
||||
const response = (await this.httpSerivce.getAbsolute(url).catch((error) => {
|
||||
console.error(error)
|
||||
})) as ListedAccountInfoResponse
|
||||
if (!response || response.error || !response.data || isString(response.data)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -219,13 +219,18 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
|
||||
return this.authorizeAction(ChallengeReason.RevokeSession)
|
||||
}
|
||||
|
||||
async authorizeListedPublishing(): Promise<boolean> {
|
||||
return this.authorizeAction(ChallengeReason.AuthorizeNoteForListed, { forcePrompt: true })
|
||||
}
|
||||
|
||||
async authorizeAction(
|
||||
reason: ChallengeReason,
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false } = {},
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||
): Promise<boolean> {
|
||||
return this.validateOrRenewSession(reason, {
|
||||
requireAccountPassword,
|
||||
fallBackToAccountPassword,
|
||||
forcePrompt,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -295,9 +300,9 @@ export class SNProtectionService extends AbstractService<ProtectionEvent> implem
|
||||
|
||||
private async validateOrRenewSession(
|
||||
reason: ChallengeReason,
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false } = {},
|
||||
{ fallBackToAccountPassword = true, requireAccountPassword = false, forcePrompt = false } = {},
|
||||
): Promise<boolean> {
|
||||
if (this.getSessionExpiryDate() > new Date()) {
|
||||
if (this.getSessionExpiryDate() > new Date() && !forcePrompt) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
||||
return isNullOrUndefined(this.apiService.getSession())
|
||||
}
|
||||
|
||||
public getUser() {
|
||||
public getUser(): Responses.User | undefined {
|
||||
return this.user
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { ClientDisplayableError, User } from '@standardnotes/responses'
|
||||
import { Base64String } from '@standardnotes/sncrypto-common'
|
||||
|
||||
export interface SessionsClientInterface {
|
||||
createDemoShareToken(): Promise<Base64String | ClientDisplayableError>
|
||||
|
||||
populateSessionFromDemoShareToken(token: Base64String): Promise<void>
|
||||
getUser(): User | undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import * as Models from '@standardnotes/models'
|
||||
|
||||
export const createNote = (payload?: Partial<Models.NoteContent>): Models.SNNote => {
|
||||
return new Models.SNNote(
|
||||
new Models.DecryptedPayload(
|
||||
{
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: Models.FillItemContent({ ...payload }),
|
||||
...Models.PayloadTimestampDefaults(),
|
||||
},
|
||||
Models.PayloadSource.Constructor,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export const createNoteWithTitle = (title: string) => {
|
||||
return new Models.SNNote(
|
||||
new Models.DecryptedPayload({
|
||||
uuid: String(Math.random()),
|
||||
content_type: ContentType.Note,
|
||||
content: Models.FillItemContent<Models.NoteContent>({
|
||||
title: title,
|
||||
}),
|
||||
...Models.PayloadTimestampDefaults(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -356,14 +356,13 @@ describe('app models', () => {
|
||||
})
|
||||
|
||||
it('maintains editor reference when duplicating note', async function () {
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
const editor = await this.application.itemManager.createItem(
|
||||
ContentType.Component,
|
||||
{ area: ComponentArea.Editor },
|
||||
{ area: ComponentArea.Editor, package_info: { identifier: 'foo-editor' } },
|
||||
true,
|
||||
)
|
||||
await this.application.itemManager.changeComponent(editor, (mutator) => {
|
||||
mutator.associateWithItem(note.uuid)
|
||||
const note = await Factory.insertItemWithOverride(this.application, ContentType.Note, {
|
||||
editorIdentifier: 'foo-editor',
|
||||
})
|
||||
|
||||
expect(this.application.componentManager.editorForNote(note).uuid).to.equal(editor.uuid)
|
||||
|
||||
@@ -114,59 +114,6 @@ describe('online conflict handling', function () {
|
||||
await this.sharedFinalAssertions()
|
||||
})
|
||||
|
||||
it('duplicating note should maintain editor ref', async function () {
|
||||
const note = await Factory.createSyncedNote(this.application)
|
||||
this.expectedItemCount++
|
||||
const basePayload = createDirtyPayload(ContentType.Component)
|
||||
const payload = basePayload.copy({
|
||||
content: {
|
||||
...basePayload.content,
|
||||
area: ComponentArea.Editor,
|
||||
},
|
||||
})
|
||||
const editor = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
this.expectedItemCount++
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
|
||||
await this.application.mutator.changeAndSaveItem(
|
||||
editor,
|
||||
(mutator) => {
|
||||
mutator.associateWithItem(note.uuid)
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
syncOptions,
|
||||
)
|
||||
|
||||
expect(this.application.componentManager.editorForNote(note)).to.be.ok
|
||||
|
||||
/** Conflict the note */
|
||||
/** First modify the item without saving so that
|
||||
* our local contents digress from the server's */
|
||||
await this.application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.title = `${Math.random()}`
|
||||
})
|
||||
|
||||
await Factory.changePayloadTimeStampAndSync(
|
||||
this.application,
|
||||
note.payload,
|
||||
Factory.dateToMicroseconds(Factory.yesterday()),
|
||||
{
|
||||
title: 'zar',
|
||||
},
|
||||
syncOptions,
|
||||
)
|
||||
|
||||
this.expectedItemCount++
|
||||
|
||||
const duplicate = this.application.itemManager.getDisplayableNotes().find((n) => {
|
||||
return n.uuid !== note.uuid
|
||||
})
|
||||
expect(duplicate).to.be.ok
|
||||
expect(this.application.componentManager.editorForNote(duplicate)).to.be.ok
|
||||
await this.sharedFinalAssertions()
|
||||
})
|
||||
|
||||
it('should create conflicted copy if incoming server item attempts to overwrite local dirty item', async function () {
|
||||
// create an item and sync it
|
||||
const note = await Factory.createMappedNote(this.application)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.141.1",
|
||||
"version": "2.142.3",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,30 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.9.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
|
||||
|
||||
# [1.9.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ui-services:** add route type getter ([401a35f](https://github.com/standardnotes/app/commit/401a35f4db9e7f090ad84c2d9e0ce5b465c7e790))
|
||||
|
||||
### Features
|
||||
|
||||
* add accepting subscription invite route type ([83b4840](https://github.com/standardnotes/app/commit/83b484002c066675e58625d7c0a1f7a7566df787))
|
||||
|
||||
## [1.8.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.8.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.8.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.8.3",
|
||||
"version": "1.9.1",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type DemoParams = {
|
||||
token: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export type OnboardingParams = {
|
||||
fromHomepage: boolean
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type PurchaseParams = {
|
||||
plan: string
|
||||
period: string
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PreferenceId } from '../../Preferences/PreferenceId'
|
||||
|
||||
export type SettingsParams = {
|
||||
panel: PreferenceId
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
|
||||
export type SubscriptionInviteParams = {
|
||||
inviteUuid: Uuid
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum RootQueryParam {
|
||||
Purchase = 'purchase',
|
||||
Settings = 'settings',
|
||||
DemoToken = 'demo-token',
|
||||
AcceptSubscriptionInvite = 'accept-subscription-invite',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum RootRoutes {
|
||||
Onboarding = '/onboard',
|
||||
None = '/',
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { PreferenceId } from '../Preferences/PreferenceId'
|
||||
|
||||
export type OnboardingParams = {
|
||||
fromHomepage: boolean
|
||||
}
|
||||
|
||||
export type SettingsParams = {
|
||||
panel: PreferenceId
|
||||
}
|
||||
|
||||
export type DemoParams = {
|
||||
token: string
|
||||
}
|
||||
|
||||
export type PurchaseParams = {
|
||||
plan: string
|
||||
period: string
|
||||
}
|
||||
@@ -48,4 +48,12 @@ describe('route parser', () => {
|
||||
|
||||
expect(() => parser.onboardingParams).toThrowError('Accessing invalid params')
|
||||
})
|
||||
|
||||
it('routes to subscription sharing', () => {
|
||||
const url = 'https://app.standardnotes.com/?accept-subscription-invite=1-2-3'
|
||||
const parser = new RouteParser(url)
|
||||
|
||||
expect(parser.type).toEqual(RouteType.AcceptSubscriptionInvite)
|
||||
expect(parser.subscriptionInviteParams.inviteUuid).toEqual('1-2-3')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,54 +1,43 @@
|
||||
import { Uuid } from '@standardnotes/common'
|
||||
import { PreferenceId } from './../Preferences/PreferenceId'
|
||||
import { DemoParams, OnboardingParams, PurchaseParams, SettingsParams } from './RouteParams'
|
||||
import { DemoParams } from './Params/DemoParams'
|
||||
import { OnboardingParams } from './Params/OnboardingParams'
|
||||
import { PurchaseParams } from './Params/PurchaseParams'
|
||||
import { SettingsParams } from './Params/SettingsParams'
|
||||
import { SubscriptionInviteParams } from './Params/SubscriptionInviteParams'
|
||||
|
||||
import { RootQueryParam } from './RootQueryParam'
|
||||
import { RootRoutes } from './RootRoutes'
|
||||
import { RouteParserInterface } from './RouteParserInterface'
|
||||
import { RouteType } from './RouteType'
|
||||
|
||||
enum RootRoutes {
|
||||
Onboarding = '/onboard',
|
||||
None = '/',
|
||||
}
|
||||
|
||||
enum RootQueryParam {
|
||||
Purchase = 'purchase',
|
||||
Settings = 'settings',
|
||||
DemoToken = 'demo-token',
|
||||
}
|
||||
|
||||
export class RouteParser {
|
||||
export class RouteParser implements RouteParserInterface {
|
||||
private url: URL
|
||||
private readonly path: string
|
||||
public readonly type: RouteType
|
||||
private readonly parsedType: RouteType
|
||||
private readonly searchParams: URLSearchParams
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = new URL(url)
|
||||
this.path = this.url.pathname
|
||||
this.searchParams = this.url.searchParams
|
||||
this.parsedType = this.parseTypeFromQueryParameters()
|
||||
}
|
||||
|
||||
const pathUsesRootQueryParams = this.path === RootRoutes.None
|
||||
get type(): RouteType {
|
||||
return this.parsedType
|
||||
}
|
||||
|
||||
if (pathUsesRootQueryParams) {
|
||||
if (this.searchParams.has(RootQueryParam.Purchase)) {
|
||||
this.type = RouteType.Purchase
|
||||
} else if (this.searchParams.has(RootQueryParam.Settings)) {
|
||||
this.type = RouteType.Settings
|
||||
} else if (this.searchParams.has(RootQueryParam.DemoToken)) {
|
||||
this.type = RouteType.Demo
|
||||
} else {
|
||||
this.type = RouteType.None
|
||||
}
|
||||
} else {
|
||||
if (this.path === RootRoutes.Onboarding) {
|
||||
this.type = RouteType.Onboarding
|
||||
} else {
|
||||
this.type = RouteType.None
|
||||
}
|
||||
get subscriptionInviteParams(): SubscriptionInviteParams {
|
||||
this.checkForProperRouteType(RouteType.AcceptSubscriptionInvite)
|
||||
|
||||
return {
|
||||
inviteUuid: this.searchParams.get(RootQueryParam.AcceptSubscriptionInvite) as Uuid,
|
||||
}
|
||||
}
|
||||
|
||||
get demoParams(): DemoParams {
|
||||
if (this.type !== RouteType.Demo) {
|
||||
throw new Error('Accessing invalid params')
|
||||
}
|
||||
this.checkForProperRouteType(RouteType.Demo)
|
||||
|
||||
return {
|
||||
token: this.searchParams.get(RootQueryParam.DemoToken) as string,
|
||||
@@ -56,9 +45,7 @@ export class RouteParser {
|
||||
}
|
||||
|
||||
get settingsParams(): SettingsParams {
|
||||
if (this.type !== RouteType.Settings) {
|
||||
throw new Error('Accessing invalid params')
|
||||
}
|
||||
this.checkForProperRouteType(RouteType.Settings)
|
||||
|
||||
return {
|
||||
panel: this.searchParams.get(RootQueryParam.Settings) as PreferenceId,
|
||||
@@ -66,9 +53,7 @@ export class RouteParser {
|
||||
}
|
||||
|
||||
get purchaseParams(): PurchaseParams {
|
||||
if (this.type !== RouteType.Purchase) {
|
||||
throw new Error('Accessing invalid params')
|
||||
}
|
||||
this.checkForProperRouteType(RouteType.Purchase)
|
||||
|
||||
return {
|
||||
plan: this.searchParams.get('plan') as string,
|
||||
@@ -77,12 +62,41 @@ export class RouteParser {
|
||||
}
|
||||
|
||||
get onboardingParams(): OnboardingParams {
|
||||
if (this.type !== RouteType.Onboarding) {
|
||||
throw new Error('Accessing invalid params')
|
||||
}
|
||||
this.checkForProperRouteType(RouteType.Onboarding)
|
||||
|
||||
return {
|
||||
fromHomepage: !!this.searchParams.get('from_homepage'),
|
||||
}
|
||||
}
|
||||
|
||||
private checkForProperRouteType(type: RouteType): void {
|
||||
if (this.parsedType !== type) {
|
||||
throw new Error('Accessing invalid params')
|
||||
}
|
||||
}
|
||||
|
||||
private parseTypeFromQueryParameters(): RouteType {
|
||||
if (this.path === RootRoutes.Onboarding) {
|
||||
return RouteType.Onboarding
|
||||
}
|
||||
|
||||
if (this.path !== RootRoutes.None) {
|
||||
return RouteType.None
|
||||
}
|
||||
|
||||
const rootQueryParametersMap: Map<RootQueryParam, RouteType> = new Map([
|
||||
[RootQueryParam.Purchase, RouteType.Purchase],
|
||||
[RootQueryParam.Settings, RouteType.Settings],
|
||||
[RootQueryParam.DemoToken, RouteType.Demo],
|
||||
[RootQueryParam.AcceptSubscriptionInvite, RouteType.AcceptSubscriptionInvite],
|
||||
])
|
||||
|
||||
for (const rootQueryParam of rootQueryParametersMap.keys()) {
|
||||
if (this.searchParams.has(rootQueryParam)) {
|
||||
return rootQueryParametersMap.get(rootQueryParam) as RouteType
|
||||
}
|
||||
}
|
||||
|
||||
return RouteType.None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { DemoParams } from './Params/DemoParams'
|
||||
import { OnboardingParams } from './Params/OnboardingParams'
|
||||
import { PurchaseParams } from './Params/PurchaseParams'
|
||||
import { SettingsParams } from './Params/SettingsParams'
|
||||
import { SubscriptionInviteParams } from './Params/SubscriptionInviteParams'
|
||||
import { RouteType } from './RouteType'
|
||||
|
||||
export interface RouteParserInterface {
|
||||
get demoParams(): DemoParams
|
||||
get settingsParams(): SettingsParams
|
||||
get purchaseParams(): PurchaseParams
|
||||
get onboardingParams(): OnboardingParams
|
||||
get subscriptionInviteParams(): SubscriptionInviteParams
|
||||
get type(): RouteType
|
||||
}
|
||||
@@ -4,10 +4,16 @@ import {
|
||||
ApplicationInterface,
|
||||
InternalEventBusInterface,
|
||||
} from '@standardnotes/services'
|
||||
import { RouteParser } from './RouteParser'
|
||||
import { RouteServiceEvent } from './RouteServiceEvent'
|
||||
|
||||
export class RouteService extends AbstractService<RouteServiceEvent, RouteParser> {
|
||||
import { RouteParser } from './RouteParser'
|
||||
import { RouteParserInterface } from './RouteParserInterface'
|
||||
import { RouteServiceEvent } from './RouteServiceEvent'
|
||||
import { RouteServiceInterface } from './RouteServiceInterface'
|
||||
|
||||
export class RouteService
|
||||
extends AbstractService<RouteServiceEvent, RouteParserInterface>
|
||||
implements RouteServiceInterface
|
||||
{
|
||||
private unsubApp!: () => void
|
||||
|
||||
constructor(
|
||||
@@ -24,7 +30,7 @@ export class RouteService extends AbstractService<RouteServiceEvent, RouteParser
|
||||
this.unsubApp()
|
||||
}
|
||||
|
||||
public getRoute(): RouteParser {
|
||||
public getRoute(): RouteParserInterface {
|
||||
return new RouteParser(window.location.href)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RouteParserInterface } from './RouteParserInterface'
|
||||
|
||||
export interface RouteServiceInterface {
|
||||
deinit(): void
|
||||
getRoute(): RouteParserInterface
|
||||
removeSettingsFromURLQueryParameters(): void
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export enum RouteType {
|
||||
Onboarding = 'onboarding',
|
||||
Settings = 'settings',
|
||||
Purchase = 'purchase',
|
||||
AcceptSubscriptionInvite = 'accept-subscription-invite',
|
||||
Demo = 'demo',
|
||||
None = 'none',
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ export * from './Alert/WebAlertService'
|
||||
export * from './Archive/ArchiveManager'
|
||||
export * from './IO/IOService'
|
||||
export * from './Preferences/PreferenceId'
|
||||
export * from './Route/RouteParams'
|
||||
export * from './Route/Params/DemoParams'
|
||||
export * from './Route/Params/OnboardingParams'
|
||||
export * from './Route/Params/PurchaseParams'
|
||||
export * from './Route/Params/SettingsParams'
|
||||
export * from './Route/Params/SubscriptionInviteParams'
|
||||
export * from './Route/RouteParser'
|
||||
export * from './Route/RouteType'
|
||||
export * from './Route/RouteService'
|
||||
export * from './Route/RouteServiceInterface'
|
||||
export * from './Route/RouteServiceEvent'
|
||||
export * from './Security/AutolockService'
|
||||
export * from './Storage/LocalStorage'
|
||||
|
||||
@@ -3,6 +3,110 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.82.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* refactor application event observing on web ([9fb85cd](https://github.com/standardnotes/app/commit/9fb85cd77a627364738a9c21706431d2a01e9c44))
|
||||
* **web:** event observer interface import ([c503d78](https://github.com/standardnotes/app/commit/c503d781aefcc1a15d3a8da1ee09294d176e4b89))
|
||||
|
||||
## [3.82.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* item selection/unselection on deleting ([4a1612c](https://github.com/standardnotes/app/commit/4a1612c7dc8ba2af0d28ac71d5dfcdf059cd5ea0))
|
||||
* template note getting unselected after typing ([d3ed439](https://github.com/standardnotes/app/commit/d3ed4391b7f6b77ced485884a97190449a1b5507))
|
||||
|
||||
## [3.82.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.82.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default auto-dark theme ([1addaf4](https://github.com/standardnotes/app/commit/1addaf4c3941364f695545669158f09f4c012dea))
|
||||
* tag collapse/expand not persisting ([c6e3580](https://github.com/standardnotes/app/commit/c6e3580678dbed6562041ce5afed47563276843a))
|
||||
|
||||
## [3.82.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* improve plaintext editor performance on mobile ([#1855](https://github.com/standardnotes/app/issues/1855)) ([f71e6fe](https://github.com/standardnotes/app/commit/f71e6fe70397cf9ab2c109cad52780088337fc47))
|
||||
|
||||
## [3.82.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "feat: ability to favorite tags (#1852)" ([5d202a8](https://github.com/standardnotes/app/commit/5d202a823e2a89c5e6fe7e07306957f2b2ea68b1)), closes [#1852](https://github.com/standardnotes/app/issues/1852)
|
||||
|
||||
# [3.82.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* mobx warnings and react render phase set state error ([76f23e1](https://github.com/standardnotes/app/commit/76f23e13ecc8795b4e6cbbb37c32db262c0084c2))
|
||||
|
||||
### Features
|
||||
|
||||
* ability to favorite tags ([#1852](https://github.com/standardnotes/app/issues/1852)) ([4f3b258](https://github.com/standardnotes/app/commit/4f3b258363ded1a340bd1a7702da7cfd6415b954))
|
||||
|
||||
# [3.81.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
### Features
|
||||
|
||||
* persist selected tag & note locally ([#1851](https://github.com/standardnotes/app/issues/1851)) ([4432f1c](https://github.com/standardnotes/app/commit/4432f1cb4cfc2042ddfb7bd574d53270626ebe0d))
|
||||
|
||||
## [3.80.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* "Forgot passcode" and "Switch workspace" lock screen options not working on mobile ([#1850](https://github.com/standardnotes/app/issues/1850)) ([57fab4f](https://github.com/standardnotes/app/commit/57fab4f670faeaa0018d9d1a678e50d1e90052aa))
|
||||
|
||||
# [3.80.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* file not linking to note when uploaded by dragging on note ([d333d4f](https://github.com/standardnotes/app/commit/d333d4f1887672b526a5d8fc659e6dd2b1b12e21))
|
||||
* help & feedback links on mobile ([#1849](https://github.com/standardnotes/app/issues/1849)) ([fb72c2f](https://github.com/standardnotes/app/commit/fb72c2fdae277cb4e348ce5ee550f0545f281dcd))
|
||||
* links for template notes ([60a8e8b](https://github.com/standardnotes/app/commit/60a8e8b92b1e5c0f4c4c007591aa115fce51930b))
|
||||
* make tags non-tabbable ([a8535cf](https://github.com/standardnotes/app/commit/a8535cfa2380a4ddd648d5d099701c0b5e8f3a22))
|
||||
* remove tinted border from change note type button ([27bb4eb](https://github.com/standardnotes/app/commit/27bb4ebdae259718c0e13134c2ffdce7c681f0da))
|
||||
|
||||
### Features
|
||||
|
||||
* make sans-serif font default ([050fa9c](https://github.com/standardnotes/app/commit/050fa9cabca65bba4845fc9b315c81ce524e8b0d))
|
||||
|
||||
# [3.79.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Features
|
||||
|
||||
* improved file drag-n-drop experience ([#1848](https://github.com/standardnotes/app/issues/1848)) ([e99c7b7](https://github.com/standardnotes/app/commit/e99c7b7c516b0c822f0e663843146c7f5cf1fb86))
|
||||
|
||||
## [3.78.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* issue with not being able to unlink a file from a note ([#1836](https://github.com/standardnotes/app/issues/1836)) ([4030953](https://github.com/standardnotes/app/commit/4030953b00ad609dc2b0f5b66cfd971d449f6121))
|
||||
|
||||
# [3.78.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* context menu opening when swiping up from bottom on iOS ([5ffd37d](https://github.com/standardnotes/app/commit/5ffd37d4478aec638392962f45d3a4428a8da86a))
|
||||
* editor width on mobile ([361f773](https://github.com/standardnotes/app/commit/361f773f2c3c65255e0d5cc8d991fac0f4521fff))
|
||||
* hide dynamic panels & focused mode options on mobile ([e4de9c1](https://github.com/standardnotes/app/commit/e4de9c1be76c758f36e5068a8395bb3abd19069a))
|
||||
* use mobx action for setting state ([eec7ae1](https://github.com/standardnotes/app/commit/eec7ae1589f7eae36ba810942911ccb3b8d863ee))
|
||||
|
||||
### Features
|
||||
|
||||
* authorize notes for listed ([#1823](https://github.com/standardnotes/app/issues/1823)) ([9954bdc](https://github.com/standardnotes/app/commit/9954bdc29f9de2a42c4539d55749991d86a72943))
|
||||
|
||||
## [3.77.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* remove switch to classic mx option ([e40cd15](https://github.com/standardnotes/app/commit/e40cd157b6d1683cf9eb9548714c604649134971))
|
||||
|
||||
# [3.77.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-10-17)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.77.0",
|
||||
"version": "3.82.6",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
MobileDeviceInterface,
|
||||
MobileUnlockTiming,
|
||||
InternalEventBus,
|
||||
DecryptedItem,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable } from 'mobx'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
AutolockService,
|
||||
IOService,
|
||||
RouteService,
|
||||
RouteServiceInterface,
|
||||
ThemeManager,
|
||||
WebAlertService,
|
||||
} from '@standardnotes/ui-services'
|
||||
@@ -49,7 +51,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
private onVisibilityChange: () => void
|
||||
private mobileWebReceiver?: MobileWebReceiver
|
||||
private androidBackHandler?: AndroidBackHandler
|
||||
public readonly routeService: RouteService
|
||||
public readonly routeService: RouteServiceInterface
|
||||
|
||||
constructor(
|
||||
deviceInterface: WebOrDesktopDevice,
|
||||
@@ -308,4 +310,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
isAuthorizedToRenderItem(item: DecryptedItem): boolean {
|
||||
if (item.protected && this.hasProtectionSources()) {
|
||||
return this.hasUnprotectedAccessSession()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</FileDragNDropProvider>
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import AttachedFilesPopover from './AttachedFilesPopover'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { PopoverTabs } from './PopoverTabs'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider'
|
||||
import { FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import { addToast, ToastType } from '@standardnotes/toast'
|
||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
featuresController: FeaturesController
|
||||
filePreviewModalController: FilePreviewModalController
|
||||
filesController: FilesController
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
selectionController: SelectedItemsController
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}
|
||||
|
||||
const AttachedFilesButton: FunctionComponent<Props> = ({
|
||||
application,
|
||||
featuresController,
|
||||
filesController,
|
||||
navigationController,
|
||||
notesController,
|
||||
selectionController,
|
||||
onClickPreprocessing,
|
||||
}: Props) => {
|
||||
const { allFiles, attachedFiles } = filesController
|
||||
const attachedFilesCount = attachedFiles.length
|
||||
|
||||
const premiumModal = usePremiumModal()
|
||||
const note: SNNote | undefined = notesController.firstSelectedNote
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(
|
||||
navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles,
|
||||
)
|
||||
|
||||
const isAttachedTabDisabled = navigationController.isInFilesView || selectionController.selectedItemsCount > 1
|
||||
|
||||
useEffect(() => {
|
||||
if (isAttachedTabDisabled && currentTab === PopoverTabs.AttachedFiles) {
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
}
|
||||
}, [currentTab, isAttachedTabDisabled])
|
||||
|
||||
const toggleAttachedFilesMenu = useCallback(async () => {
|
||||
const newOpenState = !isOpen
|
||||
|
||||
if (newOpenState && onClickPreprocessing) {
|
||||
await onClickPreprocessing()
|
||||
}
|
||||
|
||||
setIsOpen(newOpenState)
|
||||
}, [onClickPreprocessing, isOpen])
|
||||
|
||||
const prospectivelyShowFilesPremiumModal = useCallback(() => {
|
||||
if (!featuresController.hasFiles) {
|
||||
premiumModal.activate('Files')
|
||||
}
|
||||
}, [featuresController.hasFiles, premiumModal])
|
||||
|
||||
const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => {
|
||||
prospectivelyShowFilesPremiumModal()
|
||||
|
||||
await toggleAttachedFilesMenu()
|
||||
}, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal])
|
||||
|
||||
const attachFileToNote = useCallback(
|
||||
async (file: FileItem) => {
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was unselected or deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await application.items.associateFileWithNote(file, note)
|
||||
},
|
||||
[application.items, note],
|
||||
)
|
||||
|
||||
const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop()
|
||||
|
||||
useEffect(() => {
|
||||
if (isDraggingFiles && !isOpen) {
|
||||
void toggleAttachedFilesMenu()
|
||||
}
|
||||
}, [isDraggingFiles, isOpen, toggleAttachedFilesMenu])
|
||||
|
||||
const filesDragInCallback = useCallback((tab: PopoverTabs) => {
|
||||
setCurrentTab(tab)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
addFilesDragInCallback(filesDragInCallback)
|
||||
}, [addFilesDragInCallback, filesDragInCallback])
|
||||
|
||||
const filesDropCallback = useCallback(
|
||||
(uploadedFiles: FileItem[]) => {
|
||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||
uploadedFiles.forEach((file) => {
|
||||
attachFileToNote(file).catch(console.error)
|
||||
})
|
||||
}
|
||||
},
|
||||
[attachFileToNote, currentTab],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
addFilesDropCallback(filesDropCallback)
|
||||
}, [addFilesDropCallback, filesDropCallback])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<button
|
||||
className={classNames(
|
||||
'bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast',
|
||||
attachedFilesCount > 0 ? 'py-1 px-3' : '',
|
||||
)}
|
||||
title="Attached files"
|
||||
aria-label="Attached files"
|
||||
onClick={toggleAttachedFilesMenuWithEntitlementCheck}
|
||||
ref={buttonRef}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon type="folder" />
|
||||
{attachedFilesCount > 0 && <span className="ml-2 text-sm">{attachedFilesCount}</span>}
|
||||
</button>
|
||||
<Popover
|
||||
togglePopover={toggleAttachedFilesMenuWithEntitlementCheck}
|
||||
anchorElement={buttonRef.current}
|
||||
open={isOpen}
|
||||
className="pt-2 md:pt-0"
|
||||
>
|
||||
<AttachedFilesPopover
|
||||
application={application}
|
||||
filesController={filesController}
|
||||
attachedFiles={attachedFiles}
|
||||
allFiles={allFiles}
|
||||
currentTab={currentTab}
|
||||
isDraggingFiles={isDraggingFiles}
|
||||
setCurrentTab={setCurrentTab}
|
||||
attachedTabDisabled={isAttachedTabDisabled}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(AttachedFilesButton)
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FilesIllustration } from '@standardnotes/icons'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Dispatch, FunctionComponent, SetStateAction, useRef, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import PopoverFileItem from './PopoverFileItem'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import { PopoverTabs } from './PopoverTabs'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { StreamingFileReader } from '@standardnotes/filepicker'
|
||||
import ClearInputButton from '../ClearInputButton/ClearInputButton'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
filesController: FilesController
|
||||
allFiles: FileItem[]
|
||||
attachedFiles: FileItem[]
|
||||
currentTab: PopoverTabs
|
||||
isDraggingFiles: boolean
|
||||
setCurrentTab: Dispatch<SetStateAction<PopoverTabs>>
|
||||
attachedTabDisabled: boolean
|
||||
}
|
||||
|
||||
const AttachedFilesPopover: FunctionComponent<Props> = ({
|
||||
application,
|
||||
filesController,
|
||||
allFiles,
|
||||
attachedFiles,
|
||||
currentTab,
|
||||
isDraggingFiles,
|
||||
setCurrentTab,
|
||||
attachedTabDisabled,
|
||||
}) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles
|
||||
|
||||
const filteredList =
|
||||
searchQuery.length > 0
|
||||
? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1)
|
||||
: filesList
|
||||
|
||||
const attachFilesIfRequired = (files: FileItem[]) => {
|
||||
if (currentTab === PopoverTabs.AttachedFiles) {
|
||||
files.forEach((file) => {
|
||||
filesController
|
||||
.handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: { file },
|
||||
})
|
||||
.catch(console.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAttachFilesClick = async () => {
|
||||
if (!StreamingFileReader.available()) {
|
||||
fileInputRef.current?.click()
|
||||
return
|
||||
}
|
||||
|
||||
const uploadedFiles = await filesController.uploadNewFile()
|
||||
if (!uploadedFiles) {
|
||||
return
|
||||
}
|
||||
attachFilesIfRequired(uploadedFiles)
|
||||
}
|
||||
|
||||
const previewHandler = (file: FileItem) => {
|
||||
filesController
|
||||
.handleFileAction({
|
||||
type: PopoverFileItemActionType.PreviewFile,
|
||||
payload: { file, otherFiles: currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles },
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
style={{
|
||||
border: isDraggingFiles ? '2px dashed var(--sn-stylekit-info-color)' : '',
|
||||
}}
|
||||
>
|
||||
<div className="flex border-b border-solid border-border">
|
||||
<button
|
||||
id={PopoverTabs.AttachedFiles}
|
||||
className={`relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:bg-info-backdrop focus:shadow-bottom ${
|
||||
currentTab === PopoverTabs.AttachedFiles ? 'font-medium text-info shadow-bottom' : 'text-text'
|
||||
} ${attachedTabDisabled ? 'cursor-not-allowed text-neutral' : ''}`}
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AttachedFiles)
|
||||
}}
|
||||
disabled={attachedTabDisabled}
|
||||
>
|
||||
Attached
|
||||
</button>
|
||||
<button
|
||||
id={PopoverTabs.AllFiles}
|
||||
className={`relative cursor-pointer border-0 bg-default px-3 py-2.5 text-sm focus:bg-info-backdrop focus:shadow-bottom ${
|
||||
currentTab === PopoverTabs.AllFiles ? 'font-medium text-info shadow-bottom' : 'text-text'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setCurrentTab(PopoverTabs.AllFiles)
|
||||
}}
|
||||
>
|
||||
All files
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-110 min-h-0 overflow-y-auto">
|
||||
{filteredList.length > 0 || searchQuery.length > 0 ? (
|
||||
<div className="sticky top-0 left-0 border-b border-solid border-border bg-default p-3">
|
||||
<DecoratedInput
|
||||
type="text"
|
||||
className={{ container: searchQuery.length < 1 ? 'py-1.5 px-0.5' : 'py-0' }}
|
||||
placeholder="Search items..."
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
ref={searchInputRef}
|
||||
right={[
|
||||
searchQuery.length > 0 && (
|
||||
<ClearInputButton
|
||||
onClick={() => {
|
||||
setSearchQuery('')
|
||||
searchInputRef.current?.focus()
|
||||
}}
|
||||
/>
|
||||
),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{filteredList.length > 0 ? (
|
||||
filteredList.map((file: FileItem) => {
|
||||
return (
|
||||
<PopoverFileItem
|
||||
key={file.uuid}
|
||||
file={file}
|
||||
isAttachedToNote={attachedFiles.includes(file)}
|
||||
handleFileAction={filesController.handleFileAction}
|
||||
getIconType={application.iconsController.getIconForFileType}
|
||||
previewHandler={previewHandler}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center py-8">
|
||||
<div className="mb-2 h-18 w-18">
|
||||
<FilesIllustration />
|
||||
</div>
|
||||
<div className="mb-3 text-sm font-medium">
|
||||
{searchQuery.length > 0
|
||||
? 'No result found'
|
||||
: currentTab === PopoverTabs.AttachedFiles
|
||||
? 'No files attached to this note'
|
||||
: 'No files found in this account'}
|
||||
</div>
|
||||
<Button onClick={handleAttachFilesClick}>
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</Button>
|
||||
<div className="mt-3 text-xs text-passive-0">Or drop your files here</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
className="absolute top-0 left-0 -z-50 h-px w-px opacity-0"
|
||||
multiple
|
||||
ref={fileInputRef}
|
||||
onChange={async (event) => {
|
||||
const files = event.currentTarget.files
|
||||
|
||||
if (!files) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const uploadedFiles = await filesController.uploadNewFile(file)
|
||||
if (uploadedFiles) {
|
||||
attachFilesIfRequired(uploadedFiles)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{filteredList.length > 0 && (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 border-t border-solid border-border bg-transparent px-3 py-3 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||
onClick={handleAttachFilesClick}
|
||||
>
|
||||
<Icon type="add" className="mr-2 text-neutral" />
|
||||
{currentTab === PopoverTabs.AttachedFiles ? 'Attach' : 'Upload'} files
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(AttachedFilesPopover)
|
||||
@@ -1,124 +0,0 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import {
|
||||
FormEventHandler,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import PopoverFileSubmenu from './PopoverFileSubmenu'
|
||||
import { getFileIconComponent } from './getFileIconComponent'
|
||||
import { PopoverFileItemProps } from './PopoverFileItemProps'
|
||||
|
||||
const PopoverFileItem: FunctionComponent<PopoverFileItemProps> = ({
|
||||
file,
|
||||
isAttachedToNote,
|
||||
handleFileAction,
|
||||
getIconType,
|
||||
previewHandler,
|
||||
}) => {
|
||||
const [fileName, setFileName] = useState(file.name)
|
||||
const [isRenamingFile, setIsRenamingFile] = useState(false)
|
||||
const itemRef = useRef<HTMLDivElement>(null)
|
||||
const fileNameInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (isRenamingFile) {
|
||||
fileNameInputRef.current?.focus()
|
||||
}
|
||||
}, [isRenamingFile])
|
||||
|
||||
const renameFile = useCallback(
|
||||
async (file: FileItem, name: string) => {
|
||||
if (name.length < 1) {
|
||||
return
|
||||
}
|
||||
|
||||
await handleFileAction({
|
||||
type: PopoverFileItemActionType.RenameFile,
|
||||
payload: {
|
||||
file,
|
||||
name,
|
||||
},
|
||||
})
|
||||
setIsRenamingFile(false)
|
||||
},
|
||||
[handleFileAction],
|
||||
)
|
||||
|
||||
const handleFileNameInput: FormEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||
setFileName((event.target as HTMLInputElement).value)
|
||||
}, [])
|
||||
|
||||
const handleFileNameInputKeyDown: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
if (fileName.length > 0 && event.key === KeyboardKey.Enter) {
|
||||
itemRef.current?.focus()
|
||||
}
|
||||
},
|
||||
[fileName.length],
|
||||
)
|
||||
|
||||
const handleFileNameInputBlur = useCallback(() => {
|
||||
renameFile(file, fileName).catch(console.error)
|
||||
}, [file, fileName, renameFile])
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (isRenamingFile) {
|
||||
return
|
||||
}
|
||||
|
||||
previewHandler(file)
|
||||
}, [file, isRenamingFile, previewHandler])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={itemRef}
|
||||
className="flex items-center justify-between p-3 focus:shadow-none"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
>
|
||||
<div onClick={handleClick} className="flex cursor-pointer items-center">
|
||||
{getFileIconComponent(getIconType(file.mimeType), 'w-8 h-8 flex-shrink-0')}
|
||||
<div className="mx-4 flex flex-col">
|
||||
{isRenamingFile ? (
|
||||
<input
|
||||
type="text"
|
||||
className="text-input mb-1 border border-solid border-border bg-transparent px-1.5 py-1 text-foreground"
|
||||
value={fileName}
|
||||
ref={fileNameInputRef}
|
||||
onInput={handleFileNameInput}
|
||||
onKeyDown={handleFileNameInputKeyDown}
|
||||
onBlur={handleFileNameInputBlur}
|
||||
/>
|
||||
) : (
|
||||
<div className="break-word mb-1 text-mobile-menu-item md:text-sm">
|
||||
<span className="align-middle">{file.name}</span>
|
||||
{file.protected && (
|
||||
<Icon type="lock-filled" className="ml-2 inline align-middle text-neutral" size="small" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-passive-0">
|
||||
{file.created_at.toLocaleString()} · {formatSizeToReadableString(file.decryptedSize)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PopoverFileSubmenu
|
||||
file={file}
|
||||
isAttachedToNote={isAttachedToNote}
|
||||
handleFileAction={handleFileAction}
|
||||
setIsRenamingFile={setIsRenamingFile}
|
||||
previewHandler={previewHandler}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopoverFileItem
|
||||
@@ -1,20 +0,0 @@
|
||||
import { IconType, FileItem } from '@standardnotes/snjs'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { PopoverFileItemAction } from './PopoverFileItemAction'
|
||||
|
||||
type CommonProps = {
|
||||
file: FileItem
|
||||
isAttachedToNote: boolean
|
||||
handleFileAction: (action: PopoverFileItemAction) => Promise<{
|
||||
didHandleAction: boolean
|
||||
}>
|
||||
previewHandler: (file: FileItem) => void
|
||||
}
|
||||
|
||||
export type PopoverFileItemProps = CommonProps & {
|
||||
getIconType(type: string): IconType
|
||||
}
|
||||
|
||||
export type PopoverFileSubmenuProps = CommonProps & {
|
||||
setIsRenamingFile: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { FunctionComponent, useCallback, useRef, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Switch from '@/Components/Switch/Switch'
|
||||
import { PopoverFileSubmenuProps } from './PopoverFileItemProps'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import Popover from '../Popover/Popover'
|
||||
|
||||
const PopoverFileSubmenu: FunctionComponent<PopoverFileSubmenuProps> = ({
|
||||
file,
|
||||
isAttachedToNote,
|
||||
handleFileAction,
|
||||
setIsRenamingFile,
|
||||
previewHandler,
|
||||
}) => {
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isFileProtected, setIsFileProtected] = useState(file.protected)
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setIsOpen((isOpen) => !isOpen)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div ref={menuContainerRef}>
|
||||
<button
|
||||
ref={menuButtonRef}
|
||||
onClick={toggleMenu}
|
||||
className="h-7 w-7 cursor-pointer rounded-full border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</button>
|
||||
<Popover anchorElement={menuButtonRef.current} open={isOpen} togglePopover={toggleMenu} className="py-2">
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
previewHandler(file)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="file" className="mr-2 text-neutral" />
|
||||
Preview file
|
||||
</button>
|
||||
{isAttachedToNote ? (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DetachFileToNote,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link-off" className="mr-2 text-neutral" />
|
||||
Detach from note
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link" className="mr-2 text-neutral" />
|
||||
Attach to note
|
||||
</button>
|
||||
)}
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.ToggleFileProtection,
|
||||
payload: { file },
|
||||
callback: (isProtected: boolean) => {
|
||||
setIsFileProtected(isProtected)
|
||||
},
|
||||
}).catch(console.error)
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className="mr-2 text-neutral" />
|
||||
Password protection
|
||||
</span>
|
||||
<Switch
|
||||
className="pointer-events-none px-0"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
checked={isFileProtected}
|
||||
/>
|
||||
</button>
|
||||
<HorizontalSeparator classes="my-1" />
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DownloadFile,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="download" className="mr-2 text-neutral" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
setIsRenamingFile(true)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="mr-2 text-neutral" />
|
||||
Rename
|
||||
</button>
|
||||
<button
|
||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none md:text-sm"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DeleteFile,
|
||||
payload: { file },
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</button>
|
||||
<div className="px-3 py-1 text-xs font-medium text-neutral">
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">File ID:</span> {file.uuid}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Size:</span> {formatSizeToReadableString(file.decryptedSize)}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PopoverFileSubmenu
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum PopoverTabs {
|
||||
AttachedFiles = 'attached-files-tab',
|
||||
AllFiles = 'all-files-tab',
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user