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

View File

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

View File

@@ -43,11 +43,22 @@ jobs:
with:
lane: 'android dev'
subdirectory: 'packages/mobile'
- name: Upload universal apk to artifacts
uses: actions/upload-artifact@v2
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
name: dev.apk
path: android/app/build/outputs/apk/dev/release/app-dev-release.apk
path: packages/mobile
- name: Release
uses: softprops/action-gh-release@v1
with:
token: ${{ secrets.CI_PAT_TOKEN }}
tag_name: "@standardnotes/mobile@${{ steps.package-version.outputs.current-version}}"
prerelease: true
draft: false
name: "Mobile Beta ${{ steps.package-version.outputs.current-version }}"
files: |
packages/mobile/android/app/build/outputs/apk/dev/release/app-dev-release.apk
ios:
defaults:
run:

View File

@@ -44,16 +44,24 @@ jobs:
with:
lane: 'android prod'
subdirectory: 'packages/mobile'
- name: Upload universal apk to artifacts
uses: actions/upload-artifact@v2
- name: get-npm-version
id: package-version
uses: martinbeentjes/npm-get-version-action@main
with:
name: prod.apk
path: android/app/build/outputs/apk/prod/release/app-prod-release.apk
- name: Upload Android App Bundle to artifacts
uses: actions/upload-artifact@v2
path: packages/mobile
- name: Release
uses: softprops/action-gh-release@v1
with:
name: release.aab
path: android/app/build/outputs/bundle/prodRelease/app-prod-release.aab
token: ${{ secrets.CI_PAT_TOKEN }}
tag_name: "@standardnotes/mobile@${{ steps.package-version.outputs.current-version}}"
prerelease: false
draft: false
name: "Mobile ${{ steps.package-version.outputs.current-version }}"
files: |
packages/mobile/android/app/build/outputs/bundle/prodRelease/app-prod-release.aab
packages/mobile/android/app/build/outputs/apk/prod/release/app-prod-release.apk
ios:
defaults:
run:

View File

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

Binary file not shown.

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:graduate": "lerna version --conventional-graduate --conventional-commits --yes -m \"chore(release): publish\"",
"release:beta": "lerna version --conventional-prerelease --conventional-commits --yes -m \"chore(release): publish\"",
"publish:prod": "lerna publish from-git --yes",
"publish:beta": "lerna publish from-git --yes --dist-tag alpha",
"version": "yarn install --no-immutable && git add yarn.lock",
"postversion": "./scripts/push-tags-one-by-one.sh",
"lerna:list": " yarn lerna list -all",

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.22.12-alpha.3](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.12-alpha.2...@standardnotes/desktop@3.22.12-alpha.3) (2022-06-20)
### Bug Fixes
* **desktop:** linux executableName ([296873d](https://github.com/standardnotes/app/commit/296873d671d6cc7cb8744747c09bf755e3a6f1e0))
## [3.22.12-alpha.2](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.12-alpha.1...@standardnotes/desktop@3.22.12-alpha.2) (2022-06-20)
**Note:** Version bump only for package @standardnotes/desktop
## [3.22.12-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.12-alpha.0...@standardnotes/desktop@3.22.12-alpha.1) (2022-06-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.22.12-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.11...@standardnotes/desktop@3.22.12-alpha.0) (2022-06-18)
**Note:** Version bump only for package @standardnotes/desktop
## [3.22.11](https://github.com/standardnotes/app/compare/@standardnotes/desktop@3.22.11-alpha.0...@standardnotes/desktop@3.22.11) (2022-06-18)
**Note:** Version bump only for package @standardnotes/desktop

View File

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

1
packages/releases/.gitignore vendored Normal file
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.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.14-alpha.2](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.14-alpha.1...@standardnotes/web-server@1.2.14-alpha.2) (2022-06-20)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.14-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.14-alpha.0...@standardnotes/web-server@1.2.14-alpha.1) (2022-06-19)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.14-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.13...@standardnotes/web-server@1.2.14-alpha.0) (2022-06-18)
**Note:** Version bump only for package @standardnotes/web-server
## [1.2.13](https://github.com/standardnotes/app/compare/@standardnotes/web-server@1.2.13-alpha.0...@standardnotes/web-server@1.2.13) (2022-06-18)
**Note:** Version bump only for package @standardnotes/web-server

