Merge branch 'develop' of https://github.com/standardnotes/app
This commit is contained in:
11
.github/workflows/desktop.release.reuse.yml
vendored
11
.github/workflows/desktop.release.reuse.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: yarn install
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Set up Ruby
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: yarn install
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
@@ -178,12 +178,13 @@ jobs:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
tag_name: "@standardnotes/desktop@${{ steps.package-version.outputs.current-version}}"
|
||||
prerelease: true
|
||||
draft: true
|
||||
draft: ${{ inputs.channel == 'beta' && true || false }}
|
||||
name: "Desktop ${{ inputs.channel == 'beta' && 'Beta' || '' }} ${{ steps.package-version.outputs.current-version }}"
|
||||
files: packages/desktop/dist/*
|
||||
- name: Publish Snap
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sudo snap install snapcraft --classic
|
||||
echo "${{ secrets.SNAPCRAFT_LOGIN_FILE }}" >> snapauth.txt
|
||||
snapcraft login --with=snapauth.txt
|
||||
snapcraft upload dist/standard-notes-${{ steps.package-version.outputs.current-version}}-linux-amd64.snap
|
||||
|
||||
4
.github/workflows/desktop.windows.sign.yml
vendored
4
.github/workflows/desktop.windows.sign.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- run: yarn install
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
working-directory: packages/desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- uses: actions/download-artifact@v3
|
||||
|
||||
19
.github/workflows/mobile.release.dev.yml
vendored
19
.github/workflows/mobile.release.dev.yml
vendored
@@ -43,11 +43,22 @@ jobs:
|
||||
with:
|
||||
lane: 'android dev'
|
||||
subdirectory: 'packages/mobile'
|
||||
- name: Upload universal apk to artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
name: dev.apk
|
||||
path: android/app/build/outputs/apk/dev/release/app-dev-release.apk
|
||||
path: packages/mobile
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
tag_name: "@standardnotes/mobile@${{ steps.package-version.outputs.current-version}}"
|
||||
prerelease: true
|
||||
draft: false
|
||||
name: "Mobile Beta ${{ steps.package-version.outputs.current-version }}"
|
||||
files: |
|
||||
packages/mobile/android/app/build/outputs/apk/dev/release/app-dev-release.apk
|
||||
ios:
|
||||
defaults:
|
||||
run:
|
||||
|
||||
24
.github/workflows/mobile.release.prod.yml
vendored
24
.github/workflows/mobile.release.prod.yml
vendored
@@ -44,16 +44,24 @@ jobs:
|
||||
with:
|
||||
lane: 'android prod'
|
||||
subdirectory: 'packages/mobile'
|
||||
- name: Upload universal apk to artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
- name: get-npm-version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@main
|
||||
with:
|
||||
name: prod.apk
|
||||
path: android/app/build/outputs/apk/prod/release/app-prod-release.apk
|
||||
- name: Upload Android App Bundle to artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
path: packages/mobile
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: release.aab
|
||||
path: android/app/build/outputs/bundle/prodRelease/app-prod-release.aab
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
tag_name: "@standardnotes/mobile@${{ steps.package-version.outputs.current-version}}"
|
||||
prerelease: false
|
||||
draft: false
|
||||
name: "Mobile ${{ steps.package-version.outputs.current-version }}"
|
||||
files: |
|
||||
packages/mobile/android/app/build/outputs/bundle/prodRelease/app-prod-release.aab
|
||||
packages/mobile/android/app/build/outputs/apk/prod/release/app-prod-release.apk
|
||||
|
||||
ios:
|
||||
defaults:
|
||||
run:
|
||||
|
||||
3
.github/workflows/pr.yml
vendored
3
.github/workflows/pr.yml
vendored
@@ -6,7 +6,8 @@ on:
|
||||
- develop
|
||||
- main
|
||||
paths:
|
||||
- packages/!(components)/**
|
||||
- '**/**'
|
||||
- '!packages/components/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
37
.github/workflows/promote.develop.yml
vendored
Normal file
37
.github/workflows/promote.develop.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Promote Develop To Main
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
Run:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
ref: develop
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup git config
|
||||
run: |
|
||||
git config --global user.name "standardci"
|
||||
git config --global user.email "ci@standardnotes.com"
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v4
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.PASSPHRASE }}
|
||||
git_user_signingkey: true
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: Merge Develop into Main
|
||||
run: |
|
||||
git config pull.rebase false
|
||||
git checkout main
|
||||
git pull origin develop
|
||||
git push origin main
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ jobs:
|
||||
Build:
|
||||
if: contains(github.event.head_commit.message, 'chore(release)') == false
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -15,6 +17,10 @@ jobs:
|
||||
token: ${{ secrets.CI_PAT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup git config
|
||||
run: |
|
||||
git config --global user.name "standardci"
|
||||
@@ -36,7 +42,9 @@ jobs:
|
||||
continue-on-error: true
|
||||
id: graduateRelease
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: yarn release:prod:graduate
|
||||
run: |
|
||||
yarn release:prod:graduate
|
||||
yarn publish:prod
|
||||
|
||||
- name: Bump Prod Version Fallback
|
||||
if: ${{ always() && github.ref == 'refs/heads/main' && steps.graduateRelease.outcome == 'failure' }}
|
||||
@@ -44,7 +52,18 @@ jobs:
|
||||
echo Falling back to non-graduate release due to https://github.com/lerna/lerna/issues/2532
|
||||
git stash
|
||||
yarn release:prod
|
||||
yarn publish:prod
|
||||
|
||||
- name: Bump Beta Version
|
||||
if: ${{ github.ref == 'refs/heads/develop' }}
|
||||
run: yarn release:beta
|
||||
run: |
|
||||
yarn release:beta
|
||||
yarn publish:beta
|
||||
|
||||
- name: Merge release into develop
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
git config pull.rebase false
|
||||
git checkout develop
|
||||
git pull origin main
|
||||
git push origin develop
|
||||
Binary file not shown.
BIN
.yarn/cache/@octokit-openapi-types-npm-12.3.0-9978c43c2e-2f568d2e1d.zip
vendored
Normal file
BIN
.yarn/cache/@octokit-openapi-types-npm-12.3.0-9978c43c2e-2f568d2e1d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@standardnotes-filepicker-npm-1.16.19-4eec2584b5-3fb03b5978.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-filepicker-npm-1.16.19-4eec2584b5-3fb03b5978.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@testing-library-dom-npm-8.14.0-c0d87cffdc-a916ea5167.zip
vendored
Normal file
BIN
.yarn/cache/@testing-library-dom-npm-8.14.0-c0d87cffdc-a916ea5167.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@types-react-native-npm-0.67.9-ec54565b3f-eb33862284.zip
vendored
Normal file
BIN
.yarn/cache/@types-react-native-npm-0.67.9-ec54565b3f-eb33862284.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/codemirror-npm-5.65.6-2d74e8af09-d65aa28af0.zip
vendored
Normal file
BIN
.yarn/cache/codemirror-npm-5.65.6-2d74e8af09-d65aa28af0.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/memfs-npm-3.4.6-26513761db-0164d79c5d.zip
vendored
Normal file
BIN
.yarn/cache/memfs-npm-3.4.6-26513761db-0164d79c5d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/minipass-npm-3.3.3-d77c879d53-523a338f42.zip
vendored
Normal file
BIN
.yarn/cache/minipass-npm-3.3.3-d77c879d53-523a338f42.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/postcss-normalize-positions-npm-5.1.1-82275c9405-d9afc23372.zip
vendored
Normal file
BIN
.yarn/cache/postcss-normalize-positions-npm-5.1.1-82275c9405-d9afc23372.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/postcss-normalize-repeat-style-npm-5.1.1-dd2adac3b3-2c6ad2b0ae.zip
vendored
Normal file
BIN
.yarn/cache/postcss-normalize-repeat-style-npm-5.1.1-dd2adac3b3-2c6ad2b0ae.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/resolve-npm-2.0.0-next.4-3d0bd8621e-c438ac9a65.zip
vendored
Normal file
BIN
.yarn/cache/resolve-npm-2.0.0-next.4-3d0bd8621e-c438ac9a65.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/resolve-patch-00f699a708-21684b4d99.zip
vendored
BIN
.yarn/cache/resolve-patch-00f699a708-21684b4d99.zip
vendored
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/resolve-patch-bdbf6a2444-4bf9f4f8a4.zip
vendored
Normal file
BIN
.yarn/cache/resolve-patch-bdbf6a2444-4bf9f4f8a4.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/rollup-npm-2.75.7-3918160ee7-a6331d46b0.zip
vendored
Normal file
BIN
.yarn/cache/rollup-npm-2.75.7-3918160ee7-a6331d46b0.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -28,6 +28,8 @@
|
||||
"release:prod": "lerna version --conventional-commits --yes -m \"chore(release): publish\"",
|
||||
"release:prod:graduate": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish\"",
|
||||
"release:beta": "lerna version --conventional-prerelease --conventional-commits --yes -m \"chore(release): publish\"",
|
||||
"publish:prod": "lerna publish from-git --yes",
|
||||
"publish:beta": "lerna publish from-git --yes --dist-tag alpha",
|
||||
"version": "yarn install --no-immutable && git add yarn.lock",
|
||||
"postversion": "./scripts/push-tags-one-by-one.sh",
|
||||
"lerna:list": " yarn lerna list -all",
|
||||
|
||||
@@ -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.
|
||||
|
||||
## [3.22.12-alpha.3](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.12-alpha.2...@standardnotes/desktop@3.22.12-alpha.3) (2022-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **desktop:** linux executableName ([296873d](https://github.com/standardnotes/app/commit/296873d671d6cc7cb8744747c09bf755e3a6f1e0))
|
||||
|
||||
## [3.22.12-alpha.2](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.12-alpha.1...@standardnotes/desktop@3.22.12-alpha.2) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.22.12-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.12-alpha.0...@standardnotes/desktop@3.22.12-alpha.1) (2022-06-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.22.12-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.11...@standardnotes/desktop@3.22.12-alpha.0) (2022-06-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
## [3.22.11](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.11-alpha.0...@standardnotes/desktop@3.22.11) (2022-06-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/desktop
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@standardnotes/desktop",
|
||||
"main": "./app/dist/index.js",
|
||||
"version": "3.22.11",
|
||||
"version": "3.22.12-alpha.3",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
@@ -123,7 +123,7 @@
|
||||
"linux": {
|
||||
"category": "Office",
|
||||
"icon": "build/icon/",
|
||||
"executableName": "Standard Notes",
|
||||
"executableName": "standard-notes",
|
||||
"desktop": {
|
||||
"StartupWMClass": "standard notes"
|
||||
},
|
||||
|
||||
1
packages/releases/.gitignore
vendored
Normal file
1
packages/releases/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist
|
||||
14
packages/releases/CHANGELOG.md
Normal file
14
packages/releases/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.1.0-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/releases@1.1.0-alpha.0...@standardnotes/releases@1.1.0-alpha.1) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/releases
|
||||
|
||||
# 1.1.0-alpha.0 (2022-06-20)
|
||||
|
||||
### Features
|
||||
|
||||
* public releases package ([4fea3e9](https://github.com/standardnotes/app/commit/4fea3e9a6b5fdeea772ca903fccff7034b6e2928))
|
||||
15
packages/releases/index.mjs
Normal file
15
packages/releases/index.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
import Components from '../components/package.json' assert { type: 'json' }
|
||||
import Desktop from '../desktop/package.json' assert { type: 'json' }
|
||||
import Mobile from '../mobile/package.json' assert { type: 'json' }
|
||||
import Web from '../web/package.json' assert { type: 'json' }
|
||||
import { writeJson, ensureDirExists } from '../../scripts/ScriptUtils.mjs'
|
||||
|
||||
const Releases = {
|
||||
[Components.name]: Components.version,
|
||||
[Desktop.name]: Desktop.version,
|
||||
[Mobile.name]: Mobile.version,
|
||||
[Web.name]: Web.version,
|
||||
}
|
||||
|
||||
ensureDirExists('dist')
|
||||
writeJson(Releases, 'dist/releases.json')
|
||||
22
packages/releases/package.json
Normal file
22
packages/releases/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@standardnotes/releases",
|
||||
"version": "1.1.0-alpha.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/releases.json",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"author": "Standard Notes.",
|
||||
"description": "Standard Notes client release versions",
|
||||
"engines": {
|
||||
"node": ">=12.19.0 <17.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node index.mjs",
|
||||
"version": "yarn build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "*",
|
||||
"typescript": "*"
|
||||
}
|
||||
}
|
||||
@@ -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.2.14-alpha.2](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.14-alpha.1...@standardnotes/web-server@1.2.14-alpha.2) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.14-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.14-alpha.0...@standardnotes/web-server@1.2.14-alpha.1) (2022-06-19)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.14-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.13...@standardnotes/web-server@1.2.14-alpha.0) (2022-06-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
## [1.2.13](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.13-alpha.0...@standardnotes/web-server@1.2.13) (2022-06-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web-server
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@standardnotes/web-server",
|
||||
"version": "1.2.13",
|
||||
"version": "1.2.14-alpha.2",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"author": "Standard Notes.",
|
||||
|
||||
@@ -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.
|
||||
|
||||
# [3.23.0-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/web@3.23.0-alpha.0...@standardnotes/web@3.23.0-alpha.1) (2022-06-20)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
# [3.23.0-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/web@3.22.7-alpha.0...@standardnotes/web@3.23.0-alpha.0) (2022-06-19)
|
||||
|
||||
### Features
|
||||
|
||||
* ctrl+a to select all items ([#1123](https://github.com/standardnotes/app/issues/1123)) ([dcf3724](https://github.com/standardnotes/app/commit/dcf3724e2c951d7bbad276d37b3302c34d7ee78f))
|
||||
|
||||
## [3.22.7-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/web@3.22.6...@standardnotes/web@3.22.7-alpha.0) (2022-06-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* exporting multiple notes with same title ([#1119](https://github.com/standardnotes/app/issues/1119)) ([dfbd72b](https://github.com/standardnotes/app/commit/dfbd72b1dfd4a4681c452c9113248045e69880cd))
|
||||
|
||||
## [3.22.6](https://github.com/standardnotes/app/compare/@standardnotes/web@3.22.6-alpha.0...@standardnotes/web@3.22.6) (2022-06-18)
|
||||
|
||||
**Note:** Version bump only for package @standardnotes/web
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@standardnotes/web",
|
||||
"version": "3.22.6",
|
||||
"version": "3.23.0-alpha.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "dist/app.js",
|
||||
"author": "Standard Notes",
|
||||
"author": "Standard Notes.",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
|
||||
@@ -14,7 +14,7 @@ import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal'
|
||||
import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu'
|
||||
import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import RevisionHistoryModalWrapper from '@/Components/RevisionHistoryModal/RevisionHistoryModalWrapper'
|
||||
import RevisionHistoryModal from '@/Components/RevisionHistoryModal/RevisionHistoryModal'
|
||||
import PremiumModalProvider from '@/Hooks/usePremiumModal'
|
||||
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu'
|
||||
@@ -177,7 +177,18 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
<div id="app" className={appClass + ' app app-column-container'}>
|
||||
<Navigation application={application} />
|
||||
<ContentListView application={application} viewControllerManager={viewControllerManager} />
|
||||
<ContentListView
|
||||
application={application}
|
||||
accountMenuController={viewControllerManager.accountMenuController}
|
||||
filesController={viewControllerManager.filesController}
|
||||
itemListController={viewControllerManager.itemListController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
noAccountWarningController={viewControllerManager.noAccountWarningController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
/>
|
||||
<NoteGroupView application={application} />
|
||||
</div>
|
||||
|
||||
@@ -185,7 +196,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
<Footer application={application} applicationGroup={mainApplicationGroup} />
|
||||
<SessionsModal application={application} viewControllerManager={viewControllerManager} />
|
||||
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} />
|
||||
<RevisionHistoryModalWrapper application={application} viewControllerManager={viewControllerManager} />
|
||||
<RevisionHistoryModal
|
||||
application={application}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
subscriptionController={viewControllerManager.subscriptionController}
|
||||
/>
|
||||
</>
|
||||
|
||||
{renderChallenges()}
|
||||
@@ -196,6 +213,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
noteTagsController={viewControllerManager.noteTagsController}
|
||||
historyModalController={viewControllerManager.historyModalController}
|
||||
/>
|
||||
<TagsContextMenuWrapper viewControllerManager={viewControllerManager} />
|
||||
<FileContextMenuWrapper
|
||||
|
||||
@@ -15,12 +15,10 @@ import UrlMissing from '@/Components/ComponentView/UrlMissing'
|
||||
import IsDeprecated from '@/Components/ComponentView/IsDeprecated'
|
||||
import IsExpired from '@/Components/ComponentView/IsExpired'
|
||||
import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
|
||||
|
||||
interface IProps {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
componentViewer: ComponentViewer
|
||||
requestReload?: (viewer: ComponentViewer, force?: boolean) => void
|
||||
onLoad?: (component: SNComponent) => void
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { UuidString } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
import ContentListItem from './ContentListItem'
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
filesController: FilesController
|
||||
itemListController: ItemListController
|
||||
items: ListableContentItem[]
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
selectionController: SelectedItemsController
|
||||
selectedItems: Record<UuidString, ListableContentItem>
|
||||
paginate: () => void
|
||||
}
|
||||
|
||||
const ContentList: FunctionComponent<Props> = ({
|
||||
application,
|
||||
viewControllerManager,
|
||||
filesController,
|
||||
itemListController,
|
||||
items,
|
||||
navigationController,
|
||||
notesController,
|
||||
selectionController,
|
||||
selectedItems,
|
||||
paginate,
|
||||
}) => {
|
||||
const { selectPreviousItem, selectNextItem } = viewControllerManager.itemListController
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } =
|
||||
viewControllerManager.itemListController.webDisplayOptions
|
||||
const { sortBy } = viewControllerManager.itemListController.displayOptions
|
||||
const { selectPreviousItem, selectNextItem } = itemListController
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = itemListController.webDisplayOptions
|
||||
const { sortBy } = itemListController.displayOptions
|
||||
|
||||
const onScroll: UIEventHandler = useCallback(
|
||||
(e) => {
|
||||
@@ -55,7 +67,7 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
return (
|
||||
<div
|
||||
className="infinite-scroll focus:shadow-none focus:outline-none"
|
||||
id="notes-scrollable"
|
||||
id={ElementIds.ContentList}
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
@@ -71,10 +83,10 @@ const ContentList: FunctionComponent<Props> = ({
|
||||
hideTags={hideTags}
|
||||
hideIcon={hideEditorIcon}
|
||||
sortBy={sortBy}
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
filesController={filesController}
|
||||
selectionController={selectionController}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -7,22 +7,22 @@ import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
closeDisplayOptionsMenu: () => void
|
||||
isOpen: boolean
|
||||
navigationController: NavigationController
|
||||
}
|
||||
|
||||
const ContentListOptionsMenu: FunctionComponent<Props> = ({
|
||||
closeDisplayOptionsMenu,
|
||||
closeOnBlur,
|
||||
application,
|
||||
viewControllerManager,
|
||||
isOpen,
|
||||
navigationController,
|
||||
}) => {
|
||||
const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
|
||||
const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
|
||||
@@ -174,7 +174,7 @@ const ContentListOptionsMenu: FunctionComponent<Props> = ({
|
||||
</MenuItem>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">View</div>
|
||||
{viewControllerManager.navigationController.selectedUuid !== SystemViewId.Files && (
|
||||
{navigationController.selectedUuid !== SystemViewId.Files && (
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { KeyboardKey, KeyboardModifier } from '@/Services/IOService'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||
import { PrefKey, SystemViewId } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -15,20 +14,49 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import ContentList from '@/Components/ContentListView/ContentList'
|
||||
import NoAccountWarningWrapper from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import NoAccountWarning from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import SearchOptions from '@/Components/SearchOptions/SearchOptions'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import ContentListOptionsMenu from './ContentListOptionsMenu'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { ElementIds } from '@/Constants/ElementIDs'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
filesController: FilesController
|
||||
itemListController: ItemListController
|
||||
navigationController: NavigationController
|
||||
noAccountWarningController: NoAccountWarningController
|
||||
noteTagsController: NoteTagsController
|
||||
notesController: NotesController
|
||||
searchOptionsController: SearchOptionsController
|
||||
selectionController: SelectedItemsController
|
||||
}
|
||||
|
||||
const ContentListView: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
|
||||
const ContentListView: FunctionComponent<Props> = ({
|
||||
accountMenuController,
|
||||
application,
|
||||
filesController,
|
||||
itemListController,
|
||||
navigationController,
|
||||
noAccountWarningController,
|
||||
noteTagsController,
|
||||
notesController,
|
||||
searchOptionsController,
|
||||
selectionController,
|
||||
}) => {
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -47,9 +75,9 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
paginate,
|
||||
panelWidth,
|
||||
createNewNote,
|
||||
} = viewControllerManager.itemListController
|
||||
} = itemListController
|
||||
|
||||
const { selectedItems } = viewControllerManager.selectionController
|
||||
const { selectedItems } = selectionController
|
||||
|
||||
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
|
||||
const [focusedSearch, setFocusedSearch] = useState(false)
|
||||
@@ -57,17 +85,17 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu)
|
||||
|
||||
const isFilesSmartView = useMemo(
|
||||
() => viewControllerManager.navigationController.selected?.uuid === SystemViewId.Files,
|
||||
[viewControllerManager.navigationController.selected?.uuid],
|
||||
() => navigationController.selected?.uuid === SystemViewId.Files,
|
||||
[navigationController.selected?.uuid],
|
||||
)
|
||||
|
||||
const addNewItem = useCallback(() => {
|
||||
if (isFilesSmartView) {
|
||||
void viewControllerManager.filesController.uploadNewFile()
|
||||
void filesController.uploadNewFile()
|
||||
} else {
|
||||
void createNewNote()
|
||||
}
|
||||
}, [viewControllerManager.filesController, createNewNote, isFilesSmartView])
|
||||
}, [filesController, createNewNote, isFilesSmartView])
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
@@ -75,7 +103,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
* use Control modifier as well. These rules don't apply to desktop, but
|
||||
* probably better to be consistent.
|
||||
*/
|
||||
const newNoteKeyObserver = application.io.addKeyObserver({
|
||||
const disposeNewNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: 'n',
|
||||
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
|
||||
onKeyDown: (event) => {
|
||||
@@ -84,7 +112,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
},
|
||||
})
|
||||
|
||||
const nextNoteKeyObserver = application.io.addKeyObserver({
|
||||
const disposeNextNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: KeyboardKey.Down,
|
||||
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
|
||||
onKeyDown: () => {
|
||||
@@ -95,7 +123,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
},
|
||||
})
|
||||
|
||||
const previousNoteKeyObserver = application.io.addKeyObserver({
|
||||
const disposePreviousNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: KeyboardKey.Up,
|
||||
element: document.body,
|
||||
onKeyDown: () => {
|
||||
@@ -103,7 +131,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
},
|
||||
})
|
||||
|
||||
const searchKeyObserver = application.io.addKeyObserver({
|
||||
const disposeSearchKeyObserver = application.io.addKeyObserver({
|
||||
key: 'f',
|
||||
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift],
|
||||
onKeyDown: () => {
|
||||
@@ -113,13 +141,37 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
},
|
||||
})
|
||||
|
||||
const disposeSelectAllKeyObserver = application.io.addKeyObserver({
|
||||
key: 'a',
|
||||
modifiers: [KeyboardModifier.Ctrl],
|
||||
onKeyDown: (event) => {
|
||||
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)
|
||||
|
||||
if (!isTargetInsideContentList) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
selectionController.selectAll()
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
newNoteKeyObserver()
|
||||
nextNoteKeyObserver()
|
||||
previousNoteKeyObserver()
|
||||
searchKeyObserver()
|
||||
disposeNewNoteKeyObserver()
|
||||
disposeNextNoteKeyObserver()
|
||||
disposePreviousNoteKeyObserver()
|
||||
disposeSearchKeyObserver()
|
||||
disposeSelectAllKeyObserver()
|
||||
}
|
||||
}, [addNewItem, application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem])
|
||||
}, [
|
||||
addNewItem,
|
||||
application.io,
|
||||
createNewNote,
|
||||
searchBarElement,
|
||||
selectNextItem,
|
||||
selectPreviousItem,
|
||||
selectionController,
|
||||
])
|
||||
|
||||
const onNoteFilterTextChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
@@ -143,15 +195,15 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
||||
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
||||
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
|
||||
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth()
|
||||
noteTagsController.reloadTagsContainerMaxWidth()
|
||||
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed)
|
||||
},
|
||||
[viewControllerManager, application],
|
||||
[application, noteTagsController],
|
||||
)
|
||||
|
||||
const panelWidthEventCallback = useCallback(() => {
|
||||
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth()
|
||||
}, [viewControllerManager])
|
||||
noteTagsController.reloadTagsContainerMaxWidth()
|
||||
}, [noteTagsController])
|
||||
|
||||
const toggleDisplayOptionsMenu = useCallback(() => {
|
||||
setShowDisplayOptionsMenu(!showDisplayOptionsMenu)
|
||||
@@ -207,11 +259,14 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
|
||||
{(focusedSearch || noteFilterText) && (
|
||||
<div className="animate-fade-from-top">
|
||||
<SearchOptions application={application} viewControllerManager={viewControllerManager} />
|
||||
<SearchOptions application={application} searchOptions={searchOptionsController} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoAccountWarningWrapper viewControllerManager={viewControllerManager} />
|
||||
<NoAccountWarning
|
||||
accountMenuController={accountMenuController}
|
||||
noAccountWarningController={noAccountWarningController}
|
||||
/>
|
||||
</div>
|
||||
<div id="items-menu-bar" className="sn-component" ref={displayOptionsMenuRef}>
|
||||
<div className="sk-app-bar no-edges">
|
||||
@@ -234,10 +289,10 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
{showDisplayOptionsMenu && (
|
||||
<ContentListOptionsMenu
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
||||
closeOnBlur={closeDisplayOptMenuOnBlur}
|
||||
isOpen={showDisplayOptionsMenu}
|
||||
navigationController={navigationController}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
@@ -253,8 +308,12 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
|
||||
items={renderedItems}
|
||||
selectedItems={selectedItems}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
paginate={paginate}
|
||||
filesController={filesController}
|
||||
itemListController={itemListController}
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
selectionController={selectionController}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -23,6 +24,7 @@ type Props = {
|
||||
notesController: NotesController
|
||||
noteTagsController: NoteTagsController
|
||||
selectionController: SelectedItemsController
|
||||
historyModalController: HistoryModalController
|
||||
}
|
||||
|
||||
const MultipleSelectedNotes = ({
|
||||
@@ -34,6 +36,7 @@ const MultipleSelectedNotes = ({
|
||||
notesController,
|
||||
noteTagsController,
|
||||
selectionController,
|
||||
historyModalController,
|
||||
}: Props) => {
|
||||
const count = notesController.selectedNotesCount
|
||||
|
||||
@@ -65,6 +68,7 @@ const MultipleSelectedNotes = ({
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,49 +1,22 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { MouseEventHandler, useCallback } from 'react'
|
||||
import NoAccountWarningContent from './NoAccountWarningContent'
|
||||
|
||||
type Props = { viewControllerManager: ViewControllerManager }
|
||||
|
||||
const NoAccountWarning = observer(({ viewControllerManager }: Props) => {
|
||||
const showAccountMenu: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation()
|
||||
viewControllerManager.accountMenuController.setShow(true)
|
||||
},
|
||||
[viewControllerManager],
|
||||
)
|
||||
|
||||
const hideWarning = useCallback(() => {
|
||||
viewControllerManager.noAccountWarningController.hide()
|
||||
}, [viewControllerManager])
|
||||
|
||||
return (
|
||||
<div className="mt-4 p-4 rounded-md shadow-sm grid grid-template-cols-1fr">
|
||||
<h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1>
|
||||
<p className="m-0 mt-1 col-start-1 col-end-3">Sign in or register to back up your notes.</p>
|
||||
<button className="sn-button small info mt-3 col-start-1 col-end-3 justify-self-start" onClick={showAccountMenu}>
|
||||
Open Account menu
|
||||
</button>
|
||||
<button
|
||||
onClick={hideWarning}
|
||||
title="Ignore warning"
|
||||
aria-label="Ignore warning"
|
||||
style={{ height: '20px' }}
|
||||
className="border-0 m-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1 color-neutral hover:color-info"
|
||||
>
|
||||
<Icon type="close" className="block" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
NoAccountWarning.displayName = 'NoAccountWarning'
|
||||
|
||||
const NoAccountWarningWrapper = ({ viewControllerManager }: Props) => {
|
||||
const canShow = viewControllerManager.noAccountWarningController.show
|
||||
|
||||
return canShow ? <NoAccountWarning viewControllerManager={viewControllerManager} /> : null
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
noAccountWarningController: NoAccountWarningController
|
||||
}
|
||||
|
||||
export default observer(NoAccountWarningWrapper)
|
||||
const NoAccountWarning = ({ accountMenuController, noAccountWarningController }: Props) => {
|
||||
const canShow = noAccountWarningController.show
|
||||
|
||||
return canShow ? (
|
||||
<NoAccountWarningContent
|
||||
accountMenuController={accountMenuController}
|
||||
noAccountWarningController={noAccountWarningController}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default observer(NoAccountWarning)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { MouseEventHandler, useCallback } from 'react'
|
||||
|
||||
type Props = {
|
||||
accountMenuController: AccountMenuController
|
||||
noAccountWarningController: NoAccountWarningController
|
||||
}
|
||||
|
||||
const NoAccountWarningContent = ({ accountMenuController, noAccountWarningController }: Props) => {
|
||||
const showAccountMenu: MouseEventHandler = useCallback(
|
||||
(event) => {
|
||||
event.stopPropagation()
|
||||
accountMenuController.setShow(true)
|
||||
},
|
||||
[accountMenuController],
|
||||
)
|
||||
|
||||
const hideWarning = useCallback(() => {
|
||||
noAccountWarningController.hide()
|
||||
}, [noAccountWarningController])
|
||||
|
||||
return (
|
||||
<div className="mt-4 p-4 rounded-md shadow-sm grid grid-template-cols-1fr">
|
||||
<h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1>
|
||||
<p className="m-0 mt-1 col-start-1 col-end-3">Sign in or register to back up your notes.</p>
|
||||
<button className="sn-button small info mt-3 col-start-1 col-end-3 justify-self-start" onClick={showAccountMenu}>
|
||||
Open Account menu
|
||||
</button>
|
||||
<button
|
||||
onClick={hideWarning}
|
||||
title="Ignore warning"
|
||||
aria-label="Ignore warning"
|
||||
style={{ height: '20px' }}
|
||||
className="border-0 m-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1 color-neutral hover:color-info"
|
||||
>
|
||||
<Icon type="close" className="block" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(NoAccountWarningContent)
|
||||
@@ -92,6 +92,7 @@ class NoteGroupView extends PureComponent<Props, State> {
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -971,6 +971,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
navigationController={this.viewControllerManager.navigationController}
|
||||
notesController={this.viewControllerManager.notesController}
|
||||
noteTagsController={this.viewControllerManager.noteTagsController}
|
||||
historyModalController={this.viewControllerManager.historyModalController}
|
||||
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
|
||||
/>
|
||||
</div>
|
||||
@@ -1002,7 +1003,6 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
onLoad={this.onEditorComponentLoad}
|
||||
requestReload={this.editorComponentViewerRequestsReload}
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1073,12 +1073,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
|
||||
{this.state.stackComponentViewers.map((viewer) => {
|
||||
return (
|
||||
<div className="component-view component-stack-item" key={viewer.identifier}>
|
||||
<ComponentView
|
||||
key={viewer.identifier}
|
||||
componentViewer={viewer}
|
||||
application={this.application}
|
||||
viewControllerManager={this.viewControllerManager}
|
||||
/>
|
||||
<ComponentView key={viewer.identifier} componentViewer={viewer} application={this.application} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -7,15 +7,23 @@ import { WebApplication } from '@/Application/Application'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
noteTagsController: NoteTagsController
|
||||
historyModalController: HistoryModalController
|
||||
}
|
||||
|
||||
const NotesContextMenu = ({ application, navigationController, notesController, noteTagsController }: Props) => {
|
||||
const NotesContextMenu = ({
|
||||
application,
|
||||
navigationController,
|
||||
notesController,
|
||||
noteTagsController,
|
||||
historyModalController,
|
||||
}: Props) => {
|
||||
const { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = notesController
|
||||
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
@@ -49,6 +57,7 @@ const NotesContextMenu = ({ application, navigationController, notesController,
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
/>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
@@ -174,6 +174,7 @@ const NotesOptions = ({
|
||||
navigationController,
|
||||
notesController,
|
||||
noteTagsController,
|
||||
historyModalController,
|
||||
closeOnBlur,
|
||||
}: NotesOptionsProps) => {
|
||||
const [altKeyDown, setAltKeyDown] = useState(false)
|
||||
@@ -222,7 +223,7 @@ const NotesOptions = ({
|
||||
const format = editor?.package_info?.file_type || 'txt'
|
||||
return `${note.title}.${format}`
|
||||
},
|
||||
[application],
|
||||
[application.componentManager],
|
||||
)
|
||||
|
||||
const downloadSelectedItems = useCallback(async () => {
|
||||
@@ -239,7 +240,7 @@ const NotesOptions = ({
|
||||
await application.getArchiveService().downloadDataAsZip(
|
||||
notes.map((note) => {
|
||||
return {
|
||||
filename: getNoteFileName(note),
|
||||
name: getNoteFileName(note),
|
||||
content: new Blob([note.text]),
|
||||
}
|
||||
}),
|
||||
@@ -259,8 +260,8 @@ const NotesOptions = ({
|
||||
}, [application, notes])
|
||||
|
||||
const openRevisionHistoryModal = useCallback(() => {
|
||||
notesController.setShowRevisionHistoryModal(true)
|
||||
}, [notesController])
|
||||
historyModalController.openModal(notesController.firstSelectedNote)
|
||||
}, [historyModalController, notesController.firstSelectedNote])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -10,12 +10,14 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
noteTagsController: NoteTagsController
|
||||
historyModalController: HistoryModalController
|
||||
onClickPreprocessing?: () => Promise<void>
|
||||
}
|
||||
|
||||
@@ -24,6 +26,7 @@ const NotesOptionsPanel = ({
|
||||
navigationController,
|
||||
notesController,
|
||||
noteTagsController,
|
||||
historyModalController,
|
||||
onClickPreprocessing,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -95,6 +98,7 @@ const NotesOptionsPanel = ({
|
||||
navigationController={navigationController}
|
||||
notesController={notesController}
|
||||
noteTagsController={noteTagsController}
|
||||
historyModalController={historyModalController}
|
||||
closeOnBlur={closeOnBlur}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NoteTagsController } from '@/Controllers/NoteTagsController'
|
||||
@@ -8,5 +9,6 @@ export type NotesOptionsProps = {
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
noteTagsController: NoteTagsController
|
||||
historyModalController: HistoryModalController
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
}
|
||||
|
||||
@@ -1,75 +1,24 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { Action, ActionVerb, HistoryEntry, NoteHistoryEntry, RevisionListEntry, SNNote } from '@standardnotes/snjs'
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
import { FeaturesClientInterface } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useState, useEffect, SetStateAction, Dispatch } from 'react'
|
||||
import { FunctionComponent } from 'react'
|
||||
import LegacyHistoryList from './LegacyHistoryList'
|
||||
import RemoteHistoryList from './RemoteHistoryList'
|
||||
import { RevisionType } from './RevisionType'
|
||||
import SessionHistoryList from './SessionHistoryList'
|
||||
import { LegacyHistoryEntry, RemoteRevisionListGroup, sortRevisionListIntoGroups } from './utils'
|
||||
|
||||
export enum RevisionListTabType {
|
||||
Session = 'Session',
|
||||
Remote = 'Remote',
|
||||
Legacy = 'Legacy',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
isFetchingRemoteHistory: boolean
|
||||
note: SNNote
|
||||
remoteHistory: RemoteRevisionListGroup[] | undefined
|
||||
setIsFetchingSelectedRevision: Dispatch<SetStateAction<boolean>>
|
||||
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
|
||||
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>>
|
||||
setShowContentLockedScreen: Dispatch<SetStateAction<boolean>>
|
||||
features: FeaturesClientInterface
|
||||
noteHistoryController: NoteHistoryController
|
||||
}
|
||||
|
||||
const HistoryListContainer: FunctionComponent<Props> = ({
|
||||
application,
|
||||
isFetchingRemoteHistory,
|
||||
note,
|
||||
remoteHistory,
|
||||
setIsFetchingSelectedRevision,
|
||||
setSelectedRemoteEntry,
|
||||
setSelectedRevision,
|
||||
setShowContentLockedScreen,
|
||||
}) => {
|
||||
const sessionHistory = sortRevisionListIntoGroups<NoteHistoryEntry>(
|
||||
application.historyManager.sessionHistoryForItem(note) as NoteHistoryEntry[],
|
||||
)
|
||||
const [legacyHistory, setLegacyHistory] = useState<Action[]>()
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState<RevisionListTabType>(RevisionListTabType.Remote)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchLegacyHistory = async () => {
|
||||
const actionExtensions = application.actionsManager.getExtensions()
|
||||
actionExtensions.forEach(async (ext) => {
|
||||
const actionExtension = await application.actionsManager.loadExtensionInContextOfItem(ext, note)
|
||||
|
||||
if (!actionExtension) {
|
||||
return
|
||||
}
|
||||
|
||||
const isLegacyNoteHistoryExt = actionExtension?.actions.some((action) => action.verb === ActionVerb.Nested)
|
||||
|
||||
if (!isLegacyNoteHistoryExt) {
|
||||
return
|
||||
}
|
||||
|
||||
const legacyHistoryEntries = actionExtension.actions.filter((action) => action.subactions?.[0])
|
||||
|
||||
setLegacyHistory(legacyHistoryEntries)
|
||||
})
|
||||
}
|
||||
|
||||
fetchLegacyHistory().catch(console.error)
|
||||
}, [application, note])
|
||||
const HistoryListContainer: FunctionComponent<Props> = ({ features, noteHistoryController }) => {
|
||||
const { legacyHistory, currentTab, selectTab } = noteHistoryController
|
||||
|
||||
const TabButton: FunctionComponent<{
|
||||
type: RevisionListTabType
|
||||
type: RevisionType
|
||||
}> = ({ type }) => {
|
||||
const isSelected = selectedTab === type
|
||||
const isSelected = currentTab === type
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -77,8 +26,7 @@ const HistoryListContainer: FunctionComponent<Props> = ({
|
||||
isSelected ? 'color-info font-medium shadow-bottom' : 'color-text'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedTab(type)
|
||||
setSelectedRemoteEntry(undefined)
|
||||
selectTab(type)
|
||||
}}
|
||||
>
|
||||
{type}
|
||||
@@ -86,98 +34,26 @@ const HistoryListContainer: FunctionComponent<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const fetchAndSetLegacyRevision = useCallback(
|
||||
async (revisionListEntry: Action) => {
|
||||
setSelectedRemoteEntry(undefined)
|
||||
setSelectedRevision(undefined)
|
||||
setIsFetchingSelectedRevision(true)
|
||||
|
||||
try {
|
||||
if (!revisionListEntry.subactions?.[0]) {
|
||||
throw new Error('Could not find revision action url')
|
||||
}
|
||||
|
||||
const response = await application.actionsManager.runAction(revisionListEntry.subactions[0], note)
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Could not fetch revision')
|
||||
}
|
||||
|
||||
setSelectedRevision(response.item as unknown as HistoryEntry)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setSelectedRevision(undefined)
|
||||
} finally {
|
||||
setIsFetchingSelectedRevision(false)
|
||||
}
|
||||
},
|
||||
[application.actionsManager, note, setIsFetchingSelectedRevision, setSelectedRemoteEntry, setSelectedRevision],
|
||||
)
|
||||
|
||||
const fetchAndSetRemoteRevision = useCallback(
|
||||
async (revisionListEntry: RevisionListEntry) => {
|
||||
setShowContentLockedScreen(false)
|
||||
|
||||
if (application.features.hasMinimumRole(revisionListEntry.required_role)) {
|
||||
setIsFetchingSelectedRevision(true)
|
||||
setSelectedRevision(undefined)
|
||||
setSelectedRemoteEntry(undefined)
|
||||
|
||||
try {
|
||||
const remoteRevision = await application.historyManager.fetchRemoteRevision(note, revisionListEntry)
|
||||
setSelectedRevision(remoteRevision)
|
||||
setSelectedRemoteEntry(revisionListEntry)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsFetchingSelectedRevision(false)
|
||||
}
|
||||
} else {
|
||||
setShowContentLockedScreen(true)
|
||||
setSelectedRevision(undefined)
|
||||
}
|
||||
},
|
||||
[
|
||||
application,
|
||||
note,
|
||||
setIsFetchingSelectedRevision,
|
||||
setSelectedRemoteEntry,
|
||||
setSelectedRevision,
|
||||
setShowContentLockedScreen,
|
||||
],
|
||||
)
|
||||
const CurrentTabList = () => {
|
||||
switch (currentTab) {
|
||||
case RevisionType.Remote:
|
||||
return <RemoteHistoryList features={features} noteHistoryController={noteHistoryController} />
|
||||
case RevisionType.Session:
|
||||
return <SessionHistoryList noteHistoryController={noteHistoryController} />
|
||||
case RevisionType.Legacy:
|
||||
return <LegacyHistoryList legacyHistory={legacyHistory} noteHistoryController={noteHistoryController} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col min-w-60 border-0 border-r-1px border-solid border-main overflow-auto h-full'}>
|
||||
<div className="flex border-0 border-b-1 border-solid border-main">
|
||||
<TabButton type={RevisionListTabType.Remote} />
|
||||
<TabButton type={RevisionListTabType.Session} />
|
||||
{legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionListTabType.Legacy} />}
|
||||
<TabButton type={RevisionType.Remote} />
|
||||
<TabButton type={RevisionType.Session} />
|
||||
{legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionType.Legacy} />}
|
||||
</div>
|
||||
<div className={'min-h-0 overflow-auto py-1.5 h-full'}>
|
||||
{selectedTab === RevisionListTabType.Session && (
|
||||
<SessionHistoryList
|
||||
sessionHistory={sessionHistory}
|
||||
setSelectedRevision={setSelectedRevision}
|
||||
setSelectedRemoteEntry={setSelectedRemoteEntry}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === RevisionListTabType.Remote && (
|
||||
<RemoteHistoryList
|
||||
application={application}
|
||||
remoteHistory={remoteHistory}
|
||||
isFetchingRemoteHistory={isFetchingRemoteHistory}
|
||||
fetchAndSetRemoteRevision={fetchAndSetRemoteRevision}
|
||||
/>
|
||||
)}
|
||||
{selectedTab === RevisionListTabType.Legacy && (
|
||||
<LegacyHistoryList
|
||||
legacyHistory={legacyHistory}
|
||||
setSelectedRevision={setSelectedRevision}
|
||||
setSelectedRemoteEntry={setSelectedRemoteEntry}
|
||||
fetchAndSetLegacyRevision={fetchAndSetLegacyRevision}
|
||||
/>
|
||||
)}
|
||||
<CurrentTabList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import RevisionContentLocked from './RevisionContentLocked'
|
||||
import SelectedRevisionContent from './SelectedRevisionContent'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import { NoteHistoryController, RevisionContentState } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
noteHistoryController: NoteHistoryController
|
||||
notesController: NotesController
|
||||
subscriptionController: SubscriptionController
|
||||
}
|
||||
|
||||
const HistoryModalContentPane = ({
|
||||
application,
|
||||
noteHistoryController,
|
||||
notesController,
|
||||
subscriptionController,
|
||||
}: Props) => {
|
||||
const { contentState } = noteHistoryController
|
||||
|
||||
switch (contentState) {
|
||||
case RevisionContentState.Idle:
|
||||
return (
|
||||
<div className="color-passive-0 select-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
No revision selected
|
||||
</div>
|
||||
)
|
||||
case RevisionContentState.Loading:
|
||||
return (
|
||||
<div className="sk-spinner w-5 h-5 spinner-info absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
)
|
||||
case RevisionContentState.Loaded:
|
||||
return (
|
||||
<SelectedRevisionContent
|
||||
application={application}
|
||||
notesController={notesController}
|
||||
noteHistoryController={noteHistoryController}
|
||||
/>
|
||||
)
|
||||
case RevisionContentState.NotEntitled:
|
||||
return <RevisionContentLocked subscriptionController={subscriptionController} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default observer(HistoryModalContentPane)
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getPlatformString } from '@/Utils'
|
||||
import { DialogOverlay, DialogContent } from '@reach/dialog'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
onDismiss: () => void
|
||||
}
|
||||
|
||||
const HistoryModalDialog = ({ children, onDismiss }: Props) => {
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={`sn-component ${getPlatformString()}`}
|
||||
onDismiss={onDismiss}
|
||||
aria-label="Note revision history"
|
||||
>
|
||||
<DialogContent
|
||||
aria-label="Note revision history"
|
||||
className="rounded shadow-overlay"
|
||||
style={{
|
||||
width: '90%',
|
||||
maxWidth: '90%',
|
||||
minHeight: '90%',
|
||||
background: 'var(--modal-background-color)',
|
||||
}}
|
||||
>
|
||||
<div className="bg-default flex flex-col h-full overflow-hidden">{children}</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoryModalDialog
|
||||
@@ -0,0 +1,37 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useState } from 'react'
|
||||
import HistoryListContainer from './HistoryListContainer'
|
||||
import { RevisionHistoryModalContentProps } from './RevisionHistoryModalProps'
|
||||
import HistoryModalFooter from './HistoryModalFooter'
|
||||
import HistoryModalContentPane from './HistoryModalContentPane'
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
|
||||
const HistoryModalDialogContent = ({
|
||||
application,
|
||||
dismissModal,
|
||||
notesController,
|
||||
subscriptionController,
|
||||
note,
|
||||
selectionController,
|
||||
}: RevisionHistoryModalContentProps) => {
|
||||
const [noteHistoryController] = useState(() => new NoteHistoryController(application, note, selectionController))
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-grow min-h-0">
|
||||
<HistoryListContainer features={application.features} noteHistoryController={noteHistoryController} />
|
||||
<div className="flex flex-col flex-grow relative">
|
||||
<HistoryModalContentPane
|
||||
application={application}
|
||||
noteHistoryController={noteHistoryController}
|
||||
notesController={notesController}
|
||||
subscriptionController={subscriptionController}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HistoryModalFooter dismissModal={dismissModal} noteHistoryController={noteHistoryController} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(HistoryModalDialogContent)
|
||||
@@ -0,0 +1,62 @@
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
import { RevisionListEntry } from '@standardnotes/snjs/dist/@types'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useCallback, useState } from 'react'
|
||||
import Button from '../Button/Button'
|
||||
|
||||
type Props = {
|
||||
dismissModal: () => void
|
||||
noteHistoryController: NoteHistoryController
|
||||
}
|
||||
|
||||
const HistoryModalFooter = ({ dismissModal, noteHistoryController }: Props) => {
|
||||
const { selectedRevision, restoreRevision, restoreRevisionAsCopy, selectedEntry, deleteRemoteRevision } =
|
||||
noteHistoryController
|
||||
|
||||
const [isDeletingRevision, setIsDeletingRevision] = useState(false)
|
||||
|
||||
const restoreSelectedRevision = useCallback(() => {
|
||||
if (selectedRevision) {
|
||||
restoreRevision(selectedRevision)
|
||||
dismissModal()
|
||||
}
|
||||
}, [dismissModal, restoreRevision, selectedRevision])
|
||||
|
||||
const restoreAsCopy = useCallback(async () => {
|
||||
if (selectedRevision) {
|
||||
void restoreRevisionAsCopy(selectedRevision)
|
||||
dismissModal()
|
||||
}
|
||||
}, [dismissModal, restoreRevisionAsCopy, selectedRevision])
|
||||
|
||||
const deleteSelectedRevision = useCallback(async () => {
|
||||
if (!selectedEntry) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsDeletingRevision(true)
|
||||
await deleteRemoteRevision(selectedEntry as RevisionListEntry)
|
||||
setIsDeletingRevision(false)
|
||||
}, [deleteRemoteRevision, selectedEntry])
|
||||
|
||||
return (
|
||||
<div className="flex flex-shrink-0 justify-between items-center min-h-6 px-2.5 py-2 border-0 border-t-1px border-solid border-main">
|
||||
<div>
|
||||
<Button className="py-1.35" label="Close" onClick={dismissModal} variant="normal" />
|
||||
</div>
|
||||
{selectedRevision && (
|
||||
<div className="flex items-center">
|
||||
{(selectedEntry as RevisionListEntry).uuid && (
|
||||
<Button className="py-1.35 mr-2.5" onClick={deleteSelectedRevision} variant="normal">
|
||||
{isDeletingRevision ? <div className="sk-spinner my-1 w-3 h-3 spinner-info" /> : 'Delete this revision'}
|
||||
</Button>
|
||||
)}
|
||||
<Button className="py-1.35 mr-2.5" label="Restore as a copy" onClick={restoreAsCopy} variant="normal" />
|
||||
<Button className="py-1.35" label="Restore version" onClick={restoreSelectedRevision} variant="primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(HistoryModalFooter)
|
||||
@@ -1,48 +1,21 @@
|
||||
import { Action, HistoryEntry, RevisionListEntry } from '@standardnotes/snjs'
|
||||
import { Dispatch, FunctionComponent, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Action } from '@standardnotes/snjs'
|
||||
import { FunctionComponent, useRef } from 'react'
|
||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||
import HistoryListItem from './HistoryListItem'
|
||||
import { LegacyHistoryEntry } from './utils'
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
|
||||
type Props = {
|
||||
legacyHistory: Action[] | undefined
|
||||
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>>
|
||||
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
|
||||
fetchAndSetLegacyRevision: (revisionListEntry: Action) => Promise<void>
|
||||
noteHistoryController: NoteHistoryController
|
||||
}
|
||||
|
||||
const LegacyHistoryList: FunctionComponent<Props> = ({
|
||||
legacyHistory,
|
||||
setSelectedRevision,
|
||||
setSelectedRemoteEntry,
|
||||
fetchAndSetLegacyRevision,
|
||||
}) => {
|
||||
const LegacyHistoryList: FunctionComponent<Props> = ({ legacyHistory, noteHistoryController }) => {
|
||||
const { selectLegacyRevision, selectedEntry } = noteHistoryController
|
||||
|
||||
const legacyHistoryListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useListKeyboardNavigation(legacyHistoryListRef)
|
||||
|
||||
const [selectedItemUrl, setSelectedItemUrl] = useState<string>()
|
||||
|
||||
const firstEntry = useMemo(() => {
|
||||
return legacyHistory?.[0]
|
||||
}, [legacyHistory])
|
||||
|
||||
const selectFirstEntry = useCallback(() => {
|
||||
if (firstEntry) {
|
||||
setSelectedItemUrl(firstEntry.subactions?.[0].url)
|
||||
setSelectedRevision(undefined)
|
||||
fetchAndSetLegacyRevision(firstEntry).catch(console.error)
|
||||
}
|
||||
}, [fetchAndSetLegacyRevision, firstEntry, setSelectedRevision])
|
||||
|
||||
useEffect(() => {
|
||||
if (firstEntry && !selectedItemUrl) {
|
||||
selectFirstEntry()
|
||||
} else if (!firstEntry) {
|
||||
setSelectedRevision(undefined)
|
||||
}
|
||||
}, [firstEntry, selectFirstEntry, selectedItemUrl, setSelectedRevision])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col w-full h-full focus:shadow-none ${
|
||||
@@ -51,16 +24,15 @@ const LegacyHistoryList: FunctionComponent<Props> = ({
|
||||
ref={legacyHistoryListRef}
|
||||
>
|
||||
{legacyHistory?.map((entry) => {
|
||||
const selectedEntryUrl = (selectedEntry as Action)?.subactions?.[0].url
|
||||
const url = entry.subactions?.[0].url
|
||||
|
||||
return (
|
||||
<HistoryListItem
|
||||
key={url}
|
||||
isSelected={selectedItemUrl === url}
|
||||
isSelected={selectedEntryUrl === url}
|
||||
onClick={() => {
|
||||
setSelectedItemUrl(url)
|
||||
setSelectedRemoteEntry(undefined)
|
||||
fetchAndSetLegacyRevision(entry).catch(console.error)
|
||||
selectLegacyRevision(entry)
|
||||
}}
|
||||
>
|
||||
{entry.label}
|
||||
|
||||
@@ -1,50 +1,26 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { RevisionListEntry } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Fragment, FunctionComponent, useMemo, useRef } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||
import HistoryListItem from './HistoryListItem'
|
||||
import { previewHistoryEntryTitle, RemoteRevisionListGroup } from './utils'
|
||||
import { previewHistoryEntryTitle } from './utils'
|
||||
import { FeaturesClientInterface, RevisionListEntry } from '@standardnotes/snjs/dist/@types'
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
|
||||
type RemoteHistoryListProps = {
|
||||
application: WebApplication
|
||||
remoteHistory: RemoteRevisionListGroup[] | undefined
|
||||
isFetchingRemoteHistory: boolean
|
||||
fetchAndSetRemoteRevision: (revisionListEntry: RevisionListEntry) => Promise<void>
|
||||
features: FeaturesClientInterface
|
||||
noteHistoryController: NoteHistoryController
|
||||
}
|
||||
|
||||
const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({
|
||||
application,
|
||||
remoteHistory,
|
||||
isFetchingRemoteHistory,
|
||||
fetchAndSetRemoteRevision,
|
||||
}) => {
|
||||
const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({ features, noteHistoryController }) => {
|
||||
const { remoteHistory, isFetchingRemoteHistory, selectRemoteRevision, selectedEntry } = noteHistoryController
|
||||
|
||||
const remoteHistoryListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useListKeyboardNavigation(remoteHistoryListRef)
|
||||
|
||||
const remoteHistoryLength = useMemo(() => remoteHistory?.map((group) => group.entries).flat().length, [remoteHistory])
|
||||
|
||||
const [selectedEntryUuid, setSelectedEntryUuid] = useState('')
|
||||
|
||||
const firstEntry = useMemo(() => {
|
||||
return remoteHistory?.find((group) => group.entries?.length)?.entries?.[0]
|
||||
}, [remoteHistory])
|
||||
|
||||
const selectFirstEntry = useCallback(() => {
|
||||
if (firstEntry) {
|
||||
setSelectedEntryUuid(firstEntry.uuid)
|
||||
fetchAndSetRemoteRevision(firstEntry).catch(console.error)
|
||||
}
|
||||
}, [fetchAndSetRemoteRevision, firstEntry])
|
||||
|
||||
useEffect(() => {
|
||||
if (firstEntry && !selectedEntryUuid.length) {
|
||||
selectFirstEntry()
|
||||
}
|
||||
}, [fetchAndSetRemoteRevision, firstEntry, remoteHistory, selectFirstEntry, selectedEntryUuid.length])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col w-full h-full focus:shadow-none ${
|
||||
@@ -63,15 +39,14 @@ const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({
|
||||
{group.entries.map((entry) => (
|
||||
<HistoryListItem
|
||||
key={entry.uuid}
|
||||
isSelected={selectedEntryUuid === entry.uuid}
|
||||
isSelected={(selectedEntry as RevisionListEntry)?.uuid === entry.uuid}
|
||||
onClick={() => {
|
||||
setSelectedEntryUuid(entry.uuid)
|
||||
fetchAndSetRemoteRevision(entry).catch(console.error)
|
||||
selectRemoteRevision(entry)
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between">
|
||||
<div>{previewHistoryEntryTitle(entry)}</div>
|
||||
{!application.features.hasMinimumRole(entry.required_role) && <Icon type="premium-feature" />}
|
||||
{!features.hasMinimumRole(entry.required_role) && <Icon type="premium-feature" />}
|
||||
</div>
|
||||
</HistoryListItem>
|
||||
))}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { HistoryLockedIllustration } from '@standardnotes/icons'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
|
||||
const getPlanHistoryDuration = (planName: string | undefined) => {
|
||||
switch (planName) {
|
||||
@@ -20,12 +20,11 @@ const getPremiumContentCopy = (planName: string | undefined) => {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
subscriptionController: SubscriptionController
|
||||
}
|
||||
|
||||
const RevisionContentLocked: FunctionComponent<Props> = ({ viewControllerManager }) => {
|
||||
const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } =
|
||||
viewControllerManager.subscriptionController
|
||||
const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionController }) => {
|
||||
const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = subscriptionController
|
||||
|
||||
return (
|
||||
<div className="flex w-full h-full items-center justify-center">
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import HistoryModalDialogContent from './HistoryModalDialogContent'
|
||||
import HistoryModalDialog from './HistoryModalDialog'
|
||||
import { RevisionHistoryModalProps } from './RevisionHistoryModalProps'
|
||||
|
||||
const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
application,
|
||||
historyModalController,
|
||||
notesController,
|
||||
selectionController,
|
||||
subscriptionController,
|
||||
}) => {
|
||||
if (!historyModalController.note) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<HistoryModalDialog onDismiss={historyModalController.dismissModal}>
|
||||
<HistoryModalDialogContent
|
||||
application={application}
|
||||
dismissModal={historyModalController.dismissModal}
|
||||
note={historyModalController.note}
|
||||
notesController={notesController}
|
||||
selectionController={selectionController}
|
||||
subscriptionController={subscriptionController}
|
||||
/>
|
||||
</HistoryModalDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(RevisionHistoryModal)
|
||||
@@ -0,0 +1,22 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
|
||||
import { SNNote } from '@standardnotes/snjs'
|
||||
|
||||
type CommonProps = {
|
||||
application: WebApplication
|
||||
notesController: NotesController
|
||||
selectionController: SelectedItemsController
|
||||
subscriptionController: SubscriptionController
|
||||
}
|
||||
|
||||
export type RevisionHistoryModalProps = CommonProps & {
|
||||
historyModalController: HistoryModalController
|
||||
}
|
||||
|
||||
export type RevisionHistoryModalContentProps = CommonProps & {
|
||||
note: SNNote
|
||||
dismissModal: () => void
|
||||
}
|
||||
@@ -1,315 +0,0 @@
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { getPlatformString } from '@/Utils'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import {
|
||||
ButtonType,
|
||||
ContentType,
|
||||
HistoryEntry,
|
||||
PayloadEmitSource,
|
||||
RevisionListEntry,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Button from '@/Components/Button/Button'
|
||||
import HistoryListContainer from './HistoryListContainer'
|
||||
import RevisionContentLocked from './RevisionContentLocked'
|
||||
import SelectedRevisionContent from './SelectedRevisionContent'
|
||||
import { LegacyHistoryEntry, RemoteRevisionListGroup, sortRevisionListIntoGroups } from './utils'
|
||||
|
||||
type RevisionHistoryModalProps = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
const ABSOLUTE_CENTER_CLASSNAME = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'
|
||||
|
||||
type RevisionContentPlaceholderProps = {
|
||||
isFetchingSelectedRevision: boolean
|
||||
selectedRevision: HistoryEntry | LegacyHistoryEntry | undefined
|
||||
showContentLockedScreen: boolean
|
||||
}
|
||||
|
||||
const RevisionContentPlaceholder: FunctionComponent<RevisionContentPlaceholderProps> = ({
|
||||
isFetchingSelectedRevision,
|
||||
selectedRevision,
|
||||
showContentLockedScreen,
|
||||
}) => (
|
||||
<div
|
||||
className={`absolute w-full h-full top-0 left-0 ${
|
||||
(isFetchingSelectedRevision || !selectedRevision) && !showContentLockedScreen
|
||||
? 'z-index-1 bg-default'
|
||||
: '-z-index-1'
|
||||
}`}
|
||||
>
|
||||
{isFetchingSelectedRevision && <div className={`sk-spinner w-5 h-5 spinner-info ${ABSOLUTE_CENTER_CLASSNAME}`} />}
|
||||
{!isFetchingSelectedRevision && !selectedRevision ? (
|
||||
<div className={`color-passive-0 select-none ${ABSOLUTE_CENTER_CLASSNAME}`}>No revision selected</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps> = observer(
|
||||
({ application, viewControllerManager }) => {
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const dismissModal = useCallback(() => {
|
||||
viewControllerManager.notesController.setShowRevisionHistoryModal(false)
|
||||
}, [viewControllerManager.notesController])
|
||||
|
||||
const note = viewControllerManager.notesController.firstSelectedNote
|
||||
const editorForCurrentNote = useMemo(() => {
|
||||
if (note) {
|
||||
return application.componentManager.editorForNote(note)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}, [application, note])
|
||||
|
||||
const [isFetchingSelectedRevision, setIsFetchingSelectedRevision] = useState(false)
|
||||
const [selectedRevision, setSelectedRevision] = useState<HistoryEntry | LegacyHistoryEntry>()
|
||||
const [selectedRemoteEntry, setSelectedRemoteEntry] = useState<RevisionListEntry>()
|
||||
const [isDeletingRevision, setIsDeletingRevision] = useState(false)
|
||||
const [templateNoteForRevision, setTemplateNoteForRevision] = useState<SNNote>()
|
||||
const [showContentLockedScreen, setShowContentLockedScreen] = useState(false)
|
||||
|
||||
const [remoteHistory, setRemoteHistory] = useState<RemoteRevisionListGroup[]>()
|
||||
const [isFetchingRemoteHistory, setIsFetchingRemoteHistory] = useState(false)
|
||||
|
||||
const fetchRemoteHistory = useCallback(async () => {
|
||||
if (note) {
|
||||
setRemoteHistory(undefined)
|
||||
setIsFetchingRemoteHistory(true)
|
||||
try {
|
||||
const initialRemoteHistory = await application.historyManager.remoteHistoryForItem(note)
|
||||
|
||||
const remoteHistoryAsGroups = sortRevisionListIntoGroups<RevisionListEntry>(initialRemoteHistory)
|
||||
|
||||
setRemoteHistory(remoteHistoryAsGroups)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsFetchingRemoteHistory(false)
|
||||
}
|
||||
}
|
||||
}, [application, note])
|
||||
|
||||
useEffect(() => {
|
||||
if (!remoteHistory?.length) {
|
||||
fetchRemoteHistory().catch(console.error)
|
||||
}
|
||||
}, [fetchRemoteHistory, remoteHistory?.length])
|
||||
|
||||
const restore = useCallback(() => {
|
||||
if (selectedRevision) {
|
||||
const originalNote = application.items.findItem(selectedRevision.payload.uuid) as SNNote
|
||||
|
||||
if (originalNote.locked) {
|
||||
application.alertService.alert(STRING_RESTORE_LOCKED_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
confirmDialog({
|
||||
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
application.mutator
|
||||
.changeAndSaveItem(
|
||||
originalNote,
|
||||
(mutator) => {
|
||||
mutator.setCustomContent(selectedRevision.payload.content)
|
||||
},
|
||||
true,
|
||||
PayloadEmitSource.RemoteRetrieved,
|
||||
)
|
||||
.catch(console.error)
|
||||
dismissModal()
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}, [application.alertService, application.items, application.mutator, dismissModal, selectedRevision])
|
||||
|
||||
const restoreAsCopy = useCallback(async () => {
|
||||
if (selectedRevision) {
|
||||
const originalNote = application.items.findSureItem<SNNote>(selectedRevision.payload.uuid)
|
||||
|
||||
const duplicatedItem = await application.mutator.duplicateItem(originalNote, {
|
||||
...selectedRevision.payload.content,
|
||||
title: selectedRevision.payload.content.title
|
||||
? selectedRevision.payload.content.title + ' (copy)'
|
||||
: undefined,
|
||||
})
|
||||
|
||||
viewControllerManager.selectionController.selectItem(duplicatedItem.uuid).catch(console.error)
|
||||
|
||||
dismissModal()
|
||||
}
|
||||
}, [
|
||||
viewControllerManager.selectionController,
|
||||
application.items,
|
||||
application.mutator,
|
||||
dismissModal,
|
||||
selectedRevision,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTemplateNote = async () => {
|
||||
if (selectedRevision) {
|
||||
const newTemplateNote = application.mutator.createTemplateItem(
|
||||
ContentType.Note,
|
||||
selectedRevision.payload.content,
|
||||
) as SNNote
|
||||
|
||||
setTemplateNoteForRevision(newTemplateNote)
|
||||
}
|
||||
}
|
||||
|
||||
fetchTemplateNote().catch(console.error)
|
||||
}, [application, selectedRevision])
|
||||
|
||||
const deleteSelectedRevision = useCallback(() => {
|
||||
if (!selectedRemoteEntry) {
|
||||
return
|
||||
}
|
||||
|
||||
application.alertService
|
||||
.confirm(
|
||||
'Are you sure you want to delete this revision?',
|
||||
'Delete revision?',
|
||||
'Delete revision',
|
||||
ButtonType.Danger,
|
||||
'Cancel',
|
||||
)
|
||||
.then((shouldDelete) => {
|
||||
if (shouldDelete && note) {
|
||||
setIsDeletingRevision(true)
|
||||
|
||||
application.historyManager
|
||||
.deleteRemoteRevision(note, selectedRemoteEntry)
|
||||
.then((res) => {
|
||||
if (res.error?.message) {
|
||||
throw new Error(res.error.message)
|
||||
}
|
||||
|
||||
fetchRemoteHistory().catch(console.error)
|
||||
setIsDeletingRevision(false)
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [application.alertService, application.historyManager, fetchRemoteHistory, note, selectedRemoteEntry])
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
className={`sn-component ${getPlatformString()}`}
|
||||
onDismiss={dismissModal}
|
||||
initialFocusRef={closeButtonRef}
|
||||
aria-label="Note revision history"
|
||||
>
|
||||
<DialogContent
|
||||
aria-label="Note revision history"
|
||||
className="rounded shadow-overlay"
|
||||
style={{
|
||||
width: '90%',
|
||||
maxWidth: '90%',
|
||||
minHeight: '90%',
|
||||
background: 'var(--modal-background-color)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`bg-default flex flex-col h-full overflow-hidden ${
|
||||
isDeletingRevision ? 'pointer-events-none cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-grow min-h-0">
|
||||
{note && (
|
||||
<HistoryListContainer
|
||||
application={application}
|
||||
note={note}
|
||||
remoteHistory={remoteHistory}
|
||||
isFetchingRemoteHistory={isFetchingRemoteHistory}
|
||||
setSelectedRevision={setSelectedRevision}
|
||||
setSelectedRemoteEntry={setSelectedRemoteEntry}
|
||||
setShowContentLockedScreen={setShowContentLockedScreen}
|
||||
setIsFetchingSelectedRevision={setIsFetchingSelectedRevision}
|
||||
/>
|
||||
)}
|
||||
<div className={'flex flex-col flex-grow relative'}>
|
||||
<RevisionContentPlaceholder
|
||||
selectedRevision={selectedRevision}
|
||||
isFetchingSelectedRevision={isFetchingSelectedRevision}
|
||||
showContentLockedScreen={showContentLockedScreen}
|
||||
/>
|
||||
{showContentLockedScreen && !selectedRevision && (
|
||||
<RevisionContentLocked viewControllerManager={viewControllerManager} />
|
||||
)}
|
||||
{selectedRevision && templateNoteForRevision && (
|
||||
<SelectedRevisionContent
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
selectedRevision={selectedRevision}
|
||||
editorForCurrentNote={editorForCurrentNote}
|
||||
templateNoteForRevision={templateNoteForRevision}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 justify-between items-center min-h-6 px-2.5 py-2 border-0 border-t-1px border-solid border-main">
|
||||
<div>
|
||||
<Button
|
||||
className="py-1.35"
|
||||
label="Close"
|
||||
onClick={dismissModal}
|
||||
ref={closeButtonRef}
|
||||
variant="normal"
|
||||
/>
|
||||
</div>
|
||||
{selectedRevision && (
|
||||
<div className="flex items-center">
|
||||
{selectedRemoteEntry && (
|
||||
<Button className="py-1.35 mr-2.5" onClick={deleteSelectedRevision} variant="normal">
|
||||
{isDeletingRevision ? (
|
||||
<div className="sk-spinner my-1 w-3 h-3 spinner-info" />
|
||||
) : (
|
||||
'Delete this revision'
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="py-1.35 mr-2.5"
|
||||
label="Restore as a copy"
|
||||
onClick={restoreAsCopy}
|
||||
variant="normal"
|
||||
/>
|
||||
<Button className="py-1.35" label="Restore version" onClick={restore} variant="primary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
RevisionHistoryModal.displayName = 'RevisionHistoryModal'
|
||||
|
||||
const RevisionHistoryModalWrapper: FunctionComponent<RevisionHistoryModalProps> = ({
|
||||
application,
|
||||
viewControllerManager,
|
||||
}) => {
|
||||
if (!viewControllerManager.notesController.showRevisionHistoryModal) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <RevisionHistoryModal application={application} viewControllerManager={viewControllerManager} />
|
||||
}
|
||||
|
||||
export default observer(RevisionHistoryModalWrapper)
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum RevisionType {
|
||||
Session = 'Session',
|
||||
Remote = 'Remote',
|
||||
Legacy = 'Legacy',
|
||||
}
|
||||
@@ -1,39 +1,45 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { HistoryEntry, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||
import { ContentType, SNNote } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useEffect, useMemo } from 'react'
|
||||
import ComponentView from '@/Components/ComponentView/ComponentView'
|
||||
import { LegacyHistoryEntry } from './utils'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
|
||||
const ABSOLUTE_CENTER_CLASSNAME = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'
|
||||
|
||||
type SelectedRevisionContentProps = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
selectedRevision: HistoryEntry | LegacyHistoryEntry
|
||||
editorForCurrentNote: SNComponent | undefined
|
||||
templateNoteForRevision: SNNote
|
||||
noteHistoryController: NoteHistoryController
|
||||
notesController: NotesController
|
||||
}
|
||||
|
||||
const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> = ({
|
||||
application,
|
||||
viewControllerManager,
|
||||
selectedRevision,
|
||||
editorForCurrentNote,
|
||||
templateNoteForRevision,
|
||||
noteHistoryController,
|
||||
notesController,
|
||||
}) => {
|
||||
const note = notesController.firstSelectedNote
|
||||
const { selectedRevision } = noteHistoryController
|
||||
|
||||
const componentViewer = useMemo(() => {
|
||||
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
|
||||
|
||||
if (!editorForCurrentNote) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const templateNoteForRevision = application.mutator.createTemplateItem(
|
||||
ContentType.Note,
|
||||
selectedRevision?.payload.content,
|
||||
) as SNNote
|
||||
|
||||
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
|
||||
componentViewer.setReadonly(true)
|
||||
componentViewer.lockReadonly = true
|
||||
componentViewer.overrideContextItem = templateNoteForRevision
|
||||
return componentViewer
|
||||
}, [application, editorForCurrentNote, templateNoteForRevision])
|
||||
}, [application.componentManager, application.mutator, note, selectedRevision?.payload.content])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -46,17 +52,16 @@ const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> =
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
<div className="p-4 text-base font-bold w-full">
|
||||
<div className="title">{selectedRevision.payload.content.title}</div>
|
||||
<div className="title">{selectedRevision?.payload.content.title}</div>
|
||||
</div>
|
||||
{!componentViewer && (
|
||||
<div className="relative flex-grow min-h-0 overflow-hidden">
|
||||
{selectedRevision.payload.content.text.length ? (
|
||||
{selectedRevision?.payload.content.text.length ? (
|
||||
<textarea
|
||||
readOnly={true}
|
||||
className="w-full h-full resize-none p-4 pt-0 border-0 bg-default color-text text-editor font-editor"
|
||||
>
|
||||
{selectedRevision.payload.content.text}
|
||||
</textarea>
|
||||
value={selectedRevision?.payload.content.text}
|
||||
/>
|
||||
) : (
|
||||
<div className={`color-passive-0 ${ABSOLUTE_CENTER_CLASSNAME}`}>Empty note.</div>
|
||||
)}
|
||||
@@ -64,12 +69,7 @@ const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> =
|
||||
)}
|
||||
{componentViewer && (
|
||||
<div className="component-view">
|
||||
<ComponentView
|
||||
key={componentViewer.identifier}
|
||||
componentViewer={componentViewer}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
/>
|
||||
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,60 +1,25 @@
|
||||
import { HistoryEntry, NoteHistoryEntry, RevisionListEntry } from '@standardnotes/snjs'
|
||||
import {
|
||||
Dispatch,
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { Fragment, FunctionComponent, useMemo, useRef } from 'react'
|
||||
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
|
||||
import HistoryListItem from './HistoryListItem'
|
||||
import { LegacyHistoryEntry, ListGroup } from './utils'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
|
||||
|
||||
type Props = {
|
||||
sessionHistory: ListGroup<NoteHistoryEntry>[]
|
||||
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>>
|
||||
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
|
||||
noteHistoryController: NoteHistoryController
|
||||
}
|
||||
|
||||
const SessionHistoryList: FunctionComponent<Props> = ({
|
||||
sessionHistory,
|
||||
setSelectedRevision,
|
||||
setSelectedRemoteEntry,
|
||||
}) => {
|
||||
const SessionHistoryList: FunctionComponent<Props> = ({ noteHistoryController }) => {
|
||||
const { sessionHistory, selectedRevision, selectSessionRevision } = noteHistoryController
|
||||
|
||||
const sessionHistoryListRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useListKeyboardNavigation(sessionHistoryListRef)
|
||||
|
||||
const sessionHistoryLength = useMemo(
|
||||
() => sessionHistory.map((group) => group.entries).flat().length,
|
||||
() => sessionHistory?.map((group) => group.entries).flat().length,
|
||||
[sessionHistory],
|
||||
)
|
||||
|
||||
const [selectedItemCreatedAt, setSelectedItemCreatedAt] = useState<Date>()
|
||||
|
||||
const firstEntry = useMemo(() => {
|
||||
return sessionHistory?.find((group) => group.entries?.length)?.entries?.[0]
|
||||
}, [sessionHistory])
|
||||
|
||||
const selectFirstEntry = useCallback(() => {
|
||||
if (firstEntry) {
|
||||
setSelectedItemCreatedAt(firstEntry.payload.created_at)
|
||||
setSelectedRevision(firstEntry)
|
||||
}
|
||||
}, [firstEntry, setSelectedRevision])
|
||||
|
||||
useEffect(() => {
|
||||
if (firstEntry && !selectedItemCreatedAt) {
|
||||
selectFirstEntry()
|
||||
} else if (!firstEntry) {
|
||||
setSelectedRevision(undefined)
|
||||
}
|
||||
}, [firstEntry, selectFirstEntry, selectedItemCreatedAt, setSelectedRevision])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col w-full h-full focus:shadow-none ${
|
||||
@@ -72,11 +37,9 @@ const SessionHistoryList: FunctionComponent<Props> = ({
|
||||
{group.entries.map((entry, index) => (
|
||||
<HistoryListItem
|
||||
key={index}
|
||||
isSelected={selectedItemCreatedAt === entry.payload.created_at}
|
||||
isSelected={selectedRevision?.payload.created_at === entry.payload.created_at}
|
||||
onClick={() => {
|
||||
setSelectedItemCreatedAt(entry.payload.created_at)
|
||||
setSelectedRevision(entry)
|
||||
setSelectedRemoteEntry(undefined)
|
||||
selectSessionRevision(entry)
|
||||
}}
|
||||
>
|
||||
{entry.previewTitle()}
|
||||
@@ -93,4 +56,4 @@ const SessionHistoryList: FunctionComponent<Props> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionHistoryList
|
||||
export default observer(SessionHistoryList)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import Bubble from '@/Components/Bubble/Bubble'
|
||||
import { useCallback } from 'react'
|
||||
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
||||
|
||||
type Props = {
|
||||
viewControllerManager: ViewControllerManager
|
||||
application: WebApplication
|
||||
searchOptions: SearchOptionsController
|
||||
}
|
||||
|
||||
const SearchOptions = ({ viewControllerManager }: Props) => {
|
||||
const { searchOptionsController: searchOptions } = viewControllerManager
|
||||
|
||||
const SearchOptions = ({ searchOptions }: Props) => {
|
||||
const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions
|
||||
|
||||
const toggleIncludeProtectedContents = useCallback(async () => {
|
||||
|
||||
@@ -5,4 +5,5 @@ export const ElementIds = {
|
||||
FileTextPreview: 'file-text-preview',
|
||||
EditorContent: 'editor-content',
|
||||
EditorColumn: 'editor-column',
|
||||
ContentList: 'notes-scrollable',
|
||||
}
|
||||
|
||||
@@ -215,6 +215,10 @@ export class ItemListController extends AbstractViewController implements Intern
|
||||
}
|
||||
}
|
||||
|
||||
public get listLength() {
|
||||
return this.renderedItems.length
|
||||
}
|
||||
|
||||
public getActiveItemController(): NoteViewController | FileViewController | undefined {
|
||||
return this.application.itemControllerGroup.activeItemViewController
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { InternalEventBus, SNNote } from '@standardnotes/snjs'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||
|
||||
export class HistoryModalController extends AbstractViewController {
|
||||
note?: SNNote = undefined
|
||||
|
||||
override deinit(): void {
|
||||
super.deinit()
|
||||
this.note = undefined
|
||||
}
|
||||
|
||||
constructor(application: WebApplication, eventBus: InternalEventBus) {
|
||||
super(application, eventBus)
|
||||
|
||||
makeObservable(this, {
|
||||
note: observable,
|
||||
setNote: action,
|
||||
})
|
||||
}
|
||||
|
||||
setNote = (note: SNNote | undefined) => {
|
||||
this.note = note
|
||||
}
|
||||
|
||||
openModal = (note: SNNote | undefined) => {
|
||||
this.setNote(note)
|
||||
}
|
||||
|
||||
dismissModal = () => {
|
||||
this.setNote(undefined)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { RevisionType } from '@/Components/RevisionHistoryModal/RevisionType'
|
||||
import {
|
||||
LegacyHistoryEntry,
|
||||
ListGroup,
|
||||
RemoteRevisionListGroup,
|
||||
sortRevisionListIntoGroups,
|
||||
} from '@/Components/RevisionHistoryModal/utils'
|
||||
import { STRING_RESTORE_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import {
|
||||
Action,
|
||||
ActionVerb,
|
||||
ButtonType,
|
||||
HistoryEntry,
|
||||
NoteHistoryEntry,
|
||||
PayloadEmitSource,
|
||||
RevisionListEntry,
|
||||
SNNote,
|
||||
} from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action } from 'mobx'
|
||||
import { SelectedItemsController } from '../SelectedItemsController'
|
||||
|
||||
type RemoteHistory = RemoteRevisionListGroup[]
|
||||
|
||||
type SessionHistory = ListGroup<NoteHistoryEntry>[]
|
||||
|
||||
type LegacyHistory = Action[]
|
||||
|
||||
type SelectedRevision = HistoryEntry | LegacyHistoryEntry | undefined
|
||||
|
||||
type SelectedEntry = RevisionListEntry | NoteHistoryEntry | Action | undefined
|
||||
|
||||
export enum RevisionContentState {
|
||||
Idle,
|
||||
Loading,
|
||||
Loaded,
|
||||
NotEntitled,
|
||||
}
|
||||
|
||||
export class NoteHistoryController {
|
||||
remoteHistory: RemoteHistory = []
|
||||
isFetchingRemoteHistory = false
|
||||
sessionHistory: SessionHistory = []
|
||||
legacyHistory: LegacyHistory = []
|
||||
|
||||
selectedRevision: SelectedRevision = undefined
|
||||
selectedEntry: SelectedEntry = undefined
|
||||
|
||||
contentState = RevisionContentState.Idle
|
||||
|
||||
currentTab = RevisionType.Remote
|
||||
|
||||
constructor(
|
||||
private application: WebApplication,
|
||||
private note: SNNote,
|
||||
private selectionController: SelectedItemsController,
|
||||
) {
|
||||
void this.fetchAllHistory()
|
||||
|
||||
makeObservable(this, {
|
||||
selectedRevision: observable,
|
||||
setSelectedRevision: action,
|
||||
|
||||
selectedEntry: observable,
|
||||
setSelectedEntry: action,
|
||||
|
||||
remoteHistory: observable,
|
||||
setRemoteHistory: action,
|
||||
isFetchingRemoteHistory: observable,
|
||||
setIsFetchingRemoteHistory: action,
|
||||
|
||||
sessionHistory: observable,
|
||||
setSessionHistory: action,
|
||||
|
||||
legacyHistory: observable,
|
||||
setLegacyHistory: action,
|
||||
|
||||
resetHistoryState: action,
|
||||
|
||||
currentTab: observable,
|
||||
selectTab: action,
|
||||
|
||||
contentState: observable,
|
||||
setContentState: action,
|
||||
})
|
||||
}
|
||||
|
||||
setSelectedRevision = (revision: SelectedRevision) => {
|
||||
this.selectedRevision = revision
|
||||
}
|
||||
|
||||
setSelectedEntry = (entry: SelectedEntry) => {
|
||||
this.selectedEntry = entry
|
||||
}
|
||||
|
||||
clearSelection = () => {
|
||||
this.setSelectedEntry(undefined)
|
||||
this.setSelectedRevision(undefined)
|
||||
}
|
||||
|
||||
selectTab = (tab: RevisionType) => {
|
||||
this.currentTab = tab
|
||||
this.clearSelection()
|
||||
this.setContentState(RevisionContentState.Idle)
|
||||
this.selectFirstRevision()
|
||||
}
|
||||
|
||||
setIsFetchingRemoteHistory = (value: boolean) => {
|
||||
this.isFetchingRemoteHistory = value
|
||||
}
|
||||
|
||||
setContentState = (contentState: RevisionContentState) => {
|
||||
this.contentState = contentState
|
||||
}
|
||||
|
||||
selectRemoteRevision = async (entry: RevisionListEntry) => {
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.application.features.hasMinimumRole(entry.required_role)) {
|
||||
this.setContentState(RevisionContentState.NotEntitled)
|
||||
this.setSelectedRevision(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
this.setContentState(RevisionContentState.Loading)
|
||||
this.clearSelection()
|
||||
|
||||
try {
|
||||
this.setSelectedEntry(entry)
|
||||
const remoteRevision = await this.application.historyManager.fetchRemoteRevision(this.note, entry)
|
||||
this.setSelectedRevision(remoteRevision)
|
||||
} catch (err) {
|
||||
this.clearSelection()
|
||||
console.error(err)
|
||||
} finally {
|
||||
this.setContentState(RevisionContentState.Loaded)
|
||||
}
|
||||
}
|
||||
|
||||
selectLegacyRevision = async (entry: Action) => {
|
||||
this.clearSelection()
|
||||
this.setContentState(RevisionContentState.Loading)
|
||||
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!entry.subactions?.[0]) {
|
||||
throw new Error('Could not find revision action url')
|
||||
}
|
||||
|
||||
this.setSelectedEntry(entry)
|
||||
|
||||
const response = await this.application.actionsManager.runAction(entry.subactions[0], this.note)
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Could not fetch revision')
|
||||
}
|
||||
|
||||
this.setSelectedRevision(response.item as unknown as HistoryEntry)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setSelectedRevision(undefined)
|
||||
} finally {
|
||||
this.setContentState(RevisionContentState.Loaded)
|
||||
}
|
||||
}
|
||||
|
||||
selectSessionRevision = (entry: NoteHistoryEntry) => {
|
||||
this.clearSelection()
|
||||
this.setSelectedEntry(entry)
|
||||
this.setSelectedRevision(entry)
|
||||
}
|
||||
|
||||
private get flattenedRemoteHistory() {
|
||||
return this.remoteHistory.map((group) => group.entries).flat()
|
||||
}
|
||||
|
||||
private get flattenedSessionHistory() {
|
||||
return this.sessionHistory.map((group) => group.entries).flat()
|
||||
}
|
||||
|
||||
selectFirstRevision = () => {
|
||||
switch (this.currentTab) {
|
||||
case RevisionType.Remote: {
|
||||
const firstEntry = this.flattenedRemoteHistory[0]
|
||||
if (firstEntry) {
|
||||
void this.selectRemoteRevision(firstEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
case RevisionType.Session: {
|
||||
const firstEntry = this.flattenedSessionHistory[0]
|
||||
if (firstEntry) {
|
||||
void this.selectSessionRevision(firstEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
case RevisionType.Legacy: {
|
||||
const firstEntry = this.legacyHistory[0]
|
||||
if (firstEntry) {
|
||||
void this.selectLegacyRevision(firstEntry)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectPrevOrNextRemoteRevision = (revisionEntry: RevisionListEntry) => {
|
||||
const currentIndex = this.flattenedRemoteHistory.findIndex((entry) => entry?.uuid === revisionEntry.uuid)
|
||||
|
||||
const previousEntry = this.flattenedRemoteHistory[currentIndex - 1]
|
||||
const nextEntry = this.flattenedRemoteHistory[currentIndex + 1]
|
||||
|
||||
if (previousEntry) {
|
||||
void this.selectRemoteRevision(previousEntry)
|
||||
} else if (nextEntry) {
|
||||
void this.selectRemoteRevision(nextEntry)
|
||||
}
|
||||
}
|
||||
|
||||
setRemoteHistory = (remoteHistory: RemoteHistory) => {
|
||||
this.remoteHistory = remoteHistory
|
||||
}
|
||||
|
||||
fetchRemoteHistory = async () => {
|
||||
this.setRemoteHistory([])
|
||||
|
||||
if (this.note) {
|
||||
this.setIsFetchingRemoteHistory(true)
|
||||
try {
|
||||
const initialRemoteHistory = await this.application.historyManager.remoteHistoryForItem(this.note)
|
||||
|
||||
this.setRemoteHistory(sortRevisionListIntoGroups<RevisionListEntry>(initialRemoteHistory))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
} finally {
|
||||
this.setIsFetchingRemoteHistory(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLegacyHistory = (legacyHistory: LegacyHistory) => {
|
||||
this.legacyHistory = legacyHistory
|
||||
}
|
||||
|
||||
fetchLegacyHistory = async () => {
|
||||
const actionExtensions = this.application.actionsManager.getExtensions()
|
||||
|
||||
actionExtensions.forEach(async (ext) => {
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
const actionExtension = await this.application.actionsManager.loadExtensionInContextOfItem(ext, this.note)
|
||||
|
||||
if (!actionExtension) {
|
||||
return
|
||||
}
|
||||
|
||||
const isLegacyNoteHistoryExt = actionExtension?.actions.some((action) => action.verb === ActionVerb.Nested)
|
||||
|
||||
if (!isLegacyNoteHistoryExt) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setLegacyHistory(actionExtension.actions.filter((action) => action.subactions?.[0]))
|
||||
})
|
||||
}
|
||||
|
||||
setSessionHistory = (sessionHistory: SessionHistory) => {
|
||||
this.sessionHistory = sessionHistory
|
||||
}
|
||||
|
||||
fetchAllHistory = async () => {
|
||||
this.resetHistoryState()
|
||||
|
||||
if (!this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setSessionHistory(
|
||||
sortRevisionListIntoGroups<NoteHistoryEntry>(
|
||||
this.application.historyManager.sessionHistoryForItem(this.note) as NoteHistoryEntry[],
|
||||
),
|
||||
)
|
||||
await this.fetchRemoteHistory()
|
||||
await this.fetchLegacyHistory()
|
||||
|
||||
this.selectFirstRevision()
|
||||
}
|
||||
|
||||
resetHistoryState = () => {
|
||||
this.remoteHistory = []
|
||||
this.sessionHistory = []
|
||||
this.legacyHistory = []
|
||||
}
|
||||
|
||||
restoreRevision = async (revision: NonNullable<SelectedRevision>) => {
|
||||
const originalNote = this.application.items.findItem<SNNote>(revision.payload.uuid)
|
||||
|
||||
if (originalNote?.locked) {
|
||||
this.application.alertService.alert(STRING_RESTORE_LOCKED_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
const didConfirm = await confirmDialog({
|
||||
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
|
||||
if (!originalNote) {
|
||||
throw new Error('Original note not found.')
|
||||
}
|
||||
|
||||
if (didConfirm) {
|
||||
void this.application.mutator.changeAndSaveItem(
|
||||
originalNote,
|
||||
(mutator) => {
|
||||
mutator.setCustomContent(revision.payload.content)
|
||||
},
|
||||
true,
|
||||
PayloadEmitSource.RemoteRetrieved,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
restoreRevisionAsCopy = async (revision: NonNullable<SelectedRevision>) => {
|
||||
const originalNote = this.application.items.findSureItem<SNNote>(revision.payload.uuid)
|
||||
|
||||
const duplicatedItem = await this.application.mutator.duplicateItem(originalNote, {
|
||||
...revision.payload.content,
|
||||
title: revision.payload.content.title ? revision.payload.content.title + ' (copy)' : undefined,
|
||||
})
|
||||
|
||||
this.selectionController.selectItem(duplicatedItem.uuid).catch(console.error)
|
||||
}
|
||||
|
||||
deleteRemoteRevision = async (revisionEntry: RevisionListEntry) => {
|
||||
const shouldDelete = await this.application.alertService.confirm(
|
||||
'Are you sure you want to delete this revision?',
|
||||
'Delete revision?',
|
||||
'Delete revision',
|
||||
ButtonType.Danger,
|
||||
'Cancel',
|
||||
)
|
||||
|
||||
if (!shouldDelete || !this.note) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await this.application.historyManager.deleteRemoteRevision(this.note, revisionEntry)
|
||||
|
||||
if (response.error?.message) {
|
||||
throw new Error(response.error.message)
|
||||
}
|
||||
|
||||
this.clearSelection()
|
||||
|
||||
this.selectPrevOrNextRemoteRevision(revisionEntry)
|
||||
|
||||
await this.fetchRemoteHistory()
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ export class NotesController extends AbstractViewController {
|
||||
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 }
|
||||
contextMenuMaxHeight: number | 'auto' = 'auto'
|
||||
showProtectedWarning = false
|
||||
showRevisionHistoryModal = false
|
||||
private itemListController!: ItemListController
|
||||
|
||||
override deinit() {
|
||||
@@ -48,7 +47,6 @@ export class NotesController extends AbstractViewController {
|
||||
contextMenuOpen: observable,
|
||||
contextMenuPosition: observable,
|
||||
showProtectedWarning: observable,
|
||||
showRevisionHistoryModal: observable,
|
||||
|
||||
selectedNotes: computed,
|
||||
firstSelectedNote: computed,
|
||||
@@ -60,7 +58,6 @@ export class NotesController extends AbstractViewController {
|
||||
setContextMenuPosition: action,
|
||||
setContextMenuMaxHeight: action,
|
||||
setShowProtectedWarning: action,
|
||||
setShowRevisionHistoryModal: action,
|
||||
unselectNotes: action,
|
||||
})
|
||||
}
|
||||
@@ -367,8 +364,4 @@ export class NotesController extends AbstractViewController {
|
||||
private getSelectedNotesList(): SNNote[] {
|
||||
return Object.values(this.selectedNotes)
|
||||
}
|
||||
|
||||
setShowRevisionHistoryModal(show: boolean): void {
|
||||
this.showRevisionHistoryModal = show
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,11 +105,19 @@ export class SelectedItemsController extends AbstractViewController {
|
||||
this.selectedItems[item.uuid] = item
|
||||
}
|
||||
|
||||
private selectItemsRange = async (selectedItem: ListableContentItem): Promise<void> => {
|
||||
private selectItemsRange = async ({
|
||||
selectedItem,
|
||||
startingIndex,
|
||||
endingIndex,
|
||||
}: {
|
||||
selectedItem?: ListableContentItem
|
||||
startingIndex?: number
|
||||
endingIndex?: number
|
||||
}): Promise<void> => {
|
||||
const items = this.itemListController.renderedItems
|
||||
|
||||
const lastSelectedItemIndex = items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid)
|
||||
const selectedItemIndex = items.findIndex((item) => item.uuid == selectedItem.uuid)
|
||||
const lastSelectedItemIndex = startingIndex ?? items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid)
|
||||
const selectedItemIndex = endingIndex ?? items.findIndex((item) => item.uuid == selectedItem?.uuid)
|
||||
|
||||
let itemsToSelect = []
|
||||
if (selectedItemIndex > lastSelectedItemIndex) {
|
||||
@@ -151,6 +159,13 @@ export class SelectedItemsController extends AbstractViewController {
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
|
||||
selectAll = () => {
|
||||
void this.selectItemsRange({
|
||||
startingIndex: 0,
|
||||
endingIndex: this.itemListController.listLength - 1,
|
||||
})
|
||||
}
|
||||
|
||||
private deselectAll = (): void => {
|
||||
this.setSelectedItems({})
|
||||
|
||||
@@ -184,7 +199,7 @@ export class SelectedItemsController extends AbstractViewController {
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
} else if (userTriggered && hasShift) {
|
||||
await this.selectItemsRange(item)
|
||||
await this.selectItemsRange({ selectedItem: item })
|
||||
} else {
|
||||
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid]
|
||||
if (shouldSelectNote && isAuthorizedForAccess) {
|
||||
|
||||
@@ -20,7 +20,7 @@ function zippableFileName(name: string, suffix = '', format = 'txt'): string {
|
||||
}
|
||||
|
||||
type ZippableData = {
|
||||
filename: string
|
||||
name: string
|
||||
content: Blob
|
||||
}[]
|
||||
|
||||
@@ -119,9 +119,21 @@ export class ArchiveManager {
|
||||
const zip = await import('@zip.js/zip.js')
|
||||
const writer = new zip.ZipWriter(new zip.BlobWriter('application/zip'))
|
||||
|
||||
const filenameCounts: Record<string, number> = {}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const { name, ext } = parseFileName(data[i].filename)
|
||||
await writer.add(zippableFileName(name, '', ext), new zip.BlobReader(data[i].content))
|
||||
const file = data[i]
|
||||
|
||||
const { name, ext } = parseFileName(file.name)
|
||||
|
||||
filenameCounts[file.name] = filenameCounts[file.name] == undefined ? 0 : filenameCounts[file.name] + 1
|
||||
|
||||
const currentFileNameIndex = filenameCounts[file.name]
|
||||
|
||||
await writer.add(
|
||||
zippableFileName(name, currentFileNameIndex > 0 ? ` - ${currentFileNameIndex}` : '', ext),
|
||||
new zip.BlobReader(file.content),
|
||||
)
|
||||
}
|
||||
|
||||
const zipFileAsBlob = await writer.close()
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SyncStatusController } from '../Controllers/SyncStatusController'
|
||||
import { NavigationController } from '../Controllers/Navigation/NavigationController'
|
||||
import { FilePreviewModalController } from '../Controllers/FilePreviewModalController'
|
||||
import { SelectedItemsController } from '../Controllers/SelectedItemsController'
|
||||
import { HistoryModalController } from '../Controllers/NoteHistory/HistoryModalController'
|
||||
|
||||
export class ViewControllerManager {
|
||||
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
||||
@@ -45,6 +46,7 @@ export class ViewControllerManager {
|
||||
readonly syncStatusController = new SyncStatusController()
|
||||
readonly navigationController: NavigationController
|
||||
readonly selectionController: SelectedItemsController
|
||||
readonly historyModalController: HistoryModalController
|
||||
|
||||
public isSessionsModalVisible = false
|
||||
|
||||
@@ -101,6 +103,8 @@ export class ViewControllerManager {
|
||||
this.eventBus,
|
||||
)
|
||||
|
||||
this.historyModalController = new HistoryModalController(this.application, this.eventBus)
|
||||
|
||||
this.addAppEventObserver()
|
||||
|
||||
if (this.device.appVersion.includes('-beta')) {
|
||||
@@ -175,6 +179,9 @@ export class ViewControllerManager {
|
||||
this.navigationController.deinit()
|
||||
;(this.navigationController as unknown) = undefined
|
||||
|
||||
this.historyModalController.deinit()
|
||||
;(this.historyModalController as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user