This commit is contained in:
StandardNotes CI
2022-06-20 17:05:18 +00:00
102 changed files with 1382 additions and 910 deletions

View File

@@ -20,7 +20,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: yarn install - run: yarn install
@@ -54,7 +54,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Set up Ruby - name: Set up Ruby
@@ -127,7 +127,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: yarn install - run: yarn install
@@ -155,7 +155,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
@@ -178,12 +178,13 @@ jobs:
token: ${{ secrets.CI_PAT_TOKEN }} token: ${{ secrets.CI_PAT_TOKEN }}
tag_name: "@standardnotes/desktop@${{ steps.package-version.outputs.current-version}}" tag_name: "@standardnotes/desktop@${{ steps.package-version.outputs.current-version}}"
prerelease: true prerelease: true
draft: true draft: ${{ inputs.channel == 'beta' && true || false }}
name: "Desktop ${{ inputs.channel == 'beta' && 'Beta' || '' }} ${{ steps.package-version.outputs.current-version }}" name: "Desktop ${{ inputs.channel == 'beta' && 'Beta' || '' }} ${{ steps.package-version.outputs.current-version }}"
files: packages/desktop/dist/* files: packages/desktop/dist/*
- name: Publish Snap - name: Publish Snap
continue-on-error: true continue-on-error: true
run: | run: |
sudo snap install snapcraft --classic
echo "${{ secrets.SNAPCRAFT_LOGIN_FILE }}" >> snapauth.txt echo "${{ secrets.SNAPCRAFT_LOGIN_FILE }}" >> snapauth.txt
snapcraft login --with=snapauth.txt snapcraft login --with=snapauth.txt
snapcraft upload dist/standard-notes-${{ steps.package-version.outputs.current-version}}-linux-amd64.snap snapcraft upload dist/standard-notes-${{ steps.package-version.outputs.current-version}}-linux-amd64.snap

View File

@@ -15,7 +15,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- run: yarn install - run: yarn install
@@ -42,7 +42,7 @@ jobs:
working-directory: packages/desktop working-directory: packages/desktop
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v3
with: with:
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3

View File

@@ -43,11 +43,22 @@ jobs:
with: with:
lane: 'android dev' lane: 'android dev'
subdirectory: 'packages/mobile' 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: with:
name: dev.apk path: packages/mobile
path: android/app/build/outputs/apk/dev/release/app-dev-release.apk - 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: ios:
defaults: defaults:
run: run:

View File

@@ -44,16 +44,24 @@ jobs:
with: with:
lane: 'android prod' lane: 'android prod'
subdirectory: 'packages/mobile' 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: with:
name: prod.apk path: packages/mobile
path: android/app/build/outputs/apk/prod/release/app-prod-release.apk - name: Release
- name: Upload Android App Bundle to artifacts uses: softprops/action-gh-release@v1
uses: actions/upload-artifact@v2
with: with:
name: release.aab token: ${{ secrets.CI_PAT_TOKEN }}
path: android/app/build/outputs/bundle/prodRelease/app-prod-release.aab 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: ios:
defaults: defaults:
run: run:

View File

@@ -6,7 +6,8 @@ on:
- develop - develop
- main - main
paths: paths:
- packages/!(components)/** - '**/**'
- '!packages/components/**'
jobs: jobs:
test: test:

37
.github/workflows/promote.develop.yml vendored Normal file
View 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

View File

