refactor: migrate remaining angular components to react (#833)
* refactor: menuRow directive to MenuRow component * refactor: migrate footer to react * refactor: migrate actions menu to react * refactor: migrate history menu to react * fix: click outside handler use capture to trigger event before re-render occurs which would otherwise cause node.contains to return incorrect result (specifically for the account menu) * refactor: migrate revision preview modal to react * refactor: migrate permissions modal to react * refactor: migrate password wizard to react * refactor: remove unused input modal directive * refactor: remove unused delay hide component * refactor: remove unused filechange directive * refactor: remove unused elemReady directive * refactor: remove unused sn-enter directive * refactor: remove unused lowercase directive * refactor: remove unused autofocus directive * refactor(wip): note view to react * refactor: use mutation observer to deinit textarea listeners * refactor: migrate challenge modal to react * refactor: migrate note group view to react * refactor(wip): migrate remaining classes * fix: navigation parent ref * refactor: fully remove angular assets * fix: account switcher * fix: application view state * refactor: remove unused password wizard type * fix: revision preview and permissions modal * fix: remove angular comment * refactor: react panel resizers for editor * feat: simple panel resizer * fix: use simple panel resizer everywhere * fix: simplify panel resizer state * chore: rename simple panel resizer to panel resizer * refactor: simplify column layout * fix: editor mount safety check * fix: use inline onLoad callback for iframe, as setting onload after it loads will never call it * chore: fix note view test * chore(deps): upgrade snjs
This commit is contained in:
311
app/assets/javascripts/components/HistoryMenu.tsx
Normal file
311
app/assets/javascripts/components/HistoryMenu.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { NoteHistoryEntry, PayloadContent, SNNote } from '@standardnotes/snjs';
|
||||
import { RevisionListEntry } from '@standardnotes/snjs';
|
||||
import { alertDialog, confirmDialog } from '@/services/alertService';
|
||||
import { PureComponent } from './Abstract/PureComponent';
|
||||
import { MenuRow } from './MenuRow';
|
||||
import { render } from 'preact';
|
||||
import { RevisionPreviewModal } from './RevisionPreviewModal';
|
||||
|
||||
type HistoryState = {
|
||||
sessionHistory?: NoteHistoryEntry[];
|
||||
remoteHistory?: RevisionListEntry[];
|
||||
fetchingRemoteHistory: boolean;
|
||||
autoOptimize: boolean;
|
||||
diskEnabled: boolean;
|
||||
showRemoteOptions?: boolean;
|
||||
showSessionOptions?: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
item: SNNote;
|
||||
};
|
||||
|
||||
export class HistoryMenu extends PureComponent<Props, HistoryState> {
|
||||
constructor(props: Props) {
|
||||
super(props, props.application);
|
||||
|
||||
this.state = {
|
||||
fetchingRemoteHistory: false,
|
||||
autoOptimize: this.props.application.historyManager.autoOptimize,
|
||||
diskEnabled: this.props.application.historyManager.isDiskEnabled(),
|
||||
sessionHistory:
|
||||
this.props.application.historyManager.sessionHistoryForItem(
|
||||
this.props.item
|
||||
) as NoteHistoryEntry[],
|
||||
};
|
||||
}
|
||||
|
||||
reloadState() {
|
||||
this.setState({
|
||||
fetchingRemoteHistory: this.state.fetchingRemoteHistory,
|
||||
autoOptimize: this.props.application.historyManager.autoOptimize,
|
||||
diskEnabled: this.props.application.historyManager.isDiskEnabled(),
|
||||
sessionHistory:
|
||||
this.props.application.historyManager.sessionHistoryForItem(
|
||||
this.props.item
|
||||
) as NoteHistoryEntry[],
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
super.componentDidMount();
|
||||
this.fetchRemoteHistory();
|
||||
}
|
||||
|
||||
fetchRemoteHistory = async () => {
|
||||
this.setState({ fetchingRemoteHistory: true });
|
||||
try {
|
||||
const remoteHistory =
|
||||
await this.props.application.historyManager.remoteHistoryForItem(
|
||||
this.props.item
|
||||
);
|
||||
this.setState({ remoteHistory });
|
||||
} finally {
|
||||
this.setState({ fetchingRemoteHistory: false });
|
||||
}
|
||||
};
|
||||
|
||||
private presentRevisionPreviewModal = (
|
||||
uuid: string,
|
||||
content: PayloadContent,
|
||||
title: string
|
||||
) => {
|
||||
render(
|
||||
<RevisionPreviewModal
|
||||
application={this.application}
|
||||
uuid={uuid}
|
||||
content={content}
|
||||
title={title}
|
||||
/>,
|
||||
document.body.appendChild(document.createElement('div'))
|
||||
);
|
||||
};
|
||||
|
||||
openSessionRevision = (revision: NoteHistoryEntry) => {
|
||||
this.presentRevisionPreviewModal(
|
||||
revision.payload.uuid,
|
||||
revision.payload.content,
|
||||
revision.previewTitle()
|
||||
);
|
||||
};
|
||||
|
||||
openRemoteRevision = async (revision: RevisionListEntry) => {
|
||||
this.setState({ fetchingRemoteHistory: true });
|
||||
|
||||
const remoteRevision =
|
||||
await this.props.application.historyManager.fetchRemoteRevision(
|
||||
this.props.item.uuid,
|
||||
revision
|
||||
);
|
||||
|
||||
this.setState({ fetchingRemoteHistory: false });
|
||||
|
||||
if (!remoteRevision) {
|
||||
alertDialog({
|
||||
text: 'The remote revision could not be loaded. Please try again later.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.presentRevisionPreviewModal(
|
||||
remoteRevision.payload.uuid,
|
||||
remoteRevision.payload.content,
|
||||
this.previewRemoteHistoryTitle(revision)
|
||||
);
|
||||
};
|
||||
|
||||
classForSessionRevision = (revision: NoteHistoryEntry) => {
|
||||
const vector = revision.operationVector();
|
||||
if (vector === 0) {
|
||||
return 'default';
|
||||
} else if (vector === 1) {
|
||||
return 'success';
|
||||
} else if (vector === -1) {
|
||||
return 'danger';
|
||||
}
|
||||
};
|
||||
|
||||
clearItemSessionHistory = async () => {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: 'Are you sure you want to delete the local session history for this note?',
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
this.props.application.historyManager.clearHistoryForItem(
|
||||
this.props.item
|
||||
);
|
||||
this.reloadState();
|
||||
}
|
||||
};
|
||||
|
||||
clearAllSessionHistory = async () => {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: 'Are you sure you want to delete the local session history for all notes?',
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
await this.props.application.historyManager.clearAllHistory();
|
||||
this.reloadState();
|
||||
}
|
||||
};
|
||||
|
||||
toggleSessionHistoryDiskSaving = async () => {
|
||||
if (!this.state.diskEnabled) {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text:
|
||||
'Are you sure you want to save history to disk? This will decrease general ' +
|
||||
'performance, especially as you type. You are advised to disable this feature ' +
|
||||
'if you experience any lagging.',
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
this.props.application.historyManager.toggleDiskSaving();
|
||||
}
|
||||
} else {
|
||||
this.props.application.historyManager.toggleDiskSaving();
|
||||
}
|
||||
this.reloadState();
|
||||
};
|
||||
|
||||
toggleSessionHistoryAutoOptimize = () => {
|
||||
this.props.application.historyManager.toggleAutoOptimize();
|
||||
this.reloadState();
|
||||
};
|
||||
|
||||
previewRemoteHistoryTitle(revision: RevisionListEntry) {
|
||||
return new Date(revision.created_at).toLocaleString();
|
||||
}
|
||||
|
||||
toggleShowRemoteOptions = ($event: Event) => {
|
||||
$event.stopPropagation();
|
||||
this.setState({
|
||||
showRemoteOptions: !this.state.showRemoteOptions,
|
||||
});
|
||||
};
|
||||
|
||||
toggleShowSessionOptions = ($event: Event) => {
|
||||
$event.stopPropagation();
|
||||
this.setState({
|
||||
showSessionOptions: !this.state.showSessionOptions,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="history-menu" className="sn-component">
|
||||
<div className="sk-menu-panel dropdown-menu">
|
||||
<div className="sk-menu-panel-header">
|
||||
<div className="sk-menu-panel-header-title">
|
||||
Session
|
||||
<div className="sk-menu-panel-header-subtitle">
|
||||
{this.state.sessionHistory?.length || 'No'} revisions
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
className="sk-a info sk-h5"
|
||||
onClick={this.toggleShowSessionOptions}
|
||||
>
|
||||
Options
|
||||
</a>
|
||||
</div>
|
||||
{this.state.showSessionOptions && (
|
||||
<div>
|
||||
<MenuRow
|
||||
action={this.clearItemSessionHistory}
|
||||
label="Clear note local history"
|
||||
/>
|
||||
<MenuRow
|
||||
action={this.clearAllSessionHistory}
|
||||
label="Clear all local history"
|
||||
/>
|
||||
<MenuRow
|
||||
action={this.toggleSessionHistoryAutoOptimize}
|
||||
label={
|
||||
(this.state.autoOptimize ? 'Disable' : 'Enable') +
|
||||
' auto cleanup'
|
||||
}
|
||||
>
|
||||
<div className="sk-sublabel">
|
||||
Automatically cleans up small revisions to conserve space.
|
||||
</div>
|
||||
</MenuRow>
|
||||
<MenuRow
|
||||
action={this.toggleSessionHistoryDiskSaving}
|
||||
label={
|
||||
(this.state.diskEnabled ? 'Disable' : 'Enable') +
|
||||
' saving history to disk'
|
||||
}
|
||||
>
|
||||
<div className="sk-sublabel">
|
||||
Saving to disk is not recommended. Decreases performance and
|
||||
increases app loading time and memory footprint.
|
||||
</div>
|
||||
</MenuRow>
|
||||
</div>
|
||||
)}
|
||||
{this.state.sessionHistory?.map((revision, index) => {
|
||||
return (
|
||||
<MenuRow
|
||||
key={index}
|
||||
action={this.openSessionRevision}
|
||||
actionArgs={[revision]}
|
||||
label={revision.previewTitle()}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
this.classForSessionRevision(revision) +
|
||||
' sk-sublabel opaque'
|
||||
}
|
||||
>
|
||||
{revision.previewSubTitle()}
|
||||
</div>
|
||||
</MenuRow>
|
||||
);
|
||||
})}
|
||||
<div className="sk-menu-panel-header">
|
||||
<div className="sk-menu-panel-header-title">
|
||||
Remote
|
||||
<div className="sk-menu-panel-header-subtitle">
|
||||
{this.state.remoteHistory?.length || 'No'} revisions
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
onClick={this.toggleShowRemoteOptions}
|
||||
className="sk-a info sk-h5"
|
||||
>
|
||||
Options
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{this.state.showRemoteOptions && (
|
||||
<MenuRow
|
||||
action={this.fetchRemoteHistory}
|
||||
label="Refresh"
|
||||
disabled={this.state.fetchingRemoteHistory}
|
||||
spinnerClass={
|
||||
this.state.fetchingRemoteHistory ? 'info' : undefined
|
||||
}
|
||||
>
|
||||
<div className="sk-sublabel">Fetch history from server.</div>
|
||||
</MenuRow>
|
||||
)}
|
||||
{this.state.remoteHistory?.map((revision, index) => {
|
||||
return (
|
||||
<MenuRow
|
||||
key={index}
|
||||
action={this.openRemoteRevision}
|
||||
actionArgs={[revision]}
|
||||
label={this.previewRemoteHistoryTitle(revision)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user