View File

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

View File

@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [3.23.0-alpha.1](https://github.com/standardnotes/app/compare/@standardnotes/web@3.23.0-alpha.0...@standardnotes/web@3.23.0-alpha.1) (2022-06-20)
**Note:** Version bump only for package @standardnotes/web
# [3.23.0-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/web@3.22.7-alpha.0...@standardnotes/web@3.23.0-alpha.0) (2022-06-19)
### Features
* ctrl+a to select all items ([#1123](https://github.com/standardnotes/app/issues/1123)) ([dcf3724](https://github.com/standardnotes/app/commit/dcf3724e2c951d7bbad276d37b3302c34d7ee78f))
## [3.22.7-alpha.0](https://github.com/standardnotes/app/compare/@standardnotes/web@3.22.6...@standardnotes/web@3.22.7-alpha.0) (2022-06-18)
### Bug Fixes
* exporting multiple notes with same title ([#1119](https://github.com/standardnotes/app/issues/1119)) ([dfbd72b](https://github.com/standardnotes/app/commit/dfbd72b1dfd4a4681c452c9113248045e69880cd))
## [3.22.6](https://github.com/standardnotes/app/compare/@standardnotes/web@3.22.6-alpha.0...@standardnotes/web@3.22.6) (2022-06-18)
**Note:** Version bump only for package @standardnotes/web

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,49 +1,22 @@
import Icon from '@/Components/Icon/Icon'
import { ViewControllerManager } from '@/Services/ViewControllerManager'
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController'
import { observer } from 'mobx-react-lite'
import { MouseEventHandler, useCallback } from 'react'
import NoAccountWarningContent from './NoAccountWarningContent'
type Props = { viewControllerManager: ViewControllerManager }
const NoAccountWarning = observer(({ viewControllerManager }: Props) => {
const showAccountMenu: MouseEventHandler = useCallback(
(event) => {
event.stopPropagation()
viewControllerManager.accountMenuController.setShow(true)
},
[viewControllerManager],
)
const hideWarning = useCallback(() => {
viewControllerManager.noAccountWarningController.hide()
}, [viewControllerManager])
return (
<div className="mt-4 p-4 rounded-md shadow-sm grid grid-template-cols-1fr">
<h1 className="sk-h3 m-0 font-semibold">Data not backed up</h1>
<p className="m-0 mt-1 col-start-1 col-end-3">Sign in or register to back up your notes.</p>
<button className="sn-button small info mt-3 col-start-1 col-end-3 justify-self-start" onClick={showAccountMenu}>
Open Account menu
</button>
<button
onClick={hideWarning}
title="Ignore warning"
aria-label="Ignore warning"
style={{ height: '20px' }}
className="border-0 m-0 p-0 bg-transparent cursor-pointer rounded-md col-start-2 row-start-1 color-neutral hover:color-info"
>
<Icon type="close" className="block" />
</button>
</div>
)
})
NoAccountWarning.displayName = 'NoAccountWarning'
const NoAccountWarningWrapper = ({ viewControllerManager }: Props) => {
const canShow = viewControllerManager.noAccountWarningController.show
return canShow ? <NoAccountWarning viewControllerManager={viewControllerManager} /> : null
type Props = {
accountMenuController: AccountMenuController
noAccountWarningController: NoAccountWarningController
}
export default observer(NoAccountWarningWrapper)
const NoAccountWarning = ({ accountMenuController, noAccountWarningController }: Props) => {
const canShow = noAccountWarningController.show
return canShow ? (
<NoAccountWarningContent
accountMenuController={accountMenuController}
noAccountWarningController={noAccountWarningController}
/>
) : null
}
export default observer(NoAccountWarning)

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}
notesController={this.viewControllerManager.notesController}
noteTagsController={this.viewControllerManager.noteTagsController}
historyModalController={this.viewControllerManager.historyModalController}
/>
)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,75 +1,24 @@
import { WebApplication } from '@/Application/Application'
import { Action, ActionVerb, HistoryEntry, NoteHistoryEntry, RevisionListEntry, SNNote } from '@standardnotes/snjs'
import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController'
import { FeaturesClientInterface } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useState, useEffect, SetStateAction, Dispatch } from 'react'
import { FunctionComponent } from 'react'
import LegacyHistoryList from './LegacyHistoryList'
import RemoteHistoryList from './RemoteHistoryList'
import { RevisionType } from './RevisionType'
import SessionHistoryList from './SessionHistoryList'
import { LegacyHistoryEntry, RemoteRevisionListGroup, sortRevisionListIntoGroups } from './utils'
export enum RevisionListTabType {
Session = 'Session',
Remote = 'Remote',
Legacy = 'Legacy',
}
type Props = {
application: WebApplication
isFetchingRemoteHistory: boolean
note: SNNote
remoteHistory: RemoteRevisionListGroup[] | undefined
setIsFetchingSelectedRevision: Dispatch<SetStateAction<boolean>>
setSelectedRemoteEntry: Dispatch<SetStateAction<RevisionListEntry | undefined>>
setSelectedRevision: Dispatch<SetStateAction<HistoryEntry | LegacyHistoryEntry | undefined>>
setShowContentLockedScreen: Dispatch<SetStateAction<boolean>>
features: FeaturesClientInterface
noteHistoryController: NoteHistoryController
}
const HistoryListContainer: FunctionComponent<Props> = ({
application,
isFetchingRemoteHistory,
note,
remoteHistory,
setIsFetchingSelectedRevision,
setSelectedRemoteEntry,
setSelectedRevision,
setShowContentLockedScreen,
}) => {
const sessionHistory = sortRevisionListIntoGroups<NoteHistoryEntry>(
application.historyManager.sessionHistoryForItem(note) as NoteHistoryEntry[],
)
const [legacyHistory, setLegacyHistory] = useState<Action[]>()
const [selectedTab, setSelectedTab] = useState<RevisionListTabType>(RevisionListTabType.Remote)
useEffect(() => {
const fetchLegacyHistory = async () => {
const actionExtensions = application.actionsManager.getExtensions()
actionExtensions.forEach(async (ext) => {
const actionExtension = await application.actionsManager.loadExtensionInContextOfItem(ext, note)
if (!actionExtension) {
return
}
const isLegacyNoteHistoryExt = actionExtension?.actions.some((action) => action.verb === ActionVerb.Nested)
if (!isLegacyNoteHistoryExt) {
return
}
const legacyHistoryEntries = actionExtension.actions.filter((action) => action.subactions?.[0])
setLegacyHistory(legacyHistoryEntries)
})
}
fetchLegacyHistory().catch(console.error)
}, [application, note])
const HistoryListContainer: FunctionComponent<Props> = ({ features, noteHistoryController }) => {
const { legacyHistory, currentTab, selectTab } = noteHistoryController
const TabButton: FunctionComponent<{
type: RevisionListTabType
type: RevisionType
}> = ({ type }) => {
const isSelected = selectedTab === type
const isSelected = currentTab === type
return (
<button
@@ -77,8 +26,7 @@ const HistoryListContainer: FunctionComponent<Props> = ({
isSelected ? 'color-info font-medium shadow-bottom' : 'color-text'
}`}
onClick={() => {
setSelectedTab(type)
setSelectedRemoteEntry(undefined)
selectTab(type)
}}
>
{type}
@@ -86,98 +34,26 @@ const HistoryListContainer: FunctionComponent<Props> = ({
)
}
const fetchAndSetLegacyRevision = useCallback(
async (revisionListEntry: Action) => {
setSelectedRemoteEntry(undefined)
setSelectedRevision(undefined)
setIsFetchingSelectedRevision(true)
try {
if (!revisionListEntry.subactions?.[0]) {
throw new Error('Could not find revision action url')
}
const response = await application.actionsManager.runAction(revisionListEntry.subactions[0], note)
if (!response) {
throw new Error('Could not fetch revision')
}
setSelectedRevision(response.item as unknown as HistoryEntry)
} catch (error) {
console.error(error)
setSelectedRevision(undefined)
} finally {
setIsFetchingSelectedRevision(false)
}
},
[application.actionsManager, note, setIsFetchingSelectedRevision, setSelectedRemoteEntry, setSelectedRevision],
)
const fetchAndSetRemoteRevision = useCallback(
async (revisionListEntry: RevisionListEntry) => {
setShowContentLockedScreen(false)
if (application.features.hasMinimumRole(revisionListEntry.required_role)) {
setIsFetchingSelectedRevision(true)
setSelectedRevision(undefined)
setSelectedRemoteEntry(undefined)
try {
const remoteRevision = await application.historyManager.fetchRemoteRevision(note, revisionListEntry)
setSelectedRevision(remoteRevision)
setSelectedRemoteEntry(revisionListEntry)
} catch (err) {
console.error(err)
} finally {
setIsFetchingSelectedRevision(false)
}
} else {
setShowContentLockedScreen(true)
setSelectedRevision(undefined)
}
},
[
application,
note,
setIsFetchingSelectedRevision,
setSelectedRemoteEntry,
setSelectedRevision,
setShowContentLockedScreen,
],
)
const CurrentTabList = () => {
switch (currentTab) {
case RevisionType.Remote:
return <RemoteHistoryList features={features} noteHistoryController={noteHistoryController} />
case RevisionType.Session:
return <SessionHistoryList noteHistoryController={noteHistoryController} />
case RevisionType.Legacy:
return <LegacyHistoryList legacyHistory={legacyHistory} noteHistoryController={noteHistoryController} />
}
}
return (
<div className={'flex flex-col min-w-60 border-0 border-r-1px border-solid border-main overflow-auto h-full'}>
<div className="flex border-0 border-b-1 border-solid border-main">
<TabButton type={RevisionListTabType.Remote} />
<TabButton type={RevisionListTabType.Session} />
{legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionListTabType.Legacy} />}
<TabButton type={RevisionType.Remote} />
<TabButton type={RevisionType.Session} />
{legacyHistory && legacyHistory.length > 0 && <TabButton type={RevisionType.Legacy} />}
</div>
<div className={'min-h-0 overflow-auto py-1.5 h-full'}>
{selectedTab === RevisionListTabType.Session && (
<SessionHistoryList
sessionHistory={sessionHistory}
setSelectedRevision={setSelectedRevision}
setSelectedRemoteEntry={setSelectedRemoteEntry}
/>
)}
{selectedTab === RevisionListTabType.Remote && (
<RemoteHistoryList
application={application}
remoteHistory={remoteHistory}
isFetchingRemoteHistory={isFetchingRemoteHistory}
fetchAndSetRemoteRevision={fetchAndSetRemoteRevision}
/>
)}
{selectedTab === RevisionListTabType.Legacy && (
<LegacyHistoryList
legacyHistory={legacyHistory}
setSelectedRevision={setSelectedRevision}
setSelectedRemoteEntry={setSelectedRemoteEntry}
fetchAndSetLegacyRevision={fetchAndSetLegacyRevision}
/>
)}
<CurrentTabList />
</div>
</div>
)

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -5,4 +5,5 @@ export const ElementIds = {
FileTextPreview: 'file-text-preview',
EditorContent: 'editor-content',
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 {
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 }
contextMenuMaxHeight: number | 'auto' = 'auto'
showProtectedWarning = false
showRevisionHistoryModal = false
private itemListController!: ItemListController
override deinit() {
@@ -48,7 +47,6 @@ export class NotesController extends AbstractViewController {
contextMenuOpen: observable,
contextMenuPosition: observable,
showProtectedWarning: observable,
showRevisionHistoryModal: observable,
selectedNotes: computed,
firstSelectedNote: computed,
@@ -60,7 +58,6 @@ export class NotesController extends AbstractViewController {
setContextMenuPosition: action,
setContextMenuMaxHeight: action,
setShowProtectedWarning: action,
setShowRevisionHistoryModal: action,
unselectNotes: action,
})
}
@@ -367,8 +364,4 @@ export class NotesController extends AbstractViewController {
private getSelectedNotesList(): SNNote[] {
return Object.values(this.selectedNotes)
}
setShowRevisionHistoryModal(show: boolean): void {
this.showRevisionHistoryModal = show
}
}

View File

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

View File

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

View File

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

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