mirror of
https://github.com/standardnotes/app
synced 2026-09-17 03:45:49 -04:00
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47fed1bbe4 | ||
|
|
8d9aef445e | ||
|
|
8f3778bff2 | ||
|
|
d583311de7 | ||
|
|
c772b5a854 | ||
|
|
57ccab1b12 | ||
|
|
2fdbf242fb | ||
|
|
a8c0be4f63 | ||
|
|
e15fcf9fd0 | ||
|
|
00eae833b0 | ||
|
|
1b696fa504 | ||
|
|
b06e62a60d | ||
|
|
af6ae81e1d | ||
|
|
0e86a0d171 | ||
|
|
550052f677 | ||
|
|
56b7ffeafb | ||
|
|
42db3592b6 | ||
|
|
6af95ddfeb | ||
|
|
9bdda2b60a | ||
|
|
b5e75601aa | ||
|
|
9dc25cbb44 | ||
|
|
4620fe9f07 | ||
|
|
981bf6ac92 | ||
|
|
6e4a512371 | ||
|
|
3a204cdcc8 | ||
|
|
98f6cbd7d0 | ||
|
|
607ac361d1 | ||
|
|
545d2dbfa9 | ||
|
|
e7214ea73a | ||
|
|
67374ab49b | ||
|
|
e9a10123fe | ||
|
|
beccfb16b3 | ||
|
|
6e7517650b | ||
|
|
87b974bdd5 | ||
|
|
16aaef00c3 | ||
|
|
21a4eb0cf0 | ||
|
|
6a7ff423ff | ||
|
|
cb9c16047e | ||
|
|
e61ac4c54a | ||
|
|
544a28d450 | ||
|
|
391b2af4e1 |
BIN
Binary file not shown.
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** add u2f required error tag ([9bdda2b](https://github.com/standardnotes/app/commit/9bdda2b60a2a85d32b0896dc6e307d27b76d42a8))
|
||||
|
||||
## [1.24.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
## [1.24.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/api
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/api",
|
||||
"version": "1.24.2",
|
||||
"version": "1.24.11",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/domain-core": "^1.11.1",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum ErrorTag {
|
||||
MfaInvalid = 'mfa-invalid',
|
||||
MfaRequired = 'mfa-required',
|
||||
U2FRequired = 'u2f-required',
|
||||
RefreshTokenInvalid = 'invalid-refresh-token',
|
||||
RefreshTokenExpired = 'expired-refresh-token',
|
||||
AccessTokenExpired = 'expired-access-token',
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.14.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/blocks-editor
|
||||
|
||||
# [1.14.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
### Features
|
||||
|
||||
* Links in Super notes will get auto-linked when they're pasted or typed ([1b696fa](https://github.com/standardnotes/app/commit/1b696fa50440bf5ff6f0a3a92ba9c2a775e374d6))
|
||||
|
||||
# [1.13.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-12)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/blocks-editor",
|
||||
"version": "1.13.0",
|
||||
"version": "1.14.1",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
|
||||
@@ -209,8 +209,7 @@ function useFloatingLinkEditorToolbar(editor: LexicalEditor, anchorElem: HTMLEle
|
||||
const linkParent = $findMatchingParent(node, $isLinkNode)
|
||||
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode)
|
||||
|
||||
// We don't want this menu to open for auto links.
|
||||
if (linkParent != null && autoLinkParent == null) {
|
||||
if (linkParent != null || autoLinkParent != null) {
|
||||
setIsLink(true)
|
||||
} else {
|
||||
setIsLink(false)
|
||||
|
||||
@@ -3,6 +3,68 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.104.110](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.109](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where popover/modal headers would get cut off by desktop titlebar ([a8c0be4](https://github.com/standardnotes/app/commit/a8c0be4f6327a48ae5e30c45ab4b8acc0c8ad612))
|
||||
|
||||
## [3.104.108](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.107](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.106](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.105](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.104](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.103](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.102](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.101](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.100](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.104.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -117,9 +117,8 @@ async function configureWindow(remoteBridge: CrossProcessBridge) {
|
||||
} else {
|
||||
/* Use custom title bar. Take the sn-titlebar-height off of
|
||||
the app content height so its not overflowing */
|
||||
sheet.insertRule('body { padding-top: var(--sn-desktop-titlebar-height); }', sheet.cssRules.length)
|
||||
sheet.insertRule(
|
||||
'@media screen and (max-width: 768px) { [data-popover] { padding-top: calc(var(--sn-desktop-titlebar-height) + 0.5rem); } }',
|
||||
'body, [data-reach-dialog-overlay], [data-mobile-popover] { padding-top: var(--sn-desktop-titlebar-height) !important; }',
|
||||
sheet.cssRules.length,
|
||||
)
|
||||
sheet.insertRule(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.104.95",
|
||||
"version": "3.104.110",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.21.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
## [1.21.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/encryption
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/encryption",
|
||||
"version": "1.21.2",
|
||||
"version": "1.21.10",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
"@standardnotes/sncrypto-common": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.58.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
## [1.58.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixed issue with third party editors not loading ([#2174](https://github.com/standardnotes/app/issues/2174)) ([e7214ea](https://github.com/standardnotes/app/commit/e7214ea73ad5d3026d958c79022d2238a4d1cfdc))
|
||||
|
||||
## [1.58.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
## [1.58.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
## [1.58.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/features",
|
||||
"version": "1.58.2",
|
||||
"version": "1.58.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/domain-core": "^1.11.1",
|
||||
"@standardnotes/security": "^1.7.5",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
|
||||
@@ -5,7 +5,6 @@ describe('note type', () => {
|
||||
it('should return the correct note type for editor identifier', () => {
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlainEditor)).toEqual(NoteType.Plain)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.SuperEditor)).toEqual(NoteType.Super)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownVisualEditor)).toEqual(NoteType.Markdown)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.MarkdownProEditor)).toEqual(NoteType.Markdown)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.PlusEditor)).toEqual(NoteType.RichText)
|
||||
expect(noteTypeForEditorIdentifier(FeatureIdentifier.CodeEditor)).toEqual(NoteType.Code)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export enum FeatureIdentifier {
|
||||
AccountSwitcher = 'com.standardnotes.account-switcher',
|
||||
CloudLink = 'org.standardnotes.cloudlink',
|
||||
DailyDropboxBackup = 'org.standardnotes.daily-dropbox-backup',
|
||||
DailyEmailBackup = 'org.standardnotes.daily-email-backup',
|
||||
@@ -21,7 +20,6 @@ export enum FeatureIdentifier {
|
||||
AutobiographyTheme = 'org.standardnotes.theme-autobiography',
|
||||
DynamicTheme = 'org.standardnotes.theme-dynamic',
|
||||
DarkTheme = 'org.standardnotes.theme-focus',
|
||||
FocusMode = 'org.standardnotes.focus-mode',
|
||||
FuturaTheme = 'org.standardnotes.theme-futura',
|
||||
MidnightTheme = 'org.standardnotes.theme-midnight',
|
||||
SolarizedDarkTheme = 'org.standardnotes.theme-solarized-dark',
|
||||
@@ -32,12 +30,12 @@ export enum FeatureIdentifier {
|
||||
|
||||
CodeEditor = 'org.standardnotes.code-editor',
|
||||
MarkdownProEditor = 'org.standardnotes.advanced-markdown-editor',
|
||||
MarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor',
|
||||
PlusEditor = 'org.standardnotes.plus-editor',
|
||||
SheetsEditor = 'org.standardnotes.standard-sheets',
|
||||
TaskEditor = 'org.standardnotes.simple-task-editor',
|
||||
TokenVaultEditor = 'org.standardnotes.token-vault',
|
||||
|
||||
DeprecatedMarkdownVisualEditor = 'org.standardnotes.markdown-visual-editor',
|
||||
DeprecatedBoldEditor = 'org.standardnotes.bold-editor',
|
||||
DeprecatedMarkdownBasicEditor = 'org.standardnotes.simple-markdown-editor',
|
||||
DeprecatedMarkdownMathEditor = 'org.standardnotes.fancy-markdown-editor',
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { FeatureDescription } from './FeatureDescription'
|
||||
import { FeatureIdentifier } from './FeatureIdentifier'
|
||||
import { editors } from '../Lists/Editors'
|
||||
import { themes } from '../Lists/Themes'
|
||||
import { serverFeatures } from '../Lists/ServerFeatures'
|
||||
import { clientFeatures } from '../Lists/ClientFeatures'
|
||||
import { GetDeprecatedFeatures } from '../Lists/DeprecatedFeatures'
|
||||
@@ -9,14 +7,7 @@ import { experimentalFeatures } from '../Lists/ExperimentalFeatures'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
|
||||
export function GetFeatures(): FeatureDescription[] {
|
||||
return [
|
||||
...themes(),
|
||||
...editors(),
|
||||
...serverFeatures(),
|
||||
...clientFeatures(),
|
||||
...experimentalFeatures(),
|
||||
...GetDeprecatedFeatures(),
|
||||
]
|
||||
return [...serverFeatures(), ...clientFeatures(), ...experimentalFeatures(), ...GetDeprecatedFeatures()]
|
||||
}
|
||||
|
||||
export function GetFeaturesForSubscription(subscription: SubscriptionName): FeatureDescription[] {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { ClientFeatureDescription } from '../Feature/FeatureDescription'
|
||||
import { FeatureDescription } from '../Feature/FeatureDescription'
|
||||
import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
import { themes } from './Themes'
|
||||
import { editors } from './Editors'
|
||||
|
||||
export function clientFeatures(): ClientFeatureDescription[] {
|
||||
export function clientFeatures(): FeatureDescription[] {
|
||||
return [
|
||||
...themes(),
|
||||
...editors(),
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Tag Nesting',
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.TagNesting,
|
||||
permission_name: PermissionName.TagNesting,
|
||||
description: 'Organize your tags into folders.',
|
||||
@@ -17,45 +22,26 @@ export function clientFeatures(): ClientFeatureDescription[] {
|
||||
name: 'Super Notes',
|
||||
identifier: FeatureIdentifier.SuperEditor,
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
permission_name: PermissionName.SuperEditor,
|
||||
description:
|
||||
'Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note.',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
'A new way to edit notes. Type / to bring up the block selection menu, or @ to embed images or link other tags and notes. Type - then space to start a list, or [] then space to start a checklist. Drag and drop an image or file to embed it in your note. Cmd/Ctrl + F to bring up search and replace.',
|
||||
},
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Smart Filters',
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.SmartFilters,
|
||||
permission_name: PermissionName.SmartFilters,
|
||||
description: 'Create smart filters for viewing notes matching specific criteria.',
|
||||
},
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Encrypted files',
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.ProUser],
|
||||
identifier: FeatureIdentifier.Files,
|
||||
permission_name: PermissionName.Files,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Focus Mode',
|
||||
identifier: FeatureIdentifier.FocusMode,
|
||||
permission_name: PermissionName.FocusMode,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Listed Custom Domain',
|
||||
identifier: FeatureIdentifier.ListedCustomDomain,
|
||||
permission_name: PermissionName.ListedCustomDomain,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Multiple accounts',
|
||||
identifier: FeatureIdentifier.AccountSwitcher,
|
||||
permission_name: PermissionName.AccountSwitcher,
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NoteType } from '../Component/NoteType'
|
||||
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
|
||||
import { ComponentAction } from '../Component/ComponentAction'
|
||||
import { ComponentArea } from '../Component/ComponentArea'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||
const bold: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -37,6 +38,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||
permission_name: PermissionName.BoldEditor,
|
||||
description: 'A simple and peaceful rich editor that helps you write and think clearly.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/bold.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const markdownBasic: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -50,6 +52,22 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||
permission_name: PermissionName.MarkdownBasicEditor,
|
||||
description: 'A Markdown editor with dynamic split-pane preview.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/simple-markdown.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const markdownAlt: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Markdown Alternative',
|
||||
identifier: FeatureIdentifier.DeprecatedMarkdownVisualEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
file_type: 'md',
|
||||
deprecated: true,
|
||||
permission_name: PermissionName.MarkdownVisualEditor,
|
||||
spellcheckControl: true,
|
||||
description:
|
||||
'A WYSIWYG-style Markdown editor that renders Markdown in preview-mode while you type without displaying any syntax.',
|
||||
index_path: 'build/index.html',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const markdownMinimist: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -64,6 +82,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||
deprecated: true,
|
||||
description: 'A minimal Markdown editor with live rendering and in-text search via Ctrl/Cmd + F',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/min-markdown.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const markdownMath: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -78,6 +97,7 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||
index_path: 'index.html',
|
||||
description: 'A beautiful split-pane Markdown editor with synced-scroll, LaTeX support, and colorful syntax.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/fancy-markdown.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const filesafe: IframeComponentFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -104,7 +124,8 @@ export function GetDeprecatedFeatures(): FeatureDescription[] {
|
||||
description:
|
||||
'Encrypted attachments for your notes using your Dropbox, Google Drive, or WebDAV server. Limited to 50MB per file.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/FileSafe-banner.png',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
return [bold, markdownBasic, markdownMinimist, markdownMath, filesafe]
|
||||
return [bold, markdownBasic, markdownMinimist, markdownMath, markdownAlt, filesafe]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { NoteType } from '../Component/NoteType'
|
||||
import { FillEditorComponentDefaults } from './Utilities/FillEditorComponentDefaults'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
export function editors(): EditorFeatureDescription[] {
|
||||
const code: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -20,6 +21,7 @@ export function editors(): EditorFeatureDescription[] {
|
||||
'Syntax highlighting and convenient keyboard shortcuts for over 120 programming' +
|
||||
' languages. Ideal for code snippets and procedures.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/code.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const plus: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -33,6 +35,7 @@ export function editors(): EditorFeatureDescription[] {
|
||||
description:
|
||||
'From highlighting to custom font sizes and colors, to tables and lists, this editor is perfect for crafting any document.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/plus-editor.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const markdown: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -46,19 +49,7 @@ export function editors(): EditorFeatureDescription[] {
|
||||
description:
|
||||
'A fully featured Markdown editor that supports live preview, a styling toolbar, and split pane support.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/adv-markdown.jpg',
|
||||
})
|
||||
|
||||
const markdownAlt: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Markdown Alternative',
|
||||
identifier: FeatureIdentifier.MarkdownVisualEditor,
|
||||
note_type: NoteType.Markdown,
|
||||
file_type: 'md',
|
||||
permission_name: PermissionName.MarkdownVisualEditor,
|
||||
spellcheckControl: true,
|
||||
description:
|
||||
'A WYSIWYG-style Markdown editor that renders Markdown in preview-mode while you type without displaying any syntax.',
|
||||
index_path: 'build/index.html',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const task: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -73,6 +64,7 @@ export function editors(): EditorFeatureDescription[] {
|
||||
description:
|
||||
'A great way to manage short-term and long-term to-do"s. You can mark tasks as completed, change their order, and edit the text naturally in place.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/task-editor.jpg',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const tokenvault: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -86,6 +78,7 @@ export function editors(): EditorFeatureDescription[] {
|
||||
description:
|
||||
'Encrypt and protect your 2FA secrets for all your internet accounts. Authenticator handles your 2FA secrets so that you never lose them again, or have to start over when you get a new device.',
|
||||
thumbnail_url: 'https://standard-notes.s3.amazonaws.com/screenshots/models/editors/token-vault.png',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const spreadsheets: EditorFeatureDescription = FillEditorComponentDefaults({
|
||||
@@ -99,7 +92,8 @@ export function editors(): EditorFeatureDescription[] {
|
||||
description:
|
||||
'A powerful spreadsheet editor with formatting and formula support. Not recommended for large data sets, as encryption of such data may decrease editor performance.',
|
||||
thumbnail_url: 'https://s3.amazonaws.com/standard-notes/screenshots/models/editors/spreadsheets.png',
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
return [code, plus, markdown, markdownAlt, task, tokenvault, spreadsheets]
|
||||
return [code, plus, markdown, task, tokenvault, spreadsheets]
|
||||
}
|
||||
|
||||
@@ -65,5 +65,11 @@ export function serverFeatures(): ServerFeatureDescription[] {
|
||||
identifier: FeatureIdentifier.SubscriptionSharing,
|
||||
permission_name: PermissionName.SubscriptionSharing,
|
||||
},
|
||||
{
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
name: 'Listed Custom Domain',
|
||||
identifier: FeatureIdentifier.ListedCustomDomain,
|
||||
permission_name: PermissionName.ListedCustomDomain,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { PermissionName } from '../Permission/PermissionName'
|
||||
import { FeatureIdentifier } from '../Feature/FeatureIdentifier'
|
||||
import { FillThemeComponentDefaults } from './Utilities/FillThemeComponentDefaults'
|
||||
import { SubscriptionName } from '@standardnotes/common'
|
||||
import { RoleName } from '@standardnotes/domain-core'
|
||||
|
||||
export function themes(): ThemeFeatureDescription[] {
|
||||
const midnight: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
@@ -17,10 +18,12 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
foreground_color: '#ffffff',
|
||||
border_color: '#086DD6',
|
||||
},
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
})
|
||||
|
||||
const futura: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Futura',
|
||||
identifier: FeatureIdentifier.FuturaTheme,
|
||||
permission_name: PermissionName.FuturaTheme,
|
||||
@@ -35,6 +38,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
|
||||
const solarizedDark: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Solarized Dark',
|
||||
identifier: FeatureIdentifier.SolarizedDarkTheme,
|
||||
permission_name: PermissionName.SolarizedDarkTheme,
|
||||
@@ -49,6 +53,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
|
||||
const autobiography: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Autobiography',
|
||||
identifier: FeatureIdentifier.AutobiographyTheme,
|
||||
permission_name: PermissionName.AutobiographyTheme,
|
||||
@@ -77,6 +82,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
|
||||
const titanium: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Titanium',
|
||||
identifier: FeatureIdentifier.TitaniumTheme,
|
||||
permission_name: PermissionName.TitaniumTheme,
|
||||
@@ -90,6 +96,7 @@ export function themes(): ThemeFeatureDescription[] {
|
||||
|
||||
const dynamic: ThemeFeatureDescription = FillThemeComponentDefaults({
|
||||
availableInSubscriptions: [SubscriptionName.PlusPlan, SubscriptionName.ProPlan],
|
||||
availableInRoles: [RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser],
|
||||
name: 'Dynamic Panels',
|
||||
identifier: FeatureIdentifier.DynamicTheme,
|
||||
permission_name: PermissionName.ThemeDynamic,
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.28.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
## [1.28.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/filepicker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/filepicker",
|
||||
"version": "1.28.7",
|
||||
"version": "1.28.15",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -26,7 +26,7 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/files": "workspace:*",
|
||||
"@standardnotes/utils": "workspace:*",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
|
||||
@@ -28,7 +28,13 @@ function createFileInputOrReturnExisting(): HTMLInputElement {
|
||||
fileInput = document.createElement('input')
|
||||
fileInput.id = FileInputId
|
||||
fileInput.type = 'file'
|
||||
fileInput.className = 'absolute top-0 left-0 -z-50 h-px w-px opacity-0'
|
||||
fileInput.style.position = 'absolute'
|
||||
fileInput.style.top = '0'
|
||||
fileInput.style.left = '0'
|
||||
fileInput.style.height = '1px'
|
||||
fileInput.style.width = '1px'
|
||||
fileInput.style.opacity = '0'
|
||||
fileInput.style.zIndex = '-50'
|
||||
fileInput.multiple = true
|
||||
document.body.appendChild(fileInput)
|
||||
|
||||
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.14.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
## [1.14.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/files",
|
||||
"version": "1.14.17",
|
||||
"version": "1.14.25",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -28,7 +28,7 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/models": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.50.46](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.45](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.44](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.43](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.42](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.41](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.40](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.39](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.38](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.37](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.36](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.34](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.33](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.32](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
## [3.50.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/mobile
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/mobile",
|
||||
"version": "3.50.31",
|
||||
"version": "3.50.46",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
|
||||
@@ -3,6 +3,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.42.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.42.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **models:** displaced dependencies ([9dc25cb](https://github.com/standardnotes/app/commit/9dc25cbb449cd45a3a34eb68fa5304f67f74b6c2))
|
||||
|
||||
## [1.42.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **models:** dependency level ([981bf6a](https://github.com/standardnotes/app/commit/981bf6ac928a8d6d84fb25bbf608f76a1dc6304b))
|
||||
|
||||
## [1.42.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.42.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixed issue with third party editors not loading ([#2174](https://github.com/standardnotes/app/issues/2174)) ([e7214ea](https://github.com/standardnotes/app/commit/e7214ea73ad5d3026d958c79022d2238a4d1cfdc))
|
||||
|
||||
## [1.42.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **models:** dependency declaration ([beccfb1](https://github.com/standardnotes/app/commit/beccfb16b361ba70bd4f7c09d5840d73ed6e85da))
|
||||
* **models:** dependency on utils declaration ([e9a1012](https://github.com/standardnotes/app/commit/e9a10123fed9e2883e3d2af38939b1608d7b2ffa))
|
||||
|
||||
## [1.42.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.42.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
## [1.42.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/models
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/models",
|
||||
"version": "1.42.4",
|
||||
"version": "1.42.12",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -21,18 +21,20 @@
|
||||
"lint": "eslint src --ext .ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/features": "workspace:*",
|
||||
"@standardnotes/responses": "workspace:*",
|
||||
"@standardnotes/utils": "workspace:*",
|
||||
"@standardnotes/utils": "workspace:^",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.2.3",
|
||||
"@types/lodash": "^4.14.189",
|
||||
"@typescript-eslint/eslint-plugin": "*",
|
||||
"eslint": "*",
|
||||
"eslint-plugin-prettier": "*",
|
||||
"jest": "^29.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "*"
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('component model', () => {
|
||||
expect(component.noteType).toEqual(NoteType.Authentication)
|
||||
})
|
||||
|
||||
it('should return plain as noteType if no note type defined in package_info', () => {
|
||||
it('should return unknown as noteType if no note type defined in package_info', () => {
|
||||
const component = new SNComponent(
|
||||
new DecryptedPayload(
|
||||
{
|
||||
@@ -83,6 +83,6 @@ describe('component model', () => {
|
||||
),
|
||||
)
|
||||
|
||||
expect(component.noteType).toEqual(NoteType.Plain)
|
||||
expect(component.noteType).toEqual(NoteType.Unknown)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -180,7 +180,7 @@ export class SNComponent extends DecryptedItem<ComponentContent> implements Comp
|
||||
}
|
||||
|
||||
public get noteType(): NoteType {
|
||||
return this.package_info.note_type || NoteType.Plain
|
||||
return this.package_info.note_type || NoteType.Unknown
|
||||
}
|
||||
|
||||
public get isDeprecated(): boolean {
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.133](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.132](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.131](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.130](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.129](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.128](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.127](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.126](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.125](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.124](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.123](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.122](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.121](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.120](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.119](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
## [1.4.118](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.4.118",
|
||||
"version": "1.4.133",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
## [1.13.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/responses
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/responses",
|
||||
"version": "1.13.2",
|
||||
"version": "1.13.6",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -33,7 +33,7 @@
|
||||
"typescript": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/features": "workspace:*",
|
||||
"@standardnotes/security": "^1.7.5",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
|
||||
@@ -4,5 +4,6 @@ import { MinimalHttpResponse } from '../Http/MinimalHttpResponses'
|
||||
export type GetOfflineFeaturesResponse = MinimalHttpResponse & {
|
||||
data?: {
|
||||
features: FeatureDescription[]
|
||||
roles: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,48 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.56.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** bring back history tests and fix handlinf of error with revisions ([cb9c160](https://github.com/standardnotes/app/commit/cb9c16047ebea7134c1aadaf89a8023497358a8e))
|
||||
|
||||
## [1.56.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
## [1.56.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/services",
|
||||
"version": "1.56.2",
|
||||
"version": "1.56.12",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/api": "workspace:^",
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/domain-core": "^1.11.1",
|
||||
"@standardnotes/encryption": "workspace:^",
|
||||
"@standardnotes/files": "workspace:^",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeatureDescription, FeatureIdentifier } from '@standardnotes/features'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { SNComponent } from '@standardnotes/models'
|
||||
|
||||
import { FeatureStatus } from './FeatureStatus'
|
||||
@@ -7,10 +7,10 @@ import { SetOfflineFeaturesFunctionResponse } from './SetOfflineFeaturesFunction
|
||||
export interface FeaturesClientInterface {
|
||||
downloadExternalFeature(urlOrCode: string): Promise<SNComponent | undefined>
|
||||
|
||||
getUserFeature(featureId: FeatureIdentifier): FeatureDescription | undefined
|
||||
|
||||
getFeatureStatus(featureId: FeatureIdentifier): FeatureStatus
|
||||
|
||||
hasFirstPartySubscription(): boolean
|
||||
|
||||
hasMinimumRole(role: string): boolean
|
||||
|
||||
setOfflineFeaturesCode(code: string): Promise<SetOfflineFeaturesFunctionResponse>
|
||||
|
||||
@@ -16,31 +16,23 @@ export class RevisionManager extends AbstractService implements RevisionClientIn
|
||||
async listRevisions(
|
||||
itemUuid: Uuid,
|
||||
): Promise<{ uuid: string; content_type: string; created_at: string; updated_at: string; required_role: string }[]> {
|
||||
try {
|
||||
const result = await this.revisionApiService.listRevisions(itemUuid.value)
|
||||
const result = await this.revisionApiService.listRevisions(itemUuid.value)
|
||||
|
||||
if (result.data.error) {
|
||||
return []
|
||||
}
|
||||
|
||||
return result.data.revisions
|
||||
} catch (error) {
|
||||
return []
|
||||
if (result.data.error) {
|
||||
throw new Error(result.data.error.message)
|
||||
}
|
||||
|
||||
return result.data.revisions
|
||||
}
|
||||
|
||||
async deleteRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise<string> {
|
||||
try {
|
||||
const result = await this.revisionApiService.deleteRevision(itemUuid.value, revisionUuid.value)
|
||||
const result = await this.revisionApiService.deleteRevision(itemUuid.value, revisionUuid.value)
|
||||
|
||||
if (result.data.error) {
|
||||
return result.data.error.message
|
||||
}
|
||||
|
||||
return result.data.message
|
||||
} catch (error) {
|
||||
return 'An error occurred while deleting the revision.'
|
||||
if (result.data.error) {
|
||||
throw new Error(result.data.error.message)
|
||||
}
|
||||
|
||||
return result.data.message
|
||||
}
|
||||
|
||||
async getRevision(
|
||||
@@ -57,16 +49,12 @@ export class RevisionManager extends AbstractService implements RevisionClientIn
|
||||
created_at: string
|
||||
updated_at: string
|
||||
} | null> {
|
||||
try {
|
||||
const result = await this.revisionApiService.getRevision(itemUuid.value, revisionUuid.value)
|
||||
const result = await this.revisionApiService.getRevision(itemUuid.value, revisionUuid.value)
|
||||
|
||||
if (result.data.error) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result.data.revision
|
||||
} catch (error) {
|
||||
return null
|
||||
if (result.data.error) {
|
||||
throw new Error(result.data.error.message)
|
||||
}
|
||||
|
||||
return result.data.revision
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export enum StorageKey {
|
||||
StorageEncryptionPolicy = 'storage_policy',
|
||||
WebSocketUrl = 'webSocket_url',
|
||||
UserRoles = 'user_roles',
|
||||
OfflineUserRoles = 'offline_user_roles',
|
||||
UserFeatures = 'user_features',
|
||||
ExperimentalFeatures = 'experimental_features',
|
||||
DeinitMode = 'deinit_mode',
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.164.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixed issue with third party editors not loading ([#2174](https://github.com/standardnotes/app/issues/2174)) ([e7214ea](https://github.com/standardnotes/app/commit/e7214ea73ad5d3026d958c79022d2238a4d1cfdc))
|
||||
|
||||
## [2.164.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **snjs:** bring back history tests and fix handlinf of error with revisions ([cb9c160](https://github.com/standardnotes/app/commit/cb9c16047ebea7134c1aadaf89a8023497358a8e))
|
||||
* **snjs:** remove isFailed() checks during tests in order for getValue() to give a meaningful exception ([6a7ff42](https://github.com/standardnotes/app/commit/6a7ff423ff82bd6eb7fbfbc95f4b1ebda36cc842))
|
||||
* **snjs:** signed out fetching revisions test ([21a4eb0](https://github.com/standardnotes/app/commit/21a4eb0cf00add2c4cf2e69e55c55cb9fd50c239))
|
||||
|
||||
## [2.164.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
## [2.164.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/snjs
|
||||
|
||||
@@ -1160,8 +1160,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.apiService.isThirdPartyHostUsed()
|
||||
}
|
||||
|
||||
public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider, isDevEnvironment: boolean): string {
|
||||
return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName, isDevEnvironment)
|
||||
public getCloudProviderIntegrationUrl(cloudProviderName: Settings.CloudProvider): string {
|
||||
return this.settingsService.getCloudProviderIntegrationUrl(cloudProviderName)
|
||||
}
|
||||
|
||||
private constructServices() {
|
||||
|
||||
@@ -46,4 +46,18 @@ describe('DeleteRevision', () => {
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Could not delete revision: Given value is not a valid uuid: invalid')
|
||||
})
|
||||
|
||||
it('should fail if revision manager throws', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
revisionManager.deleteRevision = jest.fn().mockRejectedValue(new Error('error'))
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
revisionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Could not delete revision: error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,8 +19,12 @@ export class DeleteRevision implements UseCaseInterface<void> {
|
||||
}
|
||||
const revisionUuid = revisionUuidOrError.getValue()
|
||||
|
||||
await this.revisionManager.deleteRevision(itemUuid, revisionUuid)
|
||||
try {
|
||||
await this.revisionManager.deleteRevision(itemUuid, revisionUuid)
|
||||
|
||||
return Result.ok()
|
||||
return Result.ok()
|
||||
} catch (error) {
|
||||
return Result.fail(`Could not delete revision: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,4 +155,18 @@ describe('GetRevision', () => {
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
})
|
||||
|
||||
it('should fail if revision manager throws', async () => {
|
||||
revisionManager.getRevision = jest.fn().mockRejectedValue(new Error('error'))
|
||||
|
||||
const useCase = createUseCase()
|
||||
|
||||
const result = await useCase.execute({
|
||||
itemUuid: '00000000-0000-0000-0000-000000000000',
|
||||
revisionUuid: '00000000-0000-0000-0000-000000000000',
|
||||
})
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Could not get revision: error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,8 +29,14 @@ export class GetRevision implements UseCaseInterface<HistoryEntry> {
|
||||
}
|
||||
const revisionUuid = revisionUuidOrError.getValue()
|
||||
|
||||
const revision = await this.revisionManager.getRevision(itemUuid, revisionUuid)
|
||||
if (revision === null) {
|
||||
let revision
|
||||
try {
|
||||
revision = await this.revisionManager.getRevision(itemUuid, revisionUuid)
|
||||
} catch (error) {
|
||||
return Result.fail(`Could not get revision: ${(error as Error).message}`)
|
||||
}
|
||||
|
||||
if (!revision) {
|
||||
return Result.fail('Could not get revision: Revision not found')
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,15 @@ describe('ListRevisions', () => {
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Could not list item revisions: Given value is not a valid uuid: invalid')
|
||||
})
|
||||
|
||||
it('should fail if revision manager throws', async () => {
|
||||
const useCase = createUseCase()
|
||||
|
||||
revisionManager.listRevisions = jest.fn().mockRejectedValue(new Error('error'))
|
||||
|
||||
const result = await useCase.execute({ itemUuid: '00000000-0000-0000-0000-000000000000' })
|
||||
|
||||
expect(result.isFailed()).toBe(true)
|
||||
expect(result.getError()).toEqual('Could not list item revisions: error')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,8 +15,12 @@ export class ListRevisions implements UseCaseInterface<Array<RevisionMetadata>>
|
||||
}
|
||||
const itemUuid = itemUuidOrError.getValue()
|
||||
|
||||
const revisions = await this.revisionManager.listRevisions(itemUuid)
|
||||
try {
|
||||
const revisions = await this.revisionManager.listRevisions(itemUuid)
|
||||
|
||||
return Result.ok(revisions)
|
||||
return Result.ok(revisions)
|
||||
} catch (error) {
|
||||
return Result.fail(`Could not list item revisions: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
export const APPLICATION_DEFAULT_HOSTS = [
|
||||
'api.standardnotes.com',
|
||||
'api-dev.standardnotes.com',
|
||||
'sync.standardnotes.org',
|
||||
'syncing-server-demo.standardnotes.com',
|
||||
]
|
||||
export const APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']
|
||||
|
||||
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com', 'files-dev.standardnotes.com']
|
||||
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com']
|
||||
|
||||
export const TRUSTED_FEATURE_HOSTS = [
|
||||
'api-dev.standardnotes.com',
|
||||
'api.standardnotes.com',
|
||||
'extensions.standardnotes.com',
|
||||
'extensions.standardnotes.org',
|
||||
'extensions-server-dev.standardnotes.org',
|
||||
'extensions-server-dev.standardnotes.com',
|
||||
'features.standardnotes.com',
|
||||
]
|
||||
|
||||
export enum ExtensionsServerURL {
|
||||
Dev = 'https://extensions-server-dev.standardnotes.org',
|
||||
Prod = 'https://extensions.standardnotes.org',
|
||||
}
|
||||
|
||||
|
||||
@@ -654,7 +654,7 @@ export class SNApiService
|
||||
|
||||
public async downloadOfflineFeaturesFromRepo(
|
||||
repo: SNFeatureRepo,
|
||||
): Promise<{ features: FeatureDescription[] } | ClientDisplayableError> {
|
||||
): Promise<{ features: FeatureDescription[]; roles: string[] } | ClientDisplayableError> {
|
||||
try {
|
||||
const featuresUrl = repo.offlineFeaturesUrl
|
||||
const extensionKey = repo.offlineKey
|
||||
@@ -678,8 +678,10 @@ export class SNApiService
|
||||
if (response.error) {
|
||||
return ClientDisplayableError.FromError(response.error)
|
||||
}
|
||||
const data = (response as Responses.GetOfflineFeaturesResponse).data
|
||||
return {
|
||||
features: (response as Responses.GetOfflineFeaturesResponse).data?.features || [],
|
||||
features: data?.features || [],
|
||||
roles: data?.roles || [],
|
||||
}
|
||||
} catch {
|
||||
return new ClientDisplayableError(API_MESSAGE_FAILED_OFFLINE_ACTIVATION)
|
||||
|
||||
@@ -80,7 +80,7 @@ export class SNWebSocketsService extends AbstractService<WebSocketsServiceEvent,
|
||||
|
||||
return response.data.token
|
||||
} catch (error) {
|
||||
console.error((error as Error).message)
|
||||
console.error('Caught error:', (error as Error).message)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ describe('featuresService', () => {
|
||||
|
||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
||||
expect(
|
||||
manager.areRequestedPermissionsValid(nativeComponent(FeatureIdentifier.MarkdownVisualEditor), permissions),
|
||||
manager.areRequestedPermissionsValid(nativeComponent(FeatureIdentifier.MarkdownProEditor), permissions),
|
||||
).toEqual(true)
|
||||
})
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('featuresService', () => {
|
||||
featuresService.getExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
||||
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -191,7 +191,7 @@ describe('featuresService', () => {
|
||||
featuresService.getEnabledExperimentalFeatures = jest.fn().mockReturnValue([FeatureIdentifier.PlusEditor])
|
||||
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.createItem).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -213,7 +213,7 @@ describe('featuresService', () => {
|
||||
const mock = (featuresService['notifyEvent'] = jest.fn())
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.PlusUser]
|
||||
await featuresService.setRoles(newRoles)
|
||||
await featuresService.setOnlineRoles(newRoles)
|
||||
|
||||
expect(mock.mock.calls[0][0]).toEqual(FeaturesEvent.UserRolesChanged)
|
||||
})
|
||||
@@ -226,7 +226,7 @@ describe('featuresService', () => {
|
||||
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
|
||||
expect(spy.mock.calls[2][0]).toEqual(FeaturesEvent.DidPurchaseSubscription)
|
||||
})
|
||||
@@ -235,12 +235,12 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
featuresService['roles'] = []
|
||||
featuresService['onlineRoles'] = []
|
||||
|
||||
const spy = jest.spyOn(featuresService, 'notifyEvent' as never)
|
||||
|
||||
const newRoles = [...roles, RoleName.NAMES.ProUser]
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
|
||||
const triggeredEvents = spy.mock.calls.map((call) => call[0])
|
||||
expect(triggeredEvents).not.toContain(FeaturesEvent.DidPurchaseSubscription)
|
||||
@@ -252,7 +252,7 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
|
||||
})
|
||||
@@ -263,7 +263,7 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserRoles, newRoles)
|
||||
expect(apiService.getUserFeatures).toHaveBeenCalledWith('123')
|
||||
})
|
||||
@@ -274,7 +274,7 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(storageService.setValue).toHaveBeenCalledWith(StorageKey.UserFeatures, features)
|
||||
})
|
||||
|
||||
@@ -284,7 +284,7 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.createItem).toHaveBeenCalledTimes(2)
|
||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||
ContentType.Theme,
|
||||
@@ -328,7 +328,7 @@ describe('featuresService', () => {
|
||||
itemManager.getItems = jest.fn().mockReturnValue([existingItem])
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
|
||||
expect(itemManager.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function))
|
||||
})
|
||||
@@ -354,7 +354,7 @@ describe('featuresService', () => {
|
||||
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.createItem).toHaveBeenCalledWith(
|
||||
ContentType.Component,
|
||||
expect.objectContaining({
|
||||
@@ -401,7 +401,7 @@ describe('featuresService', () => {
|
||||
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem])
|
||||
})
|
||||
|
||||
@@ -424,7 +424,7 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -447,7 +447,7 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(itemManager.createItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -455,10 +455,10 @@ describe('featuresService', () => {
|
||||
storageService.getValue = jest.fn().mockReturnValue(roles)
|
||||
const featuresService = createService()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', roles)
|
||||
expect(storageService.setValue).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
@@ -482,7 +482,7 @@ describe('featuresService', () => {
|
||||
const nativeFeature = featuresService['mapRemoteNativeFeatureToStaticFeature'](remoteFeature)
|
||||
featuresService['mapRemoteNativeFeatureToItem'] = jest.fn()
|
||||
featuresService.initializeFromDisk()
|
||||
await featuresService.updateRolesAndFetchFeatures('123', newRoles)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', newRoles)
|
||||
expect(featuresService['mapRemoteNativeFeatureToItem']).toHaveBeenCalledWith(
|
||||
nativeFeature,
|
||||
expect.anything(),
|
||||
@@ -509,7 +509,26 @@ describe('featuresService', () => {
|
||||
await expect(() => featuresService['mapRemoteNativeFeatureToItem'](clientFeature, [], [])).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('feature status', async () => {
|
||||
it('role-based feature status', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
features = [] as jest.Mocked<FeatureDescription[]>
|
||||
|
||||
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
features,
|
||||
},
|
||||
})
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
|
||||
})
|
||||
|
||||
it('feature status with no paid role but features listings', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
features = [
|
||||
@@ -535,54 +554,21 @@ describe('featuresService', () => {
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NoUserSubscription)
|
||||
|
||||
features = [
|
||||
{
|
||||
identifier: FeatureIdentifier.MidnightTheme,
|
||||
content_type: ContentType.Theme,
|
||||
expires_at: expiredDate,
|
||||
role_name: RoleName.NAMES.PlusUser,
|
||||
},
|
||||
{
|
||||
identifier: FeatureIdentifier.PlusEditor,
|
||||
content_type: ContentType.Component,
|
||||
expires_at: expiredDate,
|
||||
role_name: RoleName.NAMES.ProUser,
|
||||
},
|
||||
] as jest.Mocked<FeatureDescription[]>
|
||||
|
||||
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
features,
|
||||
},
|
||||
})
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(
|
||||
FeatureStatus.InCurrentPlanButExpired,
|
||||
)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.PlusEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SheetsEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
})
|
||||
|
||||
it('availableInRoles-based features', async () => {
|
||||
it('role-based features while not signed into first party server', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser])
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.Entitled)
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.SuperEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
})
|
||||
|
||||
it('third party feature status', async () => {
|
||||
@@ -629,7 +615,7 @@ describe('featuresService', () => {
|
||||
} as never),
|
||||
])
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(themeFeature.identifier)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(editorFeature.identifier)).toBe(FeatureStatus.InCurrentPlanButExpired)
|
||||
@@ -641,7 +627,7 @@ describe('featuresService', () => {
|
||||
it('feature status should be not entitled if no account or offline repo', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
@@ -653,30 +639,6 @@ describe('featuresService', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('feature status should be entitled for subscriber until first successful features request made if no cached features', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
apiService.getUserFeatures = jest.fn().mockReturnValue({
|
||||
data: {
|
||||
features: [],
|
||||
},
|
||||
})
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
featuresService['completedSuccessfulFeaturesRetrieval'] = false
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
|
||||
|
||||
await featuresService.didDownloadFeatures(features)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
})
|
||||
|
||||
it('didDownloadFeatures should filter out client controlled features', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
@@ -687,31 +649,13 @@ describe('featuresService', () => {
|
||||
expect(featuresService['mapRemoteNativeFeaturesToItems']).toHaveBeenCalledWith([])
|
||||
})
|
||||
|
||||
it('feature status should be dynamic for subscriber if cached features and no successful features request made yet', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
featuresService['completedSuccessfulFeaturesRetrieval'] = false
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
|
||||
featuresService['completedSuccessfulFeaturesRetrieval'] = false
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
})
|
||||
|
||||
it('feature status for offline subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
featuresService.rolesIncludePaidSubscription = jest.fn().mockReturnValue(false)
|
||||
featuresService.onlineRolesIncludePaidSubscription = jest.fn().mockReturnValue(false)
|
||||
featuresService['completedSuccessfulFeaturesRetrieval'] = true
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.NoUserSubscription)
|
||||
@@ -720,9 +664,11 @@ describe('featuresService', () => {
|
||||
)
|
||||
|
||||
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
featuresService.hasFirstPartySubscription = jest.fn().mockReturnValue(true)
|
||||
await featuresService.setOfflineRoles([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.MidnightTheme)).toBe(FeatureStatus.Entitled)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.NotInCurrentPlan)
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.TokenVaultEditor)).toBe(FeatureStatus.Entitled)
|
||||
})
|
||||
|
||||
it('feature status for deprecated feature', async () => {
|
||||
@@ -734,7 +680,7 @@ describe('featuresService', () => {
|
||||
FeatureStatus.NoUserSubscription,
|
||||
)
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.getFeatureStatus(FeatureIdentifier.DeprecatedFileSafe as FeatureIdentifier)).toBe(
|
||||
FeatureStatus.Entitled,
|
||||
@@ -744,25 +690,25 @@ describe('featuresService', () => {
|
||||
it('has paid subscription', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toBeFalsy
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toBeFalsy
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toEqual(true)
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
|
||||
it('has paid subscription should be true if offline repo and signed into third party server', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
|
||||
featuresService.hasOfflineRepo = jest.fn().mockReturnValue(true)
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(false)
|
||||
|
||||
expect(featuresService.hasPaidOnlineOrOfflineSubscription()).toEqual(true)
|
||||
expect(featuresService.hasPaidAnyPartyOnlineOrOfflineSubscription()).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -829,7 +775,11 @@ describe('featuresService', () => {
|
||||
it('should sort given roles according to role hierarchy', () => {
|
||||
const featuresService = createService()
|
||||
|
||||
const sortedRoles = featuresService.rolesBySorting([RoleName.NAMES.ProUser, RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser])
|
||||
const sortedRoles = featuresService.rolesBySorting([
|
||||
RoleName.NAMES.ProUser,
|
||||
RoleName.NAMES.CoreUser,
|
||||
RoleName.NAMES.PlusUser,
|
||||
])
|
||||
|
||||
expect(sortedRoles).toStrictEqual([RoleName.NAMES.CoreUser, RoleName.NAMES.PlusUser, RoleName.NAMES.ProUser])
|
||||
})
|
||||
@@ -839,7 +789,7 @@ describe('featuresService', () => {
|
||||
it('should be false if core user checks for plus role', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.CoreUser])
|
||||
|
||||
const hasPlusUserRole = featuresService.hasMinimumRole(RoleName.NAMES.PlusUser)
|
||||
|
||||
@@ -849,7 +799,9 @@ describe('featuresService', () => {
|
||||
it('should be false if plus user checks for pro role', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.PlusUser, RoleName.NAMES.CoreUser])
|
||||
|
||||
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||
|
||||
@@ -859,7 +811,9 @@ describe('featuresService', () => {
|
||||
it('should be true if pro user checks for core user', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
const hasCoreUserRole = featuresService.hasMinimumRole(RoleName.NAMES.CoreUser)
|
||||
|
||||
@@ -869,7 +823,9 @@ describe('featuresService', () => {
|
||||
it('should be true if pro user checks for pro user', async () => {
|
||||
const featuresService = createService()
|
||||
|
||||
await featuresService.updateRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
sessionManager.isSignedIntoFirstPartyServer = jest.fn().mockReturnValue(true)
|
||||
|
||||
await featuresService.updateOnlineRolesAndFetchFeatures('123', [RoleName.NAMES.ProUser, RoleName.NAMES.PlusUser])
|
||||
|
||||
const hasProUserRole = featuresService.hasMinimumRole(RoleName.NAMES.ProUser)
|
||||
|
||||
|
||||
@@ -58,7 +58,8 @@ export class SNFeaturesService
|
||||
implements FeaturesClientInterface, InternalEventHandlerInterface
|
||||
{
|
||||
private deinited = false
|
||||
private roles: string[] = []
|
||||
private onlineRoles: string[] = []
|
||||
private offlineRoles: string[] = []
|
||||
private features: FeaturesImports.FeatureDescription[] = []
|
||||
private enabledExperimentalFeatures: FeaturesImports.FeatureIdentifier[] = []
|
||||
private removeWebSocketsServiceObserver: () => void
|
||||
@@ -87,7 +88,7 @@ export class SNFeaturesService
|
||||
const {
|
||||
payload: { userUuid, currentRoles },
|
||||
} = data as UserRolesChangedEvent
|
||||
await this.updateRolesAndFetchFeatures(userUuid, currentRoles)
|
||||
await this.updateOnlineRolesAndFetchFeatures(userUuid, currentRoles)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -124,6 +125,16 @@ export class SNFeaturesService
|
||||
})
|
||||
}
|
||||
|
||||
public initializeFromDisk(): void {
|
||||
this.onlineRoles = this.storageService.getValue<string[]>(StorageKey.UserRoles, undefined, [])
|
||||
|
||||
this.offlineRoles = this.storageService.getValue<string[]>(StorageKey.OfflineUserRoles, undefined, [])
|
||||
|
||||
this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, [])
|
||||
|
||||
this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, [])
|
||||
}
|
||||
|
||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||
if (event.type === ApiServiceEvent.MetaReceived) {
|
||||
if (!this.syncService) {
|
||||
@@ -142,7 +153,7 @@ export class SNFeaturesService
|
||||
}
|
||||
|
||||
const { userUuid, userRoles } = event.payload as MetaReceivedData
|
||||
await this.updateRolesAndFetchFeatures(
|
||||
await this.updateOnlineRolesAndFetchFeatures(
|
||||
userUuid,
|
||||
userRoles.map((role) => role.name),
|
||||
)
|
||||
@@ -155,7 +166,7 @@ export class SNFeaturesService
|
||||
if (stage === ApplicationStage.FullSyncCompleted_13) {
|
||||
void this.mapClientControlledFeaturesToItems()
|
||||
|
||||
if (!this.rolesIncludePaidSubscription()) {
|
||||
if (!this.hasFirstPartyOnlineSubscription()) {
|
||||
const offlineRepo = this.getOfflineRepo()
|
||||
if (offlineRepo) {
|
||||
void this.downloadOfflineFeatures(offlineRepo)
|
||||
@@ -194,7 +205,7 @@ export class SNFeaturesService
|
||||
}
|
||||
|
||||
public enableExperimentalFeature(identifier: FeaturesImports.FeatureIdentifier): void {
|
||||
const feature = this.getUserFeature(identifier)
|
||||
const feature = this.getFeatureThatOriginallyCameFromServer(identifier)
|
||||
|
||||
this.enabledExperimentalFeatures.push(identifier)
|
||||
|
||||
@@ -309,10 +320,14 @@ export class SNFeaturesService
|
||||
repo: Models.SNFeatureRepo,
|
||||
): Promise<SetOfflineFeaturesFunctionResponse | ClientDisplayableError> {
|
||||
const result = await this.apiService.downloadOfflineFeaturesFromRepo(repo)
|
||||
|
||||
if (result instanceof ClientDisplayableError) {
|
||||
return result
|
||||
}
|
||||
|
||||
await this.didDownloadFeatures(result.features)
|
||||
await this.setOfflineRoles(result.roles)
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -363,18 +378,29 @@ export class SNFeaturesService
|
||||
}
|
||||
}
|
||||
|
||||
public initializeFromDisk(): void {
|
||||
this.roles = this.storageService.getValue<string[]>(StorageKey.UserRoles, undefined, [])
|
||||
|
||||
this.features = this.storageService.getValue(StorageKey.UserFeatures, undefined, [])
|
||||
|
||||
this.enabledExperimentalFeatures = this.storageService.getValue(StorageKey.ExperimentalFeatures, undefined, [])
|
||||
hasFirstPartyOnlineSubscription(): boolean {
|
||||
return this.sessionManager.isSignedIntoFirstPartyServer() && this.onlineRolesIncludePaidSubscription()
|
||||
}
|
||||
|
||||
public async updateRolesAndFetchFeatures(userUuid: UuidString, roles: string[]): Promise<void> {
|
||||
const previousRoles = this.roles
|
||||
hasFirstPartySubscription(): boolean {
|
||||
if (this.hasFirstPartyOnlineSubscription()) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userRolesChanged = this.haveRolesChanged(roles)
|
||||
const offlineRepo = this.getOfflineRepo()
|
||||
if (!offlineRepo) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === PROD_OFFLINE_FEATURES_URL
|
||||
return hasFirstPartyOfflineSubscription
|
||||
}
|
||||
|
||||
async updateOnlineRolesAndFetchFeatures(userUuid: UuidString, roles: string[]): Promise<void> {
|
||||
const previousRoles = this.onlineRoles
|
||||
|
||||
const userRolesChanged =
|
||||
roles.some((role) => !this.onlineRoles.includes(role)) || this.onlineRoles.some((role) => !roles.includes(role))
|
||||
|
||||
const isInitialLoadRolesChange = previousRoles.length === 0 && userRolesChanged
|
||||
|
||||
@@ -384,7 +410,7 @@ export class SNFeaturesService
|
||||
|
||||
this.needsInitialFeaturesUpdate = false
|
||||
|
||||
await this.setRoles(roles)
|
||||
await this.setOnlineRoles(roles)
|
||||
|
||||
const shouldDownloadRoleBasedFeatures = !this.hasOfflineRepo()
|
||||
|
||||
@@ -398,22 +424,34 @@ export class SNFeaturesService
|
||||
}
|
||||
|
||||
if (userRolesChanged && !isInitialLoadRolesChange) {
|
||||
if (this.rolesIncludePaidSubscription()) {
|
||||
if (this.onlineRolesIncludePaidSubscription()) {
|
||||
await this.notifyEvent(FeaturesEvent.DidPurchaseSubscription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setRoles(roles: string[]): Promise<void> {
|
||||
const rolesChanged = !arraysEqual(this.roles, roles)
|
||||
async setOnlineRoles(roles: string[]): Promise<void> {
|
||||
const rolesChanged = !arraysEqual(this.onlineRoles, roles)
|
||||
|
||||
this.roles = roles
|
||||
this.onlineRoles = roles
|
||||
|
||||
if (rolesChanged) {
|
||||
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
|
||||
}
|
||||
|
||||
this.storageService.setValue(StorageKey.UserRoles, this.roles)
|
||||
this.storageService.setValue(StorageKey.UserRoles, this.onlineRoles)
|
||||
}
|
||||
|
||||
async setOfflineRoles(roles: string[]): Promise<void> {
|
||||
const rolesChanged = !arraysEqual(this.offlineRoles, roles)
|
||||
|
||||
this.offlineRoles = roles
|
||||
|
||||
if (rolesChanged) {
|
||||
void this.notifyEvent(FeaturesEvent.UserRolesChanged)
|
||||
}
|
||||
|
||||
this.storageService.setValue(StorageKey.OfflineUserRoles, this.offlineRoles)
|
||||
}
|
||||
|
||||
public async didDownloadFeatures(features: FeaturesImports.FeatureDescription[]): Promise<void> {
|
||||
@@ -465,17 +503,19 @@ export class SNFeaturesService
|
||||
return nativeFeatureCopy
|
||||
}
|
||||
|
||||
public getUserFeature(featureId: FeaturesImports.FeatureIdentifier): FeaturesImports.FeatureDescription | undefined {
|
||||
public getFeatureThatOriginallyCameFromServer(
|
||||
featureId: FeaturesImports.FeatureIdentifier,
|
||||
): FeaturesImports.FeatureDescription | undefined {
|
||||
return this.features.find((feature) => feature.identifier === featureId)
|
||||
}
|
||||
|
||||
rolesIncludePaidSubscription(): boolean {
|
||||
onlineRolesIncludePaidSubscription(): boolean {
|
||||
const unpaidRoles = [RoleName.NAMES.CoreUser]
|
||||
return this.roles.some((role) => !unpaidRoles.includes(role))
|
||||
return this.onlineRoles.some((role) => !unpaidRoles.includes(role))
|
||||
}
|
||||
|
||||
public hasPaidOnlineOrOfflineSubscription(): boolean {
|
||||
return this.rolesIncludePaidSubscription() || this.hasOfflineRepo()
|
||||
hasPaidAnyPartyOnlineOrOfflineSubscription(): boolean {
|
||||
return this.onlineRolesIncludePaidSubscription() || this.hasOfflineRepo()
|
||||
}
|
||||
|
||||
public rolesBySorting(roles: string[]): string[] {
|
||||
@@ -485,7 +525,7 @@ export class SNFeaturesService
|
||||
public hasMinimumRole(role: string): boolean {
|
||||
const sortedAllRoles = Object.values(RoleName.NAMES)
|
||||
|
||||
const sortedUserRoles = this.rolesBySorting(this.roles)
|
||||
const sortedUserRoles = this.rolesBySorting(this.rolesToUseForFeatureCheck())
|
||||
|
||||
const highestUserRoleIndex = sortedAllRoles.indexOf(lastElement(sortedUserRoles) as string)
|
||||
|
||||
@@ -508,16 +548,10 @@ export class SNFeaturesService
|
||||
}
|
||||
|
||||
const nativeFeature = FeaturesImports.FindNativeFeature(featureId)
|
||||
if (nativeFeature && nativeFeature.availableInRoles) {
|
||||
const hasRole = this.roles.some((role) => nativeFeature.availableInRoles?.includes(role))
|
||||
if (hasRole) {
|
||||
return FeatureStatus.Entitled
|
||||
}
|
||||
}
|
||||
|
||||
const isDeprecated = this.isFeatureDeprecated(featureId)
|
||||
if (isDeprecated) {
|
||||
if (this.hasPaidOnlineOrOfflineSubscription()) {
|
||||
if (this.hasPaidAnyPartyOnlineOrOfflineSubscription()) {
|
||||
return FeatureStatus.Entitled
|
||||
} else {
|
||||
return FeatureStatus.NoUserSubscription
|
||||
@@ -538,7 +572,7 @@ export class SNFeaturesService
|
||||
return FeatureStatus.Entitled
|
||||
}
|
||||
|
||||
if (this.hasPaidOnlineOrOfflineSubscription()) {
|
||||
if (this.hasPaidAnyPartyOnlineOrOfflineSubscription()) {
|
||||
if (!this.completedSuccessfulFeaturesRetrieval) {
|
||||
const hasCachedFeatures = this.features.length > 0
|
||||
const temporarilyAllowUntilServerUpdates = !hasCachedFeatures
|
||||
@@ -550,25 +584,27 @@ export class SNFeaturesService
|
||||
return FeatureStatus.NoUserSubscription
|
||||
}
|
||||
|
||||
const feature = this.getUserFeature(featureId)
|
||||
if (!feature) {
|
||||
return FeatureStatus.NotInCurrentPlan
|
||||
}
|
||||
|
||||
const expired = feature.expires_at && new Date(feature.expires_at).getTime() < new Date().getTime()
|
||||
if (expired) {
|
||||
if (!this.roles.includes(feature.role_name as string)) {
|
||||
if (nativeFeature) {
|
||||
if (!this.hasFirstPartySubscription()) {
|
||||
return FeatureStatus.NotInCurrentPlan
|
||||
} else {
|
||||
return FeatureStatus.InCurrentPlanButExpired
|
||||
}
|
||||
|
||||
const roles = this.rolesToUseForFeatureCheck()
|
||||
if (nativeFeature.availableInRoles) {
|
||||
const hasRole = roles.some((role) => {
|
||||
return nativeFeature.availableInRoles?.includes(role)
|
||||
})
|
||||
if (!hasRole) {
|
||||
return FeatureStatus.NotInCurrentPlan
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FeatureStatus.Entitled
|
||||
}
|
||||
|
||||
private haveRolesChanged(roles: string[]): boolean {
|
||||
return roles.some((role) => !this.roles.includes(role)) || this.roles.some((role) => !roles.includes(role))
|
||||
private rolesToUseForFeatureCheck(): string[] {
|
||||
return this.hasFirstPartyOnlineSubscription() ? this.onlineRoles : this.offlineRoles
|
||||
}
|
||||
|
||||
private componentContentForNativeFeatureDescription(feature: FeaturesImports.FeatureDescription): Models.ItemContent {
|
||||
@@ -776,7 +812,8 @@ export class SNFeaturesService
|
||||
;(this.removeWebSocketsServiceObserver as unknown) = undefined
|
||||
this.removefeatureReposObserver()
|
||||
;(this.removefeatureReposObserver as unknown) = undefined
|
||||
;(this.roles as unknown) = undefined
|
||||
;(this.onlineRoles as unknown) = undefined
|
||||
;(this.offlineRoles as unknown) = undefined
|
||||
;(this.storageService as unknown) = undefined
|
||||
;(this.apiService as unknown) = undefined
|
||||
;(this.itemManager as unknown) = undefined
|
||||
@@ -793,7 +830,7 @@ export class SNFeaturesService
|
||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||
return Promise.resolve({
|
||||
features: {
|
||||
roles: this.roles,
|
||||
roles: this.onlineRoles,
|
||||
features: this.features,
|
||||
enabledExperimentalFeatures: this.enabledExperimentalFeatures,
|
||||
needsInitialFeaturesUpdate: this.needsInitialFeaturesUpdate,
|
||||
|
||||
@@ -66,10 +66,9 @@ export class SNSettingsService extends AbstractService implements SettingsClient
|
||||
return this.frequencyOptionsLabels[frequency]
|
||||
}
|
||||
|
||||
getCloudProviderIntegrationUrl(cloudProviderName: CloudProvider, isDevEnvironment: boolean): string {
|
||||
const { Dev, Prod } = ExtensionsServerURL
|
||||
const extServerUrl = isDevEnvironment ? Dev : Prod
|
||||
return `${extServerUrl}/${this.cloudProviderIntegrationUrlEndpoints[cloudProviderName]}?redirect_url=${extServerUrl}/components/cloudlink?`
|
||||
getCloudProviderIntegrationUrl(cloudProviderName: CloudProvider): string {
|
||||
const { Prod } = ExtensionsServerURL
|
||||
return `${Prod}/${this.cloudProviderIntegrationUrlEndpoints[cloudProviderName]}?redirect_url=${Prod}/components/cloudlink?`
|
||||
}
|
||||
|
||||
override deinit(): void {
|
||||
|
||||
@@ -60,8 +60,8 @@ describe('features', () => {
|
||||
|
||||
describe('new user roles received on api response meta', () => {
|
||||
it('should save roles and features', async () => {
|
||||
expect(application.featuresService.roles).to.have.lengthOf(1)
|
||||
expect(application.featuresService.roles[0]).to.equal('CORE_USER')
|
||||
expect(application.featuresService.onlineRoles).to.have.lengthOf(1)
|
||||
expect(application.featuresService.onlineRoles[0]).to.equal('CORE_USER')
|
||||
|
||||
expect(application.featuresService.features).to.have.lengthOf(3)
|
||||
expect(application.featuresService.features[0]).to.containSubset(midnightThemeFeature)
|
||||
@@ -115,7 +115,7 @@ describe('features', () => {
|
||||
// Wipe items from initial sync
|
||||
await application.itemManager.removeAllItemsFromMemory()
|
||||
// Wipe roles from initial sync
|
||||
await application.featuresService.setRoles([])
|
||||
await application.featuresService.setOnlineRoles([])
|
||||
// Create pre-existing item for theme without all the info
|
||||
await application.itemManager.createItem(
|
||||
ContentType.Theme,
|
||||
@@ -165,7 +165,7 @@ describe('features', () => {
|
||||
.find((theme) => theme.identifier === midnightThemeFeature.identifier)
|
||||
|
||||
// Wipe roles from initial sync
|
||||
await application.featuresService.setRoles([])
|
||||
await application.featuresService.setOnlineRoles([])
|
||||
|
||||
// Call sync intentionally to get roles again in meta
|
||||
await application.sync.sync()
|
||||
@@ -184,7 +184,7 @@ describe('features', () => {
|
||||
})
|
||||
|
||||
it('should provide feature', async () => {
|
||||
const feature = application.features.getUserFeature(FeatureIdentifier.PlusEditor)
|
||||
const feature = application.features.getFeatureThatOriginallyCameFromServer(FeatureIdentifier.PlusEditor)
|
||||
expect(feature).to.containSubset(plusEditorFeature)
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createNoteParams } from './lib/Items.js'
|
||||
chai.use(chaiAsPromised)
|
||||
const expect = chai.expect
|
||||
|
||||
describe('history manager', () => {
|
||||
describe.skip('history manager', () => {
|
||||
const largeCharacterChange = 25
|
||||
|
||||
const syncOptions = {
|
||||
@@ -277,7 +277,7 @@ describe('history manager', () => {
|
||||
await Factory.safeDeinit(this.application)
|
||||
})
|
||||
|
||||
it('response from server should be empty if not signed in', async function () {
|
||||
it('response from server should be failed if not signed in', async function () {
|
||||
await this.application.user.signOut()
|
||||
this.application = await Factory.createInitAppWithFakeCrypto()
|
||||
this.historyManager = this.application.historyManager
|
||||
@@ -286,8 +286,7 @@ describe('history manager', () => {
|
||||
await this.application.syncService.sync(syncOptions)
|
||||
const itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid })
|
||||
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(false)
|
||||
expect(itemHistoryOrError.getValue().length).to.equal(0)
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(true)
|
||||
})
|
||||
|
||||
it('create basic history entries 2', async function () {
|
||||
@@ -295,8 +294,6 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
|
||||
let itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid })
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(false)
|
||||
|
||||
let itemHistory = itemHistoryOrError.getValue()
|
||||
|
||||
/** Server history should save initial revision */
|
||||
@@ -305,8 +302,6 @@ describe('history manager', () => {
|
||||
/** Sync within 5 seconds (ENV VAR dependend on self-hosted setup), should not create a new entry */
|
||||
await Factory.markDirtyAndSyncItem(this.application, item)
|
||||
itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid })
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(false)
|
||||
|
||||
itemHistory = itemHistoryOrError.getValue()
|
||||
expect(itemHistory.length).to.equal(1)
|
||||
|
||||
@@ -322,8 +317,6 @@ describe('history manager', () => {
|
||||
)
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid })
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(false)
|
||||
|
||||
itemHistory = itemHistoryOrError.getValue()
|
||||
expect(itemHistory.length).to.equal(1)
|
||||
})
|
||||
@@ -346,15 +339,11 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
|
||||
const itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: item.uuid })
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(false)
|
||||
|
||||
const itemHistory = itemHistoryOrError.getValue()
|
||||
expect(itemHistory.length).to.equal(2)
|
||||
|
||||
const oldestEntry = lastElement(itemHistory)
|
||||
let revisionFromServerOrError = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid })
|
||||
expect(revisionFromServerOrError.isFailed()).to.equal(false)
|
||||
|
||||
const revisionFromServer = revisionFromServerOrError.getValue()
|
||||
expect(revisionFromServer).to.be.ok
|
||||
|
||||
@@ -376,12 +365,9 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
|
||||
const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid })
|
||||
expect(dupeHistoryOrError.isFailed()).to.equal(false)
|
||||
const dupeHistory = dupeHistoryOrError.getValue()
|
||||
|
||||
const dupeRevisionOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid })
|
||||
expect(dupeRevisionOrError.isFailed()).to.equal(false)
|
||||
|
||||
const dupeRevision = dupeRevisionOrError.getValue()
|
||||
expect(dupeRevision.payload.uuid).to.equal(dupe.uuid)
|
||||
})
|
||||
@@ -405,11 +391,9 @@ describe('history manager', () => {
|
||||
|
||||
const expectedRevisions = 4
|
||||
const noteHistoryOrError = await this.application.listRevisions.execute({ itemUuid: note.uuid })
|
||||
expect(noteHistoryOrError.isFailed()).to.equal(false)
|
||||
const noteHistory = noteHistoryOrError.getValue()
|
||||
|
||||
const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid })
|
||||
expect(dupeHistoryOrError.isFailed()).to.equal(false)
|
||||
const dupeHistory = dupeHistoryOrError.getValue()
|
||||
|
||||
expect(noteHistory.length).to.equal(expectedRevisions)
|
||||
@@ -432,15 +416,11 @@ describe('history manager', () => {
|
||||
await Factory.sleep(Factory.ServerRevisionCreationDelay)
|
||||
|
||||
const itemHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid })
|
||||
expect(itemHistoryOrError.isFailed()).to.equal(false)
|
||||
|
||||
const itemHistory = itemHistoryOrError.getValue()
|
||||
expect(itemHistory.length).to.be.above(1)
|
||||
const newestRevision = itemHistory[0]
|
||||
|
||||
const fetchedOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: newestRevision.uuid })
|
||||
expect(fetchedOrError.isFailed()).to.equal(false)
|
||||
|
||||
const fetched = fetchedOrError.getValue()
|
||||
expect(fetched.payload.errorDecrypting).to.not.be.ok
|
||||
expect(fetched.payload.content.title).to.equal(changedText)
|
||||
|
||||
@@ -645,6 +645,8 @@ describe('keys', function () {
|
||||
|
||||
expect(Object.keys(clientBUndecryptables).length).to.equal(1)
|
||||
expect(Object.keys(clientAUndecryptables).length).to.equal(0)
|
||||
|
||||
await contextB.deinit()
|
||||
})
|
||||
|
||||
describe('changing password on 003 client while signed into 004 client should', function () {
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('online conflict handling', function () {
|
||||
|
||||
afterEach(async function () {
|
||||
if (!this.application.dealloced) {
|
||||
await Factory.safeDeinit(this.application)
|
||||
await this.context.deinit()
|
||||
}
|
||||
localStorage.clear()
|
||||
})
|
||||
@@ -950,6 +950,7 @@ describe('online conflict handling', function () {
|
||||
expect(contextA.findNoteByTitle('title-B').payload.updated_at_timestamp).to.equal(noteBExpectedTimestamp)
|
||||
|
||||
await this.sharedFinalAssertions()
|
||||
await contextB.deinit()
|
||||
}).timeout(20000)
|
||||
|
||||
it('editing original note many times after conflict on other client should only result in 2 cumulative notes', async function () {
|
||||
@@ -979,5 +980,6 @@ describe('online conflict handling', function () {
|
||||
expect(contextB.noteCount).to.equal(2)
|
||||
|
||||
await this.sharedFinalAssertions()
|
||||
await contextB.deinit()
|
||||
}).timeout(20000)
|
||||
})
|
||||
|
||||
@@ -1052,5 +1052,7 @@ describe('online syncing', function () {
|
||||
await contextB.sync()
|
||||
|
||||
expect(contextB.application.items.allCountableNotesCount()).to.equal(0)
|
||||
|
||||
await contextB.deinit()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/snjs",
|
||||
"version": "2.164.2",
|
||||
"version": "2.164.12",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -36,7 +36,7 @@
|
||||
"@babel/core": "*",
|
||||
"@babel/preset-env": "*",
|
||||
"@standardnotes/api": "workspace:*",
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/domain-events": "^2.106.0",
|
||||
"@standardnotes/encryption": "workspace:*",
|
||||
"@standardnotes/features": "workspace:*",
|
||||
|
||||
@@ -3,6 +3,46 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.24.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
## [1.24.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/ui-services
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/ui-services",
|
||||
"version": "1.24.6",
|
||||
"version": "1.24.16",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -15,7 +15,7 @@
|
||||
"test": "jest spec"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"@standardnotes/features": "workspace:^",
|
||||
"@standardnotes/filepicker": "workspace:^",
|
||||
"@standardnotes/services": "workspace:^",
|
||||
|
||||
@@ -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.16.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/utils
|
||||
|
||||
## [1.16.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/utils
|
||||
|
||||
## [1.16.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/utils
|
||||
|
||||
## [1.16.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/utils
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/utils",
|
||||
"version": "1.16.1",
|
||||
"version": "1.16.4",
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <17.0.0"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"test": "jest spec"
|
||||
},
|
||||
"dependencies": {
|
||||
"@standardnotes/common": "^1.46.3",
|
||||
"@standardnotes/common": "^1.46.4",
|
||||
"dompurify": "^2.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"reflect-metadata": "^0.1.13"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const classNames = (...values: (string | boolean | undefined)[]): string => {
|
||||
export const classNames = (...values: any[]): string => {
|
||||
return values.map((value) => (typeof value === 'string' ? value : null)).join(' ')
|
||||
}
|
||||
|
||||
@@ -3,6 +3,74 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.142.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-26)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.142.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Fixed issue where popover/modal headers would get cut off by desktop titlebar ([a8c0be4](https://github.com/standardnotes/app/commit/a8c0be4f6327a48ae5e30c45ab4b8acc0c8ad612))
|
||||
|
||||
# [3.142.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)
|
||||
|
||||
### Features
|
||||
|
||||
* Links in Super notes will get auto-linked when they're pasted or typed ([1b696fa](https://github.com/standardnotes/app/commit/1b696fa50440bf5ff6f0a3a92ba9c2a775e374d6))
|
||||
|
||||
# [3.141.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
### Features
|
||||
|
||||
* Added ability to change the note type of multiple notes at once ([#2180](https://github.com/standardnotes/app/issues/2180)) ([af6ae81](https://github.com/standardnotes/app/commit/af6ae81e1d4af4cc3ecd528eddf101eadc923fc9))
|
||||
|
||||
## [3.140.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fixed issue with third party editors not loading ([#2174](https://github.com/standardnotes/app/issues/2174)) ([e7214ea](https://github.com/standardnotes/app/commit/e7214ea73ad5d3026d958c79022d2238a4d1cfdc))
|
||||
|
||||
## [3.140.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
## [3.140.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
@@ -28,7 +96,6 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
### Features
|
||||
|
||||
* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))
|
||||
* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01))
|
||||
|
||||
## [3.138.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-17)
|
||||
|
||||
|
||||
@@ -1,5 +1,182 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"version": "3.142.2",
|
||||
"title": "[3.142.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-26)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.142.1",
|
||||
"title": "[3.142.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* Fixed issue where popover/modal headers would get cut off by desktop titlebar ([a8c0be4](https://github.com/standardnotes/app/commit/a8c0be4f6327a48ae5e30c45ab4b8acc0c8ad612))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Fixed issue where popover/modal headers would get cut off by desktop titlebar (a8c0be4)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"Fixed issue where popover/modal headers would get cut off by desktop titlebar (a8c0be4)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.142.0",
|
||||
"title": "[3.142.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-25)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Links in Super notes will get auto-linked when they're pasted or typed ([1b696fa](https://github.com/standardnotes/app/commit/1b696fa50440bf5ff6f0a3a92ba9c2a775e374d6))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Links in Super notes will get auto-linked when they're pasted or typed (1b696fa)"
|
||||
],
|
||||
"Features": [
|
||||
"Links in Super notes will get auto-linked when they're pasted or typed (1b696fa)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.141.0",
|
||||
"title": "[3.141.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Added ability to change the note type of multiple notes at once ([#2180](https://github.com/standardnotes/app/issues/2180)) ([af6ae81](https://github.com/standardnotes/app/commit/af6ae81e1d4af4cc3ecd528eddf101eadc923fc9))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Added ability to change the note type of multiple notes at once (#2180) (af6ae81)"
|
||||
],
|
||||
"Features": [
|
||||
"Added ability to change the note type of multiple notes at once (#2180) (af6ae81)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.14",
|
||||
"title": "[3.140.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.13",
|
||||
"title": "[3.140.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.12",
|
||||
"title": "[3.140.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-24)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.11",
|
||||
"title": "[3.140.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.10",
|
||||
"title": "[3.140.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-23)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.9",
|
||||
"title": "[3.140.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-21)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.8",
|
||||
"title": "[3.140.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)",
|
||||
"date": null,
|
||||
"body": "### Bug Fixes\n\n* fixed issue with third party editors not loading ([#2174](https://github.com/standardnotes/app/issues/2174)) ([e7214ea](https://github.com/standardnotes/app/commit/e7214ea73ad5d3026d958c79022d2238a4d1cfdc))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"fixed issue with third party editors not loading (#2174) (e7214ea)"
|
||||
],
|
||||
"Bug Fixes": [
|
||||
"fixed issue with third party editors not loading (#2174) (e7214ea)"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.6",
|
||||
"title": "[3.140.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.5",
|
||||
"title": "[3.140.5](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.4",
|
||||
"title": "[3.140.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.3",
|
||||
"title": "[3.140.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-20)",
|
||||
"date": null,
|
||||
"body": "**Note:** Version bump only for package @standardnotes/web",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Note: Version bump only for package @standardnotes/web"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"version": "3.140.2",
|
||||
"title": "[3.140.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-19)",
|
||||
@@ -47,15 +224,13 @@
|
||||
"version": "3.139.0",
|
||||
"title": "[3.139.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-18)",
|
||||
"date": null,
|
||||
"body": "### Features\n\n* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))\n* **snjs:** add revisions api v2 ([#2154](https://github.com/standardnotes/app/issues/2154)) ([880a537](https://github.com/standardnotes/app/commit/880a537774ddcefaedb0d4e5dc50b363f4b93e01))",
|
||||
"body": "### Features\n\n* Added rename option to file preview modal ([aa88966](https://github.com/standardnotes/app/commit/aa8896678315de67551c786e64aec7dfd10479e3))",
|
||||
"parsed": {
|
||||
"_": [
|
||||
"Added rename option to file preview modal (aa88966)",
|
||||
"snjs: add revisions api v2 (#2154) (880a537)"
|
||||
"Added rename option to file preview modal (aa88966)"
|
||||
],
|
||||
"Features": [
|
||||
"Added rename option to file preview modal (aa88966)",
|
||||
"snjs: add revisions api v2 (#2154) (880a537)"
|
||||
"Added rename option to file preview modal (aa88966)"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.140.2",
|
||||
"version": "3.142.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes.",
|
||||
|
||||
@@ -4,14 +4,10 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Dropdown from '../Dropdown/Dropdown'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import Modal from '../Modal/Modal'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
@@ -87,10 +83,43 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
close()
|
||||
}, [capturedPhoto, close, fileName, filesController])
|
||||
|
||||
const retryPhoto = () => {
|
||||
setCapturedPhoto(undefined)
|
||||
setRecorder(new PhotoRecorder())
|
||||
}
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Take a photo</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<Modal
|
||||
title="Take a photo"
|
||||
close={close}
|
||||
actions={[
|
||||
{
|
||||
label: 'Capture',
|
||||
onClick: takePhoto,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !!capturedPhoto,
|
||||
},
|
||||
{
|
||||
label: 'Upload',
|
||||
onClick: savePhoto,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !capturedPhoto,
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: close,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Retry',
|
||||
onClick: retryPhoto,
|
||||
type: 'secondary',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
@@ -133,7 +162,7 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
onChange={(value: string) => {
|
||||
void recorder.setDevice(value)
|
||||
}}
|
||||
className={{
|
||||
classNameOverride={{
|
||||
wrapper: 'mt-1',
|
||||
popover: 'z-modal',
|
||||
}}
|
||||
@@ -141,40 +170,8 @@ const PhotoCaptureModal = ({ filesController, close }: Props) => {
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedPhoto && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void takePhoto()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Take photo
|
||||
</Button>
|
||||
)}
|
||||
{capturedPhoto && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedPhoto(undefined)
|
||||
setRecorder(new PhotoRecorder())
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={savePhoto}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,9 @@ import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import Modal from '../Modal/Modal'
|
||||
|
||||
type Props = {
|
||||
filesController: FilesController
|
||||
@@ -77,10 +73,59 @@ const VideoCaptureModal = ({ filesController, close }: Props) => {
|
||||
return URL.createObjectURL(capturedVideo)
|
||||
}, [capturedVideo])
|
||||
|
||||
const stopRecording = async () => {
|
||||
const capturedVideo = await recorder.stop()
|
||||
setIsRecording(false)
|
||||
setCapturedVideo(capturedVideo)
|
||||
}
|
||||
|
||||
const retryRecording = () => {
|
||||
setCapturedVideo(undefined)
|
||||
setRecorder(new VideoRecorder(fileName))
|
||||
setIsRecorderReady(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={close}>Record a video</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<Modal
|
||||
title="Record a video"
|
||||
close={close}
|
||||
actions={[
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: close,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Record',
|
||||
onClick: startRecording,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !!capturedVideo || isRecording,
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
onClick: stopRecording,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !!capturedVideo || !isRecording,
|
||||
},
|
||||
{
|
||||
label: 'Retry',
|
||||
onClick: retryRecording,
|
||||
type: 'secondary',
|
||||
hidden: !capturedVideo,
|
||||
},
|
||||
{
|
||||
label: 'Upload',
|
||||
onClick: saveVideo,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
hidden: !capturedVideo,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="mb-4 flex flex-col">
|
||||
<label className="text-sm font-medium text-neutral">
|
||||
File name:
|
||||
@@ -111,55 +156,8 @@ const VideoCaptureModal = ({ filesController, close }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{!capturedVideo && !isRecording && (
|
||||
<Button
|
||||
primary
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
void startRecording()
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Start recording
|
||||
</Button>
|
||||
)}
|
||||
{!capturedVideo && isRecording && (
|
||||
<Button
|
||||
primary
|
||||
colorStyle="danger"
|
||||
className="flex items-center gap-2"
|
||||
onClick={async () => {
|
||||
const capturedVideo = await recorder.stop()
|
||||
setIsRecording(false)
|
||||
setCapturedVideo(capturedVideo)
|
||||
}}
|
||||
>
|
||||
<Icon type="camera" />
|
||||
Stop recording
|
||||
</Button>
|
||||
)}
|
||||
{capturedVideo && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setCapturedVideo(undefined)
|
||||
setRecorder(new VideoRecorder(fileName))
|
||||
setIsRecorderReady(false)
|
||||
}}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
<Button primary className="flex items-center gap-2" onClick={saveVideo}>
|
||||
<Icon type="upload" />
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import {
|
||||
ButtonType,
|
||||
Challenge,
|
||||
@@ -19,8 +18,10 @@ import { ApplicationGroup } from '@/Application/ApplicationGroup'
|
||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { ChallengeModalValues } from './ChallengeModalValues'
|
||||
import { InputValue } from './InputValue'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import Modal from '../Modal/Modal'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -209,100 +210,115 @@ const ChallengeModal: FunctionComponent<Props> = ({
|
||||
}
|
||||
}, [application, cancelChallenge, challenge.cancelable])
|
||||
|
||||
if (!challenge.prompts) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
const isFullScreenBlocker = challenge.reason === ChallengeReason.ApplicationUnlock
|
||||
const isMobileOverlay = isMobileScreen() && !isFullScreenBlocker
|
||||
|
||||
const contentClasses = classNames(
|
||||
'challenge-modal relative flex flex-col items-center rounded border-solid border-border p-8 md:border',
|
||||
!isMobileScreen() && 'shadow-overlay-light',
|
||||
isMobileOverlay && 'border border-solid border-border shadow-overlay-light',
|
||||
isFullScreenBlocker && isMobileScreen() ? 'bg-passive-5' : 'bg-default',
|
||||
)
|
||||
const isMobileOverlay = isMobileScreen && !isFullScreenBlocker
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={`sn-component ${isFullScreenBlocker ? 'bg-passive-5' : ''}`}
|
||||
<ModalOverlay
|
||||
isOpen={true}
|
||||
className={`sn-component p-0 ${isFullScreenBlocker ? 'bg-passive-5' : ''}`}
|
||||
onDismiss={cancelChallenge}
|
||||
dangerouslyBypassFocusLock={bypassModalFocusLock}
|
||||
key={challenge.id}
|
||||
>
|
||||
<DialogContent aria-label="Challenge modal" className={contentClasses}>
|
||||
<Modal
|
||||
title="Authenticate"
|
||||
close={cancelChallenge}
|
||||
className={{
|
||||
content: classNames(
|
||||
'challenge-modal relative m-0 flex h-full w-full flex-col items-center rounded border-solid border-border bg-default p-0 md:h-auto md:!w-auto md:border',
|
||||
!isMobileScreen && 'shadow-overlay-light',
|
||||
isMobileOverlay && 'shadow-overlay-light border border-solid border-border',
|
||||
),
|
||||
}}
|
||||
customHeader={<></>}
|
||||
customFooter={<></>}
|
||||
disableCustomHeader={isMobileScreen}
|
||||
actions={[
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: cancelChallenge,
|
||||
type: 'primary',
|
||||
hidden: !challenge.cancelable,
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{challenge.cancelable && (
|
||||
<button
|
||||
onClick={cancelChallenge}
|
||||
aria-label="Close modal"
|
||||
className="absolute top-4 right-4 flex cursor-pointer border-0 bg-transparent p-1"
|
||||
className="absolute top-4 right-4 hidden cursor-pointer border-0 bg-transparent p-1 md:flex"
|
||||
>
|
||||
<Icon type="close" className="text-neutral" />
|
||||
</button>
|
||||
)}
|
||||
<ProtectedIllustration className="mb-4 h-30 w-30" />
|
||||
<div className="mb-3 max-w-76 text-center text-lg font-bold">{challenge.heading}</div>
|
||||
{challenge.subheading && (
|
||||
<div className="break-word mb-4 max-w-76 text-center text-sm">{challenge.subheading}</div>
|
||||
)}
|
||||
<form
|
||||
className="flex min-w-76 flex-col items-center"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}}
|
||||
ref={promptsContainerRef}
|
||||
>
|
||||
{challenge.prompts.map((prompt, index) => (
|
||||
<ChallengeModalPrompt
|
||||
application={application}
|
||||
key={prompt.id}
|
||||
prompt={prompt}
|
||||
values={values}
|
||||
index={index}
|
||||
onValueChange={onValueChange}
|
||||
isInvalid={values[prompt.id].invalid}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
<Button primary disabled={isProcessing} className="mt-1 mb-3.5 min-w-76" onClick={submit}>
|
||||
{isProcessing ? 'Generating Keys...' : 'Submit'}
|
||||
</Button>
|
||||
{shouldShowForgotPasscode && (
|
||||
<Button
|
||||
className="flex min-w-76 items-center justify-center"
|
||||
onClick={() => {
|
||||
setBypassModalFocusLock(true)
|
||||
application.alertService
|
||||
.confirm(
|
||||
'If you forgot your local passcode, your only option is to clear your local data from this device and sign back in to your account.',
|
||||
'Forgot passcode?',
|
||||
'Delete local data',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
.then((shouldDeleteLocalData) => {
|
||||
if (shouldDeleteLocalData) {
|
||||
application.user.signOut().catch(console.error)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setBypassModalFocusLock(false)
|
||||
})
|
||||
<div className="flex min-h-0 w-full flex-grow flex-col items-center overflow-auto p-8">
|
||||
<ProtectedIllustration className="mb-4 h-30 w-30 flex-shrink-0" />
|
||||
<div className="mb-3 max-w-76 text-center text-lg font-bold">{challenge.heading}</div>
|
||||
{challenge.subheading && (
|
||||
<div className="break-word mb-4 max-w-76 text-center text-sm">{challenge.subheading}</div>
|
||||
)}
|
||||
<form
|
||||
className="flex w-full max-w-76 flex-col items-center md:min-w-76"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
submit()
|
||||
}}
|
||||
ref={promptsContainerRef}
|
||||
>
|
||||
<Icon type="help" className="mr-2 text-neutral" />
|
||||
Forgot passcode?
|
||||
{challenge.prompts.map((prompt, index) => (
|
||||
<ChallengeModalPrompt
|
||||
application={application}
|
||||
key={prompt.id}
|
||||
prompt={prompt}
|
||||
values={values}
|
||||
index={index}
|
||||
onValueChange={onValueChange}
|
||||
isInvalid={values[prompt.id].invalid}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
<Button primary disabled={isProcessing} className="mt-1 mb-3.5 min-w-76" onClick={submit}>
|
||||
{isProcessing ? 'Generating Keys...' : 'Submit'}
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowWorkspaceSwitcher && (
|
||||
<LockscreenWorkspaceSwitcher
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
{shouldShowForgotPasscode && (
|
||||
<Button
|
||||
className="flex min-w-76 items-center justify-center"
|
||||
onClick={() => {
|
||||
setBypassModalFocusLock(true)
|
||||
application.alertService
|
||||
.confirm(
|
||||
'If you forgot your local passcode, your only option is to clear your local data from this device and sign back in to your account.',
|
||||
'Forgot passcode?',
|
||||
'Delete local data',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
.then((shouldDeleteLocalData) => {
|
||||
if (shouldDeleteLocalData) {
|
||||
application.user.signOut().catch(console.error)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
setBypassModalFocusLock(false)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Icon type="help" className="mr-2 text-neutral" />
|
||||
Forgot passcode?
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowWorkspaceSwitcher && (
|
||||
<LockscreenWorkspaceSwitcher
|
||||
mainApplicationGroup={mainApplicationGroup}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -99,14 +99,16 @@ const ChallengeModalPrompt: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<label
|
||||
key={option.label}
|
||||
className={`cursor-pointer rounded px-2 py-1.5 focus-within:ring-2 focus-within:ring-info ${
|
||||
className={`relative flex cursor-pointer items-center justify-center rounded px-2 py-1.5 text-center focus-within:ring-2 focus-within:ring-info ${
|
||||
selected ? 'bg-default font-semibold text-foreground' : 'text-passive-0 hover:bg-passive-3'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={`session-duration-${prompt.id}`}
|
||||
className={'m-0 appearance-none focus:shadow-none focus:outline-none'}
|
||||
className={
|
||||
'absolute top-0 left-0 m-0 h-px w-px appearance-none focus:shadow-none focus:outline-none'
|
||||
}
|
||||
style={{
|
||||
marginRight: 0,
|
||||
}}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/Premium
|
||||
import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter'
|
||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||
import { Pill } from '../Preferences/PreferencesComponents/Content'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
|
||||
type ChangeEditorMenuProps = {
|
||||
application: WebApplication
|
||||
@@ -174,6 +175,8 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
closeMenu()
|
||||
}, [note, pendingSuperItem, selectNonComponent, closeMenu])
|
||||
|
||||
const closeSuperNoteImporter = () => setShowSuperImporter(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu className="pt-0.5 pb-1" a11yLabel="Change note type menu" isOpen={isVisible}>
|
||||
@@ -220,14 +223,16 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
)
|
||||
})}
|
||||
</Menu>
|
||||
{showSuperImporter && note && (
|
||||
<SuperNoteImporter
|
||||
note={note}
|
||||
application={application}
|
||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
||||
closeDialog={() => setShowSuperImporter(false)}
|
||||
/>
|
||||
)}
|
||||
<ModalOverlay isOpen={showSuperImporter} onDismiss={closeSuperNoteImporter}>
|
||||
{note && (
|
||||
<SuperNoteImporter
|
||||
note={note}
|
||||
application={application}
|
||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
||||
closeDialog={closeSuperNoteImporter}
|
||||
/>
|
||||
)}
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { useRef, useState } from 'react'
|
||||
import RoundIconButton from '../Button/RoundIconButton'
|
||||
import Popover from '../Popover/Popover'
|
||||
import ChangeMultipleMenu from './ChangeMultipleMenu'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
notesController: NotesController
|
||||
}
|
||||
|
||||
const ChangeMultipleButton = ({ application, notesController }: Props) => {
|
||||
const changeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const [isChangeMenuOpen, setIsChangeMenuOpen] = useState(false)
|
||||
const toggleMenu = () => setIsChangeMenuOpen((open) => !open)
|
||||
const [disableClickOutside, setDisableClickOutside] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<RoundIconButton label={'Change note type'} onClick={toggleMenu} ref={changeButtonRef} icon="plain-text" />
|
||||
<Popover
|
||||
title="Change note type"
|
||||
togglePopover={toggleMenu}
|
||||
disableClickOutside={disableClickOutside}
|
||||
anchorElement={changeButtonRef.current}
|
||||
open={isChangeMenuOpen}
|
||||
className="pt-2 md:pt-0"
|
||||
>
|
||||
<ChangeMultipleMenu
|
||||
application={application}
|
||||
notes={notesController.selectedNotes}
|
||||
setDisableClickOutside={setDisableClickOutside}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeMultipleButton
|
||||
@@ -0,0 +1,197 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { createEditorMenuGroups } from '@/Utils/createEditorMenuGroups'
|
||||
import { ComponentArea, NoteMutator, NoteType, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { PremiumFeatureIconName, PremiumFeatureIconClass } from '../Icon/PremiumFeatureIcon'
|
||||
import Menu from '../Menu/Menu'
|
||||
import MenuItem from '../Menu/MenuItem'
|
||||
import { EditorMenuGroup } from '../NotesOptions/EditorMenuGroup'
|
||||
import { EditorMenuItem } from '../NotesOptions/EditorMenuItem'
|
||||
import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter'
|
||||
import { Pill } from '../Preferences/PreferencesComponents/Content'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
|
||||
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
notes: SNNote[]
|
||||
setDisableClickOutside: (value: boolean) => void
|
||||
}
|
||||
|
||||
const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Props) => {
|
||||
const premiumModal = usePremiumModal()
|
||||
|
||||
const [itemToBeSelected, setItemToBeSelected] = useState<EditorMenuItem | undefined>()
|
||||
const [confirmationQueue, setConfirmationQueue] = useState<SNNote[]>([])
|
||||
|
||||
const hasSelectedLockedNotes = useMemo(() => notes.some((note) => note.locked), [notes])
|
||||
|
||||
const editors = useMemo(
|
||||
() =>
|
||||
application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => {
|
||||
return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1
|
||||
}),
|
||||
[application.componentManager],
|
||||
)
|
||||
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
||||
|
||||
const selectComponent = useCallback(
|
||||
async (component: SNComponent, note: SNNote) => {
|
||||
if (component.conflictOf) {
|
||||
void application.mutator.changeAndSaveItem(component, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
}
|
||||
|
||||
await application.mutator.changeAndSaveItem(note, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
noteMutator.noteType = component.noteType
|
||||
noteMutator.editorIdentifier = component.identifier
|
||||
})
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const selectNonComponent = useCallback(
|
||||
async (item: EditorMenuItem, note: SNNote) => {
|
||||
await application.mutator.changeAndSaveItem(note, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator
|
||||
noteMutator.noteType = item.noteType
|
||||
noteMutator.editorIdentifier = undefined
|
||||
})
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const selectItem = useCallback(
|
||||
async (itemToBeSelected: EditorMenuItem) => {
|
||||
if (!itemToBeSelected.isEntitled) {
|
||||
premiumModal.activate(itemToBeSelected.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (hasSelectedLockedNotes) {
|
||||
void application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT)
|
||||
return
|
||||
}
|
||||
|
||||
if (itemToBeSelected.noteType === NoteType.Super) {
|
||||
setDisableClickOutside(true)
|
||||
setItemToBeSelected(itemToBeSelected)
|
||||
setConfirmationQueue(notes)
|
||||
return
|
||||
}
|
||||
|
||||
if (itemToBeSelected.component) {
|
||||
const changeRequiresConfirmation = notes.some((note) => {
|
||||
const editorForNote = application.componentManager.editorForNote(note)
|
||||
return application.componentManager.doesEditorChangeRequireAlert(editorForNote, itemToBeSelected.component)
|
||||
})
|
||||
|
||||
if (changeRequiresConfirmation) {
|
||||
const canChange = await application.componentManager.showEditorChangeAlert()
|
||||
if (!canChange) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (const note of notes) {
|
||||
void selectComponent(itemToBeSelected.component, note)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const note of notes) {
|
||||
void selectNonComponent(itemToBeSelected, note)
|
||||
}
|
||||
},
|
||||
[
|
||||
application.alertService,
|
||||
application.componentManager,
|
||||
hasSelectedLockedNotes,
|
||||
notes,
|
||||
premiumModal,
|
||||
selectComponent,
|
||||
selectNonComponent,
|
||||
setDisableClickOutside,
|
||||
],
|
||||
)
|
||||
|
||||
const groupsWithItems = groups.filter((group) => group.items && group.items.length)
|
||||
|
||||
const showSuperImporter = itemToBeSelected?.noteType === NoteType.Super && confirmationQueue.length > 0
|
||||
|
||||
const closeCurrentSuperNoteImporter = useCallback(() => {
|
||||
const remainingNotes = confirmationQueue.slice(1)
|
||||
|
||||
if (remainingNotes.length === 0) {
|
||||
setItemToBeSelected(undefined)
|
||||
setConfirmationQueue([])
|
||||
setDisableClickOutside(false)
|
||||
return
|
||||
}
|
||||
|
||||
setConfirmationQueue(remainingNotes)
|
||||
}, [confirmationQueue, setDisableClickOutside])
|
||||
|
||||
const handleSuperNoteConversionCompletion = useCallback(() => {
|
||||
if (!itemToBeSelected) {
|
||||
return
|
||||
}
|
||||
|
||||
void selectNonComponent(itemToBeSelected, confirmationQueue[0])
|
||||
|
||||
closeCurrentSuperNoteImporter()
|
||||
}, [closeCurrentSuperNoteImporter, confirmationQueue, itemToBeSelected, selectNonComponent])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu isOpen={true} a11yLabel="Change note type">
|
||||
{groupsWithItems.map((group, index) => (
|
||||
<Fragment key={getGroupId(group)}>
|
||||
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
|
||||
{group.items.map((item) => {
|
||||
const onClickEditorItem = () => {
|
||||
selectItem(item).catch(console.error)
|
||||
}
|
||||
return (
|
||||
<MenuItem key={item.name} onClick={onClickEditorItem} className={'flex-row-reversed py-2'}>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<div className={'flex items-center'}>
|
||||
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
|
||||
{item.name}
|
||||
{item.isLabs && (
|
||||
<Pill className="py-0.5 px-1.5" style="success">
|
||||
Labs
|
||||
</Pill>
|
||||
)}
|
||||
</div>
|
||||
{!item.isEntitled && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</Menu>
|
||||
<ModalOverlay isOpen={showSuperImporter} onDismiss={closeCurrentSuperNoteImporter}>
|
||||
{confirmationQueue[0] && (
|
||||
<SuperNoteImporter
|
||||
note={confirmationQueue[0]}
|
||||
application={application}
|
||||
onConvertComplete={handleSuperNoteConversionCompletion}
|
||||
closeDialog={closeCurrentSuperNoteImporter}
|
||||
/>
|
||||
)}
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangeMultipleMenu
|
||||
@@ -42,6 +42,7 @@ import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/u
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import { PaneController } from '@/Controllers/PaneController/PaneController'
|
||||
import EmptyFilesView from './EmptyFilesView'
|
||||
import { PaneLayout } from '@/Controllers/PaneController/PaneLayout'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
@@ -86,7 +87,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { toggleAppPane, panes } = useResponsiveAppPane()
|
||||
const { setPaneLayout, panes } = useResponsiveAppPane()
|
||||
const { selectedUuids, selectNextItem, selectPreviousItem } = selectionController
|
||||
const { selected: selectedTag, selectedAsTag } = navigationController
|
||||
const {
|
||||
@@ -185,9 +186,9 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
||||
void filesController.selectAndUploadNewFiles()
|
||||
} else {
|
||||
await createNewNote()
|
||||
toggleAppPane(AppPaneId.Editor)
|
||||
setPaneLayout(PaneLayout.Editing)
|
||||
}
|
||||
}, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application])
|
||||
}, [isFilesSmartView, application, filesController, createNewNote, setPaneLayout])
|
||||
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
const shouldUseTableView = (isFilesSmartView || isTableViewEnabled) && !isMobileScreen
|
||||
|
||||
+11
-16
@@ -4,6 +4,7 @@ import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import Popover from '@/Components/Popover/Popover'
|
||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { PhotoRecorder } from '@/Controllers/Moments/PhotoRecorder'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
@@ -40,6 +41,10 @@ const AddItemMenuButton = ({
|
||||
|
||||
const canShowMenu = isInFilesSmartView && deviceHasCamera
|
||||
|
||||
const closeCaptureModal = () => {
|
||||
setCaptureType(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
@@ -101,22 +106,12 @@ const AddItemMenuButton = ({
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Popover>
|
||||
{captureType === 'photo' && (
|
||||
<PhotoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{captureType === 'video' && (
|
||||
<VideoCaptureModal
|
||||
filesController={filesController}
|
||||
close={() => {
|
||||
setCaptureType(undefined)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ModalOverlay isOpen={captureType === 'photo'} onDismiss={closeCaptureModal}>
|
||||
<PhotoCaptureModal filesController={filesController} close={closeCaptureModal} />
|
||||
</ModalOverlay>
|
||||
<ModalOverlay isOpen={captureType === 'video'} onDismiss={closeCaptureModal}>
|
||||
<VideoCaptureModal filesController={filesController} close={closeCaptureModal} />
|
||||
</ModalOverlay>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type DropdownProps = {
|
||||
value: string
|
||||
onChange: (value: string, item: DropdownItem) => void
|
||||
disabled?: boolean
|
||||
className?: {
|
||||
classNameOverride?: {
|
||||
wrapper?: string
|
||||
button?: string
|
||||
popover?: string
|
||||
@@ -56,7 +56,7 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
className,
|
||||
classNameOverride = {},
|
||||
fullWidth,
|
||||
portal = true,
|
||||
}) => {
|
||||
@@ -68,16 +68,12 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
onChange(value, selectedItem)
|
||||
}
|
||||
|
||||
const wrapperClassName = className?.wrapper ?? ''
|
||||
const buttonClassName = className?.button ?? ''
|
||||
const popoverClassName = className?.popover ?? ''
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<div className={classNameOverride.wrapper}>
|
||||
<VisuallyHidden id={labelId}>{label}</VisuallyHidden>
|
||||
<ListboxInput value={value} onChange={handleChange} aria-labelledby={labelId} disabled={disabled}>
|
||||
<StyledListboxButton
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', buttonClassName)}
|
||||
className={classNames('w-full', !fullWidth && 'md:w-fit', classNameOverride.button)}
|
||||
children={({ value, label, isExpanded }) => {
|
||||
const current = items.find((item) => item.value === value)
|
||||
const icon = current ? current?.icon : null
|
||||
@@ -91,7 +87,10 @@ const Dropdown: FunctionComponent<DropdownProps> = ({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ListboxPopover portal={portal} className={classNames('sn-dropdown sn-dropdown-popover', popoverClassName)}>
|
||||
<ListboxPopover
|
||||
portal={portal}
|
||||
className={classNames('sn-dropdown sn-dropdown-popover', classNameOverride.popover)}
|
||||
>
|
||||
<div className="sn-component">
|
||||
<ListboxList>
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import { DialogContent } from '@reach/dialog'
|
||||
import { FunctionComponent, KeyboardEventHandler, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { getFileIconComponent } from './getFileIconComponent'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
@@ -17,19 +17,18 @@ import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContaine
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import DecoratedInput from '../Input/DecoratedInput'
|
||||
import { mergeRefs } from '@/Hooks/mergeRefs'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { isIOS } from '@/Utils'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
const FilePreviewModal: FunctionComponent<Props> = observer(({ application, viewControllerManager }) => {
|
||||
const FilePreviewModal = observer(({ application, viewControllerManager }: Props) => {
|
||||
const { currentFile, setCurrentFile, otherFiles, dismiss } = viewControllerManager.filePreviewModalController
|
||||
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [isRenaming, setIsRenaming] = useState(false)
|
||||
const renameInputRef = useRef<HTMLInputElement>(null)
|
||||
const [showLinkedBubblesContainer, setShowLinkedBubblesContainer] = useState(false)
|
||||
@@ -40,6 +39,10 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
|
||||
|
||||
const keyDownHandler: KeyboardEventHandler = useCallback(
|
||||
(event) => {
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const KeysToHandle: string[] = [KeyboardKey.Left, KeyboardKey.Right, KeyboardKey.Escape]
|
||||
|
||||
if (!KeysToHandle.includes(event.key) || event.target === renameInputRef.current) {
|
||||
@@ -72,13 +75,12 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
|
||||
break
|
||||
}
|
||||
},
|
||||
[currentFile.uuid, dismiss, otherFiles, setCurrentFile],
|
||||
[currentFile, dismiss, otherFiles, setCurrentFile],
|
||||
)
|
||||
|
||||
const IconComponent = useMemo(
|
||||
() => getFileIconComponent(getIconForFileType(currentFile.mimeType), 'w-6 h-6 flex-shrink-0'),
|
||||
[currentFile.mimeType],
|
||||
)
|
||||
const IconComponent = useMemo(() => {
|
||||
return currentFile ? getFileIconComponent(getIconForFileType(currentFile.mimeType), 'w-6 h-6 flex-shrink-0') : null
|
||||
}, [currentFile])
|
||||
|
||||
const focusInputOnMount = useCallback((input: HTMLInputElement | null) => {
|
||||
if (input) {
|
||||
@@ -87,6 +89,9 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
|
||||
}, [])
|
||||
|
||||
const handleRename = useCallback(async () => {
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
if (renameInputRef.current) {
|
||||
const newName = renameInputRef.current.value
|
||||
if (newName && newName !== currentFile.name) {
|
||||
@@ -99,150 +104,156 @@ const FilePreviewModal: FunctionComponent<Props> = observer(({ application, view
|
||||
}
|
||||
}, [application.items, currentFile, setCurrentFile])
|
||||
|
||||
if (!currentFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className="sn-component"
|
||||
aria-label="File preview modal"
|
||||
onDismiss={dismiss}
|
||||
initialFocusRef={closeButtonRef}
|
||||
dangerouslyBypassScrollLock
|
||||
<DialogContent
|
||||
className={classNames(
|
||||
'm-0 flex h-full w-full flex-col rounded bg-[color:var(--modal-background-color)] p-0 shadow-main md:max-h-[90%] md:max-w-[90%]',
|
||||
isIOS() && 'pt-safe-top',
|
||||
)}
|
||||
>
|
||||
<DialogContent
|
||||
aria-label="File preview modal"
|
||||
className="flex min-h-[90%] min-w-[90%] flex-col rounded bg-[color:var(--modal-background-color)] p-0 shadow-main "
|
||||
<div
|
||||
className="flex h-full w-full flex-col focus:shadow-none focus:outline-none"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
onKeyDown={keyDownHandler}
|
||||
>
|
||||
<div
|
||||
className="flex h-full w-full flex-col focus:shadow-none focus:outline-none"
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
onKeyDown={keyDownHandler}
|
||||
>
|
||||
<div className="min-h-6 flex flex-shrink-0 flex-wrap items-center justify-between gap-2 border-0 border-b border-solid border-border px-4 py-3 focus:shadow-none">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6">{IconComponent}</div>
|
||||
{isRenaming ? (
|
||||
<DecoratedInput
|
||||
defaultValue={currentFile.name}
|
||||
className={{ container: 'ml-3', input: 'p-1', right: 'items-stretch !p-0' }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
void handleRename()
|
||||
}
|
||||
}}
|
||||
right={[
|
||||
<button
|
||||
className="flex h-full items-center justify-center border-l border-border px-2 py-1.5 text-neutral hover:bg-passive-4"
|
||||
title="Submit"
|
||||
onClick={handleRename}
|
||||
>
|
||||
<Icon type="check" size="small" />
|
||||
</button>,
|
||||
]}
|
||||
ref={mergeRefs([renameInputRef, focusInputOnMount])}
|
||||
/>
|
||||
) : (
|
||||
<span className="ml-3 font-medium">{currentFile.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<StyledTooltip label="Rename file" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
|
||||
aria-label="Rename file"
|
||||
>
|
||||
<Icon type="pencil-filled" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show linked items" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowLinkedBubblesContainer((show) => !show)}
|
||||
aria-label="Show linked items"
|
||||
>
|
||||
<Icon type="link" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show file options" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowOptionsMenu((show) => !show)}
|
||||
ref={menuButtonRef}
|
||||
aria-label="Show file options"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover
|
||||
title="File options"
|
||||
open={showOptionsMenu}
|
||||
anchorElement={menuButtonRef.current}
|
||||
togglePopover={() => {
|
||||
setShowOptionsMenu(false)
|
||||
<div className="min-h-6 flex flex-shrink-0 flex-wrap items-center justify-between gap-2 border-0 border-b border-solid border-border px-4 py-3 focus:shadow-none">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6">{IconComponent}</div>
|
||||
{isRenaming ? (
|
||||
<DecoratedInput
|
||||
defaultValue={currentFile.name}
|
||||
className={{ container: 'ml-3', input: 'p-1', right: 'items-stretch !p-0' }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KeyboardKey.Enter) {
|
||||
void handleRename()
|
||||
}
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="py-2"
|
||||
overrideZIndex="z-modal"
|
||||
>
|
||||
<Menu a11yLabel="File context menu" isOpen={showOptionsMenu}>
|
||||
<FileMenuOptions
|
||||
filesController={viewControllerManager.filesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
selectedFiles={[currentFile]}
|
||||
closeMenu={() => {
|
||||
setShowOptionsMenu(false)
|
||||
}}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
<StyledTooltip label="Show file info" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowFileInfoPanel((show) => !show)}
|
||||
aria-label="Show file info"
|
||||
>
|
||||
<Icon type="info" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
onClick={dismiss}
|
||||
aria-label="Close modal"
|
||||
className="flex cursor-pointer rounded border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="close" className="text-neutral" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{showLinkedBubblesContainer && (
|
||||
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border py-1.5 px-3.5">
|
||||
<LinkedItemBubblesContainer
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
item={currentFile}
|
||||
right={[
|
||||
<button
|
||||
className="flex h-full items-center justify-center border-l border-border px-2 py-1.5 text-neutral hover:bg-passive-4"
|
||||
title="Submit"
|
||||
onClick={handleRename}
|
||||
>
|
||||
<Icon type="check" size="small" />
|
||||
</button>,
|
||||
]}
|
||||
ref={mergeRefs([renameInputRef, focusInputOnMount])}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-grow">
|
||||
<div className="relative flex max-w-full flex-grow items-center justify-center">
|
||||
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
|
||||
</div>
|
||||
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
|
||||
) : (
|
||||
<span className="ml-3 font-medium">{currentFile.name}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<StyledTooltip label="Rename file" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setIsRenaming((isRenaming) => !isRenaming)}
|
||||
aria-label="Rename file"
|
||||
>
|
||||
<Icon type="pencil-filled" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show linked items" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowLinkedBubblesContainer((show) => !show)}
|
||||
aria-label="Show linked items"
|
||||
>
|
||||
<Icon type="link" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<StyledTooltip label="Show file options" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowOptionsMenu((show) => !show)}
|
||||
ref={menuButtonRef}
|
||||
aria-label="Show file options"
|
||||
>
|
||||
<Icon type="more" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<Popover
|
||||
title="File options"
|
||||
open={showOptionsMenu}
|
||||
anchorElement={menuButtonRef.current}
|
||||
togglePopover={() => {
|
||||
setShowOptionsMenu(false)
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="py-2"
|
||||
overrideZIndex="z-modal"
|
||||
>
|
||||
<Menu a11yLabel="File context menu" isOpen={showOptionsMenu}>
|
||||
<FileMenuOptions
|
||||
filesController={viewControllerManager.filesController}
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
selectedFiles={[currentFile]}
|
||||
closeMenu={() => {
|
||||
setShowOptionsMenu(false)
|
||||
}}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
<StyledTooltip label="Show file info" className="!z-modal">
|
||||
<button
|
||||
className="mr-4 flex cursor-pointer rounded border border-solid border-border bg-transparent p-1.5 hover:bg-contrast"
|
||||
onClick={() => setShowFileInfoPanel((show) => !show)}
|
||||
aria-label="Show file info"
|
||||
>
|
||||
<Icon type="info" className="text-neutral" />
|
||||
</button>
|
||||
</StyledTooltip>
|
||||
<button
|
||||
ref={closeButtonRef}
|
||||
onClick={dismiss}
|
||||
aria-label="Close modal"
|
||||
className="flex cursor-pointer rounded border-0 bg-transparent p-1 hover:bg-contrast"
|
||||
>
|
||||
<Icon type="close" className="text-neutral" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
{showLinkedBubblesContainer && (
|
||||
<div className="-mt-1 min-h-0 flex-shrink-0 border-b border-border py-1.5 px-3.5">
|
||||
<LinkedItemBubblesContainer
|
||||
linkingController={viewControllerManager.linkingController}
|
||||
item={currentFile}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-grow">
|
||||
<div className="relative flex max-w-full flex-grow items-center justify-center">
|
||||
<FilePreview file={currentFile} application={application} key={currentFile.uuid} />
|
||||
</div>
|
||||
{showFileInfoPanel && <FilePreviewInfoPanel file={currentFile} />}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
)
|
||||
})
|
||||
|
||||
FilePreviewModal.displayName = 'FilePreviewModal'
|
||||
|
||||
const FilePreviewModalWrapper: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
|
||||
return viewControllerManager.filePreviewModalController.isOpen ? (
|
||||
<FilePreviewModal application={application} viewControllerManager={viewControllerManager} />
|
||||
) : null
|
||||
return (
|
||||
<ModalOverlay
|
||||
className="sn-component p-0"
|
||||
aria-label="File preview modal"
|
||||
isOpen={viewControllerManager.filePreviewModalController.isOpen}
|
||||
onDismiss={viewControllerManager.filePreviewModalController.dismiss}
|
||||
dangerouslyBypassScrollLock
|
||||
>
|
||||
<FilePreviewModal application={application} viewControllerManager={viewControllerManager} />
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(FilePreviewModalWrapper)
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
const UpgradeNow = ({ application, featuresController, subscriptionContoller }: Props) => {
|
||||
const shouldShowCTA = !featuresController.hasFolders
|
||||
const hasAccount = subscriptionContoller.hasAccount
|
||||
const hasAccessToFeatures = subscriptionContoller.hasFirstPartySubscription
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (hasAccount && application.isNativeIOS()) {
|
||||
@@ -22,16 +23,20 @@ const UpgradeNow = ({ application, featuresController, subscriptionContoller }:
|
||||
}
|
||||
}, [application, hasAccount])
|
||||
|
||||
return shouldShowCTA ? (
|
||||
if (!shouldShowCTA || hasAccessToFeatures) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center px-2">
|
||||
<button
|
||||
className="rounded bg-info py-0.5 px-1.5 text-sm font-bold uppercase text-info-contrast hover:brightness-125 lg:text-xs"
|
||||
onClick={onClick}
|
||||
>
|
||||
{hasAccount ? 'Unlock features' : 'Sign up to sync'}
|
||||
{!hasAccount ? 'Sign up to sync' : 'Unlock features'}
|
||||
</button>
|
||||
</div>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(UpgradeNow)
|
||||
|
||||
@@ -2,17 +2,14 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
||||
import { ContentType, DecryptedTransferPayload, pluralize, SNTag, TagContent, UuidGenerator } from '@standardnotes/snjs'
|
||||
import { Importer } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useReducer, useState } from 'react'
|
||||
import { useCallback, useMemo, useReducer, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Button from '../Button/Button'
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import { ImportModalFileItem } from './ImportModalFileItem'
|
||||
import ImportModalInitialPage from './InitialPage'
|
||||
import { ImportModalAction, ImportModalFile, ImportModalState } from './Types'
|
||||
import Modal, { ModalAction } from '../Modal/Modal'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
|
||||
const reducer = (state: ImportModalState, action: ImportModalAction): ImportModalState => {
|
||||
switch (action.type) {
|
||||
@@ -190,36 +187,44 @@ const ImportModal = ({ viewControllerManager }: { viewControllerManager: ViewCon
|
||||
})
|
||||
}, [state.importTag, viewControllerManager.isImportModalVisible, viewControllerManager.navigationController])
|
||||
|
||||
if (!viewControllerManager.isImportModalVisible.get()) {
|
||||
return null
|
||||
}
|
||||
const isReadyToImport = files.length > 0 && files.every((file) => file.status === 'ready')
|
||||
const importSuccessOrError =
|
||||
files.length > 0 && files.every((file) => file.status === 'success' || file.status === 'error')
|
||||
|
||||
const modalActions: ModalAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Import',
|
||||
type: 'primary',
|
||||
onClick: parseAndImport,
|
||||
hidden: !isReadyToImport,
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
{
|
||||
label: importSuccessOrError ? 'Close' : 'Cancel',
|
||||
type: 'cancel',
|
||||
onClick: closeDialog,
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[closeDialog, importSuccessOrError, isReadyToImport, parseAndImport],
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>Import</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
{!files.length && <ImportModalInitialPage dispatch={dispatch} />}
|
||||
{files.length > 0 && (
|
||||
<div className="divide-y divide-border">
|
||||
{files.map((file) => (
|
||||
<ImportModalFileItem file={file} key={file.id} dispatch={dispatch} importer={importer} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
{files.length > 0 && files.every((file) => file.status === 'ready') && (
|
||||
<Button primary onClick={parseAndImport}>
|
||||
Import
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={closeDialog}>
|
||||
{files.length > 0 && files.every((file) => file.status === 'success' || file.status === 'error')
|
||||
? 'Close'
|
||||
: 'Cancel'}
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<ModalOverlay isOpen={viewControllerManager.isImportModalVisible.get()} onDismiss={closeDialog}>
|
||||
<Modal title="Import" close={closeDialog} actions={modalActions}>
|
||||
<div className="px-4 py-4">
|
||||
{!files.length && <ImportModalInitialPage dispatch={dispatch} />}
|
||||
{files.length > 0 && (
|
||||
<div className="divide-y divide-border">
|
||||
{files.map((file) => (
|
||||
<ImportModalFileItem file={file} key={file.id} dispatch={dispatch} importer={importer} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ComponentPropsWithoutRef, ForwardedRef, forwardRef, ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
action: () => void
|
||||
slot: 'left' | 'right'
|
||||
type?: 'primary' | 'secondary' | 'destructive' | 'cancel'
|
||||
} & Omit<ComponentPropsWithoutRef<'button'>, 'onClick' | 'type'>
|
||||
|
||||
const MobileModalAction = forwardRef(
|
||||
({ children, action, type = 'primary', slot, className, ...props }: Props, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
'flex whitespace-nowrap py-1 px-1 text-base font-semibold focus:shadow-none focus:outline-none active:shadow-none active:outline-none disabled:text-neutral md:hidden',
|
||||
slot === 'left' ? 'justify-start text-left' : 'justify-end text-right',
|
||||
type === 'cancel' || type === 'destructive' ? 'text-danger' : 'text-info',
|
||||
className,
|
||||
)}
|
||||
onClick={action}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default MobileModalAction
|
||||
@@ -0,0 +1,13 @@
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const MobileModalHeader = ({ className, children }: Props) => {
|
||||
return <div className={classNames('grid w-full grid-cols-[0.35fr_1fr_0.35fr] gap-2', className)}>{children}</div>
|
||||
}
|
||||
|
||||
export default MobileModalHeader
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
import { isIOS } from '@/Utils'
|
||||
import { DialogContent } from '@reach/dialog'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ReactNode, useMemo, useRef, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
import Popover from '../Popover/Popover'
|
||||
import MobileModalAction from './MobileModalAction'
|
||||
import MobileModalHeader from './MobileModalHeader'
|
||||
import ModalAndroidBackHandler from './ModalAndroidBackHandler'
|
||||
|
||||
export type ModalAction = {
|
||||
label: NonNullable<ReactNode>
|
||||
type: 'primary' | 'secondary' | 'destructive' | 'cancel'
|
||||
onClick: () => void
|
||||
mobileSlot?: 'left' | 'right'
|
||||
hidden?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
close: () => void
|
||||
actions?: ModalAction[]
|
||||
className?: {
|
||||
content?: string
|
||||
description?: string
|
||||
}
|
||||
customHeader?: ReactNode
|
||||
disableCustomHeader?: boolean
|
||||
customFooter?: ReactNode
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Modal = ({
|
||||
title,
|
||||
close,
|
||||
actions = [],
|
||||
className = {},
|
||||
customHeader,
|
||||
disableCustomHeader = false,
|
||||
customFooter,
|
||||
children,
|
||||
}: Props) => {
|
||||
const sortedActions = useMemo(
|
||||
() =>
|
||||
actions
|
||||
.sort((a, b) => {
|
||||
if (a.type === 'cancel') {
|
||||
return -1
|
||||
}
|
||||
if (b.type === 'cancel') {
|
||||
return 1
|
||||
}
|
||||
if (a.type === 'destructive') {
|
||||
return -1
|
||||
}
|
||||
if (b.type === 'destructive') {
|
||||
return 1
|
||||
}
|
||||
if (a.type === 'secondary') {
|
||||
return -1
|
||||
}
|
||||
if (b.type === 'secondary') {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.filter((action) => !action.hidden),
|
||||
[actions],
|
||||
)
|
||||
|
||||
const primaryActions = sortedActions.filter((action) => action.type === 'primary')
|
||||
if (primaryActions.length > 1) {
|
||||
throw new Error('Modal can only have 1 primary action')
|
||||
}
|
||||
|
||||
const cancelActions = sortedActions.filter((action) => action.type === 'cancel')
|
||||
if (cancelActions.length > 1) {
|
||||
throw new Error('Modal can only have 1 cancel action')
|
||||
}
|
||||
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
const leftSlotAction = sortedActions.find((action) => action.mobileSlot === 'left')
|
||||
const rightSlotAction = sortedActions.find((action) => action.mobileSlot === 'right')
|
||||
const firstPrimaryActionIndex = sortedActions.findIndex((action) => action.type === 'primary')
|
||||
|
||||
const extraActions = sortedActions.filter((action) => action.type !== 'primary' && action.type !== 'cancel')
|
||||
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const advancedOptionRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalAndroidBackHandler close={close} />
|
||||
<DialogContent
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'm-0 flex h-full w-full flex-col border-solid border-border bg-default p-0 md:h-auto md:max-h-[85vh] md:w-160 md:rounded md:border md:shadow-main',
|
||||
className.content,
|
||||
)}
|
||||
aria-label={title}
|
||||
>
|
||||
{customHeader && !disableCustomHeader ? (
|
||||
customHeader
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex w-full flex-shrink-0 items-center justify-between rounded-t border-b border-solid border-border bg-default text-text md:px-4.5 md:py-3',
|
||||
isIOS() ? 'px-2 pt-safe-top pb-1.5' : 'py-1.5 px-2',
|
||||
)}
|
||||
>
|
||||
<MobileModalHeader className="flex-row items-center justify-between md:flex md:gap-0">
|
||||
{leftSlotAction ? (
|
||||
<MobileModalAction
|
||||
type={leftSlotAction.type}
|
||||
action={leftSlotAction.onClick}
|
||||
disabled={leftSlotAction.disabled}
|
||||
slot="left"
|
||||
>
|
||||
{leftSlotAction.label}
|
||||
</MobileModalAction>
|
||||
) : (
|
||||
<div className="md:hidden" />
|
||||
)}
|
||||
<div className="flex items-center justify-center gap-2 overflow-hidden text-center text-base font-semibold text-text md:flex-grow md:text-left md:text-lg">
|
||||
{extraActions.length > 0 && (
|
||||
<>
|
||||
<MobileModalAction
|
||||
type="secondary"
|
||||
action={() => setShowAdvanced((show) => !show)}
|
||||
slot="left"
|
||||
ref={advancedOptionRef}
|
||||
>
|
||||
<div className="rounded-full border border-border p-0.5">
|
||||
<Icon type="more" />
|
||||
</div>
|
||||
</MobileModalAction>
|
||||
<Popover
|
||||
title="Advanced"
|
||||
open={showAdvanced}
|
||||
anchorElement={advancedOptionRef.current}
|
||||
disableMobileFullscreenTakeover={true}
|
||||
togglePopover={() => setShowAdvanced((show) => !show)}
|
||||
align="start"
|
||||
portal={false}
|
||||
className="!fixed w-1/2 !min-w-0 divide-y divide-border border border-border"
|
||||
>
|
||||
{extraActions
|
||||
.filter((action) => action.type !== 'cancel')
|
||||
.map((action, index) => (
|
||||
<button
|
||||
className={classNames(
|
||||
'p-2 text-base font-semibold hover:bg-contrast focus:bg-info-backdrop focus:shadow-none focus:outline-none',
|
||||
action.type === 'destructive' && 'text-danger',
|
||||
)}
|
||||
key={index}
|
||||
onClick={action.onClick}
|
||||
disabled={action.disabled}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap ">{title}</span>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<button tabIndex={0} className="ml-2 rounded p-1 font-bold hover:bg-contrast" onClick={close}>
|
||||
<Icon type="close" />
|
||||
</button>
|
||||
</div>
|
||||
{rightSlotAction ? (
|
||||
<MobileModalAction
|
||||
type={rightSlotAction.type}
|
||||
action={rightSlotAction.onClick}
|
||||
disabled={rightSlotAction.disabled}
|
||||
slot="right"
|
||||
>
|
||||
{rightSlotAction.label}
|
||||
</MobileModalAction>
|
||||
) : null}
|
||||
</MobileModalHeader>
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames('flex-grow overflow-y-auto', className.description)}>{children}</div>
|
||||
{customFooter
|
||||
? customFooter
|
||||
: sortedActions.length > 0 && (
|
||||
<div
|
||||
className={classNames(
|
||||
'hidden items-center justify-start gap-3 border-t border-border py-2 px-2.5 md:flex md:px-4 md:py-4',
|
||||
isIOS() && 'pb-safe-bottom',
|
||||
)}
|
||||
>
|
||||
{sortedActions.map((action, index) => (
|
||||
<Button
|
||||
primary={action.type === 'primary'}
|
||||
colorStyle={action.type === 'destructive' ? 'danger' : undefined}
|
||||
key={action.label.toString()}
|
||||
onClick={action.onClick}
|
||||
className={classNames(
|
||||
action.mobileSlot ? 'hidden md:block' : '',
|
||||
index === firstPrimaryActionIndex && 'ml-auto',
|
||||
)}
|
||||
data-type={action.type}
|
||||
disabled={action.disabled}
|
||||
small={isMobileScreen}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useStateRef } from '@/Hooks/useStateRef'
|
||||
import { useAndroidBackHandler } from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
type Props = {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const ModalAndroidBackHandler = ({ close }: Props) => {
|
||||
const addAndroidBackHandler = useAndroidBackHandler()
|
||||
const closeFnRef = useStateRef(close)
|
||||
|
||||
useEffect(() => {
|
||||
const removeListener = addAndroidBackHandler(() => {
|
||||
closeFnRef.current()
|
||||
return true
|
||||
})
|
||||
return () => {
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
}
|
||||
}
|
||||
}, [addAndroidBackHandler, closeFnRef])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default ModalAndroidBackHandler
|
||||
+10
-4
@@ -1,3 +1,4 @@
|
||||
import { isIOS } from '@/Utils'
|
||||
import { classNames } from '@standardnotes/utils'
|
||||
import { FunctionComponent, ReactNode } from 'react'
|
||||
|
||||
@@ -7,10 +8,15 @@ type Props = {
|
||||
}
|
||||
|
||||
const ModalDialogButtons: FunctionComponent<Props> = ({ children, className }) => (
|
||||
<>
|
||||
<hr className="m-0 h-[1px] border-none bg-border" />
|
||||
<div className={classNames('flex items-center justify-end gap-3 px-4 py-4', className)}>{children}</div>
|
||||
</>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center justify-end gap-3 border-t border-border px-4 py-4',
|
||||
isIOS() && 'pb-safe-bottom',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default ModalDialogButtons
|
||||
@@ -0,0 +1,32 @@
|
||||
import { DialogOverlay, DialogOverlayProps } from '@reach/dialog'
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { ReactNode } from 'react'
|
||||
import { useModalAnimation } from '../Modal/useModalAnimation'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onDismiss?: () => void
|
||||
children: ReactNode
|
||||
className?: string
|
||||
} & DialogOverlayProps
|
||||
|
||||
const ModalOverlay = ({ isOpen, onDismiss, children, className, ...props }: Props) => {
|
||||
const [isMounted, setElement] = useModalAnimation(isOpen)
|
||||
|
||||
if (!isMounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={classNames('p-0 md:px-0 md:opacity-100', className)}
|
||||
onDismiss={onDismiss}
|
||||
ref={setElement}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalOverlay
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useLifecycleAnimation } from '@/Hooks/useLifecycleAnimation'
|
||||
import { useMediaQuery, MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||
|
||||
export const useModalAnimation = (isOpen: boolean) => {
|
||||
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
return useLifecycleAnimation(
|
||||
{
|
||||
open: isOpen,
|
||||
enter: {
|
||||
keyframes: [
|
||||
{
|
||||
transform: 'translateY(100%)',
|
||||
},
|
||||
{
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
easing: 'cubic-bezier(.36,.66,.04,1)',
|
||||
duration: 250,
|
||||
fill: 'forwards',
|
||||
},
|
||||
initialStyle: {
|
||||
transformOrigin: 'bottom',
|
||||
},
|
||||
},
|
||||
enterCallback: (element) => {
|
||||
element.scrollTop = 0
|
||||
},
|
||||
exit: {
|
||||
keyframes: [
|
||||
{
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
{
|
||||
transform: 'translateY(100%)',
|
||||
},
|
||||
],
|
||||
options: {
|
||||
easing: 'cubic-bezier(.36,.66,.04,1)',
|
||||
duration: 250,
|
||||
fill: 'forwards',
|
||||
},
|
||||
initialStyle: {
|
||||
transformOrigin: 'bottom',
|
||||
},
|
||||
},
|
||||
},
|
||||
!isMobileScreen,
|
||||
)
|
||||
}
|
||||
+4
@@ -10,6 +10,7 @@ import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import ChangeMultipleButton from '../ChangeEditor/ChangeMultipleButton'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -39,6 +40,9 @@ const MultipleSelectedNotes = ({
|
||||
<div className="flex w-full items-center justify-between p-4">
|
||||
<h1 className="m-0 text-lg font-bold">{count} selected notes</h1>
|
||||
<div className="flex">
|
||||
<div className="mr-3">
|
||||
<ChangeMultipleButton application={application} notesController={notesController} />
|
||||
</div>
|
||||
<div className="mr-3">
|
||||
<PinNoteButton notesController={notesController} />
|
||||
</div>
|
||||
|
||||
+25
-1
@@ -1,9 +1,29 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { AutoLinkPlugin as LexicalAutoLinkPlugin } from '@lexical/react/LexicalAutoLinkPlugin'
|
||||
import { COMMAND_PRIORITY_EDITOR, KEY_MODIFIER_COMMAND, $getSelection } from 'lexical'
|
||||
import { useEffect } from 'react'
|
||||
import { TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||
import { mergeRegister } from '@lexical/utils'
|
||||
|
||||
const URL_MATCHER =
|
||||
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/
|
||||
|
||||
const MATCHERS = [
|
||||
(text: string) => {
|
||||
const match = URL_MATCHER.exec(text)
|
||||
if (match === null) {
|
||||
return null
|
||||
}
|
||||
const fullMatch = match[0]
|
||||
return {
|
||||
index: match.index,
|
||||
length: fullMatch.length,
|
||||
text: fullMatch,
|
||||
url: fullMatch.startsWith('http') ? fullMatch : `https://${fullMatch}`,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
export default function AutoLinkPlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
|
||||
@@ -27,5 +47,9 @@ export default function AutoLinkPlugin(): JSX.Element | null {
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
return null
|
||||
return (
|
||||
<>
|
||||
<LexicalAutoLinkPlugin matchers={MATCHERS} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import { getPlaintextFontSize } from '@/Utils/getPlaintextFontSize'
|
||||
import ReadonlyPlugin from './Plugins/ReadonlyPlugin/ReadonlyPlugin'
|
||||
import { SuperSearchContextProvider } from './Plugins/SearchPlugin/Context'
|
||||
import { SearchPlugin } from './Plugins/SearchPlugin/SearchPlugin'
|
||||
import ModalOverlay from '@/Components/Modal/ModalOverlay'
|
||||
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
@@ -205,7 +206,9 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
||||
</BlocksEditorComposer>
|
||||
</FilesControllerProvider>
|
||||
</LinkingControllerProvider>
|
||||
{showMarkdownPreview && <SuperNoteMarkdownPreview note={note.current} closeDialog={closeMarkdownPreview} />}
|
||||
<ModalOverlay isOpen={showMarkdownPreview} onDismiss={closeMarkdownPreview}>
|
||||
<SuperNoteMarkdownPreview note={note.current} closeDialog={closeMarkdownPreview} />
|
||||
</ModalOverlay>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
|
||||
+47
-47
@@ -1,16 +1,12 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NoteType, SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ImportPlugin from './Plugins/ImportPlugin/ImportPlugin'
|
||||
import { NoteViewController } from '../Controller/NoteViewController'
|
||||
import { spaceSeparatedStrings } from '@standardnotes/utils'
|
||||
import Modal, { ModalAction } from '@/Components/Modal/Modal'
|
||||
|
||||
const NotePreviewCharLimit = 160
|
||||
|
||||
@@ -82,51 +78,55 @@ export const SuperNoteImporter: FunctionComponent<Props> = ({ note, application,
|
||||
onConvertComplete()
|
||||
}, [closeDialog, application, note, onConvertComplete, performConvert])
|
||||
|
||||
const modalActions: ModalAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: closeDialog,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label: 'Convert',
|
||||
onClick: confirmConvert,
|
||||
mobileSlot: 'right',
|
||||
type: 'primary',
|
||||
},
|
||||
{
|
||||
label: 'Convert As-Is',
|
||||
onClick: convertAsIs,
|
||||
type: 'secondary',
|
||||
},
|
||||
],
|
||||
[closeDialog, confirmConvert, convertAsIs],
|
||||
)
|
||||
|
||||
if (isSeamlessConvert) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>
|
||||
Convert to Super note
|
||||
<p className="text-sm font-normal text-neutral">
|
||||
The following is a preview of how your note will look when converted to Super. Super notes use a custom format
|
||||
under the hood. Converting your note will transition it from plaintext to the custom Super format.
|
||||
</p>
|
||||
</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="relative w-full">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={undefined}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
onChange={handleChange}
|
||||
ignoreFirstChange={false}
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
previewLength={NotePreviewCharLimit}
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<ImportPlugin text={note.text} format={format} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<div className="flex w-full justify-between">
|
||||
<div>
|
||||
<Button onClick={convertAsIs}>Convert As-Is</Button>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button onClick={closeDialog}>Cancel</Button>
|
||||
<div className="min-w-3" />
|
||||
<Button primary onClick={confirmConvert}>
|
||||
Convert to Super
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<Modal title="Convert to Super note" close={closeDialog} actions={modalActions}>
|
||||
<div className="border-b border-border px-4 py-4 text-sm font-normal text-neutral md:py-3">
|
||||
The following is a preview of how your note will look when converted to Super. Super notes use a custom format
|
||||
under the hood. Converting your note will transition it from plaintext to the custom Super format.
|
||||
</div>
|
||||
<div className="relative w-full px-4 py-4">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={undefined}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
onChange={handleChange}
|
||||
ignoreFirstChange={false}
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
previewLength={NotePreviewCharLimit}
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<ImportPlugin text={note.text} format={format} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
+29
-33
@@ -1,16 +1,12 @@
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
import { BlocksEditor, BlocksEditorComposer } from '@standardnotes/blocks-editor'
|
||||
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
|
||||
import ModalDialog from '@/Components/Shared/ModalDialog'
|
||||
import ModalDialogButtons from '@/Components/Shared/ModalDialogButtons'
|
||||
import ModalDialogDescription from '@/Components/Shared/ModalDialogDescription'
|
||||
import ModalDialogLabel from '@/Components/Shared/ModalDialogLabel'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import MarkdownPreviewPlugin from './Plugins/MarkdownPreviewPlugin/MarkdownPreviewPlugin'
|
||||
import { FileNode } from './Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
||||
import { BubbleNode } from './Plugins/ItemBubblePlugin/Nodes/BubbleNode'
|
||||
import { copyTextToClipboard } from '../../../Utils/copyTextToClipboard'
|
||||
import Modal, { ModalAction } from '@/Components/Modal/Modal'
|
||||
|
||||
type Props = {
|
||||
note: SNNote
|
||||
@@ -33,33 +29,33 @@ export const SuperNoteMarkdownPreview: FunctionComponent<Props> = ({ note, close
|
||||
setMarkdown(markdown)
|
||||
}, [])
|
||||
|
||||
const modalActions: ModalAction[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: didCopy ? 'Copied' : 'Copy',
|
||||
type: 'primary',
|
||||
onClick: copy,
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
],
|
||||
[copy, didCopy],
|
||||
)
|
||||
|
||||
return (
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={closeDialog}>Markdown Preview</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="relative w-full">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={note.text} nodes={[FileNode, BubbleNode]}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<MarkdownPreviewPlugin onMarkdown={onMarkdown} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<div className="flex">
|
||||
<Button onClick={closeDialog}>Close</Button>
|
||||
<div className="min-w-3" />
|
||||
<Button primary onClick={copy}>
|
||||
{didCopy ? 'Copied' : 'Copy'}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
<Modal title="Markdown Preview" close={closeDialog} actions={modalActions}>
|
||||
<div className="relative w-full px-4 py-4">
|
||||
<ErrorBoundary>
|
||||
<BlocksEditorComposer readonly initialValue={note.text} nodes={[FileNode, BubbleNode]}>
|
||||
<BlocksEditor
|
||||
readonly
|
||||
className="relative resize-none text-base focus:shadow-none focus:outline-none"
|
||||
spellcheck={note.spellcheck}
|
||||
>
|
||||
<MarkdownPreviewPlugin onMarkdown={onMarkdown} />
|
||||
</BlocksEditor>
|
||||
</BlocksEditorComposer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { createRef } from 'react'
|
||||
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import DecoratedPasswordInput from '../Input/DecoratedPasswordInput'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import Modal from '../Modal/Modal'
|
||||
import { isMobileScreen } from '@/Utils'
|
||||
import Spinner from '../Spinner/Spinner'
|
||||
|
||||
interface Props {
|
||||
application: WebApplication
|
||||
@@ -25,6 +23,8 @@ type State = {
|
||||
}
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = 'Continue'
|
||||
const GENERATING_CONTINUE_TITLE = 'Generating Keys...'
|
||||
const FINISH_CONTINUE_TITLE = 'Finish'
|
||||
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
@@ -89,7 +89,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
this.setState({
|
||||
isContinuing: true,
|
||||
showSpinner: true,
|
||||
continueTitle: 'Generating Keys...',
|
||||
continueTitle: GENERATING_CONTINUE_TITLE,
|
||||
})
|
||||
|
||||
const valid = await this.validateCurrentPassword()
|
||||
@@ -107,7 +107,7 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
this.setState({
|
||||
isContinuing: false,
|
||||
showSpinner: false,
|
||||
continueTitle: 'Finish',
|
||||
continueTitle: FINISH_CONTINUE_TITLE,
|
||||
step: Steps.FinishStep,
|
||||
})
|
||||
}
|
||||
@@ -228,10 +228,32 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
|
||||
override render() {
|
||||
return (
|
||||
<div className="sn-component" id="password-wizard">
|
||||
<ModalDialog>
|
||||
<ModalDialogLabel closeDialog={this.dismiss}>{this.state.title}</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<div className="sn-component h-full w-full md:h-auto md:w-auto" id="password-wizard">
|
||||
<Modal
|
||||
title={this.state.title}
|
||||
close={this.dismiss}
|
||||
actions={[
|
||||
{
|
||||
label: 'Cancel',
|
||||
onClick: this.dismiss,
|
||||
type: 'cancel',
|
||||
mobileSlot: 'left',
|
||||
},
|
||||
{
|
||||
label:
|
||||
this.state.continueTitle === GENERATING_CONTINUE_TITLE && isMobileScreen() ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
this.state.continueTitle
|
||||
),
|
||||
onClick: this.nextStep,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
disabled: this.state.lockContinue,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
{this.state.step === Steps.PasswordStep && (
|
||||
<div className="flex flex-col pb-1.5">
|
||||
<form>
|
||||
@@ -284,13 +306,8 @@ class PasswordWizard extends AbstractComponent<Props, State> {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button primary onClick={this.nextStep} disabled={this.state.lockContinue} className="min-w-20">
|
||||
{this.state.continueTitle}
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { SNComponent } from '@standardnotes/snjs'
|
||||
import { useCallback } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import ModalDialog from '../Shared/ModalDialog'
|
||||
import ModalDialogLabel from '../Shared/ModalDialogLabel'
|
||||
import ModalDialogDescription from '../Shared/ModalDialogDescription'
|
||||
import ModalDialogButtons from '../Shared/ModalDialogButtons'
|
||||
import ModalDialogButtons from '../Modal/ModalDialogButtons'
|
||||
import Modal from '../Modal/Modal'
|
||||
|
||||
type Props = {
|
||||
callback: (approved: boolean) => void
|
||||
@@ -25,9 +23,28 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
|
||||
}, [callback, dismiss])
|
||||
|
||||
return (
|
||||
<ModalDialog className="w-full md:!w-[350px]">
|
||||
<ModalDialogLabel closeDialog={deny}>Activate Component</ModalDialogLabel>
|
||||
<ModalDialogDescription>
|
||||
<Modal
|
||||
title="Activate Component"
|
||||
close={deny}
|
||||
actions={[
|
||||
{ label: 'Cancel', onClick: deny, type: 'cancel', mobileSlot: 'left' },
|
||||
{
|
||||
label: 'Continue',
|
||||
onClick: accept,
|
||||
type: 'primary',
|
||||
mobileSlot: 'right',
|
||||
},
|
||||
]}
|
||||
className={{ content: 'md:!w-[350px]' }}
|
||||
customFooter={
|
||||
<ModalDialogButtons className="hidden md:flex">
|
||||
<Button primary fullWidth onClick={accept} className="block">
|
||||
Continue
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
}
|
||||
>
|
||||
<div className="px-4 py-4">
|
||||
<div className="text-base">
|
||||
<strong>{component.displayName}</strong>
|
||||
{' would like to interact with your '}
|
||||
@@ -41,13 +58,8 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</ModalDialogDescription>
|
||||
<ModalDialogButtons>
|
||||
<Button primary fullWidth onClick={accept} className="block">
|
||||
Continue
|
||||
</Button>
|
||||
</ModalDialogButtons>
|
||||
</ModalDialog>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -1,6 +1,7 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ApplicationEvent, PermissionDialog } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import PermissionsModal from './PermissionsModal'
|
||||
|
||||
type Props = {
|
||||
@@ -42,14 +43,18 @@ const PermissionsModalWrapper: FunctionComponent<Props> = ({ application }) => {
|
||||
}
|
||||
}, [application, onAppStart])
|
||||
|
||||
return dialog ? (
|
||||
<PermissionsModal
|
||||
callback={dialog.callback}
|
||||
dismiss={dismissPermissionsDialog}
|
||||
component={dialog.component}
|
||||
permissionsString={dialog.permissionsString}
|
||||
/>
|
||||
) : null
|
||||
return (
|
||||
<ModalOverlay isOpen={!!dialog}>
|
||||
{dialog && (
|
||||
<PermissionsModal
|
||||
callback={dialog.callback}
|
||||
dismiss={dismissPermissionsDialog}
|
||||
component={dialog.component}
|
||||
permissionsString={dialog.permissionsString}
|
||||
/>
|
||||
)}
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default PermissionsModalWrapper
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user