@@ -8,6 +8,8 @@ jobs:
Build: Build:
if: contains(github.event.head_commit.message, 'chore(release)') == false if: contains(github.event.head_commit.message, 'chore(release)') == false
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -15,6 +17,10 @@ jobs:
token: ${{ secrets.CI_PAT_TOKEN }} token: ${{ secrets.CI_PAT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
- name: Setup git config - name: Setup git config
run: | run: |
git config --global user.name "standardci" git config --global user.name "standardci"
@@ -36,7 +42,9 @@ jobs:
continue-on-error: true continue-on-error: true
id: graduateRelease id: graduateRelease
if: ${{ github.ref == 'refs/heads/main' }} if: ${{ github.ref == 'refs/heads/main' }}
run: yarn release:prod:graduate run: |
yarn release:prod:graduate
yarn publish:prod
- name: Bump Prod Version Fallback - name: Bump Prod Version Fallback
if: ${{ always() && github.ref == 'refs/heads/main' && steps.graduateRelease.outcome == 'failure' }} 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 echo Falling back to non-graduate release due to https://github.com/lerna/lerna/issues/2532
git stash git stash
yarn release:prod yarn release:prod
yarn publish:prod
- name: Bump Beta Version - name: Bump Beta Version
if: ${{ github.ref == 'refs/heads/develop' }} 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -28,6 +28,8 @@
"release:prod": "lerna version --conventional-commits --yes -m \"chore(release): publish\"", "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: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\"", "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", "version": "yarn install --no-immutable && git add yarn.lock",
"postversion": "./scripts/push-tags-one-by-one.sh", "postversion": "./scripts/push-tags-one-by-one.sh",
"lerna:list": " yarn lerna list -all", "lerna:list": " yarn lerna list -all",

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @standardnotes/desktop

View File

@@ -1,7 +1,7 @@
{ {
"name": "@standardnotes/desktop", "name": "@standardnotes/desktop",
"main": "./app/dist/index.js", "main": "./app/dist/index.js",
"version": "3.22.11", "version": "3.22.12-alpha.3",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"author": "Standard Notes.", "author": "Standard Notes.",
"private": true, "private": true,
@@ -123,7 +123,7 @@
"linux": { "linux": {
"category": "Office", "category": "Office",
"icon": "build/icon/", "icon": "build/icon/",
"executableName": "Standard Notes", "executableName": "standard-notes",
"desktop": { "desktop": {
"StartupWMClass": "standard notes" "StartupWMClass": "standard notes"
}, },

1
packages/releases/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View 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))

View 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')

View 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": "*"
}
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @standardnotes/web-server

View File

@@ -1,6 +1,6 @@
{ {
"name": "@standardnotes/web-server", "name": "@standardnotes/web-server",
"version": "1.2.13", "version": "1.2.14-alpha.2",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"private": true, "private": true,
"author": "Standard Notes.", "author": "Standard Notes.",

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 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) ## [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 **Note:** Version bump only for package @standardnotes/web

View File

@@ -1,9 +1,9 @@
{ {
"name": "@standardnotes/web", "name": "@standardnotes/web",
"version": "3.22.6", "version": "3.23.0-alpha.1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"main": "dist/app.js", "main": "dist/app.js",
"author": "Standard Notes", "author": "Standard Notes.",
"private": true, "private": true,
"files": [ "files": [
"dist" "dist"

View File

@@ -14,7 +14,7 @@ import ChallengeModal from '@/Components/ChallengeModal/ChallengeModal'
import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu' import NotesContextMenu from '@/Components/NotesContextMenu/NotesContextMenu'
import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper' import PurchaseFlowWrapper from '@/Components/PurchaseFlow/PurchaseFlowWrapper'
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' 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 PremiumModalProvider from '@/Hooks/usePremiumModal'
import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal' import ConfirmSignoutContainer from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
import TagsContextMenuWrapper from '@/Components/Tags/TagContextMenu' 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 className={platformString + ' main-ui-view sn-component'}>
<div id="app" className={appClass + ' app app-column-container'}> <div id="app" className={appClass + ' app app-column-container'}>
<Navigation application={application} /> <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} /> <NoteGroupView application={application} />
</div> </div>
@@ -185,7 +196,13 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<Footer application={application} applicationGroup={mainApplicationGroup} /> <Footer application={application} applicationGroup={mainApplicationGroup} />
<SessionsModal application={application} viewControllerManager={viewControllerManager} /> <SessionsModal application={application} viewControllerManager={viewControllerManager} />
<PreferencesViewWrapper viewControllerManager={viewControllerManager} application={application} /> <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()} {renderChallenges()}
@@ -196,6 +213,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
navigationController={viewControllerManager.navigationController} navigationController={viewControllerManager.navigationController}
notesController={viewControllerManager.notesController} notesController={viewControllerManager.notesController}
noteTagsController={viewControllerManager.noteTagsController} noteTagsController={viewControllerManager.noteTagsController}
historyModalController={viewControllerManager.historyModalController}
/> />
<TagsContextMenuWrapper viewControllerManager={viewControllerManager} /> <TagsContextMenuWrapper viewControllerManager={viewControllerManager} />
<FileContextMenuWrapper <FileContextMenuWrapper

View File

@@ -15,12 +15,10 @@ import UrlMissing from '@/Components/ComponentView/UrlMissing'
import IsDeprecated from '@/Components/ComponentView/IsDeprecated' import IsDeprecated from '@/Components/ComponentView/IsDeprecated'
import IsExpired from '@/Components/ComponentView/IsExpired' import IsExpired from '@/Components/ComponentView/IsExpired'
import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading' import IssueOnLoading from '@/Components/ComponentView/IssueOnLoading'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { openSubscriptionDashboard } from '@/Utils/ManageSubscription' import { openSubscriptionDashboard } from '@/Utils/ManageSubscription'
interface IProps { interface IProps {
application: WebApplication application: WebApplication
viewControllerManager: ViewControllerManager
componentViewer: ComponentViewer componentViewer: ComponentViewer
requestReload?: (viewer: ComponentViewer, force?: boolean) => void requestReload?: (viewer: ComponentViewer, force?: boolean) => void
onLoad?: (component: SNComponent) => void onLoad?: (component: SNComponent) => void

View File

@@ -1,32 +1,44 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { KeyboardKey } from '@/Services/IOService' import { KeyboardKey } from '@/Services/IOService'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { UuidString } from '@standardnotes/snjs' import { UuidString } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react' import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
import { ListableContentItem } from './Types/ListableContentItem' import { ListableContentItem } from './Types/ListableContentItem'
import ContentListItem from './ContentListItem' 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 = { type Props = {
application: WebApplication application: WebApplication
viewControllerManager: ViewControllerManager filesController: FilesController
itemListController: ItemListController
items: ListableContentItem[] items: ListableContentItem[]
navigationController: NavigationController
notesController: NotesController
selectionController: SelectedItemsController
selectedItems: Record<UuidString, ListableContentItem> selectedItems: Record<UuidString, ListableContentItem>
paginate: () => void paginate: () => void
} }
const ContentList: FunctionComponent<Props> = ({ const ContentList: FunctionComponent<Props> = ({
application, application,
viewControllerManager, filesController,
itemListController,
items, items,
navigationController,
notesController,
selectionController,
selectedItems, selectedItems,
paginate, paginate,
}) => { }) => {
const { selectPreviousItem, selectNextItem } = viewControllerManager.itemListController const { selectPreviousItem, selectNextItem } = itemListController
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = itemListController.webDisplayOptions
viewControllerManager.itemListController.webDisplayOptions const { sortBy } = itemListController.displayOptions
const { sortBy } = viewControllerManager.itemListController.displayOptions
const onScroll: UIEventHandler = useCallback( const onScroll: UIEventHandler = useCallback(
(e) => { (e) => {
@@ -55,7 +67,7 @@ const ContentList: FunctionComponent<Props> = ({
return ( return (
<div <div
className="infinite-scroll focus:shadow-none focus:outline-none" className="infinite-scroll focus:shadow-none focus:outline-none"
id="notes-scrollable" id={ElementIds.ContentList}
onScroll={onScroll} onScroll={onScroll}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
@@ -71,10 +83,10 @@ const ContentList: FunctionComponent<Props> = ({
hideTags={hideTags} hideTags={hideTags}
hideIcon={hideEditorIcon} hideIcon={hideEditorIcon}
sortBy={sortBy} sortBy={sortBy}
filesController={viewControllerManager.filesController} filesController={filesController}
selectionController={viewControllerManager.selectionController} selectionController={selectionController}
navigationController={viewControllerManager.navigationController} navigationController={navigationController}
notesController={viewControllerManager.notesController} notesController={notesController}
/> />
))} ))}
</div> </div>

View File

@@ -7,22 +7,22 @@ import Menu from '@/Components/Menu/Menu'
import MenuItem from '@/Components/Menu/MenuItem' import MenuItem from '@/Components/Menu/MenuItem'
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator' import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
import { MenuItemType } from '@/Components/Menu/MenuItemType' import { MenuItemType } from '@/Components/Menu/MenuItemType'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
type Props = { type Props = {
application: WebApplication application: WebApplication
viewControllerManager: ViewControllerManager
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
closeDisplayOptionsMenu: () => void closeDisplayOptionsMenu: () => void
isOpen: boolean isOpen: boolean
navigationController: NavigationController
} }
const ContentListOptionsMenu: FunctionComponent<Props> = ({ const ContentListOptionsMenu: FunctionComponent<Props> = ({
closeDisplayOptionsMenu, closeDisplayOptionsMenu,
closeOnBlur, closeOnBlur,
application, application,
viewControllerManager,
isOpen, isOpen,
navigationController,
}) => { }) => {
const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt)) const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false)) const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
@@ -174,7 +174,7 @@ const ContentListOptionsMenu: FunctionComponent<Props> = ({
</MenuItem> </MenuItem>
<MenuItemSeparator /> <MenuItemSeparator />
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">View</div> <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 <MenuItem
type={MenuItemType.SwitchButton} type={MenuItemType.SwitchButton}
className="py-1 hover:bg-contrast focus:bg-info-backdrop" className="py-1 hover:bg-contrast focus:bg-info-backdrop"

View File

@@ -1,6 +1,5 @@
import { KeyboardKey, KeyboardModifier } from '@/Services/IOService' import { KeyboardKey, KeyboardModifier } from '@/Services/IOService'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { PANEL_NAME_NOTES } from '@/Constants/Constants' import { PANEL_NAME_NOTES } from '@/Constants/Constants'
import { PrefKey, SystemViewId } from '@standardnotes/snjs' import { PrefKey, SystemViewId } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
@@ -15,20 +14,49 @@ import {
useState, useState,
} from 'react' } from 'react'
import ContentList from '@/Components/ContentListView/ContentList' 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 SearchOptions from '@/Components/SearchOptions/SearchOptions'
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import ContentListOptionsMenu from './ContentListOptionsMenu' import ContentListOptionsMenu from './ContentListOptionsMenu'
import Icon from '@/Components/Icon/Icon' 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 = { type Props = {
accountMenuController: AccountMenuController
application: WebApplication 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 itemsViewPanelRef = useRef<HTMLDivElement>(null)
const displayOptionsMenuRef = useRef<HTMLDivElement>(null) const displayOptionsMenuRef = useRef<HTMLDivElement>(null)
@@ -47,9 +75,9 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
paginate, paginate,
panelWidth, panelWidth,
createNewNote, createNewNote,
} = viewControllerManager.itemListController } = itemListController
const { selectedItems } = viewControllerManager.selectionController const { selectedItems } = selectionController
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false) const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
const [focusedSearch, setFocusedSearch] = useState(false) const [focusedSearch, setFocusedSearch] = useState(false)
@@ -57,17 +85,17 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu) const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu)
const isFilesSmartView = useMemo( const isFilesSmartView = useMemo(
() => viewControllerManager.navigationController.selected?.uuid === SystemViewId.Files, () => navigationController.selected?.uuid === SystemViewId.Files,
[viewControllerManager.navigationController.selected?.uuid], [navigationController.selected?.uuid],
) )
const addNewItem = useCallback(() => { const addNewItem = useCallback(() => {
if (isFilesSmartView) { if (isFilesSmartView) {
void viewControllerManager.filesController.uploadNewFile() void filesController.uploadNewFile()
} else { } else {
void createNewNote() void createNewNote()
} }
}, [viewControllerManager.filesController, createNewNote, isFilesSmartView]) }, [filesController, createNewNote, isFilesSmartView])
useEffect(() => { useEffect(() => {
/** /**
@@ -75,7 +103,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
* use Control modifier as well. These rules don't apply to desktop, but * use Control modifier as well. These rules don't apply to desktop, but
* probably better to be consistent. * probably better to be consistent.
*/ */
const newNoteKeyObserver = application.io.addKeyObserver({ const disposeNewNoteKeyObserver = application.io.addKeyObserver({
key: 'n', key: 'n',
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl], modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
onKeyDown: (event) => { 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, key: KeyboardKey.Down,
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])], elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
onKeyDown: () => { onKeyDown: () => {
@@ -95,7 +123,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
}, },
}) })
const previousNoteKeyObserver = application.io.addKeyObserver({ const disposePreviousNoteKeyObserver = application.io.addKeyObserver({
key: KeyboardKey.Up, key: KeyboardKey.Up,
element: document.body, element: document.body,
onKeyDown: () => { onKeyDown: () => {
@@ -103,7 +131,7 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
}, },
}) })
const searchKeyObserver = application.io.addKeyObserver({ const disposeSearchKeyObserver = application.io.addKeyObserver({
key: 'f', key: 'f',
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift], modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift],
onKeyDown: () => { 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 () => { return () => {
newNoteKeyObserver() disposeNewNoteKeyObserver()
nextNoteKeyObserver() disposeNextNoteKeyObserver()
previousNoteKeyObserver() disposePreviousNoteKeyObserver()
searchKeyObserver() disposeSearchKeyObserver()
disposeSelectAllKeyObserver()
} }
}, [addNewItem, application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem]) }, [
addNewItem,
application.io,
createNewNote,
searchBarElement,
selectNextItem,
selectPreviousItem,
selectionController,
])
const onNoteFilterTextChange: ChangeEventHandler<HTMLInputElement> = useCallback( const onNoteFilterTextChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => { (e) => {
@@ -143,15 +195,15 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
const panelResizeFinishCallback: ResizeFinishCallback = useCallback( const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => { (width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error) application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth() noteTagsController.reloadTagsContainerMaxWidth()
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed) application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed)
}, },
[viewControllerManager, application], [application, noteTagsController],
) )
const panelWidthEventCallback = useCallback(() => { const panelWidthEventCallback = useCallback(() => {
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth() noteTagsController.reloadTagsContainerMaxWidth()
}, [viewControllerManager]) }, [noteTagsController])
const toggleDisplayOptionsMenu = useCallback(() => { const toggleDisplayOptionsMenu = useCallback(() => {
setShowDisplayOptionsMenu(!showDisplayOptionsMenu) setShowDisplayOptionsMenu(!showDisplayOptionsMenu)
@@ -207,11 +259,14 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
{(focusedSearch || noteFilterText) && ( {(focusedSearch || noteFilterText) && (
<div className="animate-fade-from-top"> <div className="animate-fade-from-top">
<SearchOptions application={application} viewControllerManager={viewControllerManager} /> <SearchOptions application={application} searchOptions={searchOptionsController} />
</div> </div>
)} )}
</div> </div>
<NoAccountWarningWrapper viewControllerManager={viewControllerManager} /> <NoAccountWarning
accountMenuController={accountMenuController}
noAccountWarningController={noAccountWarningController}
/>
</div> </div>
<div id="items-menu-bar" className="sn-component" ref={displayOptionsMenuRef}> <div id="items-menu-bar" className="sn-component" ref={displayOptionsMenuRef}>
<div className="sk-app-bar no-edges"> <div className="sk-app-bar no-edges">
@@ -234,10 +289,10 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
{showDisplayOptionsMenu && ( {showDisplayOptionsMenu && (
<ContentListOptionsMenu <ContentListOptionsMenu
application={application} application={application}
viewControllerManager={viewControllerManager}
closeDisplayOptionsMenu={toggleDisplayOptionsMenu} closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
closeOnBlur={closeDisplayOptMenuOnBlur} closeOnBlur={closeDisplayOptMenuOnBlur}
isOpen={showDisplayOptionsMenu} isOpen={showDisplayOptionsMenu}
navigationController={navigationController}
/> />
)} )}
</DisclosurePanel> </DisclosurePanel>
@@ -253,8 +308,12 @@ const ContentListView: FunctionComponent<Props> = ({ application, viewController
items={renderedItems} items={renderedItems}
selectedItems={selectedItems} selectedItems={selectedItems}
application={application} application={application}
viewControllerManager={viewControllerManager}
paginate={paginate} paginate={paginate}
filesController={filesController}
itemListController={itemListController}
navigationController={navigationController}
notesController={notesController}
selectionController={selectionController}
/> />
) : null} ) : null}
</div> </div>

View File

@@ -13,6 +13,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -23,6 +24,7 @@ type Props = {
notesController: NotesController notesController: NotesController
noteTagsController: NoteTagsController noteTagsController: NoteTagsController
selectionController: SelectedItemsController selectionController: SelectedItemsController
historyModalController: HistoryModalController
} }
const MultipleSelectedNotes = ({ const MultipleSelectedNotes = ({
@@ -34,6 +36,7 @@ const MultipleSelectedNotes = ({
notesController, notesController,
noteTagsController, noteTagsController,
selectionController, selectionController,
historyModalController,
}: Props) => { }: Props) => {
const count = notesController.selectedNotesCount const count = notesController.selectedNotesCount
@@ -65,6 +68,7 @@ const MultipleSelectedNotes = ({
navigationController={navigationController} navigationController={navigationController}
notesController={notesController} notesController={notesController}
noteTagsController={noteTagsController} noteTagsController={noteTagsController}
historyModalController={historyModalController}
/> />
</div> </div>
</div> </div>

View File

@@ -1,49 +1,22 @@
import Icon from '@/Components/Icon/Icon' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { MouseEventHandler, useCallback } from 'react' import NoAccountWarningContent from './NoAccountWarningContent'
type Props = { viewControllerManager: ViewControllerManager } type Props = {
accountMenuController: AccountMenuController
const NoAccountWarning = observer(({ viewControllerManager }: Props) => { noAccountWarningController: NoAccountWarningController
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
} }
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)

View File

@@ -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)

View File

@@ -92,6 +92,7 @@ class NoteGroupView extends PureComponent<Props, State> {
navigationController={this.viewControllerManager.navigationController} navigationController={this.viewControllerManager.navigationController}
notesController={this.viewControllerManager.notesController} notesController={this.viewControllerManager.notesController}
noteTagsController={this.viewControllerManager.noteTagsController} noteTagsController={this.viewControllerManager.noteTagsController}
historyModalController={this.viewControllerManager.historyModalController}
/> />
)} )}

View File

@@ -971,6 +971,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
navigationController={this.viewControllerManager.navigationController} navigationController={this.viewControllerManager.navigationController}
notesController={this.viewControllerManager.notesController} notesController={this.viewControllerManager.notesController}
noteTagsController={this.viewControllerManager.noteTagsController} noteTagsController={this.viewControllerManager.noteTagsController}
historyModalController={this.viewControllerManager.historyModalController}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction} onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
/> />
</div> </div>
@@ -1002,7 +1003,6 @@ class NoteView extends PureComponent<NoteViewProps, State> {
onLoad={this.onEditorComponentLoad} onLoad={this.onEditorComponentLoad}
requestReload={this.editorComponentViewerRequestsReload} requestReload={this.editorComponentViewerRequestsReload}
application={this.application} application={this.application}
viewControllerManager={this.viewControllerManager}
/> />
</div> </div>
)} )}
@@ -1073,12 +1073,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
{this.state.stackComponentViewers.map((viewer) => { {this.state.stackComponentViewers.map((viewer) => {
return ( return (
<div className="component-view component-stack-item" key={viewer.identifier}> <div className="component-view component-stack-item" key={viewer.identifier}>
<ComponentView <ComponentView key={viewer.identifier} componentViewer={viewer} application={this.application} />
key={viewer.identifier}
componentViewer={viewer}
application={this.application}
viewControllerManager={this.viewControllerManager}
/>
</div> </div>
) )
})} })}

View File

@@ -7,15 +7,23 @@ import { WebApplication } from '@/Application/Application'
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
type Props = { type Props = {
application: WebApplication application: WebApplication
navigationController: NavigationController navigationController: NavigationController
notesController: NotesController notesController: NotesController
noteTagsController: NoteTagsController 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 { contextMenuOpen, contextMenuPosition, contextMenuMaxHeight } = notesController
const contextMenuRef = useRef<HTMLDivElement>(null) const contextMenuRef = useRef<HTMLDivElement>(null)
@@ -49,6 +57,7 @@ const NotesContextMenu = ({ application, navigationController, notesController,
navigationController={navigationController} navigationController={navigationController}
notesController={notesController} notesController={notesController}
noteTagsController={noteTagsController} noteTagsController={noteTagsController}
historyModalController={historyModalController}
/> />
</div> </div>
) : null ) : null

View File

@@ -174,6 +174,7 @@ const NotesOptions = ({
navigationController, navigationController,
notesController, notesController,
noteTagsController, noteTagsController,
historyModalController,
closeOnBlur, closeOnBlur,
}: NotesOptionsProps) => { }: NotesOptionsProps) => {
const [altKeyDown, setAltKeyDown] = useState(false) const [altKeyDown, setAltKeyDown] = useState(false)
@@ -222,7 +223,7 @@ const NotesOptions = ({
const format = editor?.package_info?.file_type || 'txt' const format = editor?.package_info?.file_type || 'txt'
return `${note.title}.${format}` return `${note.title}.${format}`
}, },
[application], [application.componentManager],
) )
const downloadSelectedItems = useCallback(async () => { const downloadSelectedItems = useCallback(async () => {
@@ -239,7 +240,7 @@ const NotesOptions = ({
await application.getArchiveService().downloadDataAsZip( await application.getArchiveService().downloadDataAsZip(
notes.map((note) => { notes.map((note) => {
return { return {
filename: getNoteFileName(note), name: getNoteFileName(note),
content: new Blob([note.text]), content: new Blob([note.text]),
} }
}), }),
@@ -259,8 +260,8 @@ const NotesOptions = ({
}, [application, notes]) }, [application, notes])
const openRevisionHistoryModal = useCallback(() => { const openRevisionHistoryModal = useCallback(() => {
notesController.setShowRevisionHistoryModal(true) historyModalController.openModal(notesController.firstSelectedNote)
}, [notesController]) }, [historyModalController, notesController.firstSelectedNote])
return ( return (
<> <>

View File

@@ -10,12 +10,14 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
type Props = { type Props = {
application: WebApplication application: WebApplication
navigationController: NavigationController navigationController: NavigationController
notesController: NotesController notesController: NotesController
noteTagsController: NoteTagsController noteTagsController: NoteTagsController
historyModalController: HistoryModalController
onClickPreprocessing?: () => Promise<void> onClickPreprocessing?: () => Promise<void>
} }
@@ -24,6 +26,7 @@ const NotesOptionsPanel = ({
navigationController, navigationController,
notesController, notesController,
noteTagsController, noteTagsController,
historyModalController,
onClickPreprocessing, onClickPreprocessing,
}: Props) => { }: Props) => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -95,6 +98,7 @@ const NotesOptionsPanel = ({
navigationController={navigationController} navigationController={navigationController}
notesController={notesController} notesController={notesController}
noteTagsController={noteTagsController} noteTagsController={noteTagsController}
historyModalController={historyModalController}
closeOnBlur={closeOnBlur} closeOnBlur={closeOnBlur}
/> />
)} )}

View File

@@ -1,4 +1,5 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { NavigationController } from '@/Controllers/Navigation/NavigationController'
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { NoteTagsController } from '@/Controllers/NoteTagsController' import { NoteTagsController } from '@/Controllers/NoteTagsController'
@@ -8,5 +9,6 @@ export type NotesOptionsProps = {
navigationController: NavigationController navigationController: NavigationController
notesController: NotesController notesController: NotesController
noteTagsController: NoteTagsController noteTagsController: NoteTagsController
historyModalController: HistoryModalController
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
} }

View File

@@ -1,75 +1,24 @@
import { WebApplication } from '@/Application/Application' import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
import { Action, ActionVerb, HistoryEntry, NoteHistoryEntry, RevisionListEntry, SNNote } from '@standardnotes/snjs' import { FeaturesClientInterface } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useState, useEffect, SetStateAction, Dispatch } from 'react' import { FunctionComponent } from 'react'
import LegacyHistoryList from './LegacyHistoryList' import LegacyHistoryList from './LegacyHistoryList'
import RemoteHistoryList from './RemoteHistoryList' import RemoteHistoryList from './RemoteHistoryList'
import { RevisionType } from './RevisionType'
import SessionHistoryList from './SessionHistoryList' import SessionHistoryList from './SessionHistoryList'
import { LegacyHistoryEntry, RemoteRevisionListGroup, sortRevisionListIntoGroups } from './utils'
export enum RevisionListTabType {
Session = 'Session',
Remote = 'Remote',
Legacy = 'Legacy',
}
type Props = { type Props = {
application: WebApplication features: FeaturesClientInterface
isFetchingRemoteHistory: boolean noteHistoryController: NoteHistoryController
note: SNNote
remoteHistory: RemoteRevisionListGroup[] | undefined
setIsFetchingSelectedRevision: Dispatch<SetStateAction<boolean>>
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>>
setShowContentLockedScreen: Dispatch<SetStateAction<boolean>>
} }
const HistoryListContainer: FunctionComponent<Props> = ({ const HistoryListContainer: FunctionComponent<Props> = ({ features, noteHistoryController }) => {
application, const { legacyHistory, currentTab, selectTab } = noteHistoryController
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 TabButton: FunctionComponent<{ const TabButton: FunctionComponent<{
type: RevisionListTabType type: RevisionType
}> = ({ type }) => { }> = ({ type }) => {
const isSelected = selectedTab === type const isSelected = currentTab === type
return ( return (
<button <button
@@ -77,8 +26,7 @@ const HistoryListContainer: FunctionComponent<Props> = ({
isSelected ? 'color-info font-medium shadow-bottom' : 'color-text' isSelected ? 'color-info font-medium shadow-bottom' : 'color-text'
}`} }`}
onClick={() => { onClick={() => {
setSelectedTab(type) selectTab(type)
setSelectedRemoteEntry(undefined)
}} }}
> >
{type} {type}
@@ -86,98 +34,26 @@ const HistoryListContainer: FunctionComponent<Props> = ({
) )
} }
const fetchAndSetLegacyRevision = useCallback( const CurrentTabList = () => {
async (revisionListEntry: Action) => { switch (currentTab) {
setSelectedRemoteEntry(undefined) case RevisionType.Remote:
setSelectedRevision(undefined) return <RemoteHistoryList features={features} noteHistoryController={noteHistoryController} />
setIsFetchingSelectedRevision(true) case RevisionType.Session:
return <SessionHistoryList noteHistoryController={noteHistoryController} />
try { case RevisionType.Legacy:
if (!revisionListEntry.subactions?.[0]) { return <LegacyHistoryList legacyHistory={legacyHistory} noteHistoryController={noteHistoryController} />
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,
],
)
return ( 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 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"> <div className="flex border-0 border-b-1 border-solid border-main">
<TabButton type={RevisionListTabType.Remote} /> <TabButton type={RevisionType.Remote} />
<TabButton type={RevisionListTabType.Session} /> <TabButton type={RevisionType.Session} />
{legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionListTabType.Legacy} />} {legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionType.Legacy} />}
</div> </div>
<div className={'min-h-0 overflow-auto py-1.5 h-full'}> <div className={'min-h-0 overflow-auto py-1.5 h-full'}>
{selectedTab === RevisionListTabType.Session && ( <CurrentTabList />
<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}
/>
)}
</div> </div>
</div> </div>
) )

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -1,48 +1,21 @@
import { Action, HistoryEntry, RevisionListEntry } from '@standardnotes/snjs' import { Action } from '@standardnotes/snjs'
import { Dispatch, FunctionComponent, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FunctionComponent, useRef } from 'react'
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation' import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
import HistoryListItem from './HistoryListItem' import HistoryListItem from './HistoryListItem'
import { LegacyHistoryEntry } from './utils' import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
type Props = { type Props = {
legacyHistory: Action[] | undefined legacyHistory: Action[] | undefined
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>> noteHistoryController: NoteHistoryController
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
fetchAndSetLegacyRevision: (revisionListEntry: Action) => Promise<void>
} }
const LegacyHistoryList: FunctionComponent<Props> = ({ const LegacyHistoryList: FunctionComponent<Props> = ({ legacyHistory, noteHistoryController }) => {
legacyHistory, const { selectLegacyRevision, selectedEntry } = noteHistoryController
setSelectedRevision,
setSelectedRemoteEntry,
fetchAndSetLegacyRevision,
}) => {
const legacyHistoryListRef = useRef<HTMLDivElement>(null) const legacyHistoryListRef = useRef<HTMLDivElement>(null)
useListKeyboardNavigation(legacyHistoryListRef) 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 ( return (
<div <div
className={`flex flex-col w-full h-full focus:shadow-none ${ className={`flex flex-col w-full h-full focus:shadow-none ${
@@ -51,16 +24,15 @@ const LegacyHistoryList: FunctionComponent<Props> = ({
ref={legacyHistoryListRef} ref={legacyHistoryListRef}
> >
{legacyHistory?.map((entry) => { {legacyHistory?.map((entry) => {
const selectedEntryUrl = (selectedEntry as Action)?.subactions?.[0].url
const url = entry.subactions?.[0].url const url = entry.subactions?.[0].url
return ( return (
<HistoryListItem <HistoryListItem
key={url} key={url}
isSelected={selectedItemUrl === url} isSelected={selectedEntryUrl === url}
onClick={() => { onClick={() => {
setSelectedItemUrl(url) selectLegacyRevision(entry)
setSelectedRemoteEntry(undefined)
fetchAndSetLegacyRevision(entry).catch(console.error)
}} }}
> >
{entry.label} {entry.label}

View File

@@ -1,50 +1,26 @@
import { WebApplication } from '@/Application/Application'
import { RevisionListEntry } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' 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 Icon from '@/Components/Icon/Icon'
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation' import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
import HistoryListItem from './HistoryListItem' 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 = { type RemoteHistoryListProps = {
application: WebApplication features: FeaturesClientInterface
remoteHistory: RemoteRevisionListGroup[] | undefined noteHistoryController: NoteHistoryController
isFetchingRemoteHistory: boolean
fetchAndSetRemoteRevision: (revisionListEntry: RevisionListEntry) => Promise<void>
} }
const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({ const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({ features, noteHistoryController }) => {
application, const { remoteHistory, isFetchingRemoteHistory, selectRemoteRevision, selectedEntry } = noteHistoryController
remoteHistory,
isFetchingRemoteHistory,
fetchAndSetRemoteRevision,
}) => {
const remoteHistoryListRef = useRef<HTMLDivElement>(null) const remoteHistoryListRef = useRef<HTMLDivElement>(null)
useListKeyboardNavigation(remoteHistoryListRef) useListKeyboardNavigation(remoteHistoryListRef)
const remoteHistoryLength = useMemo(() => remoteHistory?.map((group) => group.entries).flat().length, [remoteHistory]) 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 ( return (
<div <div
className={`flex flex-col w-full h-full focus:shadow-none ${ className={`flex flex-col w-full h-full focus:shadow-none ${
@@ -63,15 +39,14 @@ const RemoteHistoryList: FunctionComponent<RemoteHistoryListProps> = ({
{group.entries.map((entry) => ( {group.entries.map((entry) => (
<HistoryListItem <HistoryListItem
key={entry.uuid} key={entry.uuid}
isSelected={selectedEntryUuid === entry.uuid} isSelected={(selectedEntry as RevisionListEntry)?.uuid === entry.uuid}
onClick={() => { onClick={() => {
setSelectedEntryUuid(entry.uuid) selectRemoteRevision(entry)
fetchAndSetRemoteRevision(entry).catch(console.error)
}} }}
> >
<div className="flex flex-grow items-center justify-between"> <div className="flex flex-grow items-center justify-between">
<div>{previewHistoryEntryTitle(entry)}</div> <div>{previewHistoryEntryTitle(entry)}</div>
{!application.features.hasMinimumRole(entry.required_role) && <Icon type="premium-feature" />} {!features.hasMinimumRole(entry.required_role) && <Icon type="premium-feature" />}
</div> </div>
</HistoryListItem> </HistoryListItem>
))} ))}

View File

@@ -1,8 +1,8 @@
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent } from 'react' import { FunctionComponent } from 'react'
import { HistoryLockedIllustration } from '@standardnotes/icons' import { HistoryLockedIllustration } from '@standardnotes/icons'
import Button from '@/Components/Button/Button' import Button from '@/Components/Button/Button'
import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController'
const getPlanHistoryDuration = (planName: string | undefined) => { const getPlanHistoryDuration = (planName: string | undefined) => {
switch (planName) { switch (planName) {
@@ -20,12 +20,11 @@ const getPremiumContentCopy = (planName: string | undefined) => {
} }
type Props = { type Props = {
viewControllerManager: ViewControllerManager subscriptionController: SubscriptionController
} }
const RevisionContentLocked: FunctionComponent<Props> = ({ viewControllerManager }) => { const RevisionContentLocked: FunctionComponent<Props> = ({ subscriptionController }) => {
const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = const { userSubscriptionName, isUserSubscriptionExpired, isUserSubscriptionCanceled } = subscriptionController
viewControllerManager.subscriptionController
return ( return (
<div className="flex w-full h-full items-center justify-center"> <div className="flex w-full h-full items-center justify-center">

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -0,0 +1,5 @@
export enum RevisionType {
Session = 'Session',
Remote = 'Remote',
Legacy = 'Legacy',
}

View File

@@ -1,39 +1,45 @@
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { ViewControllerManager } from '@/Services/ViewControllerManager' import { ContentType, SNNote } from '@standardnotes/snjs'
import { HistoryEntry, SNComponent, SNNote } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useEffect, useMemo } from 'react' import { FunctionComponent, useEffect, useMemo } from 'react'
import ComponentView from '@/Components/ComponentView/ComponentView' 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' const ABSOLUTE_CENTER_CLASSNAME = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'
type SelectedRevisionContentProps = { type SelectedRevisionContentProps = {
application: WebApplication application: WebApplication
viewControllerManager: ViewControllerManager noteHistoryController: NoteHistoryController
selectedRevision: HistoryEntry | LegacyHistoryEntry notesController: NotesController
editorForCurrentNote: SNComponent | undefined
templateNoteForRevision: SNNote
} }
const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> = ({ const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> = ({
application, application,
viewControllerManager, noteHistoryController,
selectedRevision, notesController,
editorForCurrentNote,
templateNoteForRevision,
}) => { }) => {
const note = notesController.firstSelectedNote
const { selectedRevision } = noteHistoryController
const componentViewer = useMemo(() => { const componentViewer = useMemo(() => {
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
if (!editorForCurrentNote) { if (!editorForCurrentNote) {
return undefined return undefined
} }
const templateNoteForRevision = application.mutator.createTemplateItem(
ContentType.Note,
selectedRevision?.payload.content,
) as SNNote
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote) const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
componentViewer.setReadonly(true) componentViewer.setReadonly(true)
componentViewer.lockReadonly = true componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision componentViewer.overrideContextItem = templateNoteForRevision
return componentViewer return componentViewer
}, [application, editorForCurrentNote, templateNoteForRevision]) }, [application.componentManager, application.mutator, note, selectedRevision?.payload.content])
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -46,17 +52,16 @@ const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> =
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">
<div className="p-4 text-base font-bold w-full"> <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> </div>
{!componentViewer && ( {!componentViewer && (
<div className="relative flex-grow min-h-0 overflow-hidden"> <div className="relative flex-grow min-h-0 overflow-hidden">
{selectedRevision.payload.content.text.length ? ( {selectedRevision?.payload.content.text.length ? (
<textarea <textarea
readOnly={true} readOnly={true}
className="w-full h-full resize-none p-4 pt-0 border-0 bg-default color-text text-editor font-editor" className="w-full h-full resize-none p-4 pt-0 border-0 bg-default color-text text-editor font-editor"
> value={selectedRevision?.payload.content.text}
{selectedRevision.payload.content.text} />
</textarea>
) : ( ) : (
<div className={`color-passive-0 ${ABSOLUTE_CENTER_CLASSNAME}`}>Empty note.</div> <div className={`color-passive-0 ${ABSOLUTE_CENTER_CLASSNAME}`}>Empty note.</div>
)} )}
@@ -64,12 +69,7 @@ const SelectedRevisionContent: FunctionComponent<SelectedRevisionContentProps> =
)} )}
{componentViewer && ( {componentViewer && (
<div className="component-view"> <div className="component-view">
<ComponentView <ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
key={componentViewer.identifier}
componentViewer={componentViewer}
application={application}
viewControllerManager={viewControllerManager}
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,60 +1,25 @@
import { HistoryEntry, NoteHistoryEntry, RevisionListEntry } from '@standardnotes/snjs' import { Fragment, FunctionComponent, useMemo, useRef } from 'react'
import {
Dispatch,
Fragment,
FunctionComponent,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation' import { useListKeyboardNavigation } from '@/Hooks/useListKeyboardNavigation'
import HistoryListItem from './HistoryListItem' import HistoryListItem from './HistoryListItem'
import { LegacyHistoryEntry, ListGroup } from './utils' import { observer } from 'mobx-react-lite'
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
type Props = { type Props = {
sessionHistory: ListGroup<NoteHistoryEntry>[] noteHistoryController: NoteHistoryController
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>>
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
} }
const SessionHistoryList: FunctionComponent<Props> = ({ const SessionHistoryList: FunctionComponent<Props> = ({ noteHistoryController }) => {
sessionHistory, const { sessionHistory, selectedRevision, selectSessionRevision } = noteHistoryController
setSelectedRevision,
setSelectedRemoteEntry,
}) => {
const sessionHistoryListRef = useRef<HTMLDivElement>(null) const sessionHistoryListRef = useRef<HTMLDivElement>(null)
useListKeyboardNavigation(sessionHistoryListRef) useListKeyboardNavigation(sessionHistoryListRef)
const sessionHistoryLength = useMemo( const sessionHistoryLength = useMemo(
() => sessionHistory.map((group) => group.entries).flat().length, () => sessionHistory?.map((group) => group.entries).flat().length,
[sessionHistory], [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 ( return (
<div <div
className={`flex flex-col w-full h-full focus:shadow-none ${ className={`flex flex-col w-full h-full focus:shadow-none ${
@@ -72,11 +37,9 @@ const SessionHistoryList: FunctionComponent<Props> = ({
{group.entries.map((entry, index) => ( {group.entries.map((entry, index) => (
<HistoryListItem <HistoryListItem
key={index} key={index}
isSelected={selectedItemCreatedAt === entry.payload.created_at} isSelected={selectedRevision?.payload.created_at === entry.payload.created_at}
onClick={() => { onClick={() => {
setSelectedItemCreatedAt(entry.payload.created_at) selectSessionRevision(entry)
setSelectedRevision(entry)
setSelectedRemoteEntry(undefined)
}} }}
> >
{entry.previewTitle()} {entry.previewTitle()}
@@ -93,4 +56,4 @@ const SessionHistoryList: FunctionComponent<Props> = ({
) )
} }
export default SessionHistoryList export default observer(SessionHistoryList)

View File

@@ -1,17 +1,15 @@
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import Bubble from '@/Components/Bubble/Bubble' import Bubble from '@/Components/Bubble/Bubble'
import { useCallback } from 'react' import { useCallback } from 'react'
import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
type Props = { type Props = {
viewControllerManager: ViewControllerManager
application: WebApplication application: WebApplication
searchOptions: SearchOptionsController
} }
const SearchOptions = ({ viewControllerManager }: Props) => { const SearchOptions = ({ searchOptions }: Props) => {
const { searchOptionsController: searchOptions } = viewControllerManager
const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions const { includeProtectedContents, includeArchived, includeTrashed } = searchOptions
const toggleIncludeProtectedContents = useCallback(async () => { const toggleIncludeProtectedContents = useCallback(async () => {

View File

@@ -5,4 +5,5 @@ export const ElementIds = {
FileTextPreview: 'file-text-preview', FileTextPreview: 'file-text-preview',
EditorContent: 'editor-content', EditorContent: 'editor-content',
EditorColumn: 'editor-column', EditorColumn: 'editor-column',
ContentList: 'notes-scrollable',
} }

View File

@@ -215,6 +215,10 @@ export class ItemListController extends AbstractViewController implements Intern
} }
} }
public get listLength() {
return this.renderedItems.length
}
public getActiveItemController(): NoteViewController | FileViewController | undefined { public getActiveItemController(): NoteViewController | FileViewController | undefined {
return this.application.itemControllerGroup.activeItemViewController return this.application.itemControllerGroup.activeItemViewController
} }

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -21,7 +21,6 @@ export class NotesController extends AbstractViewController {
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 } contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 }
contextMenuMaxHeight: number | 'auto' = 'auto' contextMenuMaxHeight: number | 'auto' = 'auto'
showProtectedWarning = false showProtectedWarning = false
showRevisionHistoryModal = false
private itemListController!: ItemListController private itemListController!: ItemListController
override deinit() { override deinit() {
@@ -48,7 +47,6 @@ export class NotesController extends AbstractViewController {
contextMenuOpen: observable, contextMenuOpen: observable,
contextMenuPosition: observable, contextMenuPosition: observable,
showProtectedWarning: observable, showProtectedWarning: observable,
showRevisionHistoryModal: observable,
selectedNotes: computed, selectedNotes: computed,
firstSelectedNote: computed, firstSelectedNote: computed,
@@ -60,7 +58,6 @@ export class NotesController extends AbstractViewController {
setContextMenuPosition: action, setContextMenuPosition: action,
setContextMenuMaxHeight: action, setContextMenuMaxHeight: action,
setShowProtectedWarning: action, setShowProtectedWarning: action,
setShowRevisionHistoryModal: action,
unselectNotes: action, unselectNotes: action,
}) })
} }
@@ -367,8 +364,4 @@ export class NotesController extends AbstractViewController {
private getSelectedNotesList(): SNNote[] { private getSelectedNotesList(): SNNote[] {
return Object.values(this.selectedNotes) return Object.values(this.selectedNotes)
} }
setShowRevisionHistoryModal(show: boolean): void {
this.showRevisionHistoryModal = show
}
} }

View File

@@ -105,11 +105,19 @@ export class SelectedItemsController extends AbstractViewController {
this.selectedItems[item.uuid] = item 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 items = this.itemListController.renderedItems
const lastSelectedItemIndex = items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid) const lastSelectedItemIndex = startingIndex ?? items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid)
const selectedItemIndex = items.findIndex((item) => item.uuid == selectedItem.uuid) const selectedItemIndex = endingIndex ?? items.findIndex((item) => item.uuid == selectedItem?.uuid)
let itemsToSelect = [] let itemsToSelect = []
if (selectedItemIndex > lastSelectedItemIndex) { if (selectedItemIndex > lastSelectedItemIndex) {
@@ -151,6 +159,13 @@ export class SelectedItemsController extends AbstractViewController {
this.lastSelectedItem = item this.lastSelectedItem = item
} }
selectAll = () => {
void this.selectItemsRange({
startingIndex: 0,
endingIndex: this.itemListController.listLength - 1,
})
}
private deselectAll = (): void => { private deselectAll = (): void => {
this.setSelectedItems({}) this.setSelectedItems({})
@@ -184,7 +199,7 @@ export class SelectedItemsController extends AbstractViewController {
this.lastSelectedItem = item this.lastSelectedItem = item
} }
} else if (userTriggered && hasShift) { } else if (userTriggered && hasShift) {
await this.selectItemsRange(item) await this.selectItemsRange({ selectedItem: item })
} else { } else {
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid] const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid]
if (shouldSelectNote && isAuthorizedForAccess) { if (shouldSelectNote && isAuthorizedForAccess) {

View File

@@ -20,7 +20,7 @@ function zippableFileName(name: string, suffix = '', format = 'txt'): string {
} }
type ZippableData = { type ZippableData = {
filename: string name: string
content: Blob content: Blob
}[] }[]
@@ -119,9 +119,21 @@ export class ArchiveManager {
const zip = await import('@zip.js/zip.js') const zip = await import('@zip.js/zip.js')
const writer = new zip.ZipWriter(new zip.BlobWriter('application/zip')) const writer = new zip.ZipWriter(new zip.BlobWriter('application/zip'))
const filenameCounts: Record<string, number> = {}
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const { name, ext } = parseFileName(data[i].filename) const file = data[i]
await writer.add(zippableFileName(name, '', ext), new zip.BlobReader(data[i].content))
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() const zipFileAsBlob = await writer.close()

View File

@@ -20,6 +20,7 @@ import { SyncStatusController } from '../Controllers/SyncStatusController'
import { NavigationController } from '../Controllers/Navigation/NavigationController' import { NavigationController } from '../Controllers/Navigation/NavigationController'
import { FilePreviewModalController } from '../Controllers/FilePreviewModalController' import { FilePreviewModalController } from '../Controllers/FilePreviewModalController'
import { SelectedItemsController } from '../Controllers/SelectedItemsController' import { SelectedItemsController } from '../Controllers/SelectedItemsController'
import { HistoryModalController } from '../Controllers/NoteHistory/HistoryModalController'
export class ViewControllerManager { export class ViewControllerManager {
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
@@ -45,6 +46,7 @@ export class ViewControllerManager {
readonly syncStatusController = new SyncStatusController() readonly syncStatusController = new SyncStatusController()
readonly navigationController: NavigationController readonly navigationController: NavigationController
readonly selectionController: SelectedItemsController readonly selectionController: SelectedItemsController
readonly historyModalController: HistoryModalController
public isSessionsModalVisible = false public isSessionsModalVisible = false
@@ -101,6 +103,8 @@ export class ViewControllerManager {
this.eventBus, this.eventBus,
) )
this.historyModalController = new HistoryModalController(this.application, this.eventBus)
this.addAppEventObserver() this.addAppEventObserver()
if (this.device.appVersion.includes('-beta')) { if (this.device.appVersion.includes('-beta')) {
@@ -175,6 +179,9 @@ export class ViewControllerManager {
this.navigationController.deinit() this.navigationController.deinit()
;(this.navigationController as unknown) = undefined ;(this.navigationController as unknown) = undefined
this.historyModalController.deinit()
;(this.historyModalController as unknown) = undefined
destroyAllObjectProperties(this) destroyAllObjectProperties(this)
} }

Some files were not shown because too many files have changed in this diff Show More