Release/3.6.0 (#527)
* feat: (wip) authorize note access
* fix: remove multiEditorEnabled
* refactor: update SNJS + eslint
* refactor: remove privileges in favor of SNJS protections
* fix: do not close editor when editing an archived note
* chore: remove progress indicator for webpack dev server
* fix: add rel="noreferrer" to bugsnag links
* chore(deps): upgrade snjs
* chore(deps): upgrade snjs
* feat: batch manager protection + react challenge modal + eslint fix
* fix: lint errors
* fix: launch state error
* fix: challenge modal: cancel instead of dismiss when pressing escape
* feat: improve focus styles
* fix: cancel session revoking when pressing escape on confirm dialog
* fix: lint warning
* chore(deps): upgrade minor versions
* feat: make SNWebCrypto a constant
* feat: add random identifier to bugsnag reports
* fix: check onKeyUp instead of onKeyDown
* feat: implement SNJS backup file password retrieval
* chore(deps): upgrade snjs
* feat: display warning banner when using the app with no account
* fix: properly color svg button
* fix: wording
* fix: hide account warning after login + improve key storage wording
* chore(deps): upgrade stylekit
* feat: use stylekit fonts for the editor
* chore(deps): bump nokogiri from 1.10.8 to 1.11.1 (#511)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.8 to 1.11.1.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.8...v1.11.1)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
* chore(deps): bump ini from 1.3.5 to 1.3.8 (#504)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
* fix: rename master branch to main
* fix: add missing placeholders for submodules (#516)
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
* chore(deps): upgrade snjs, babel, typescript, reach, mobx, preact
* feat: clear protection session
* fix: use correct close icon size
* fix: hide protections paragraph when no account or passcode exist
* chore(deps): remove unused dependencies
* fix: button casing
* feat: implement SNApplication.hasProtectionSources
* chore(version): 3.6.0
* feat: enable sessions management for every build
* feat: make "Protected" flag more subtle
* fix: only match protected note title
* fix: remove inconsistencies between protected note label and date
* feat: show warning when protecting a note with no protection source
* feat: make unprotecting a note a protected action
* chore(deps): upgrade snjs
* chore(version): 3.6.0-beta01
* fix: run docker with root to fix crashing on Linux (undoes 62da387d3a) (#525)
* feat: make encrypted backups protected (#524)
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: proletarius101 <54175165+proletarius101@users.noreply.github.com>
Co-authored-by: Darius JJ Chuck <79410894+standarius@users.noreply.github.com>
Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
This commit is contained in:
@@ -30,7 +30,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
this.state = {
|
||||
...this.getInitialState(),
|
||||
...this.state,
|
||||
}
|
||||
};
|
||||
this.addAppEventObserver();
|
||||
this.addAppStateObserver();
|
||||
this.templateReady = true;
|
||||
@@ -77,10 +77,16 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
|
||||
*/
|
||||
this.state = Object.freeze(Object.assign({}, this.state, state));
|
||||
resolve();
|
||||
this.afterStateChange();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
afterStateChange(): void {
|
||||
}
|
||||
|
||||
/** @returns a promise that resolves after the UI has been updated. */
|
||||
flushUI() {
|
||||
return this.$timeout();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { WebDirective } from '@/types';
|
||||
|
||||
class AccountSwitcherCtrl extends PureViewCtrl<{}, {
|
||||
class AccountSwitcherCtrl extends PureViewCtrl<unknown, {
|
||||
descriptors: ApplicationDescriptor[];
|
||||
editingDescriptor?: ApplicationDescriptor
|
||||
}> {
|
||||
@@ -38,7 +38,7 @@ class AccountSwitcherCtrl extends PureViewCtrl<{}, {
|
||||
reloadApplications() {
|
||||
this.setState({
|
||||
descriptors: this.mainApplicationGroup.getDescriptors()
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/** @template */
|
||||
@@ -63,7 +63,7 @@ class AccountSwitcherCtrl extends PureViewCtrl<{}, {
|
||||
this.setState({ editingDescriptor: descriptor }).then(() => {
|
||||
const input = this.inputForDescriptor(descriptor);
|
||||
input?.focus();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/** @template */
|
||||
@@ -71,7 +71,7 @@ class AccountSwitcherCtrl extends PureViewCtrl<{}, {
|
||||
this.mainApplicationGroup.renameDescriptor(
|
||||
this.state.editingDescriptor!,
|
||||
this.state.editingDescriptor!.label
|
||||
)
|
||||
);
|
||||
this.setState({ editingDescriptor: undefined });
|
||||
}
|
||||
|
||||
|
||||
@@ -26,4 +26,12 @@
|
||||
path(d="M480 256l-75.53-33.53L256.1 290.6l-148.77-68.17L32 256l224 102 224-102z")
|
||||
sessions-modal(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
challenge-modal(
|
||||
ng-repeat="challenge in self.challenges track by challenge.id"
|
||||
class="sk-modal"
|
||||
application="self.application"
|
||||
challenge="challenge"
|
||||
on-dismiss="self.removeChallenge(challenge)"
|
||||
)
|
||||
|
||||
@@ -2,8 +2,8 @@ import { RootScopeMessages } from './../../messages';
|
||||
import { WebDirective } from '@/types';
|
||||
import { getPlatformString } from '@/utils';
|
||||
import template from './application-view.pug';
|
||||
import { AppStateEvent } from '@/ui_models/app_state';
|
||||
import { ApplicationEvent } from '@standardnotes/snjs';
|
||||
import { AppStateEvent, PanelResizedData } from '@/ui_models/app_state';
|
||||
import { ApplicationEvent, Challenge, removeFromArray } from '@standardnotes/snjs';
|
||||
import {
|
||||
PANEL_NAME_NOTES,
|
||||
PANEL_NAME_TAGS
|
||||
@@ -14,37 +14,44 @@ import {
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { alertDialog } from '@/services/alertService';
|
||||
|
||||
class ApplicationViewCtrl extends PureViewCtrl {
|
||||
private $location?: ng.ILocationService
|
||||
private $rootScope?: ng.IRootScopeService
|
||||
class ApplicationViewCtrl extends PureViewCtrl<unknown, {
|
||||
ready?: boolean,
|
||||
needsUnlock?: boolean,
|
||||
appClass: string,
|
||||
}> {
|
||||
public platformString: string
|
||||
private notesCollapsed = false
|
||||
private tagsCollapsed = false
|
||||
/**
|
||||
* To prevent stale state reads (setState is async),
|
||||
* challenges is a mutable array
|
||||
*/
|
||||
private challenges: Challenge[] = [];
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$location: ng.ILocationService,
|
||||
$rootScope: ng.IRootScopeService,
|
||||
private $location: ng.ILocationService,
|
||||
private $rootScope: ng.IRootScopeService,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
this.$location = $location;
|
||||
this.$rootScope = $rootScope;
|
||||
this.platformString = getPlatformString();
|
||||
this.state = { appClass: '' };
|
||||
this.state = this.getInitialState();
|
||||
this.onDragDrop = this.onDragDrop.bind(this);
|
||||
this.onDragOver = this.onDragOver.bind(this);
|
||||
this.addDragDropHandlers();
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.$location = undefined;
|
||||
this.$rootScope = undefined;
|
||||
(this.application as any) = undefined;
|
||||
(this.$location as unknown) = undefined;
|
||||
(this.$rootScope as unknown) = undefined;
|
||||
(this.application as unknown) = undefined;
|
||||
window.removeEventListener('dragover', this.onDragOver, true);
|
||||
window.removeEventListener('drop', this.onDragDrop, true);
|
||||
(this.onDragDrop as any) = undefined;
|
||||
(this.onDragOver as any) = undefined;
|
||||
(this.onDragDrop as unknown) = undefined;
|
||||
(this.onDragOver as unknown) = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -53,23 +60,38 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
||||
this.loadApplication();
|
||||
}
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
appClass: '',
|
||||
challenges: [],
|
||||
};
|
||||
}
|
||||
|
||||
async loadApplication() {
|
||||
this.application!.componentManager!.setDesktopManager(
|
||||
this.application!.getDesktopService()
|
||||
this.application.componentManager.setDesktopManager(
|
||||
this.application.getDesktopService()
|
||||
);
|
||||
await this.application!.prepareForLaunch({
|
||||
await this.application.prepareForLaunch({
|
||||
receiveChallenge: async (challenge) => {
|
||||
this.application!.promptForChallenge(challenge);
|
||||
this.$timeout(() => {
|
||||
this.challenges.push(challenge);
|
||||
});
|
||||
}
|
||||
});
|
||||
await this.application!.launch();
|
||||
await this.application.launch();
|
||||
}
|
||||
|
||||
public async removeChallenge(challenge: Challenge) {
|
||||
this.$timeout(() => {
|
||||
removeFromArray(this.challenges, challenge);
|
||||
});
|
||||
}
|
||||
|
||||
async onAppStart() {
|
||||
super.onAppStart();
|
||||
this.setState({
|
||||
ready: true,
|
||||
needsUnlock: this.application!.hasPasscode()
|
||||
needsUnlock: this.application.hasPasscode()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,8 +102,8 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
||||
}
|
||||
|
||||
onUpdateAvailable() {
|
||||
this.$rootScope!.$broadcast(RootScopeMessages.NewUpdateAvailable);
|
||||
};
|
||||
this.$rootScope.$broadcast(RootScopeMessages.NewUpdateAvailable);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async onAppEvent(eventName: ApplicationEvent) {
|
||||
@@ -98,21 +120,22 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async onAppStateEvent(eventName: AppStateEvent, data?: any) {
|
||||
async onAppStateEvent(eventName: AppStateEvent, data?: unknown) {
|
||||
if (eventName === AppStateEvent.PanelResized) {
|
||||
if (data.panel === PANEL_NAME_NOTES) {
|
||||
this.notesCollapsed = data.collapsed;
|
||||
const { panel, collapsed } = data as PanelResizedData;
|
||||
if (panel === PANEL_NAME_NOTES) {
|
||||
this.notesCollapsed = collapsed;
|
||||
}
|
||||
if (data.panel === PANEL_NAME_TAGS) {
|
||||
this.tagsCollapsed = data.collapsed;
|
||||
if (panel === PANEL_NAME_TAGS) {
|
||||
this.tagsCollapsed = collapsed;
|
||||
}
|
||||
let appClass = "";
|
||||
if (this.notesCollapsed) { appClass += "collapsed-notes"; }
|
||||
if (this.tagsCollapsed) { appClass += " collapsed-tags"; }
|
||||
this.setState({ appClass });
|
||||
} else if (eventName === AppStateEvent.WindowDidFocus) {
|
||||
if (!(await this.application!.isLocked())) {
|
||||
this.application!.sync();
|
||||
if (!(await this.application.isLocked())) {
|
||||
this.application.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,29 +151,29 @@ class ApplicationViewCtrl extends PureViewCtrl {
|
||||
}
|
||||
|
||||
onDragOver(event: DragEvent) {
|
||||
if (event.dataTransfer!.files.length > 0) {
|
||||
if (event.dataTransfer?.files.length) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onDragDrop(event: DragEvent) {
|
||||
if (event.dataTransfer!.files.length > 0) {
|
||||
if (event.dataTransfer?.files.length) {
|
||||
event.preventDefault();
|
||||
this.application!.alertService!.alert(
|
||||
STRING_DEFAULT_FILE_ERROR
|
||||
);
|
||||
void alertDialog({
|
||||
text: STRING_DEFAULT_FILE_ERROR
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleDemoSignInFromParams() {
|
||||
if (
|
||||
this.$location!.search().demo === 'true' &&
|
||||
this.$location.search().demo === 'true' &&
|
||||
!this.application.hasAccount()
|
||||
) {
|
||||
await this.application!.setHost(
|
||||
await this.application.setHost(
|
||||
'https://syncing-server-demo.standardnotes.org'
|
||||
);
|
||||
this.application!.signIn(
|
||||
this.application.signIn(
|
||||
'demo@standardnotes.org',
|
||||
'password',
|
||||
);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
.sk-modal-background(ng-click="ctrl.cancel()")
|
||||
.challenge-modal.sk-modal-content(ng-if='ctrl.templateReady')
|
||||
.sn-component
|
||||
.sk-panel
|
||||
.sk-panel-header
|
||||
.sk-panel-header-title {{ctrl.challenge.modalTitle}}
|
||||
.sk-panel-content
|
||||
.sk-panel-section
|
||||
.sk-p.sk-panel-row.centered.prompt
|
||||
strong {{ctrl.challenge.heading}}
|
||||
.sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.challenge.subheading')
|
||||
| {{ctrl.challenge.subheading}}
|
||||
.sk-panel-section
|
||||
div(ng-repeat="prompt in ctrl.state.prompts track by prompt.id")
|
||||
.sk-panel-row
|
||||
input.sk-input.contrast(
|
||||
ng-model="ctrl.state.values[prompt.id].value"
|
||||
should-focus="$index == 0"
|
||||
sn-autofocus="true"
|
||||
sn-enter="ctrl.submit()" ,
|
||||
ng-change="ctrl.onTextValueChange(prompt)"
|
||||
ng-attr-type="{{prompt.secureTextEntry ? 'password' : 'text'}}",
|
||||
ng-attr-placeholder="{{prompt.title}}"
|
||||
)
|
||||
.sk-panel-row.centered
|
||||
label.sk-label.danger(
|
||||
ng-if="ctrl.state.values[prompt.id].invalid"
|
||||
) Invalid authentication. Please try again.
|
||||
.sk-panel-footer.extra-padding
|
||||
.sk-button.info.big.block.bold(
|
||||
ng-click="ctrl.submit()",
|
||||
ng-class="{'info' : !ctrl.state.processing, 'neutral': ctrl.state.processing}"
|
||||
ng-disabled="ctrl.state.processing"
|
||||
)
|
||||
.sk-label {{ctrl.state.processing ? 'Generating Keys...' : 'Submit'}}
|
||||
.sk-panel-row(ng-if="ctrl.challenge.cancelable")
|
||||
a.sk-panel-row.sk-a.info.centered(
|
||||
ng-if="ctrl.challenge.cancelable"
|
||||
ng-click="ctrl.cancel()"
|
||||
) Cancel
|
||||
|
||||
.sk-panel-footer(ng-if="ctrl.state.showForgotPasscodeLink")
|
||||
a.sk-panel-row.sk-a.info.centered(
|
||||
ng-if="!ctrl.state.forgotPasscode"
|
||||
ng-click="ctrl.onForgotPasscodeClick()"
|
||||
) Forgot your passcode?
|
||||
p.sk-panel-row.sk-p(ng-if="ctrl.state.forgotPasscode").
|
||||
{{
|
||||
ctrl.state.hasAccount
|
||||
? "If you forgot your application passcode, your only option is to clear
|
||||
your local data from this device and sign back in to your account."
|
||||
: "If you forgot your application passcode, your only option is
|
||||
to delete your data."
|
||||
}}
|
||||
a.sk-panel-row.sk-a.danger.centered(
|
||||
ng-if="ctrl.state.forgotPasscode"
|
||||
ng-click="ctrl.destroyLocalData()"
|
||||
) Delete Local Data
|
||||
.sk-panel-row
|
||||
@@ -1,208 +0,0 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import template from './challenge-modal.pug';
|
||||
import {
|
||||
ChallengeValue,
|
||||
removeFromArray,
|
||||
Challenge,
|
||||
ChallengeReason,
|
||||
ChallengePrompt
|
||||
} from '@standardnotes/snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { WebDirective } from '@/types';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import {
|
||||
STRING_SIGN_OUT_CONFIRMATION,
|
||||
} from '@/strings';
|
||||
|
||||
type InputValue = {
|
||||
prompt: ChallengePrompt;
|
||||
value: string;
|
||||
invalid: boolean;
|
||||
}
|
||||
|
||||
type Values = Record<number, InputValue>
|
||||
|
||||
type ChallengeModalState = {
|
||||
prompts: ChallengePrompt[]
|
||||
values: Partial<Values>
|
||||
processing: boolean,
|
||||
forgotPasscode: boolean,
|
||||
showForgotPasscodeLink: boolean,
|
||||
processingPrompts: ChallengePrompt[],
|
||||
hasAccount: boolean,
|
||||
}
|
||||
|
||||
class ChallengeModalCtrl extends PureViewCtrl<{}, ChallengeModalState> {
|
||||
private $element: JQLite
|
||||
application!: WebApplication
|
||||
challenge!: Challenge
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state as ChallengeModalState;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
const values = {} as Values;
|
||||
const prompts = this.challenge.prompts;
|
||||
for (const prompt of prompts) {
|
||||
values[prompt.id] = {
|
||||
prompt,
|
||||
value: '',
|
||||
invalid: false
|
||||
};
|
||||
}
|
||||
const showForgotPasscodeLink = [
|
||||
ChallengeReason.ApplicationUnlock,
|
||||
ChallengeReason.Migration
|
||||
].includes(this.challenge.reason);
|
||||
this.setState({
|
||||
prompts,
|
||||
values,
|
||||
processing: false,
|
||||
forgotPasscode: false,
|
||||
showForgotPasscodeLink,
|
||||
hasAccount: this.application.hasAccount(),
|
||||
processingPrompts: []
|
||||
});
|
||||
this.application.addChallengeObserver(
|
||||
this.challenge,
|
||||
{
|
||||
onValidValue: (value) => {
|
||||
this.getState().values[value.prompt.id]!.invalid = false;
|
||||
removeFromArray(this.state.processingPrompts, value.prompt);
|
||||
this.reloadProcessingStatus();
|
||||
},
|
||||
onInvalidValue: (value) => {
|
||||
this.getState().values[value.prompt.id]!.invalid = true;
|
||||
/** If custom validation, treat all values together and not individually */
|
||||
if (!value.prompt.validates) {
|
||||
this.setState({ processingPrompts: [], processing: false });
|
||||
} else {
|
||||
removeFromArray(this.state.processingPrompts, value.prompt);
|
||||
this.reloadProcessingStatus();
|
||||
}
|
||||
},
|
||||
onComplete: () => {
|
||||
this.dismiss();
|
||||
},
|
||||
onCancel: () => {
|
||||
this.dismiss();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deinit() {
|
||||
(this.application as any) = undefined;
|
||||
(this.challenge as any) = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
reloadProcessingStatus() {
|
||||
return this.setState({
|
||||
processing: this.state.processingPrompts.length > 0
|
||||
});
|
||||
}
|
||||
|
||||
async destroyLocalData() {
|
||||
if (await confirmDialog({
|
||||
text: STRING_SIGN_OUT_CONFIRMATION,
|
||||
confirmButtonStyle: "danger"
|
||||
})) {
|
||||
await this.application.signOut();
|
||||
this.dismiss();
|
||||
};
|
||||
}
|
||||
|
||||
/** @template */
|
||||
cancel() {
|
||||
if (this.challenge.cancelable) {
|
||||
this.application!.cancelChallenge(this.challenge);
|
||||
}
|
||||
}
|
||||
|
||||
onForgotPasscodeClick() {
|
||||
this.setState({
|
||||
forgotPasscode: true
|
||||
});
|
||||
}
|
||||
|
||||
onTextValueChange(prompt: ChallengePrompt) {
|
||||
const values = this.getState().values;
|
||||
values[prompt.id]!.invalid = false;
|
||||
this.setState({ values });
|
||||
}
|
||||
|
||||
validate() {
|
||||
const failed = [];
|
||||
for (const prompt of this.getState().prompts) {
|
||||
const value = this.getState().values[prompt.id];
|
||||
if (!value || value.value.length === 0) {
|
||||
this.getState().values[prompt.id]!.invalid = true;
|
||||
}
|
||||
}
|
||||
return failed.length === 0;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
await this.setState({ processing: true });
|
||||
const values: ChallengeValue[] = [];
|
||||
for (const inputValue of Object.values(this.getState().values)) {
|
||||
const rawValue = inputValue!!.value;
|
||||
const value = new ChallengeValue(inputValue!.prompt, rawValue);
|
||||
values.push(value);
|
||||
}
|
||||
const processingPrompts = values.map((v) => v.prompt);
|
||||
await this.setState({
|
||||
processingPrompts: processingPrompts,
|
||||
processing: processingPrompts.length > 0
|
||||
})
|
||||
/**
|
||||
* Unfortunately neccessary to wait 50ms so that the above setState call completely
|
||||
* updates the UI to change processing state, before we enter into UI blocking operation
|
||||
* (crypto key generation)
|
||||
*/
|
||||
this.$timeout(() => {
|
||||
if (values.length > 0) {
|
||||
this.application.submitValuesForChallenge(this.challenge, values);
|
||||
} else {
|
||||
this.setState({ processing: false });
|
||||
}
|
||||
}, 50)
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class ChallengeModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = ChallengeModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
challenge: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
409
app/assets/javascripts/views/challenge_modal/challenge_modal.tsx
Normal file
409
app/assets/javascripts/views/challenge_modal/challenge_modal.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { Dialog } from '@reach/dialog';
|
||||
import {
|
||||
ChallengeValue,
|
||||
removeFromArray,
|
||||
Challenge,
|
||||
ChallengeReason,
|
||||
ChallengePrompt,
|
||||
ChallengeValidation,
|
||||
ProtectionSessionDurations,
|
||||
} from '@standardnotes/snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { WebDirective } from '@/types';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
import { STRING_SIGN_OUT_CONFIRMATION } from '@/strings';
|
||||
import { Ref, render } from 'preact';
|
||||
import { useRef } from 'preact/hooks';
|
||||
import ng from 'angular';
|
||||
|
||||
type InputValue = {
|
||||
prompt: ChallengePrompt;
|
||||
value: string | number | boolean;
|
||||
invalid: boolean;
|
||||
};
|
||||
|
||||
type Values = Record<number, InputValue>;
|
||||
|
||||
type ChallengeModalState = {
|
||||
prompts: ChallengePrompt[];
|
||||
values: Partial<Values>;
|
||||
processing: boolean;
|
||||
forgotPasscode: boolean;
|
||||
showForgotPasscodeLink: boolean;
|
||||
processingPrompts: ChallengePrompt[];
|
||||
hasAccount: boolean;
|
||||
protectedNoteAccessDuration: number;
|
||||
};
|
||||
|
||||
class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
|
||||
application!: WebApplication;
|
||||
challenge!: Challenge;
|
||||
onDismiss!: () => void;
|
||||
submitting = false;
|
||||
|
||||
/** @template */
|
||||
protectionsSessionDurations = ProtectionSessionDurations;
|
||||
protectionsSessionValidation = ChallengeValidation.ProtectionSessionDuration;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
private $element: ng.IRootElementService,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state as ChallengeModalState;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
const values = {} as Values;
|
||||
const prompts = this.challenge.prompts;
|
||||
for (const prompt of prompts) {
|
||||
values[prompt.id] = {
|
||||
prompt,
|
||||
value: prompt.initialValue ?? '',
|
||||
invalid: false,
|
||||
};
|
||||
}
|
||||
const showForgotPasscodeLink = [
|
||||
ChallengeReason.ApplicationUnlock,
|
||||
ChallengeReason.Migration,
|
||||
].includes(this.challenge.reason);
|
||||
this.setState({
|
||||
prompts,
|
||||
values,
|
||||
processing: false,
|
||||
forgotPasscode: false,
|
||||
showForgotPasscodeLink,
|
||||
hasAccount: this.application.hasAccount(),
|
||||
processingPrompts: [],
|
||||
protectedNoteAccessDuration: ProtectionSessionDurations[0].valueInSeconds,
|
||||
});
|
||||
this.application.addChallengeObserver(this.challenge, {
|
||||
onValidValue: (value) => {
|
||||
this.state.values[value.prompt.id]!.invalid = false;
|
||||
removeFromArray(this.state.processingPrompts, value.prompt);
|
||||
this.reloadProcessingStatus();
|
||||
/** Trigger UI update */
|
||||
this.afterStateChange();
|
||||
},
|
||||
onInvalidValue: (value) => {
|
||||
this.state.values[value.prompt.id]!.invalid = true;
|
||||
/** If custom validation, treat all values together and not individually */
|
||||
if (!value.prompt.validates) {
|
||||
this.setState({ processingPrompts: [], processing: false });
|
||||
} else {
|
||||
removeFromArray(this.state.processingPrompts, value.prompt);
|
||||
this.reloadProcessingStatus();
|
||||
}
|
||||
/** Trigger UI update */
|
||||
this.afterStateChange();
|
||||
},
|
||||
onComplete: () => {
|
||||
this.dismiss();
|
||||
},
|
||||
onCancel: () => {
|
||||
this.dismiss();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
deinit() {
|
||||
(this.application as any) = undefined;
|
||||
(this.challenge as any) = undefined;
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
reloadProcessingStatus() {
|
||||
return this.setState({
|
||||
processing: this.state.processingPrompts.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
async destroyLocalData() {
|
||||
if (
|
||||
await confirmDialog({
|
||||
text: STRING_SIGN_OUT_CONFIRMATION,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
) {
|
||||
await this.application.signOut();
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/** @template */
|
||||
cancel() {
|
||||
if (this.challenge.cancelable) {
|
||||
this.application!.cancelChallenge(this.challenge);
|
||||
}
|
||||
}
|
||||
|
||||
onForgotPasscodeClick() {
|
||||
this.setState({
|
||||
forgotPasscode: true,
|
||||
});
|
||||
}
|
||||
|
||||
onTextValueChange(prompt: ChallengePrompt) {
|
||||
const values = this.getState().values;
|
||||
values[prompt.id]!.invalid = false;
|
||||
this.setState({ values });
|
||||
}
|
||||
|
||||
onNumberValueChange(prompt: ChallengePrompt, value: number) {
|
||||
const values = this.state.values;
|
||||
values[prompt.id]!.invalid = false;
|
||||
values[prompt.id]!.value = value;
|
||||
this.setState({ values });
|
||||
}
|
||||
|
||||
validate() {
|
||||
let failed = 0;
|
||||
for (const prompt of this.state.prompts) {
|
||||
const value = this.state.values[prompt.id]!;
|
||||
if (typeof value.value === 'string' && value.value.length === 0) {
|
||||
this.state.values[prompt.id]!.invalid = true;
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
return failed === 0;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
if (this.submitting) {
|
||||
return;
|
||||
}
|
||||
this.submitting = true;
|
||||
await this.setState({ processing: true });
|
||||
const values: ChallengeValue[] = [];
|
||||
for (const inputValue of Object.values(this.getState().values)) {
|
||||
const rawValue = inputValue!.value;
|
||||
const value = new ChallengeValue(inputValue!.prompt, rawValue);
|
||||
values.push(value);
|
||||
}
|
||||
const processingPrompts = values.map((v) => v.prompt);
|
||||
await this.setState({
|
||||
processingPrompts: processingPrompts,
|
||||
processing: processingPrompts.length > 0,
|
||||
});
|
||||
/**
|
||||
* Unfortunately neccessary to wait 50ms so that the above setState call completely
|
||||
* updates the UI to change processing state, before we enter into UI blocking operation
|
||||
* (crypto key generation)
|
||||
*/
|
||||
this.$timeout(() => {
|
||||
if (values.length > 0) {
|
||||
this.application.submitValuesForChallenge(this.challenge, values);
|
||||
} else {
|
||||
this.setState({ processing: false });
|
||||
}
|
||||
this.submitting = false;
|
||||
}, 50);
|
||||
}
|
||||
|
||||
afterStateChange() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.onDismiss();
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
render(<></>, this.$element[0]);
|
||||
}
|
||||
|
||||
private render() {
|
||||
if (!this.state.prompts) return;
|
||||
render(<ChallengeModalView ctrl={this} />, this.$element[0]);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChallengeModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
// this.template = template;
|
||||
this.controller = ChallengeModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
challenge: '=',
|
||||
application: '=',
|
||||
onDismiss: '&',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function ChallengeModalView({ ctrl }: { ctrl: ChallengeModalCtrl }) {
|
||||
const initialFocusRef = useRef<HTMLInputElement>();
|
||||
return (
|
||||
<Dialog
|
||||
initialFocusRef={initialFocusRef}
|
||||
onDismiss={() => {
|
||||
if (ctrl.challenge.cancelable) {
|
||||
ctrl.cancel();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="challenge-modal sk-modal-content">
|
||||
<div className="sn-component">
|
||||
<div className="sk-panel">
|
||||
<div className="sk-panel-header">
|
||||
<div className="sk-panel-header-title">
|
||||
{ctrl.challenge.modalTitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sk-panel-content">
|
||||
<div className="sk-panel-section">
|
||||
<div className="sk-p sk-panel-row centered prompt">
|
||||
<strong>{ctrl.challenge.heading}</strong>
|
||||
</div>
|
||||
{ctrl.challenge.subheading && (
|
||||
<div className="sk-p sk-panel-row centered subprompt">
|
||||
{ctrl.challenge.subheading}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="sk-panel-section">
|
||||
{ChallengePrompts({ ctrl, initialFocusRef })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sk-panel-footer extra-padding">
|
||||
<div
|
||||
className={
|
||||
'sk-button big block bold ' +
|
||||
(ctrl.state.processing ? 'neutral' : 'info')
|
||||
}
|
||||
disabled={ctrl.state.processing}
|
||||
onClick={() => ctrl.submit()}
|
||||
>
|
||||
<div className="sk-label">
|
||||
{ctrl.state.processing ? 'Generating Keys…' : 'Submit'}
|
||||
</div>
|
||||
</div>
|
||||
{ctrl.challenge.cancelable && (
|
||||
<>
|
||||
<div className="sk-panel-row"></div>
|
||||
<a
|
||||
className="sk-panel-row sk-a info centered"
|
||||
onClick={() => ctrl.cancel()}
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{ctrl.state.showForgotPasscodeLink && (
|
||||
<div className="sk-panel-footer">
|
||||
{ctrl.state.forgotPasscode ? (
|
||||
<>
|
||||
<p className="sk-panel-row sk-p">
|
||||
{ctrl.state.hasAccount
|
||||
? 'If you forgot your application passcode, your ' +
|
||||
'only option is to clear your local data from this ' +
|
||||
'device and sign back in to your account.'
|
||||
: 'If you forgot your application passcode, your ' +
|
||||
'only option is to delete your data.'}
|
||||
</p>
|
||||
<a
|
||||
className="sk-panel-row sk-a danger centered"
|
||||
onClick={() => {
|
||||
ctrl.destroyLocalData();
|
||||
}}
|
||||
>
|
||||
Delete Local Data
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
className="sk-panel-row sk-a info centered"
|
||||
onClick={() => ctrl.onForgotPasscodeClick()}
|
||||
>
|
||||
Forgot your passcode?
|
||||
</a>
|
||||
)}
|
||||
<div className="sk-panel-row"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ChallengePrompts({
|
||||
ctrl,
|
||||
initialFocusRef,
|
||||
}: {
|
||||
ctrl: ChallengeModalCtrl;
|
||||
initialFocusRef: Ref<HTMLInputElement>;
|
||||
}) {
|
||||
return ctrl.state.prompts.map((prompt, index) => (
|
||||
<>
|
||||
{/** ProtectionSessionDuration can't just be an input field */}
|
||||
{prompt.validation === ChallengeValidation.ProtectionSessionDuration ? (
|
||||
<div key={prompt.id} className="sk-panel-row">
|
||||
<div className="sk-horizontal-group">
|
||||
<div className="sk-p sk-bold">Remember For</div>
|
||||
{ProtectionSessionDurations.map((option) => (
|
||||
<a
|
||||
className={
|
||||
'sk-a info ' +
|
||||
(option.valueInSeconds === ctrl.state.values[prompt.id]!.value
|
||||
? 'boxed'
|
||||
: '')
|
||||
}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
ctrl.onNumberValueChange(prompt, option.valueInSeconds);
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div key={prompt.id} className="sk-panel-row">
|
||||
<input
|
||||
className="sk-input contrast"
|
||||
value={ctrl.state.values[prompt.id]!.value as string | number}
|
||||
onChange={(event) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
ctrl.state.values[prompt.id]!.value = value;
|
||||
ctrl.onTextValueChange(prompt);
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
ctrl.submit();
|
||||
}
|
||||
}}
|
||||
ref={index === 0 ? initialFocusRef : undefined}
|
||||
placeholder={prompt.title}
|
||||
type={prompt.secureTextEntry ? 'password' : 'text'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ctrl.state.values[prompt.id]!.invalid && (
|
||||
<div className="sk-panel-row centered">
|
||||
<label className="sk-label danger">
|
||||
Invalid authentication. Please try again.
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
));
|
||||
}
|
||||
@@ -80,8 +80,7 @@
|
||||
)
|
||||
menu-row(
|
||||
action='self.selectedMenuItem(true); self.toggleProtectNote()'
|
||||
desc=`'Protecting a note will require credentials to view
|
||||
it (Manage Privileges via Account menu)'`,
|
||||
desc=`'Protecting a note will require credentials to view it'`,
|
||||
label="self.note.protected ? 'Unprotect' : 'Protect'"
|
||||
)
|
||||
menu-row(
|
||||
@@ -210,7 +209,7 @@
|
||||
on-load='self.onEditorLoad',
|
||||
application='self.application'
|
||||
)
|
||||
textarea#note-text-editor.editable(
|
||||
textarea#note-text-editor.editable.font-editor(
|
||||
dir='auto',
|
||||
ng-attr-spellcheck='{{self.state.spellcheck}}',
|
||||
ng-change='self.contentChanged()',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STRING_ARCHIVE_LOCKED_ATTEMPT, STRING_SAVING_WHILE_DOCUMENT_HIDDEN, STRING_UNARCHIVE_LOCKED_ATTEMPT } from './../../strings';
|
||||
import { Strings, STRING_ARCHIVE_LOCKED_ATTEMPT, STRING_SAVING_WHILE_DOCUMENT_HIDDEN, STRING_UNARCHIVE_LOCKED_ATTEMPT } from './../../strings';
|
||||
import { Editor } from '@/ui_models/editor';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PanelPuppet, WebDirective } from '@/types';
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
isPayloadSourceRetrieved,
|
||||
isPayloadSourceInternalChange,
|
||||
ContentType,
|
||||
ProtectedAction,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
SNTag,
|
||||
@@ -24,7 +23,7 @@ import { isDesktopApplication } from '@/utils';
|
||||
import { KeyboardModifier, KeyboardKey } from '@/services/keyboardManager';
|
||||
import template from './editor-view.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { AppStateEvent, EventSource } from '@/ui_models/app_state';
|
||||
import { EventSource } from '@/ui_models/app_state';
|
||||
import {
|
||||
STRING_DELETED_NOTE,
|
||||
STRING_INVALID_NOTE,
|
||||
@@ -48,11 +47,6 @@ const ElementIds = {
|
||||
EditorContent: 'editor-content',
|
||||
NoteTagsComponentContainer: 'note-tags-component-container'
|
||||
};
|
||||
const Fonts = {
|
||||
DesktopMonospaceFamily: `Menlo,Consolas,'DejaVu Sans Mono',monospace`,
|
||||
WebMonospaceFamily: `monospace`,
|
||||
SansSerifFamily: `inherit`
|
||||
};
|
||||
|
||||
type NoteStatus = {
|
||||
message?: string
|
||||
@@ -85,7 +79,7 @@ type EditorState = {
|
||||
* then re-initialized. Used when reloading spellcheck status. */
|
||||
textareaUnloading: boolean
|
||||
/** Fields that can be directly mutated by the template */
|
||||
mutable: {}
|
||||
mutable: any
|
||||
}
|
||||
|
||||
type EditorValues = {
|
||||
@@ -98,7 +92,7 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
||||
return array.sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1);
|
||||
}
|
||||
|
||||
class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
class EditorViewCtrl extends PureViewCtrl<unknown, EditorState> {
|
||||
/** Passed through template */
|
||||
readonly application!: WebApplication
|
||||
readonly editor!: Editor
|
||||
@@ -143,7 +137,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
this.onPanelResizeFinish = this.onPanelResizeFinish.bind(this);
|
||||
this.onEditorLoad = () => {
|
||||
this.application!.getDesktopService().redoSearch();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
deinit() {
|
||||
@@ -200,7 +194,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
if (note.lastSyncBegan) {
|
||||
if (note.lastSyncEnd) {
|
||||
if (note.lastSyncBegan!.getTime() > note.lastSyncEnd!.getTime()) {
|
||||
this.showSavingStatus()
|
||||
this.showSavingStatus();
|
||||
} else if (note.lastSyncEnd!.getTime() > note.lastSyncBegan!.getTime()) {
|
||||
this.showAllChangesSavedStatus();
|
||||
}
|
||||
@@ -248,7 +242,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
case ApplicationEvent.HighLatencySync:
|
||||
this.setState({ syncTakingTooLong: true });
|
||||
break;
|
||||
case ApplicationEvent.CompletedFullSync:
|
||||
case ApplicationEvent.CompletedFullSync: {
|
||||
this.setState({ syncTakingTooLong: false });
|
||||
const isInErrorState = this.state.saveError;
|
||||
/** if we're still dirty, don't change status, a sync is likely upcoming. */
|
||||
@@ -256,6 +250,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
this.showAllChangesSavedStatus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ApplicationEvent.FailedSync:
|
||||
/**
|
||||
* Only show error status in editor if the note is dirty.
|
||||
@@ -412,7 +407,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
await this.application.changeItem(this.note.uuid, (mutator) => {
|
||||
const noteMutator = mutator as NoteMutator;
|
||||
noteMutator.prefersPlainEditor = false;
|
||||
})
|
||||
});
|
||||
}
|
||||
await this.associateComponentWithCurrentNote(component);
|
||||
}
|
||||
@@ -471,7 +466,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
(mutator) => {
|
||||
mutator.addItemAsRelationship(note);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
if (!this.application.findItem(note.uuid)) {
|
||||
this.application.alertService!.alert(
|
||||
@@ -494,7 +489,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
noteMutator.preview_plain = previewPlain;
|
||||
noteMutator.preview_html = undefined;
|
||||
}
|
||||
}, isUserModified)
|
||||
}, isUserModified);
|
||||
if (this.saveTimeout) {
|
||||
this.$timeout.cancel(this.saveTimeout);
|
||||
}
|
||||
@@ -549,7 +544,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
this.statusTimeout = this.$timeout(() => {
|
||||
this.setState({
|
||||
noteStatus: status
|
||||
})
|
||||
});
|
||||
}, MINIMUM_STATUS_DURATION);
|
||||
} else {
|
||||
this.setState({
|
||||
@@ -601,10 +596,12 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
this.setMenuState('showOptionsMenu', false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onTitleFocus() {
|
||||
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onTitleBlur() {
|
||||
|
||||
}
|
||||
@@ -627,50 +624,35 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const run = async () => {
|
||||
if (this.note.locked) {
|
||||
this.application.alertService!.alert(
|
||||
STRING_DELETE_LOCKED_ATTEMPT
|
||||
);
|
||||
return;
|
||||
}
|
||||
const title = this.note.safeTitle().length
|
||||
? `'${this.note.title}'`
|
||||
: "this note";
|
||||
const text = StringDeleteNote(
|
||||
title,
|
||||
permanently
|
||||
if (this.note.locked) {
|
||||
this.application.alertService!.alert(
|
||||
STRING_DELETE_LOCKED_ATTEMPT
|
||||
);
|
||||
if (await confirmDialog({
|
||||
text,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
if (permanently) {
|
||||
this.performNoteDeletion(this.note);
|
||||
} else {
|
||||
this.saveNote(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.trashed = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
const requiresPrivilege = await this.application.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.DeleteNote
|
||||
return;
|
||||
}
|
||||
const title = this.note.safeTitle().length
|
||||
? `'${this.note.title}'`
|
||||
: "this note";
|
||||
const text = StringDeleteNote(
|
||||
title,
|
||||
permanently
|
||||
);
|
||||
if (requiresPrivilege) {
|
||||
this.application.presentPrivilegesModal(
|
||||
ProtectedAction.DeleteNote,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
if (await confirmDialog({
|
||||
text,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
if (permanently) {
|
||||
this.performNoteDeletion(this.note);
|
||||
} else {
|
||||
this.saveNote(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.trashed = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,7 +697,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.pinned = !this.note.pinned
|
||||
mutator.pinned = !this.note.pinned;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -726,28 +708,26 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.locked = !this.note.locked
|
||||
mutator.locked = !this.note.locked;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleProtectNote() {
|
||||
this.saveNote(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.protected = !this.note.protected
|
||||
async toggleProtectNote() {
|
||||
if (this.note.protected) {
|
||||
void this.application.unprotectNote(this.note);
|
||||
} else {
|
||||
const note = await this.application.protectNote(this.note);
|
||||
if (note?.protected && !this.application.hasProtectionSources()) {
|
||||
if (await confirmDialog({
|
||||
text: Strings.protectingNoteWithoutProtectionSources,
|
||||
confirmButtonText: Strings.openAccountMenu,
|
||||
confirmButtonStyle: 'info',
|
||||
})) {
|
||||
this.appState.accountMenu.setShow(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
/** Show privileges manager if protection is not yet set up */
|
||||
this.application.privilegesService!.actionHasPrivilegesConfigured(
|
||||
ProtectedAction.ViewProtectedNotes
|
||||
).then((configured) => {
|
||||
if (!configured) {
|
||||
this.application.presentPrivilegesManagementModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleNotePreview() {
|
||||
@@ -756,7 +736,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.hidePreview = !this.note.hidePreview
|
||||
mutator.hidePreview = !this.note.hidePreview;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -775,7 +755,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
false,
|
||||
true,
|
||||
(mutator) => {
|
||||
mutator.archived = !this.note.archived
|
||||
mutator.archived = !this.note.archived;
|
||||
},
|
||||
/** If we are unarchiving, and we are in the archived tag, close the editor */
|
||||
this.note.archived && this.appState.selectedTag?.isArchiveTag
|
||||
@@ -884,7 +864,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
(mutator) => {
|
||||
mutator.addItemAsRelationship(note);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
this.application.sync();
|
||||
this.reloadTags();
|
||||
@@ -966,20 +946,18 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
}
|
||||
|
||||
reloadFont() {
|
||||
const editor = document.getElementById(
|
||||
ElementIds.NoteTextEditor
|
||||
);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const root = document.querySelector(':root') as HTMLElement;
|
||||
const propertyName = '--sn-stylekit-editor-font-family';
|
||||
if (this.state.monospaceFont) {
|
||||
if (this.state.isDesktop) {
|
||||
editor.style.fontFamily = Fonts.DesktopMonospaceFamily;
|
||||
} else {
|
||||
editor.style.fontFamily = Fonts.WebMonospaceFamily;
|
||||
}
|
||||
root.style.setProperty(
|
||||
propertyName,
|
||||
'var(--sn-stylekit-monospace-font)'
|
||||
);
|
||||
} else {
|
||||
editor.style.fontFamily = Fonts.SansSerifFamily;
|
||||
root.style.setProperty(
|
||||
propertyName,
|
||||
'var(--sn-stylekit-sans-serif-font)'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,7 +969,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
);
|
||||
await this.setState({
|
||||
[key]: !currentValue
|
||||
})
|
||||
});
|
||||
this.reloadFont();
|
||||
|
||||
if (key === PrefKey.EditorSpellcheck) {
|
||||
@@ -1082,7 +1060,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
(mutator) => {
|
||||
mutator.addItemAsRelationship(this.note);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1141,7 +1119,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.removeAssociatedItemId(note.uuid);
|
||||
mutator.disassociateWithItem(note.uuid);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async associateComponentWithCurrentNote(component: SNComponent) {
|
||||
@@ -1150,7 +1128,7 @@ class EditorViewCtrl extends PureViewCtrl<{}, EditorState> {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.removeDisassociatedItemId(note.uuid);
|
||||
mutator.associateWithItem(note.uuid);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
registerKeyboardShortcuts() {
|
||||
|
||||
@@ -8,15 +8,10 @@ class EditorGroupViewCtrl {
|
||||
private application!: WebApplication
|
||||
public editors: Editor[] = []
|
||||
|
||||
/* @ngInject */
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.application.editorGroup.addChangeObserver(() => {
|
||||
this.editors = this.application.editorGroup.editors;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { dateToLocalizedString, preventRefreshing } from '@/utils';
|
||||
import {
|
||||
ApplicationEvent,
|
||||
SyncQueueStrategy,
|
||||
ProtectedAction,
|
||||
ContentType,
|
||||
SNComponent,
|
||||
SNTheme,
|
||||
@@ -44,7 +43,7 @@ type DockShortcut = {
|
||||
}
|
||||
}
|
||||
|
||||
class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
class FooterViewCtrl extends PureViewCtrl<unknown, {
|
||||
outOfSync: boolean;
|
||||
hasPasscode: boolean;
|
||||
dataUpgradeAvailable: boolean;
|
||||
@@ -63,7 +62,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
public arbitraryStatusMessage?: string
|
||||
public user?: any
|
||||
private offline = true
|
||||
private showAccountMenu = false
|
||||
public showAccountMenu = false
|
||||
private didCheckForOffline = false
|
||||
private queueExtReload = false
|
||||
private reloadInProgress = false
|
||||
@@ -76,7 +75,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
private observerRemovers: Array<() => void> = [];
|
||||
private completedInitialSync = false;
|
||||
private showingDownloadStatus = false;
|
||||
private removeBetaWarningListener?: IReactionDisposer;
|
||||
private autorunDisposer?: IReactionDisposer;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
@@ -104,7 +103,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
this.rootScopeListener2 = undefined;
|
||||
(this.closeAccountMenu as any) = undefined;
|
||||
(this.toggleSyncResolutionMenu as any) = undefined;
|
||||
this.removeBetaWarningListener?.();
|
||||
this.autorunDisposer?.();
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
@@ -116,8 +115,9 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
});
|
||||
});
|
||||
this.loadAccountSwitcherState();
|
||||
this.removeBetaWarningListener = autorun(() => {
|
||||
this.autorunDisposer = autorun(() => {
|
||||
const showBetaWarning = this.appState.showBetaWarning;
|
||||
this.showAccountMenu = this.appState.accountMenu.show;
|
||||
this.setState({
|
||||
showBetaWarning: showBetaWarning,
|
||||
showDataUpgrade: !showBetaWarning
|
||||
@@ -207,9 +207,9 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
case AppStateEvent.BeganBackupDownload:
|
||||
statusService.setMessage("Saving local backup…");
|
||||
break;
|
||||
case AppStateEvent.EndedBackupDownload:
|
||||
case AppStateEvent.EndedBackupDownload: {
|
||||
const successMessage = "Successfully saved backup.";
|
||||
const errorMessage = "Unable to save local backup."
|
||||
const errorMessage = "Unable to save local backup.";
|
||||
statusService.setMessage(data.success ? successMessage : errorMessage);
|
||||
|
||||
const twoSeconds = 2000;
|
||||
@@ -222,6 +222,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
}
|
||||
}, twoSeconds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +256,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
if (!this.didCheckForOffline) {
|
||||
this.didCheckForOffline = true;
|
||||
if (this.offline && this.application.getNoteCount() === 0) {
|
||||
this.showAccountMenu = true;
|
||||
this.appState.accountMenu.setShow(true);
|
||||
}
|
||||
}
|
||||
this.syncUpdated();
|
||||
@@ -297,7 +298,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
theme.package_info.dock_icon
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
this.observerRemovers.push(this.application.streamItems(
|
||||
ContentType.Component,
|
||||
@@ -437,7 +438,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
}
|
||||
|
||||
accountMenuPressed() {
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.appState.accountMenu.toggleShow();
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
@@ -446,7 +447,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
}
|
||||
|
||||
closeAccountMenu() {
|
||||
this.showAccountMenu = false;
|
||||
this.appState.accountMenu.setShow(false);
|
||||
}
|
||||
|
||||
lockApp() {
|
||||
@@ -544,28 +545,9 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
}
|
||||
|
||||
async selectRoom(room: SNComponent) {
|
||||
const run = () => {
|
||||
this.$timeout(() => {
|
||||
this.roomShowState[room.uuid] = !this.roomShowState[room.uuid];
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.roomShowState[room.uuid]) {
|
||||
const requiresPrivilege = await this.application.privilegesService!
|
||||
.actionRequiresPrivilege(
|
||||
ProtectedAction.ManageExtensions
|
||||
);
|
||||
if (requiresPrivilege) {
|
||||
this.application.presentPrivilegesModal(
|
||||
ProtectedAction.ManageExtensions,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
this.$timeout(() => {
|
||||
this.roomShowState[room.uuid] = !this.roomShowState[room.uuid];
|
||||
});
|
||||
}
|
||||
|
||||
displayBetaDialog() {
|
||||
@@ -582,7 +564,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
|
||||
if (this.application && this.application.authenticationInProgress()) {
|
||||
return;
|
||||
}
|
||||
this.showAccountMenu = false;
|
||||
this.appState.accountMenu.setShow(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,4 @@ export { EditorView } from './editor/editor_view';
|
||||
export { FooterView } from './footer/footer_view';
|
||||
export { NotesView } from './notes/notes_view';
|
||||
export { TagsView } from './tags/tags_view';
|
||||
export { ChallengeModal } from './challenge_modal/challenge_modal'
|
||||
export { ChallengeModal } from './challenge_modal/challenge_modal';
|
||||
@@ -5,44 +5,54 @@ export function notePassesFilter(
|
||||
showArchived: boolean,
|
||||
hidePinned: boolean,
|
||||
filterText: string
|
||||
) {
|
||||
|
||||
let canShowArchived = showArchived;
|
||||
): boolean {
|
||||
const canShowArchived = showArchived;
|
||||
const canShowPinned = !hidePinned;
|
||||
if (
|
||||
(note.archived && !canShowArchived) ||
|
||||
(note.pinned && !canShowPinned)
|
||||
) {
|
||||
if ((note.archived && !canShowArchived) || (note.pinned && !canShowPinned)) {
|
||||
return false;
|
||||
}
|
||||
return noteMatchesQuery(note, filterText);
|
||||
if (note.protected) {
|
||||
const match = noteMatchesQuery(note, filterText);
|
||||
/** Only match title to prevent leaking protected note text */
|
||||
return match === Match.Title || match === Match.TitleAndText;
|
||||
} else {
|
||||
return noteMatchesQuery(note, filterText) !== Match.None;
|
||||
}
|
||||
}
|
||||
|
||||
function noteMatchesQuery(
|
||||
note: SNNote,
|
||||
query: string
|
||||
) {
|
||||
enum Match {
|
||||
None = 0,
|
||||
Title = 1,
|
||||
Text = 2,
|
||||
TitleAndText = Title + Text,
|
||||
Uuid = 5,
|
||||
}
|
||||
|
||||
function noteMatchesQuery(note: SNNote, query: string): Match {
|
||||
if (query.length === 0) {
|
||||
return true;
|
||||
return Match.TitleAndText;
|
||||
}
|
||||
const title = note.safeTitle().toLowerCase();
|
||||
const text = note.safeText().toLowerCase();
|
||||
const lowercaseText = query.toLowerCase();
|
||||
const words = lowercaseText.split(' ');
|
||||
const quotedText = stringBetweenQuotes(lowercaseText);
|
||||
if (quotedText) {
|
||||
return title.includes(quotedText) || text.includes(quotedText);
|
||||
return (
|
||||
(title.includes(quotedText) ? Match.Title : Match.None) +
|
||||
(text.includes(quotedText) ? Match.Text : Match.None)
|
||||
);
|
||||
}
|
||||
if (stringIsUuid(lowercaseText)) {
|
||||
return note.uuid === lowercaseText;
|
||||
return note.uuid === lowercaseText ? Match.Uuid : Match.None;
|
||||
}
|
||||
const words = lowercaseText.split(" ");
|
||||
const matchesTitle = words.every((word) => {
|
||||
return title.indexOf(word) >= 0;
|
||||
});
|
||||
const matchesBody = words.every((word) => {
|
||||
return text.indexOf(word) >= 0;
|
||||
});
|
||||
return matchesTitle || matchesBody;
|
||||
return (matchesTitle ? Match.Title : 0) + (matchesBody ? Match.Text : 0);
|
||||
}
|
||||
|
||||
function stringBetweenQuotes(text: string) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#notes-title-bar.section-title-bar
|
||||
.padded
|
||||
.section-title-bar-header
|
||||
.title {{self.state.panelTitle}}
|
||||
.sk-h2.font-semibold.title {{self.state.panelTitle}}
|
||||
.sk-button.contrast.wide(
|
||||
ng-click='self.createNewNote()',
|
||||
title='Create a new note in the selected tag'
|
||||
@@ -24,6 +24,10 @@
|
||||
ng-click='self.clearFilterText();',
|
||||
ng-show='self.state.noteFilter.text'
|
||||
) ✕
|
||||
no-account-warning(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
#notes-menu-bar.sn-component
|
||||
.sk-app-bar.no-edges
|
||||
.left
|
||||
@@ -139,10 +143,12 @@
|
||||
.default-preview(
|
||||
ng-show='!note.preview_html && !note.preview_plain'
|
||||
) {{note.text}}
|
||||
.date.faded(ng-show='!self.state.hideDate')
|
||||
span(ng-show="self.state.sortBy == 'userModifiedDate'")
|
||||
.bottom-info.faded(ng-show='!self.state.hideDate || note.protected')
|
||||
span(ng-if="note.protected")
|
||||
| Protected{{self.state.hideDate ? '' : ' • '}}
|
||||
span(ng-show="!self.state.hideDate && self.state.sortBy == 'userModifiedDate'")
|
||||
| Modified {{note.updatedAtString || 'Now'}}
|
||||
span(ng-show="self.state.sortBy != 'userModifiedDate'")
|
||||
span(ng-show="!self.state.hideDate && self.state.sortBy != 'userModifiedDate'")
|
||||
| {{note.createdAtString || 'Now'}}
|
||||
.tags-string(ng-if='!self.state.hideTags && self.state.renderedNotesTags[$index]')
|
||||
.faded {{self.state.renderedNotesTags[$index]}}
|
||||
|
||||
@@ -59,7 +59,7 @@ const DEFAULT_LIST_NUM_NOTES = 20;
|
||||
const ELEMENT_ID_SEARCH_BAR = 'search-bar';
|
||||
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable';
|
||||
|
||||
class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
|
||||
class NotesViewCtrl extends PureViewCtrl<unknown, NotesState> {
|
||||
|
||||
private panelPuppet?: PanelPuppet
|
||||
private reloadNotesPromise?: any
|
||||
@@ -410,7 +410,7 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
|
||||
if (activeNote && activeNote.conflictOf) {
|
||||
this.application!.changeAndSaveItem(activeNote.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
})
|
||||
});
|
||||
}
|
||||
if (this.isFiltering()) {
|
||||
this.application!.getDesktopService().searchText(this.getState().noteFilter.text);
|
||||
@@ -576,12 +576,6 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
|
||||
class: 'warning'
|
||||
});
|
||||
}
|
||||
if (note.protected) {
|
||||
flags.push({
|
||||
text: "Protected",
|
||||
class: 'success'
|
||||
});
|
||||
}
|
||||
if (note.locked) {
|
||||
flags.push({
|
||||
text: "Locked",
|
||||
@@ -641,7 +635,7 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
|
||||
selectNextNote() {
|
||||
const displayableNotes = this.displayableNotes();
|
||||
const currentIndex = displayableNotes.findIndex((candidate) => {
|
||||
return candidate.uuid === this.activeEditorNote!.uuid
|
||||
return candidate.uuid === this.activeEditorNote!.uuid;
|
||||
});
|
||||
if (currentIndex + 1 < displayableNotes.length) {
|
||||
this.selectNote(displayableNotes[currentIndex + 1]);
|
||||
@@ -798,7 +792,7 @@ class NotesViewCtrl extends PureViewCtrl<{}, NotesState> {
|
||||
],
|
||||
onKeyDown: () => {
|
||||
const searchBar = this.getSearchBar();
|
||||
if (searchBar) { searchBar.focus(); };
|
||||
if (searchBar) { searchBar.focus(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
.tag-info
|
||||
.title(ng-if="!tag.errorDecrypting") {{tag.title}}
|
||||
.count(ng-show='tag.isAllTag') {{self.state.noteCounts[tag.uuid]}}
|
||||
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.tags-title-section.section-title-bar
|
||||
.section-title-bar-header
|
||||
.sk-h3.title
|
||||
@@ -52,9 +52,9 @@
|
||||
spellcheck='false'
|
||||
)
|
||||
.count {{self.state.noteCounts[tag.uuid]}}
|
||||
.danger.small-text.bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.danger.small-text.font-bold(ng-show='tag.conflictOf') Conflicted Copy
|
||||
.danger.small-text.font-bold(ng-show='tag.errorDecrypting && !tag.waitingForKey') Missing Keys
|
||||
.info.small-text.font-bold(ng-show='tag.errorDecrypting && tag.waitingForKey') Waiting For Keys
|
||||
.menu(ng-show='self.state.selectedTag == tag')
|
||||
a.item(ng-click='self.selectedRenameTag(tag)' ng-show='!self.state.editingTag') Rename
|
||||
a.item(ng-click='self.saveTag($event, tag)' ng-show='self.state.editingTag') Save
|
||||
|
||||
@@ -36,7 +36,7 @@ type TagState = {
|
||||
templateTag?: SNTag
|
||||
}
|
||||
|
||||
class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
|
||||
class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
|
||||
/** Passed through template */
|
||||
readonly application!: WebApplication
|
||||
@@ -136,7 +136,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
|
||||
} else {
|
||||
this.setState({
|
||||
selectedTag: matchingTag
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,14 +186,14 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
|
||||
const notes = this.application.notesMatchingSmartTag(tag as SNSmartTag)
|
||||
.filter((note) => {
|
||||
return !note.archived && !note.trashed;
|
||||
})
|
||||
});
|
||||
noteCounts[tag.uuid] = notes.length;
|
||||
}
|
||||
} else {
|
||||
const notes = this.application.referencesForItem(tag, ContentType.Note)
|
||||
.filter((note) => {
|
||||
return !note.archived && !note.trashed;
|
||||
})
|
||||
});
|
||||
noteCounts[tag.uuid] = notes.length;
|
||||
}
|
||||
}
|
||||
@@ -264,7 +264,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
|
||||
if (tag.conflictOf) {
|
||||
this.application.changeAndSaveItem(tag.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
})
|
||||
});
|
||||
}
|
||||
this.application.getAppState().setSelectedTag(tag);
|
||||
}
|
||||
@@ -326,7 +326,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
|
||||
"A tag with this name already exists."
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
await this.application.changeAndSaveItem<TagMutator>(tag.uuid, (mutator) => {
|
||||
mutator.title = newTitle;
|
||||
});
|
||||
@@ -350,7 +350,7 @@ class TagsViewCtrl extends PureViewCtrl<{}, TagState> {
|
||||
"A tag with this name already exists."
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
const insertedTag = await this.application.insertItem(newTag);
|
||||
const changedTag = await this.application.changeItem<TagMutator>(insertedTag.uuid, (m) => {
|
||||
m.title = newTitle;
|
||||
|
||||
Reference in New Issue
Block a user