Merge branch 'release/3.5.1'
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function autofocus($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: '='
|
||||
},
|
||||
link: function($scope, $element) {
|
||||
$timeout(function() {
|
||||
if ($scope.shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
19
app/assets/javascripts/directives/functional/autofocus.ts
Normal file
19
app/assets/javascripts/directives/functional/autofocus.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* @ngInject */
|
||||
export function autofocus($timeout: ng.ITimeoutService) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: '='
|
||||
},
|
||||
link: function (
|
||||
$scope: ng.IScope,
|
||||
$element: JQLite
|
||||
) {
|
||||
$timeout(() => {
|
||||
if (($scope as any).shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function clickOutside($document) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: false,
|
||||
link: function($scope, $element, attrs) {
|
||||
var didApplyClickOutside = false;
|
||||
|
||||
$element.bind('click', function(e) {
|
||||
didApplyClickOutside = false;
|
||||
if (attrs.isOpen) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
$document.bind('click', function() {
|
||||
// Ignore click if on SKAlert
|
||||
if (event.target.closest(".sk-modal")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!didApplyClickOutside) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* @ngInject */
|
||||
export function clickOutside($document: ng.IDocumentService) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
replace: false,
|
||||
link($scope: ng.IScope, $element: JQLite, attrs: any) {
|
||||
let didApplyClickOutside = false;
|
||||
|
||||
function onElementClick(event: JQueryEventObject) {
|
||||
didApplyClickOutside = false;
|
||||
if (attrs.isOpen) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
function onDocumentClick(event: JQueryEventObject) {
|
||||
/** Ignore click if on SKAlert */
|
||||
if (event.target.closest('.sk-modal')) {
|
||||
return;
|
||||
}
|
||||
if (!didApplyClickOutside) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
attrs.clickOutside = undefined;
|
||||
$element.unbind('click', onElementClick);
|
||||
$document.unbind('click', onDocumentClick);
|
||||
});
|
||||
|
||||
$element.bind('click', onElementClick);
|
||||
$document.bind('click', onDocumentClick);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import angular from 'angular';
|
||||
|
||||
/* @ngInject */
|
||||
export function delayHide($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
show: '=',
|
||||
delay: '@'
|
||||
},
|
||||
link: function(scope, elem, attrs) {
|
||||
showElement(false);
|
||||
|
||||
// This is where all the magic happens!
|
||||
// Whenever the scope variable updates we simply
|
||||
// show if it evaluates to 'true' and hide if 'false'
|
||||
scope.$watch('show', function(newVal) {
|
||||
newVal ? showSpinner() : hideSpinner();
|
||||
});
|
||||
|
||||
function showSpinner() {
|
||||
if (scope.hidePromise) {
|
||||
$timeout.cancel(scope.hidePromise);
|
||||
scope.hidePromise = null;
|
||||
}
|
||||
showElement(true);
|
||||
}
|
||||
|
||||
function hideSpinner() {
|
||||
scope.hidePromise = $timeout(showElement.bind(this, false), getDelay());
|
||||
}
|
||||
|
||||
function showElement(show) {
|
||||
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
|
||||
}
|
||||
|
||||
function getDelay() {
|
||||
var delay = parseInt(scope.delay);
|
||||
|
||||
return angular.isNumber(delay) ? delay : 200;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
45
app/assets/javascripts/directives/functional/delay-hide.ts
Normal file
45
app/assets/javascripts/directives/functional/delay-hide.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import angular from 'angular';
|
||||
|
||||
/* @ngInject */
|
||||
export function delayHide($timeout: ng.ITimeoutService) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
show: '=',
|
||||
delay: '@'
|
||||
},
|
||||
link: function (scope: ng.IScope, elem: JQLite) {
|
||||
const scopeAny = scope as any;
|
||||
const showSpinner = () => {
|
||||
if (scopeAny.hidePromise) {
|
||||
$timeout.cancel(scopeAny.hidePromise);
|
||||
scopeAny.hidePromise = null;
|
||||
}
|
||||
showElement(true);
|
||||
}
|
||||
|
||||
const hideSpinner = () => {
|
||||
scopeAny.hidePromise = $timeout(
|
||||
showElement.bind(this as any, false),
|
||||
getDelay()
|
||||
);
|
||||
}
|
||||
|
||||
const showElement = (show: boolean) => {
|
||||
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
|
||||
}
|
||||
|
||||
const getDelay = () => {
|
||||
const delay = parseInt(scopeAny.delay);
|
||||
return angular.isNumber(delay) ? delay : 200;
|
||||
}
|
||||
|
||||
showElement(false);
|
||||
// Whenever the scope variable updates we simply
|
||||
// show if it evaluates to 'true' and hide if 'false'
|
||||
scope.$watch('show', function (newVal) {
|
||||
newVal ? showSpinner() : hideSpinner();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/* @ngInject */
|
||||
export function elemReady($parse) {
|
||||
export function elemReady($parse: ng.IParseService) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function($scope, elem, attrs) {
|
||||
link: function($scope: ng.IScope, elem: JQLite, attrs: any) {
|
||||
elem.ready(function() {
|
||||
$scope.$apply(function() {
|
||||
var func = $parse(attrs.elemReady);
|
||||
@@ -1,16 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function fileChange() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
handler: '&'
|
||||
},
|
||||
link: function(scope, element) {
|
||||
element.on('change', function(event) {
|
||||
scope.$apply(function() {
|
||||
scope.handler({ files: event.target.files });
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
19
app/assets/javascripts/directives/functional/file-change.ts
Normal file
19
app/assets/javascripts/directives/functional/file-change.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* @ngInject */
|
||||
export function fileChange() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
handler: '&'
|
||||
},
|
||||
link: function (scope: ng.IScope, element: JQLite) {
|
||||
element.on('change', (event) => {
|
||||
scope.$apply(() => {
|
||||
const files = (event.target as HTMLInputElement).files;
|
||||
(scope as any).handler({
|
||||
files: files
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,5 +5,5 @@ export { elemReady } from './elemReady';
|
||||
export { fileChange } from './file-change';
|
||||
export { infiniteScroll } from './infiniteScroll';
|
||||
export { lowercase } from './lowercase';
|
||||
export { selectOnClick } from './selectOnClick';
|
||||
export { selectOnFocus } from './selectOnFocus';
|
||||
export { snEnter } from './snEnter';
|
||||
@@ -1,17 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function infiniteScroll($rootScope, $window, $timeout) {
|
||||
return {
|
||||
link: function(scope, elem, attrs) {
|
||||
const offset = parseInt(attrs.threshold) || 0;
|
||||
const e = elem[0];
|
||||
elem.on('scroll', function() {
|
||||
if (
|
||||
scope.$eval(attrs.canLoad) &&
|
||||
e.scrollTop + e.offsetHeight >= e.scrollHeight - offset
|
||||
) {
|
||||
scope.$apply(attrs.infiniteScroll);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { debounce } from '@/utils';
|
||||
/* @ngInject */
|
||||
export function infiniteScroll() {
|
||||
return {
|
||||
link: function (scope: ng.IScope, elem: JQLite, attrs: any) {
|
||||
const scopeAny = scope as any;
|
||||
const offset = parseInt(attrs.threshold) || 0;
|
||||
const element = elem[0];
|
||||
scopeAny.paginate = debounce(() => {
|
||||
scope.$apply(attrs.infiniteScroll);
|
||||
}, 10);
|
||||
scopeAny.onScroll = () => {
|
||||
if (
|
||||
scope.$eval(attrs.canLoad) &&
|
||||
element.scrollTop + element.offsetHeight >= element.scrollHeight - offset
|
||||
) {
|
||||
scopeAny.paginate();
|
||||
}
|
||||
};
|
||||
elem.on('scroll', scopeAny.onScroll);
|
||||
scope.$on('$destroy', () => {
|
||||
elem.off('scroll', scopeAny.onScroll);;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function lowercase() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, modelCtrl) {
|
||||
var lowercase = function(inputValue) {
|
||||
if (inputValue === undefined) inputValue = '';
|
||||
var lowercased = inputValue.toLowerCase();
|
||||
if (lowercased !== inputValue) {
|
||||
modelCtrl.$setViewValue(lowercased);
|
||||
modelCtrl.$render();
|
||||
}
|
||||
return lowercased;
|
||||
};
|
||||
modelCtrl.$parsers.push(lowercase);
|
||||
lowercase(scope[attrs.ngModel]);
|
||||
}
|
||||
};
|
||||
}
|
||||
24
app/assets/javascripts/directives/functional/lowercase.ts
Normal file
24
app/assets/javascripts/directives/functional/lowercase.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/* @ngInject */
|
||||
export function lowercase() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function (
|
||||
scope: ng.IScope,
|
||||
_: JQLite,
|
||||
attrs: any,
|
||||
ctrl: any
|
||||
) {
|
||||
const lowercase = (inputValue: string) => {
|
||||
if (inputValue === undefined) inputValue = '';
|
||||
const lowercased = inputValue.toLowerCase();
|
||||
if (lowercased !== inputValue) {
|
||||
ctrl.$setViewValue(lowercased);
|
||||
ctrl.$render();
|
||||
}
|
||||
return lowercased;
|
||||
};
|
||||
ctrl.$parsers.push(lowercase);
|
||||
lowercase((scope as any)[attrs.ngModel]);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/* @ngInject */
|
||||
export function selectOnClick($window) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, element, attrs) {
|
||||
element.on('focus', function() {
|
||||
if (!$window.getSelection().toString()) {
|
||||
/** Required for mobile Safari */
|
||||
this.setSelectionRange(0, this.value.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/* @ngInject */
|
||||
export function selectOnFocus($window: ng.IWindowService) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope: ng.IScope, element: JQLite) {
|
||||
element.on('focus', () => {
|
||||
if (!$window.getSelection()!.toString()) {
|
||||
const input = element[0] as HTMLInputElement;
|
||||
/** Allow text to populate */
|
||||
setTimeout(() => {
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
/* @ngInject */
|
||||
export function snEnter() {
|
||||
return function(scope, element, attrs) {
|
||||
element.bind('keydown keypress', function(event) {
|
||||
return function (
|
||||
scope: ng.IScope,
|
||||
element: JQLite,
|
||||
attrs: any
|
||||
) {
|
||||
element.bind('keydown keypress', function (event) {
|
||||
if (event.which === 13) {
|
||||
scope.$apply(function() {
|
||||
scope.$apply(function () {
|
||||
scope.$eval(attrs.snEnter, { event: event });
|
||||
});
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
import { isDesktopApplication, isNullOrUndefined } from '@/utils';
|
||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||
import template from '%/directives/account-menu.pug';
|
||||
import { protocolManager } from 'snjs';
|
||||
import { PureCtrl } from '@Controllers';
|
||||
import {
|
||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
STRING_SIGN_OUT_CONFIRMATION,
|
||||
STRING_ERROR_DECRYPTING_IMPORT,
|
||||
STRING_E2E_ENABLED,
|
||||
STRING_LOCAL_ENC_ENABLED,
|
||||
STRING_ENC_NOT_ENABLED,
|
||||
STRING_IMPORT_SUCCESS,
|
||||
STRING_REMOVE_PASSCODE_CONFIRMATION,
|
||||
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
|
||||
STRING_NON_MATCHING_PASSCODES,
|
||||
STRING_NON_MATCHING_PASSWORDS,
|
||||
STRING_INVALID_IMPORT_FILE,
|
||||
STRING_GENERATING_LOGIN_KEYS,
|
||||
STRING_GENERATING_REGISTER_KEYS,
|
||||
StringImportError
|
||||
} from '@/strings';
|
||||
import { STRING_IMPORT_FAILED_NEWER_BACKUP } from '../../strings';
|
||||
|
||||
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
|
||||
|
||||
const ELEMENT_NAME_AUTH_EMAIL = 'email';
|
||||
const ELEMENT_NAME_AUTH_PASSWORD = 'password';
|
||||
const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf';
|
||||
|
||||
class AccountMenuCtrl extends PureCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$scope,
|
||||
$rootScope,
|
||||
$timeout,
|
||||
alertManager,
|
||||
archiveManager,
|
||||
appVersion,
|
||||
authManager,
|
||||
modelManager,
|
||||
passcodeManager,
|
||||
privilegesManager,
|
||||
storageManager,
|
||||
syncManager,
|
||||
) {
|
||||
super($timeout);
|
||||
this.$scope = $scope;
|
||||
this.$rootScope = $rootScope;
|
||||
this.$timeout = $timeout;
|
||||
this.alertManager = alertManager;
|
||||
this.archiveManager = archiveManager;
|
||||
this.authManager = authManager;
|
||||
this.modelManager = modelManager;
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.privilegesManager = privilegesManager;
|
||||
this.storageManager = storageManager;
|
||||
this.syncManager = syncManager;
|
||||
|
||||
this.state = {
|
||||
appVersion: 'v' + (window.electronAppVersion || appVersion),
|
||||
user: this.authManager.user,
|
||||
canAddPasscode: !this.authManager.isEphemeralSession(),
|
||||
passcodeAutoLockOptions: this.passcodeManager.getAutoLockIntervalOptions(),
|
||||
formData: {
|
||||
mergeLocal: true,
|
||||
ephemeral: false
|
||||
},
|
||||
mutable: {
|
||||
backupEncrypted: this.encryptedBackupsAvailable()
|
||||
}
|
||||
};
|
||||
|
||||
this.syncStatus = this.syncManager.syncStatus;
|
||||
this.syncManager.getServerURL().then((url) => {
|
||||
this.setState({
|
||||
server: url,
|
||||
formData: { ...this.state.formData, url: url }
|
||||
});
|
||||
});
|
||||
this.authManager.checkForSecurityUpdate().then((available) => {
|
||||
this.setState({
|
||||
securityUpdateAvailable: available
|
||||
});
|
||||
});
|
||||
this.reloadAutoLockInterval();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.initProps({
|
||||
closeFunction: this.closeFunction
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$timeout(() => {
|
||||
this.props.closeFunction()();
|
||||
});
|
||||
}
|
||||
|
||||
encryptedBackupsAvailable() {
|
||||
return !isNullOrUndefined(this.authManager.user) || this.passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
submitMfaForm() {
|
||||
const params = {
|
||||
[this.state.formData.mfa.payload.mfa_key]: this.state.formData.userMfaCode
|
||||
};
|
||||
this.login(params);
|
||||
}
|
||||
|
||||
blurAuthFields() {
|
||||
const names = [
|
||||
ELEMENT_NAME_AUTH_EMAIL,
|
||||
ELEMENT_NAME_AUTH_PASSWORD,
|
||||
ELEMENT_NAME_AUTH_PASSWORD_CONF
|
||||
];
|
||||
for(const name of names) {
|
||||
const element = document.getElementsByName(name)[0];
|
||||
if(element) {
|
||||
element.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitAuthForm() {
|
||||
if (!this.state.formData.email || !this.state.formData.user_password) {
|
||||
return;
|
||||
}
|
||||
this.blurAuthFields();
|
||||
if (this.state.formData.showLogin) {
|
||||
this.login();
|
||||
} else {
|
||||
this.register();
|
||||
}
|
||||
}
|
||||
|
||||
async setFormDataState(formData) {
|
||||
return this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
...formData
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async login(extraParams) {
|
||||
/** Prevent a timed sync from occuring while signing in. */
|
||||
this.syncManager.lockSyncing();
|
||||
await this.setFormDataState({
|
||||
status: STRING_GENERATING_LOGIN_KEYS,
|
||||
authenticating: true
|
||||
});
|
||||
const response = await this.authManager.login(
|
||||
this.state.formData.url,
|
||||
this.state.formData.email,
|
||||
this.state.formData.user_password,
|
||||
this.state.formData.ephemeral,
|
||||
this.state.formData.strictSignin,
|
||||
extraParams
|
||||
);
|
||||
const hasError = !response || response.error;
|
||||
if (!hasError) {
|
||||
this.setFormDataState({
|
||||
user_password: null
|
||||
});
|
||||
await this.onAuthSuccess();
|
||||
this.syncManager.unlockSyncing();
|
||||
this.syncManager.sync({ performIntegrityCheck: true });
|
||||
return;
|
||||
}
|
||||
this.syncManager.unlockSyncing();
|
||||
await this.setFormDataState({
|
||||
status: null
|
||||
});
|
||||
const error = response
|
||||
? response.error
|
||||
: { message: "An unknown error occured." };
|
||||
|
||||
if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') {
|
||||
await this.setFormDataState({
|
||||
showLogin: false,
|
||||
mfa: error
|
||||
});
|
||||
} else {
|
||||
await this.setFormDataState({
|
||||
showLogin: true,
|
||||
mfa: null
|
||||
});
|
||||
if (error.message) {
|
||||
this.alertManager.alert({
|
||||
text: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.setFormDataState({
|
||||
authenticating: false,
|
||||
});
|
||||
}
|
||||
|
||||
async register() {
|
||||
const confirmation = this.state.formData.password_conf;
|
||||
if (confirmation !== this.state.formData.user_password) {
|
||||
this.alertManager.alert({
|
||||
text: STRING_NON_MATCHING_PASSWORDS
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.setFormDataState({
|
||||
confirmPassword: false,
|
||||
status: STRING_GENERATING_REGISTER_KEYS,
|
||||
authenticating: true
|
||||
});
|
||||
const response = await this.authManager.register(
|
||||
this.state.formData.url,
|
||||
this.state.formData.email,
|
||||
this.state.formData.user_password,
|
||||
this.state.formData.ephemeral
|
||||
);
|
||||
if (!response || response.error) {
|
||||
await this.setFormDataState({
|
||||
status: null
|
||||
});
|
||||
const error = response
|
||||
? response.error
|
||||
: { message: "An unknown error occured." };
|
||||
await this.setFormDataState({
|
||||
authenticating: false
|
||||
});
|
||||
this.alertManager.alert({
|
||||
text: error.message
|
||||
});
|
||||
} else {
|
||||
await this.onAuthSuccess();
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
|
||||
mergeLocalChanged() {
|
||||
if (!this.state.formData.mergeLocal) {
|
||||
this.alertManager.confirm({
|
||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
destructive: true,
|
||||
onCancel: () => {
|
||||
this.setFormDataState({
|
||||
mergeLocal: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onAuthSuccess() {
|
||||
if (this.state.formData.mergeLocal) {
|
||||
this.$rootScope.$broadcast('major-data-change');
|
||||
await this.clearDatabaseAndRewriteAllItems({ alternateUuids: true });
|
||||
} else {
|
||||
this.modelManager.removeAllItemsFromMemory();
|
||||
await this.storageManager.clearAllModels();
|
||||
}
|
||||
await this.setFormDataState({
|
||||
authenticating: false
|
||||
});
|
||||
this.syncManager.refreshErroredItems();
|
||||
this.close();
|
||||
}
|
||||
|
||||
openPasswordWizard(type) {
|
||||
this.close();
|
||||
this.authManager.presentPasswordWizard(type);
|
||||
}
|
||||
|
||||
async openPrivilegesModal() {
|
||||
this.close();
|
||||
const run = () => {
|
||||
this.privilegesManager.presentPrivilegesManagementModal();
|
||||
};
|
||||
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||
PrivilegesManager.ActionManagePrivileges
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.privilegesManager.presentPrivilegesModal(
|
||||
PrivilegesManager.ActionManagePrivileges,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows IndexedDB unencrypted logs to be deleted
|
||||
* `clearAllModels` will remove data from backing store,
|
||||
* but not from working memory See:
|
||||
* https://github.com/standardnotes/desktop/issues/131
|
||||
*/
|
||||
async clearDatabaseAndRewriteAllItems({ alternateUuids } = {}) {
|
||||
await this.storageManager.clearAllModels();
|
||||
await this.syncManager.markAllItemsDirtyAndSaveOffline(alternateUuids);
|
||||
}
|
||||
|
||||
destroyLocalData() {
|
||||
this.alertManager.confirm({
|
||||
text: STRING_SIGN_OUT_CONFIRMATION,
|
||||
destructive: true,
|
||||
onConfirm: async () => {
|
||||
await this.authManager.signout(true);
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submitImportPassword() {
|
||||
await this.performImport(
|
||||
this.state.importData.data,
|
||||
this.state.importData.password
|
||||
);
|
||||
}
|
||||
|
||||
async readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
this.alertManager.alert({
|
||||
text: STRING_INVALID_IMPORT_FILE
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template
|
||||
*/
|
||||
async importFileSelected(files) {
|
||||
const run = async () => {
|
||||
const file = files[0];
|
||||
const data = await this.readFile(file);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const version = data.version || data.auth_params?.version || data?.keyParams?.version;
|
||||
if (version && !protocolManager.supportedVersions().includes(version)) {
|
||||
this.setState({ importData: null });
|
||||
this.alertManager.alert({ text: STRING_IMPORT_FAILED_NEWER_BACKUP });
|
||||
return;
|
||||
}
|
||||
if (data.auth_params) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.state.importData,
|
||||
requestPassword: true,
|
||||
data: data
|
||||
}
|
||||
});
|
||||
const element = document.getElementById(
|
||||
ELEMENT_ID_IMPORT_PASSWORD_INPUT
|
||||
);
|
||||
if (element) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
} else {
|
||||
await this.performImport(data, null);
|
||||
}
|
||||
};
|
||||
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||
PrivilegesManager.ActionManageBackups
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.privilegesManager.presentPrivilegesModal(
|
||||
PrivilegesManager.ActionManageBackups,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
async performImport(data, password) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.state.importData,
|
||||
loading: true
|
||||
}
|
||||
});
|
||||
const errorCount = await this.importJSONData(data, password);
|
||||
this.setState({
|
||||
importData: null
|
||||
});
|
||||
if (errorCount > 0) {
|
||||
const message = StringImportError({ errorCount: errorCount });
|
||||
this.alertManager.alert({
|
||||
text: message
|
||||
});
|
||||
} else {
|
||||
this.alertManager.alert({
|
||||
text: STRING_IMPORT_SUCCESS
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async importJSONData(data, password) {
|
||||
let errorCount = 0;
|
||||
if (data.auth_params) {
|
||||
const keys = await protocolManager.computeEncryptionKeysForUser(
|
||||
password,
|
||||
data.auth_params
|
||||
);
|
||||
try {
|
||||
const throws = false;
|
||||
await protocolManager.decryptMultipleItems(data.items, keys, throws);
|
||||
const items = [];
|
||||
for (const item of data.items) {
|
||||
item.enc_item_key = null;
|
||||
item.auth_hash = null;
|
||||
if (item.errorDecrypting) {
|
||||
errorCount++;
|
||||
} else {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
data.items = items;
|
||||
} catch (e) {
|
||||
this.alertManager.alert({
|
||||
text: STRING_ERROR_DECRYPTING_IMPORT
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const items = await this.modelManager.importItems(data.items);
|
||||
for (const item of items) {
|
||||
/**
|
||||
* Don't want to activate any components during import process in
|
||||
* case of exceptions breaking up the import proccess
|
||||
*/
|
||||
if (item.content_type === 'SN|Component') {
|
||||
item.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.syncManager.sync();
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
async downloadDataArchive() {
|
||||
this.archiveManager.downloadBackup(this.state.mutable.backupEncrypted);
|
||||
this.close();
|
||||
}
|
||||
|
||||
notesAndTagsCount() {
|
||||
return this.modelManager.allItemsMatchingTypes([
|
||||
'Note',
|
||||
'Tag'
|
||||
]).length;
|
||||
}
|
||||
|
||||
encryptionStatusForNotes() {
|
||||
const length = this.notesAndTagsCount();
|
||||
return length + "/" + length + " notes and tags encrypted";
|
||||
}
|
||||
|
||||
encryptionEnabled() {
|
||||
return this.passcodeManager.hasPasscode() || !this.authManager.offline();
|
||||
}
|
||||
|
||||
encryptionSource() {
|
||||
if (!this.authManager.offline()) {
|
||||
return "Account keys";
|
||||
} else if (this.passcodeManager.hasPasscode()) {
|
||||
return "Local Passcode";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
encryptionStatusString() {
|
||||
if (!this.authManager.offline()) {
|
||||
return STRING_E2E_ENABLED;
|
||||
} else if (this.passcodeManager.hasPasscode()) {
|
||||
return STRING_LOCAL_ENC_ENABLED;
|
||||
} else {
|
||||
return STRING_ENC_NOT_ENABLED;
|
||||
}
|
||||
}
|
||||
|
||||
async reloadAutoLockInterval() {
|
||||
const interval = await this.passcodeManager.getAutoLockInterval();
|
||||
this.setState({
|
||||
selectedAutoLockInterval: interval
|
||||
});
|
||||
}
|
||||
|
||||
async selectAutoLockInterval(interval) {
|
||||
const run = async () => {
|
||||
await this.passcodeManager.setAutoLockInterval(interval);
|
||||
this.reloadAutoLockInterval();
|
||||
};
|
||||
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||
PrivilegesManager.ActionManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.privilegesManager.presentPrivilegesModal(
|
||||
PrivilegesManager.ActionManagePasscode,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
hidePasswordForm() {
|
||||
this.setFormDataState({
|
||||
showLogin: false,
|
||||
showRegister: false,
|
||||
user_password: null,
|
||||
password_conf: null
|
||||
});
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this.passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
addPasscodeClicked() {
|
||||
this.setFormDataState({
|
||||
showPasscodeForm: true
|
||||
});
|
||||
}
|
||||
|
||||
submitPasscodeForm() {
|
||||
const passcode = this.state.formData.passcode;
|
||||
if (passcode !== this.state.formData.confirmPasscode) {
|
||||
this.alertManager.alert({
|
||||
text: STRING_NON_MATCHING_PASSCODES
|
||||
});
|
||||
return;
|
||||
}
|
||||
const func = this.state.formData.changingPasscode
|
||||
? this.passcodeManager.changePasscode.bind(this.passcodeManager)
|
||||
: this.passcodeManager.setPasscode.bind(this.passcodeManager);
|
||||
func(passcode, async () => {
|
||||
await this.setFormDataState({
|
||||
passcode: null,
|
||||
confirmPasscode: null,
|
||||
showPasscodeForm: false
|
||||
});
|
||||
if (await this.authManager.offline()) {
|
||||
this.$rootScope.$broadcast('major-data-change');
|
||||
this.clearDatabaseAndRewriteAllItems();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async changePasscodePressed() {
|
||||
const run = () => {
|
||||
this.state.formData.changingPasscode = true;
|
||||
this.addPasscodeClicked();
|
||||
};
|
||||
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||
PrivilegesManager.ActionManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.privilegesManager.presentPrivilegesModal(
|
||||
PrivilegesManager.ActionManagePasscode,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
async removePasscodePressed() {
|
||||
const run = () => {
|
||||
const signedIn = !this.authManager.offline();
|
||||
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
|
||||
if (!signedIn) {
|
||||
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
|
||||
}
|
||||
this.alertManager.confirm({
|
||||
text: message,
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.passcodeManager.clearPasscode();
|
||||
if (this.authManager.offline()) {
|
||||
this.syncManager.markAllItemsDirtyAndSaveOffline();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const needsPrivilege = await this.privilegesManager.actionRequiresPrivilege(
|
||||
PrivilegesManager.ActionManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.privilegesManager.presentPrivilegesModal(
|
||||
PrivilegesManager.ActionManagePasscode,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
isDesktopApplication() {
|
||||
return isDesktopApplication();
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountMenu {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = AccountMenuCtrl;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
closeFunction: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
608
app/assets/javascripts/directives/views/accountMenu.ts
Normal file
608
app/assets/javascripts/directives/views/accountMenu.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { isDesktopApplication, preventRefreshing } from '@/utils';
|
||||
import template from '%/directives/account-menu.pug';
|
||||
import { ProtectedAction, ContentType } from 'snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import {
|
||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
STRING_SIGN_OUT_CONFIRMATION,
|
||||
STRING_E2E_ENABLED,
|
||||
STRING_LOCAL_ENC_ENABLED,
|
||||
STRING_ENC_NOT_ENABLED,
|
||||
STRING_IMPORT_SUCCESS,
|
||||
STRING_REMOVE_PASSCODE_CONFIRMATION,
|
||||
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
|
||||
STRING_NON_MATCHING_PASSCODES,
|
||||
STRING_NON_MATCHING_PASSWORDS,
|
||||
STRING_INVALID_IMPORT_FILE,
|
||||
STRING_GENERATING_LOGIN_KEYS,
|
||||
STRING_GENERATING_REGISTER_KEYS,
|
||||
StringImportError,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
||||
STRING_UNSUPPORTED_BACKUP_FILE_VERSION
|
||||
} from '@/strings';
|
||||
import { SyncOpStatus } from 'snjs/dist/@types/services/sync/sync_op_status';
|
||||
import { PasswordWizardType } from '@/types';
|
||||
import { BackupFile } from 'snjs/dist/@types/services/protocol_service';
|
||||
import { confirmDialog, alertDialog } from '@/services/alertService';
|
||||
import { autorun, IReactionDisposer } from 'mobx';
|
||||
|
||||
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
|
||||
|
||||
const ELEMENT_NAME_AUTH_EMAIL = 'email';
|
||||
const ELEMENT_NAME_AUTH_PASSWORD = 'password';
|
||||
const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf';
|
||||
|
||||
type FormData = {
|
||||
email: string
|
||||
user_password: string
|
||||
password_conf: string
|
||||
confirmPassword: boolean
|
||||
showLogin: boolean
|
||||
showRegister: boolean
|
||||
showPasscodeForm: boolean
|
||||
strictSignin?: boolean
|
||||
ephemeral: boolean
|
||||
mergeLocal?: boolean
|
||||
url: string
|
||||
authenticating: boolean
|
||||
status: string
|
||||
passcode: string
|
||||
confirmPasscode: string
|
||||
changingPasscode: boolean
|
||||
}
|
||||
|
||||
type AccountMenuState = {
|
||||
formData: Partial<FormData>;
|
||||
appVersion: string;
|
||||
passcodeAutoLockOptions: any;
|
||||
user: any;
|
||||
mutable: any;
|
||||
importData: any;
|
||||
encryptionStatusString: string;
|
||||
server: string;
|
||||
encryptionEnabled: boolean;
|
||||
selectedAutoLockInterval: any;
|
||||
showBetaWarning: boolean;
|
||||
}
|
||||
|
||||
class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
|
||||
public appVersion: string
|
||||
/** @template */
|
||||
syncStatus?: SyncOpStatus
|
||||
private closeFunction?: () => void
|
||||
private removeBetaWarningListener?: IReactionDisposer
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
appVersion: string,
|
||||
) {
|
||||
super($timeout);
|
||||
this.appVersion = appVersion;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getInitialState() {
|
||||
return {
|
||||
appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion),
|
||||
passcodeAutoLockOptions: this.application!.getAutolockService().getAutoLockIntervalOptions(),
|
||||
user: this.application!.getUser(),
|
||||
formData: {
|
||||
mergeLocal: true,
|
||||
ephemeral: false,
|
||||
},
|
||||
mutable: {},
|
||||
showBetaWarning: false,
|
||||
} as AccountMenuState;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state as AccountMenuState;
|
||||
}
|
||||
|
||||
async onAppKeyChange() {
|
||||
super.onAppKeyChange();
|
||||
this.setState(this.refreshedCredentialState());
|
||||
}
|
||||
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.setState(this.refreshedCredentialState());
|
||||
this.loadHost();
|
||||
this.reloadAutoLockInterval();
|
||||
this.refreshEncryptionStatus();
|
||||
}
|
||||
|
||||
refreshedCredentialState() {
|
||||
return {
|
||||
user: this.application!.getUser(),
|
||||
canAddPasscode: !this.application!.isEphemeralSession(),
|
||||
hasPasscode: this.application!.hasPasscode(),
|
||||
showPasscodeForm: false
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.syncStatus = this.application!.getSyncStatus();
|
||||
this.removeBetaWarningListener = autorun(() => {
|
||||
this.setState({
|
||||
showBetaWarning: this.appState.showBetaWarning
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeBetaWarningListener?.();
|
||||
super.deinit();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$timeout(() => {
|
||||
this.closeFunction?.();
|
||||
});
|
||||
}
|
||||
|
||||
async loadHost() {
|
||||
const host = await this.application!.getHost();
|
||||
this.setState({
|
||||
server: host,
|
||||
formData: {
|
||||
...this.getState().formData,
|
||||
url: host
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onHostInputChange() {
|
||||
const url = this.getState().formData.url!;
|
||||
this.application!.setHost(url);
|
||||
}
|
||||
|
||||
refreshEncryptionStatus() {
|
||||
const hasUser = this.application!.hasAccount();
|
||||
const hasPasscode = this.application!.hasPasscode();
|
||||
const encryptionEnabled = hasUser || hasPasscode;
|
||||
|
||||
this.setState({
|
||||
encryptionStatusString: hasUser
|
||||
? STRING_E2E_ENABLED
|
||||
: hasPasscode
|
||||
? STRING_LOCAL_ENC_ENABLED
|
||||
: STRING_ENC_NOT_ENABLED,
|
||||
encryptionEnabled,
|
||||
mutable: {
|
||||
...this.getState().mutable,
|
||||
backupEncrypted: encryptionEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitMfaForm() {
|
||||
this.login();
|
||||
}
|
||||
|
||||
blurAuthFields() {
|
||||
const names = [
|
||||
ELEMENT_NAME_AUTH_EMAIL,
|
||||
ELEMENT_NAME_AUTH_PASSWORD,
|
||||
ELEMENT_NAME_AUTH_PASSWORD_CONF
|
||||
];
|
||||
for (const name of names) {
|
||||
const element = document.getElementsByName(name)[0];
|
||||
if (element) {
|
||||
element.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitAuthForm() {
|
||||
if (!this.getState().formData.email || !this.getState().formData.user_password) {
|
||||
return;
|
||||
}
|
||||
this.blurAuthFields();
|
||||
if (this.getState().formData.showLogin) {
|
||||
this.login();
|
||||
} else {
|
||||
this.register();
|
||||
}
|
||||
}
|
||||
|
||||
async setFormDataState(formData: Partial<FormData>) {
|
||||
return this.setState({
|
||||
formData: {
|
||||
...this.getState().formData,
|
||||
...formData
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async login() {
|
||||
await this.setFormDataState({
|
||||
status: STRING_GENERATING_LOGIN_KEYS,
|
||||
authenticating: true
|
||||
});
|
||||
const formData = this.getState().formData;
|
||||
const response = await this.application!.signIn(
|
||||
formData.email!,
|
||||
formData.user_password!,
|
||||
formData.strictSignin,
|
||||
formData.ephemeral,
|
||||
formData.mergeLocal
|
||||
);
|
||||
const error = response.error;
|
||||
if (!error) {
|
||||
await this.setFormDataState({
|
||||
authenticating: false,
|
||||
user_password: undefined
|
||||
});
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
await this.setFormDataState({
|
||||
showLogin: true,
|
||||
status: undefined,
|
||||
user_password: undefined
|
||||
});
|
||||
if (error.message) {
|
||||
this.application!.alertService!.alert(error.message);
|
||||
}
|
||||
await this.setFormDataState({
|
||||
authenticating: false
|
||||
});
|
||||
}
|
||||
|
||||
async register() {
|
||||
const confirmation = this.getState().formData.password_conf;
|
||||
if (confirmation !== this.getState().formData.user_password) {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_NON_MATCHING_PASSWORDS
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.setFormDataState({
|
||||
confirmPassword: false,
|
||||
status: STRING_GENERATING_REGISTER_KEYS,
|
||||
authenticating: true
|
||||
});
|
||||
const response = await this.application!.register(
|
||||
this.getState().formData.email!,
|
||||
this.getState().formData.user_password!,
|
||||
this.getState().formData.ephemeral,
|
||||
this.getState().formData.mergeLocal
|
||||
);
|
||||
const error = response.error;
|
||||
if (error) {
|
||||
await this.setFormDataState({
|
||||
status: undefined
|
||||
});
|
||||
await this.setFormDataState({
|
||||
authenticating: false
|
||||
});
|
||||
this.application!.alertService!.alert(
|
||||
error.message
|
||||
);
|
||||
} else {
|
||||
await this.setFormDataState({ authenticating: false });
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
async mergeLocalChanged() {
|
||||
if (!this.getState().formData.mergeLocal) {
|
||||
this.setFormDataState({
|
||||
mergeLocal: !(await confirmDialog({
|
||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
confirmButtonStyle: 'danger'
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openPasswordWizard() {
|
||||
this.close();
|
||||
this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword);
|
||||
}
|
||||
|
||||
async openPrivilegesModal() {
|
||||
const run = () => {
|
||||
this.application!.presentPrivilegesManagementModal();
|
||||
this.close();
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePrivileges
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePrivileges,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
async destroyLocalData() {
|
||||
if (await confirmDialog({
|
||||
text: STRING_SIGN_OUT_CONFIRMATION,
|
||||
confirmButtonStyle: "danger"
|
||||
})) {
|
||||
this.application.signOut();
|
||||
}
|
||||
}
|
||||
|
||||
async submitImportPassword() {
|
||||
await this.performImport(
|
||||
this.getState().importData.data,
|
||||
this.getState().importData.password
|
||||
);
|
||||
}
|
||||
|
||||
showRegister() {
|
||||
this.setFormDataState({
|
||||
showRegister: true
|
||||
});
|
||||
}
|
||||
|
||||
async readFile(file: File): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target!.result as string);
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_INVALID_IMPORT_FILE
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @template
|
||||
*/
|
||||
async importFileSelected(files: File[]) {
|
||||
const run = async () => {
|
||||
const file = files[0];
|
||||
const data = await this.readFile(file);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
if (data.version || data.auth_params || data.keyParams) {
|
||||
const version = data.version || data.keyParams?.version || data.auth_params?.version;
|
||||
if (
|
||||
!this.application!.protocolService!.supportedVersions().includes(version)
|
||||
) {
|
||||
await this.setState({ importData: null });
|
||||
alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
|
||||
return;
|
||||
}
|
||||
if (data.keyParams || data.auth_params) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.getState().importData,
|
||||
requestPassword: true,
|
||||
data,
|
||||
}
|
||||
});
|
||||
const element = document.getElementById(
|
||||
ELEMENT_ID_IMPORT_PASSWORD_INPUT
|
||||
);
|
||||
if (element) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
} else {
|
||||
await this.performImport(data, undefined);
|
||||
}
|
||||
} else {
|
||||
await this.performImport(data, undefined);
|
||||
}
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManageBackups
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManageBackups,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
async performImport(data: BackupFile, password?: string) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.getState().importData,
|
||||
loading: true
|
||||
}
|
||||
});
|
||||
const result = await this.application!.importData(
|
||||
data,
|
||||
password
|
||||
);
|
||||
this.setState({
|
||||
importData: null
|
||||
});
|
||||
if ('error' in result) {
|
||||
this.application!.alertService!.alert(
|
||||
result.error
|
||||
);
|
||||
} else if (result.errorCount) {
|
||||
const message = StringImportError(result.errorCount);
|
||||
this.application!.alertService!.alert(
|
||||
message
|
||||
);
|
||||
} else {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_IMPORT_SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async downloadDataArchive() {
|
||||
this.application!.getArchiveService().downloadBackup(this.getState().mutable.backupEncrypted);
|
||||
}
|
||||
|
||||
notesAndTagsCount() {
|
||||
return this.application!.getItems(
|
||||
[
|
||||
ContentType.Note,
|
||||
ContentType.Tag
|
||||
]
|
||||
).length;
|
||||
}
|
||||
|
||||
encryptionStatusForNotes() {
|
||||
const length = this.notesAndTagsCount();
|
||||
return length + "/" + length + " notes and tags encrypted";
|
||||
}
|
||||
|
||||
async reloadAutoLockInterval() {
|
||||
const interval = await this.application!.getAutolockService().getAutoLockInterval();
|
||||
this.setState({
|
||||
selectedAutoLockInterval: interval
|
||||
});
|
||||
}
|
||||
|
||||
async selectAutoLockInterval(interval: number) {
|
||||
const run = async () => {
|
||||
await this.application!.getAutolockService().setAutoLockInterval(interval);
|
||||
this.reloadAutoLockInterval();
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePasscode,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
hidePasswordForm() {
|
||||
this.setFormDataState({
|
||||
showLogin: false,
|
||||
showRegister: false,
|
||||
user_password: undefined,
|
||||
password_conf: undefined
|
||||
});
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this.application!.hasPasscode();
|
||||
}
|
||||
|
||||
addPasscodeClicked() {
|
||||
this.setFormDataState({
|
||||
showPasscodeForm: true
|
||||
});
|
||||
}
|
||||
|
||||
async submitPasscodeForm() {
|
||||
const passcode = this.getState().formData.passcode!;
|
||||
if (passcode !== this.getState().formData.confirmPasscode!) {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_NON_MATCHING_PASSCODES
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => {
|
||||
if (this.application!.hasPasscode()) {
|
||||
await this.application!.changePasscode(passcode);
|
||||
} else {
|
||||
await this.application!.setPasscode(passcode);
|
||||
}
|
||||
});
|
||||
this.setFormDataState({
|
||||
passcode: undefined,
|
||||
confirmPasscode: undefined,
|
||||
showPasscodeForm: false
|
||||
});
|
||||
this.refreshEncryptionStatus();
|
||||
}
|
||||
|
||||
async changePasscodePressed() {
|
||||
const run = () => {
|
||||
this.getState().formData.changingPasscode = true;
|
||||
this.addPasscodeClicked();
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePasscode,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
async removePasscodePressed() {
|
||||
const run = async () => {
|
||||
const signedIn = this.application!.hasAccount();
|
||||
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
|
||||
if (!signedIn) {
|
||||
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
|
||||
}
|
||||
if (await confirmDialog({
|
||||
text: message,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => {
|
||||
await this.application.getAutolockService().deleteAutolockPreference();
|
||||
await this.application!.removePasscode();
|
||||
await this.reloadAutoLockInterval();
|
||||
});
|
||||
this.refreshEncryptionStatus();
|
||||
}
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePasscode,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
isDesktopApplication() {
|
||||
return isDesktopApplication();
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountMenu extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = AccountMenuCtrl;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
closeFunction: '&',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import template from '%/directives/actions-menu.pug';
|
||||
import { PureCtrl } from '@Controllers';
|
||||
|
||||
class ActionsMenuCtrl extends PureCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$scope,
|
||||
$timeout,
|
||||
actionsManager,
|
||||
) {
|
||||
super($timeout);
|
||||
this.$timeout = $timeout;
|
||||
this.actionsManager = actionsManager;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.initProps({
|
||||
item: this.item
|
||||
});
|
||||
this.loadExtensions();
|
||||
};
|
||||
|
||||
async loadExtensions() {
|
||||
const extensions = this.actionsManager.extensions.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
for (const extension of extensions) {
|
||||
extension.loading = true;
|
||||
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
|
||||
extension.loading = false;
|
||||
}
|
||||
this.setState({
|
||||
extensions: extensions
|
||||
});
|
||||
}
|
||||
|
||||
async executeAction(action, extension) {
|
||||
if (action.verb === 'nested') {
|
||||
if (!action.subrows) {
|
||||
action.subrows = this.subRowsForAction(action, extension);
|
||||
} else {
|
||||
action.subrows = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
action.running = true;
|
||||
const result = await this.actionsManager.executeAction(
|
||||
action,
|
||||
extension,
|
||||
this.props.item
|
||||
);
|
||||
if (action.error) {
|
||||
return;
|
||||
}
|
||||
action.running = false;
|
||||
this.handleActionResult(action, result);
|
||||
await this.actionsManager.loadExtensionInContextOfItem(extension, this.props.item);
|
||||
this.setState({
|
||||
extensions: this.state.extensions
|
||||
});
|
||||
}
|
||||
|
||||
handleActionResult(action, result) {
|
||||
switch (action.verb) {
|
||||
case 'render': {
|
||||
const item = result.item;
|
||||
this.actionsManager.presentRevisionPreviewModal(
|
||||
item.uuid,
|
||||
item.content
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subRowsForAction(parentAction, extension) {
|
||||
if (!parentAction.subactions) {
|
||||
return null;
|
||||
}
|
||||
return parentAction.subactions.map((subaction) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
this.executeAction(subaction, extension, parentAction);
|
||||
},
|
||||
label: subaction.label,
|
||||
subtitle: subaction.desc,
|
||||
spinnerClass: subaction.running ? 'info' : null
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsMenu {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.replace = true;
|
||||
this.controller = ActionsMenuCtrl;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
item: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
285
app/assets/javascripts/directives/views/actionsMenu.ts
Normal file
285
app/assets/javascripts/directives/views/actionsMenu.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/actions-menu.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { SNItem, Action, SNActionsExtension, UuidString } from 'snjs/dist/@types';
|
||||
import { ActionResponse } from 'snjs';
|
||||
import { ActionsExtensionMutator } from 'snjs/dist/@types/models/app/extension';
|
||||
import { autorun, IReactionDisposer } from 'mobx';
|
||||
|
||||
type ActionsMenuScope = {
|
||||
application: WebApplication
|
||||
item: SNItem
|
||||
}
|
||||
|
||||
type ActionSubRow = {
|
||||
onClick: () => void
|
||||
label: string
|
||||
subtitle: string
|
||||
spinnerClass?: string
|
||||
}
|
||||
|
||||
type ExtensionState = {
|
||||
loading: boolean
|
||||
error: boolean
|
||||
}
|
||||
|
||||
type ActionsMenuState = {
|
||||
extensions: SNActionsExtension[]
|
||||
extensionsState: Record<UuidString, ExtensionState>
|
||||
selectedActionId?: number
|
||||
menu: {
|
||||
uuid: UuidString,
|
||||
name: string,
|
||||
loading: boolean,
|
||||
error: boolean,
|
||||
hidden: boolean,
|
||||
actions: (Action & {
|
||||
subrows?: ActionSubRow[]
|
||||
})[]
|
||||
}[]
|
||||
}
|
||||
|
||||
class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements ActionsMenuScope {
|
||||
application!: WebApplication
|
||||
item!: SNItem
|
||||
private removeHiddenExtensionsListener?: IReactionDisposer;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.initProps({
|
||||
item: this.item
|
||||
});
|
||||
this.loadExtensions();
|
||||
this.removeHiddenExtensionsListener = autorun(() => {
|
||||
this.rebuildMenu({
|
||||
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deinit() {
|
||||
this.removeHiddenExtensionsListener?.();
|
||||
}
|
||||
|
||||
/** @override */
|
||||
getInitialState() {
|
||||
const extensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
let extensionsState: Record<UuidString, ExtensionState> = {};
|
||||
extensions.map((extension) => {
|
||||
extensionsState[extension.uuid] = {
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
extensions,
|
||||
extensionsState,
|
||||
hiddenExtensions: {},
|
||||
menu: [],
|
||||
};
|
||||
}
|
||||
|
||||
rebuildMenu({
|
||||
extensions = this.state.extensions,
|
||||
extensionsState = this.state.extensionsState,
|
||||
selectedActionId = this.state.selectedActionId,
|
||||
hiddenExtensions = this.appState.actionsMenu.hiddenExtensions,
|
||||
} = {}) {
|
||||
return this.setState({
|
||||
extensions,
|
||||
extensionsState,
|
||||
selectedActionId,
|
||||
menu: extensions.map(extension => {
|
||||
const state = extensionsState[extension.uuid];
|
||||
const hidden = hiddenExtensions[extension.uuid];
|
||||
return {
|
||||
uuid: extension.uuid,
|
||||
name: extension.name,
|
||||
loading: state?.loading ?? false,
|
||||
error: state?.error ?? false,
|
||||
hidden: hidden ?? false,
|
||||
deprecation: extension.deprecation!,
|
||||
actions: extension.actionsWithContextForItem(this.item).map(action => {
|
||||
if (action.id === selectedActionId) {
|
||||
return {
|
||||
...action,
|
||||
subrows: this.subRowsForAction(action, extension)
|
||||
}
|
||||
} else {
|
||||
return action;
|
||||
}
|
||||
})
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async loadExtensions() {
|
||||
await Promise.all(this.state.extensions.map(async (extension: SNActionsExtension) => {
|
||||
this.setLoadingExtension(extension.uuid, true);
|
||||
const updatedExtension = await this.application.actionsManager!.loadExtensionInContextOfItem(
|
||||
extension,
|
||||
this.item
|
||||
);
|
||||
if (updatedExtension) {
|
||||
await this.updateExtension(updatedExtension!);
|
||||
} else {
|
||||
this.setErrorExtension(extension.uuid, true);
|
||||
}
|
||||
this.setLoadingExtension(extension.uuid, false);
|
||||
}));
|
||||
}
|
||||
|
||||
async executeAction(action: Action, extensionUuid: UuidString) {
|
||||
if (action.verb === 'nested') {
|
||||
this.rebuildMenu({
|
||||
selectedActionId: action.id
|
||||
});
|
||||
return;
|
||||
}
|
||||
const extension = this.application.findItem(extensionUuid) as SNActionsExtension;
|
||||
await this.updateAction(action, extension, { running: true });
|
||||
const response = await this.application.actionsManager!.runAction(
|
||||
action,
|
||||
this.item,
|
||||
async () => {
|
||||
/** @todo */
|
||||
return '';
|
||||
}
|
||||
);
|
||||
if (response.error) {
|
||||
await this.updateAction(action, extension, { error: true });
|
||||
return;
|
||||
}
|
||||
await this.updateAction(action, extension, { running: false });
|
||||
this.handleActionResponse(action, response);
|
||||
await this.reloadExtension(extension);
|
||||
}
|
||||
|
||||
handleActionResponse(action: Action, result: ActionResponse) {
|
||||
switch (action.verb) {
|
||||
case 'render': {
|
||||
const item = result.item;
|
||||
this.application.presentRevisionPreviewModal(
|
||||
item.uuid,
|
||||
item.content
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private subRowsForAction(parentAction: Action, extension: Pick<SNActionsExtension, 'uuid'>): ActionSubRow[] | undefined {
|
||||
if (!parentAction.subactions) {
|
||||
return undefined;
|
||||
}
|
||||
return parentAction.subactions.map((subaction) => {
|
||||
return {
|
||||
id: subaction.id,
|
||||
onClick: () => {
|
||||
this.executeAction(subaction, extension.uuid);
|
||||
},
|
||||
label: subaction.label,
|
||||
subtitle: subaction.desc,
|
||||
spinnerClass: subaction.running ? 'info' : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async updateAction(
|
||||
action: Action,
|
||||
extension: SNActionsExtension,
|
||||
params: {
|
||||
running?: boolean
|
||||
error?: boolean
|
||||
}
|
||||
) {
|
||||
const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => {
|
||||
const extensionMutator = mutator as ActionsExtensionMutator;
|
||||
extensionMutator.actions = extension!.actions.map((act) => {
|
||||
if (act && params && act.verb === action.verb && act.url === action.url) {
|
||||
return {
|
||||
...action,
|
||||
running: params?.running,
|
||||
error: params?.error,
|
||||
} as Action;
|
||||
}
|
||||
return act;
|
||||
});
|
||||
}) as SNActionsExtension;
|
||||
await this.updateExtension(updatedExtension);
|
||||
}
|
||||
|
||||
private async updateExtension(extension: SNActionsExtension) {
|
||||
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
|
||||
if (extension.uuid === ext.uuid) {
|
||||
return extension;
|
||||
}
|
||||
return ext;
|
||||
});
|
||||
await this.rebuildMenu({
|
||||
extensions
|
||||
});
|
||||
}
|
||||
|
||||
private async reloadExtension(extension: SNActionsExtension) {
|
||||
const extensionInContext = await this.application.actionsManager!.loadExtensionInContextOfItem(
|
||||
extension,
|
||||
this.item
|
||||
);
|
||||
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
|
||||
if (extension.uuid === ext.uuid) {
|
||||
return extensionInContext!;
|
||||
}
|
||||
return ext;
|
||||
});
|
||||
this.rebuildMenu({
|
||||
extensions
|
||||
});
|
||||
}
|
||||
|
||||
public toggleExtensionVisibility(extensionUuid: UuidString) {
|
||||
this.appState.actionsMenu.toggleExtensionVisibility(extensionUuid);
|
||||
}
|
||||
|
||||
private setLoadingExtension(extensionUuid: UuidString, value = false) {
|
||||
const { extensionsState } = this.state;
|
||||
extensionsState[extensionUuid].loading = value;
|
||||
this.rebuildMenu({
|
||||
extensionsState
|
||||
});
|
||||
}
|
||||
|
||||
private setErrorExtension(extensionUuid: UuidString, value = false) {
|
||||
const { extensionsState } = this.state;
|
||||
extensionsState[extensionUuid].error = value;
|
||||
this.rebuildMenu({
|
||||
extensionsState
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionsMenu extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.replace = true;
|
||||
this.controller = ActionsMenuCtrl;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
item: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import template from '%/directives/component-modal.pug';
|
||||
|
||||
export class ComponentModalCtrl {
|
||||
/* @ngInject */
|
||||
constructor($scope, $element) {
|
||||
this.$element = $element;
|
||||
this.$scope = $scope;
|
||||
}
|
||||
|
||||
dismiss(callback) {
|
||||
this.$element.remove();
|
||||
this.$scope.$destroy();
|
||||
if(this.onDismiss && this.onDismiss()) {
|
||||
this.onDismiss()(this.component);
|
||||
}
|
||||
callback && callback();
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = ComponentModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
show: '=',
|
||||
component: '=',
|
||||
callback: '=',
|
||||
onDismiss: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
64
app/assets/javascripts/directives/views/componentModal.ts
Normal file
64
app/assets/javascripts/directives/views/componentModal.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNComponent, LiveItem } from 'snjs';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/component-modal.pug';
|
||||
|
||||
export type ComponentModalScope = {
|
||||
componentUuid: string
|
||||
onDismiss: () => void
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
export class ComponentModalCtrl implements ComponentModalScope {
|
||||
$element: JQLite
|
||||
componentUuid!: string
|
||||
onDismiss!: () => void
|
||||
application!: WebApplication
|
||||
liveComponent!: LiveItem<SNComponent>
|
||||
component!: SNComponent
|
||||
|
||||
/* @ngInject */
|
||||
constructor($element: JQLite) {
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.liveComponent = new LiveItem(
|
||||
this.componentUuid,
|
||||
this.application,
|
||||
(component) => {
|
||||
this.component = component;
|
||||
}
|
||||
);
|
||||
this.application.componentGroup.activateComponent(this.component);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
this.application.componentGroup.deactivateComponent(this.component);
|
||||
this.liveComponent.deinit();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.onDismiss && this.onDismiss();
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = ComponentModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
componentUuid: '=',
|
||||
onDismiss: '&',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
269
app/assets/javascripts/directives/views/componentView.ts
Normal file
269
app/assets/javascripts/directives/views/componentView.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import { RootScopeMessages } from './../../messages';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNComponent, ComponentAction, LiveItem } from 'snjs';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/component-view.pug';
|
||||
import { isDesktopApplication } from '../../utils';
|
||||
/**
|
||||
* The maximum amount of time we'll wait for a component
|
||||
* to load before displaying error
|
||||
*/
|
||||
const MaxLoadThreshold = 4000;
|
||||
const VisibilityChangeKey = 'visibilitychange';
|
||||
|
||||
interface ComponentViewScope {
|
||||
componentUuid: string
|
||||
onLoad?: (component: SNComponent) => void
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
class ComponentViewCtrl implements ComponentViewScope {
|
||||
|
||||
/** @scope */
|
||||
onLoad?: (component: SNComponent) => void
|
||||
componentUuid!: string
|
||||
templateComponent!: SNComponent
|
||||
application!: WebApplication
|
||||
liveComponent!: LiveItem<SNComponent>
|
||||
|
||||
private $rootScope: ng.IRootScopeService
|
||||
private $timeout: ng.ITimeoutService
|
||||
private componentValid = true
|
||||
private cleanUpOn: () => void
|
||||
private unregisterComponentHandler!: () => void
|
||||
private unregisterDesktopObserver!: () => void
|
||||
private issueLoading = false
|
||||
public reloading = false
|
||||
private expired = false
|
||||
private loading = false
|
||||
private didAttemptReload = false
|
||||
public error: 'offline-restricted' | 'url-missing' | undefined
|
||||
private loadTimeout: any
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$scope: ng.IScope,
|
||||
$rootScope: ng.IRootScopeService,
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.$timeout = $timeout;
|
||||
this.cleanUpOn = $scope.$on('ext-reload-complete', () => {
|
||||
this.reloadStatus(false);
|
||||
});
|
||||
/** To allow for registering events */
|
||||
this.onVisibilityChange = this.onVisibilityChange.bind(this);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if(this.application.componentManager) {
|
||||
/** Component manager Can be destroyed already via locking */
|
||||
this.application.componentManager.onComponentIframeDestroyed(this.component.uuid);
|
||||
if(this.templateComponent) {
|
||||
this.application.componentManager.removeTemporaryTemplateComponent(this.templateComponent);
|
||||
}
|
||||
}
|
||||
if(this.liveComponent) {
|
||||
this.liveComponent.deinit();
|
||||
}
|
||||
this.cleanUpOn();
|
||||
(this.cleanUpOn as any) = undefined;
|
||||
this.unregisterComponentHandler();
|
||||
(this.unregisterComponentHandler as any) = undefined;
|
||||
this.unregisterDesktopObserver();
|
||||
(this.unregisterDesktopObserver as any) = undefined;
|
||||
(this.templateComponent as any) = undefined;
|
||||
(this.liveComponent as any) = undefined;
|
||||
(this.application as any) = undefined;
|
||||
(this.onVisibilityChange as any) = undefined;
|
||||
this.onLoad = undefined;
|
||||
document.removeEventListener(
|
||||
VisibilityChangeKey,
|
||||
this.onVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
if(this.componentUuid) {
|
||||
this.liveComponent = new LiveItem(this.componentUuid, this.application);
|
||||
} else {
|
||||
this.application.componentManager.addTemporaryTemplateComponent(this.templateComponent);
|
||||
}
|
||||
this.registerComponentHandlers();
|
||||
this.registerPackageUpdateObserver();
|
||||
}
|
||||
|
||||
get component() {
|
||||
return this.templateComponent || this.liveComponent?.item;
|
||||
}
|
||||
|
||||
/** @template */
|
||||
public onIframeInit() {
|
||||
/** Perform in timeout required so that dynamic iframe id is set (based on ctrl values) */
|
||||
this.$timeout(() => {
|
||||
this.loadComponent();
|
||||
});
|
||||
}
|
||||
|
||||
private loadComponent() {
|
||||
if (!this.component) {
|
||||
throw Error('Component view is missing component');
|
||||
}
|
||||
if (!this.component.active && !this.component.isEditor()) {
|
||||
/** Editors don't need to be active to be displayed */
|
||||
throw Error('Component view component must be active');
|
||||
}
|
||||
const iframe = this.application.componentManager!.iframeForComponent(
|
||||
this.component.uuid
|
||||
);
|
||||
if (!iframe) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
if (this.loadTimeout) {
|
||||
this.$timeout.cancel(this.loadTimeout);
|
||||
}
|
||||
this.loadTimeout = this.$timeout(() => {
|
||||
this.handleIframeLoadTimeout();
|
||||
}, MaxLoadThreshold);
|
||||
iframe.onload = () => {
|
||||
this.reloadStatus();
|
||||
this.handleIframeLoad(iframe);
|
||||
};
|
||||
}
|
||||
|
||||
private registerPackageUpdateObserver() {
|
||||
this.unregisterDesktopObserver = this.application.getDesktopService()
|
||||
.registerUpdateObserver((component: SNComponent) => {
|
||||
if (component.uuid === this.component.uuid && component.active) {
|
||||
this.reloadIframe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private registerComponentHandlers() {
|
||||
this.unregisterComponentHandler = this.application.componentManager!.registerHandler({
|
||||
identifier: 'component-view-' + Math.random(),
|
||||
areas: [this.component.area],
|
||||
actionHandler: (component, action, data) => {
|
||||
if (action === ComponentAction.SetSize) {
|
||||
this.application.componentManager!.handleSetSizeEvent(component, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private reloadIframe() {
|
||||
this.$timeout(() => {
|
||||
this.reloading = true;
|
||||
this.$timeout(() => {
|
||||
this.reloading = false;
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private onVisibilityChange() {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
return;
|
||||
}
|
||||
if (this.issueLoading) {
|
||||
this.reloadIframe();
|
||||
}
|
||||
}
|
||||
|
||||
public reloadStatus(doManualReload = true) {
|
||||
const component = this.component;
|
||||
const offlineRestricted = component.offlineOnly && !isDesktopApplication();
|
||||
const hasUrlError = function () {
|
||||
if (isDesktopApplication()) {
|
||||
return !component.local_url && !component.hasValidHostedUrl();
|
||||
} else {
|
||||
return !component.hasValidHostedUrl();
|
||||
}
|
||||
}();
|
||||
this.expired = component.valid_until && component.valid_until <= new Date();
|
||||
const readonlyState = this.application.componentManager!
|
||||
.getReadonlyStateForComponent(component);
|
||||
if (!readonlyState.lockReadonly) {
|
||||
this.application.componentManager!
|
||||
.setReadonlyStateForComponent(component, this.expired);
|
||||
}
|
||||
this.componentValid = !offlineRestricted && !hasUrlError;
|
||||
if (!this.componentValid) {
|
||||
this.loading = false;
|
||||
}
|
||||
if (offlineRestricted) {
|
||||
this.error = 'offline-restricted';
|
||||
} else if (hasUrlError) {
|
||||
this.error = 'url-missing';
|
||||
} else {
|
||||
this.error = undefined;
|
||||
}
|
||||
if (this.expired && doManualReload) {
|
||||
this.$rootScope.$broadcast(RootScopeMessages.ReloadExtendedData);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleIframeLoadTimeout() {
|
||||
if (this.loading) {
|
||||
this.loading = false;
|
||||
this.issueLoading = true;
|
||||
if (!this.didAttemptReload) {
|
||||
this.didAttemptReload = true;
|
||||
this.reloadIframe();
|
||||
} else {
|
||||
document.addEventListener(
|
||||
VisibilityChangeKey,
|
||||
this.onVisibilityChange
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleIframeLoad(iframe: HTMLIFrameElement) {
|
||||
let desktopError = false;
|
||||
if (isDesktopApplication()) {
|
||||
try {
|
||||
/** Accessing iframe.contentWindow.origin only allowed in desktop app. */
|
||||
if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') {
|
||||
desktopError = true;
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
this.$timeout.cancel(this.loadTimeout);
|
||||
await this.application.componentManager!.registerComponentWindow(
|
||||
this.component,
|
||||
iframe.contentWindow!
|
||||
);
|
||||
const avoidFlickerTimeout = 7;
|
||||
this.$timeout(() => {
|
||||
this.loading = false;
|
||||
// eslint-disable-next-line no-unneeded-ternary
|
||||
this.issueLoading = desktopError ? true : false;
|
||||
this.onLoad && this.onLoad(this.component!);
|
||||
}, avoidFlickerTimeout);
|
||||
}
|
||||
|
||||
/** @template */
|
||||
public getUrl() {
|
||||
const url = this.application.componentManager!.urlForComponent(this.component);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentView extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.scope = {
|
||||
componentUuid: '=',
|
||||
templateComponent: '=?',
|
||||
onLoad: '=?',
|
||||
application: '='
|
||||
};
|
||||
this.controller = ComponentViewCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import template from '%/directives/conflict-resolution-modal.pug';
|
||||
|
||||
class ConflictResolutionCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element,
|
||||
alertManager,
|
||||
archiveManager,
|
||||
modelManager,
|
||||
syncManager
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.alertManager = alertManager;
|
||||
this.archiveManager = archiveManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.contentType = this.item1.content_type;
|
||||
this.item1Content = this.createContentString(this.item1);
|
||||
this.item2Content = this.createContentString(this.item2);
|
||||
};
|
||||
|
||||
createContentString(item) {
|
||||
const data = Object.assign({
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at
|
||||
}, item.content);
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
keepItem1() {
|
||||
this.alertManager.confirm({
|
||||
text: `Are you sure you want to delete the item on the right?`,
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.modelManager.setItemToBeDeleted(this.item2);
|
||||
this.syncManager.sync().then(() => {
|
||||
this.applyCallback();
|
||||
});
|
||||
this.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
keepItem2() {
|
||||
this.alertManager.confirm({
|
||||
text: `Are you sure you want to delete the item on the left?`,
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.modelManager.setItemToBeDeleted(this.item1);
|
||||
this.syncManager.sync().then(() => {
|
||||
this.applyCallback();
|
||||
});
|
||||
this.dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
keepBoth() {
|
||||
this.applyCallback();
|
||||
this.dismiss();
|
||||
}
|
||||
|
||||
export() {
|
||||
this.archiveManager.downloadBackupOfItems(
|
||||
[this.item1, this.item2],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
applyCallback() {
|
||||
this.callback && this.callback();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictResolutionModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = ConflictResolutionCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
item1: '=',
|
||||
item2: '=',
|
||||
callback: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import template from '%/directives/editor-menu.pug';
|
||||
import { PureCtrl } from '@Controllers';
|
||||
|
||||
class EditorMenuCtrl extends PureCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
componentManager,
|
||||
modelManager,
|
||||
syncManager,
|
||||
) {
|
||||
super($timeout);
|
||||
this.$timeout = $timeout;
|
||||
this.componentManager = componentManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.state = {
|
||||
isDesktop: isDesktopApplication()
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
const editors = this.componentManager.componentsForArea('editor-editor')
|
||||
.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
|
||||
this.setState({
|
||||
editors: editors,
|
||||
defaultEditor: defaultEditor
|
||||
});
|
||||
};
|
||||
|
||||
selectComponent(component) {
|
||||
if(component) {
|
||||
if(component.content.conflict_of) {
|
||||
component.content.conflict_of = null;
|
||||
this.modelManager.setItemDirty(component, true);
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
this.$timeout(() => {
|
||||
this.callback()(component);
|
||||
});
|
||||
}
|
||||
|
||||
toggleDefaultForEditor(editor) {
|
||||
if(this.state.defaultEditor === editor) {
|
||||
this.removeEditorDefault(editor);
|
||||
} else {
|
||||
this.makeEditorDefault(editor);
|
||||
}
|
||||
}
|
||||
|
||||
offlineAvailableForComponent(component) {
|
||||
return component.local_url && this.state.isDesktop;
|
||||
}
|
||||
|
||||
makeEditorDefault(component) {
|
||||
const currentDefault = this.componentManager
|
||||
.componentsForArea('editor-editor')
|
||||
.filter((e) => e.isDefaultEditor())[0];
|
||||
if(currentDefault) {
|
||||
currentDefault.setAppDataItem('defaultEditor', false);
|
||||
this.modelManager.setItemDirty(currentDefault);
|
||||
}
|
||||
component.setAppDataItem('defaultEditor', true);
|
||||
this.modelManager.setItemDirty(component);
|
||||
this.syncManager.sync();
|
||||
this.setState({
|
||||
defaultEditor: component
|
||||
});
|
||||
}
|
||||
|
||||
removeEditorDefault(component) {
|
||||
component.setAppDataItem('defaultEditor', false);
|
||||
this.modelManager.setItemDirty(component);
|
||||
this.syncManager.sync();
|
||||
this.setState({
|
||||
defaultEditor: null
|
||||
});
|
||||
}
|
||||
|
||||
shouldDisplayRunningLocallyLabel(component) {
|
||||
if(!component.runningLocally) {
|
||||
return false;
|
||||
}
|
||||
if(component === this.selectedEditor) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorMenu {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = EditorMenuCtrl;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
callback: '&',
|
||||
selectedEditor: '=',
|
||||
currentItem: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
127
app/assets/javascripts/directives/views/editorMenu.ts
Normal file
127
app/assets/javascripts/directives/views/editorMenu.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNComponent, SNItem, ComponentArea } from 'snjs';
|
||||
import { isDesktopApplication } from '@/utils';
|
||||
import template from '%/directives/editor-menu.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { ComponentMutator } from 'snjs/dist/@types/models';
|
||||
|
||||
interface EditorMenuScope {
|
||||
callback: (component: SNComponent) => void
|
||||
selectedEditorUuid: string
|
||||
currentItem: SNItem
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
||||
|
||||
callback!: () => (component: SNComponent) => void
|
||||
selectedEditorUuid!: string
|
||||
currentItem!: SNItem
|
||||
application!: WebApplication
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
super($timeout);
|
||||
this.state = {
|
||||
isDesktop: isDesktopApplication()
|
||||
};
|
||||
}
|
||||
|
||||
public isEditorSelected(editor: SNComponent) {
|
||||
if(!this.selectedEditorUuid) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedEditorUuid === editor.uuid;
|
||||
}
|
||||
|
||||
public isEditorDefault(editor: SNComponent) {
|
||||
return this.state.defaultEditor?.uuid === editor.uuid;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
const editors = this.application.componentManager!.componentsForArea(ComponentArea.Editor)
|
||||
.sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
const defaultEditor = editors.filter((e) => e.isDefaultEditor())[0];
|
||||
this.setState({
|
||||
editors: editors,
|
||||
defaultEditor: defaultEditor
|
||||
});
|
||||
};
|
||||
|
||||
selectComponent(component: SNComponent) {
|
||||
if (component) {
|
||||
if (component.conflictOf) {
|
||||
this.application.changeAndSaveItem(component.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
})
|
||||
}
|
||||
}
|
||||
this.$timeout(() => {
|
||||
this.callback()(component);
|
||||
});
|
||||
}
|
||||
|
||||
toggleDefaultForEditor(editor: SNComponent) {
|
||||
if (this.state.defaultEditor === editor) {
|
||||
this.removeEditorDefault(editor);
|
||||
} else {
|
||||
this.makeEditorDefault(editor);
|
||||
}
|
||||
}
|
||||
|
||||
offlineAvailableForComponent(component: SNComponent) {
|
||||
return component.local_url && this.state.isDesktop;
|
||||
}
|
||||
|
||||
makeEditorDefault(component: SNComponent) {
|
||||
const currentDefault = this.application.componentManager!
|
||||
.componentsForArea(ComponentArea.Editor)
|
||||
.filter((e) => e.isDefaultEditor())[0];
|
||||
if (currentDefault) {
|
||||
this.application.changeItem(currentDefault.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = false;
|
||||
})
|
||||
}
|
||||
this.application.changeAndSaveItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = true;
|
||||
});
|
||||
this.setState({
|
||||
defaultEditor: component
|
||||
});
|
||||
}
|
||||
|
||||
removeEditorDefault(component: SNComponent) {
|
||||
this.application.changeAndSaveItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = false;
|
||||
});
|
||||
this.setState({
|
||||
defaultEditor: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class EditorMenu extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = EditorMenuCtrl;
|
||||
this.controllerAs = 'self';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
callback: '&',
|
||||
selectedEditorUuid: '=',
|
||||
currentItem: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
191
app/assets/javascripts/directives/views/historyMenu.ts
Normal file
191
app/assets/javascripts/directives/views/historyMenu.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { WebDirective } from '../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import template from '%/directives/history-menu.pug';
|
||||
import { SNItem, ItemHistoryEntry } from 'snjs/dist/@types';
|
||||
import { PureViewCtrl } from '@/views';
|
||||
import { ItemSessionHistory } from 'snjs/dist/@types/services/history/session/item_session_history';
|
||||
import { RevisionListEntry, SingleRevision } from 'snjs/dist/@types/services/api/responses';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
|
||||
type HistoryState = {
|
||||
fetchingRemoteHistory: boolean
|
||||
}
|
||||
|
||||
interface HistoryScope {
|
||||
application: WebApplication
|
||||
item: SNItem
|
||||
}
|
||||
|
||||
class HistoryMenuCtrl extends PureViewCtrl<{}, HistoryState> implements HistoryScope {
|
||||
|
||||
diskEnabled = false
|
||||
autoOptimize = false
|
||||
application!: WebApplication
|
||||
item!: SNItem
|
||||
sessionHistory?: ItemSessionHistory
|
||||
remoteHistory?: RevisionListEntry[]
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
this.state = {
|
||||
fetchingRemoteHistory: false
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.reloadSessionHistory();
|
||||
this.fetchRemoteHistory();
|
||||
this.diskEnabled = this.application.historyManager!.isDiskEnabled();
|
||||
this.autoOptimize = this.application.historyManager!.isAutoOptimizeEnabled();
|
||||
}
|
||||
|
||||
reloadSessionHistory() {
|
||||
this.sessionHistory = this.application.historyManager!.sessionHistoryForItem(this.item);
|
||||
}
|
||||
|
||||
get isfetchingRemoteHistory() {
|
||||
return this.state.fetchingRemoteHistory;
|
||||
}
|
||||
|
||||
set fetchingRemoteHistory(value: boolean) {
|
||||
this.setState({
|
||||
fetchingRemoteHistory: value
|
||||
});
|
||||
}
|
||||
|
||||
async fetchRemoteHistory() {
|
||||
this.fetchingRemoteHistory = true;
|
||||
this.remoteHistory = await this.application.historyManager!.remoteHistoryForItem(this.item)
|
||||
.finally(() => {
|
||||
this.fetchingRemoteHistory = false;
|
||||
});
|
||||
}
|
||||
|
||||
async openSessionRevision(revision: ItemHistoryEntry) {
|
||||
this.application.presentRevisionPreviewModal(
|
||||
revision.payload.uuid,
|
||||
revision.payload.content
|
||||
);
|
||||
}
|
||||
|
||||
async openRemoteRevision(revision: RevisionListEntry) {
|
||||
this.fetchingRemoteHistory = true;
|
||||
const remoteRevision = await this.application.historyManager!.fetchRemoteRevision(this.item.uuid, revision);
|
||||
this.fetchingRemoteHistory = false;
|
||||
if (!remoteRevision) {
|
||||
this.application.alertService!.alert("The remote revision could not be loaded. Please try again later.");
|
||||
return;
|
||||
}
|
||||
this.application.presentRevisionPreviewModal(
|
||||
remoteRevision.payload.uuid,
|
||||
remoteRevision.payload.content
|
||||
);
|
||||
}
|
||||
|
||||
classForSessionRevision(revision: ItemHistoryEntry) {
|
||||
const vector = revision.operationVector();
|
||||
if (vector === 0) {
|
||||
return 'default';
|
||||
} else if (vector === 1) {
|
||||
return 'success';
|
||||
} else if (vector === -1) {
|
||||
return 'danger';
|
||||
}
|
||||
}
|
||||
|
||||
clearItemSessionHistory() {
|
||||
confirmDialog({
|
||||
text: "Are you sure you want to delete the local session history for this note?",
|
||||
confirmButtonStyle: "danger"
|
||||
}).then((confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.application.historyManager!.clearHistoryForItem(this.item).then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadSessionHistory();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clearAllSessionHistory() {
|
||||
confirmDialog({
|
||||
text: "Are you sure you want to delete the local session history for all notes?",
|
||||
confirmButtonStyle: "danger"
|
||||
}).then((confirmed) => {
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.application.historyManager!.clearAllHistory().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadSessionHistory();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get sessionHistoryEntries() {
|
||||
return this.sessionHistory?.entries;
|
||||
}
|
||||
|
||||
get remoteHistoryEntries() {
|
||||
return this.remoteHistory;
|
||||
}
|
||||
|
||||
toggleSessionHistoryDiskSaving() {
|
||||
const run = () => {
|
||||
this.application.historyManager!.toggleDiskSaving().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.diskEnabled = this.application.historyManager!.isDiskEnabled();
|
||||
});
|
||||
});
|
||||
};
|
||||
if (!this.application.historyManager!.isDiskEnabled()) {
|
||||
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"
|
||||
}).then((confirmed) => {
|
||||
if (confirmed) {
|
||||
run();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
toggleSessionHistoryAutoOptimize() {
|
||||
this.application.historyManager!.toggleAutoOptimize().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.autoOptimize = this.application.historyManager!.autoOptimize;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
previewRemoteHistoryTitle(revision: SingleRevision) {
|
||||
const createdAt = revision.created_at!;
|
||||
return new Date(createdAt).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
export class HistoryMenu extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = HistoryMenuCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
item: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export { AccountMenu } from './accountMenu';
|
||||
export { ActionsMenu } from './actionsMenu';
|
||||
export { ComponentModal } from './componentModal';
|
||||
export { ComponentView } from './componentView';
|
||||
export { ConflictResolutionModal } from './conflictResolutionModal';
|
||||
export { EditorMenu } from './editorMenu';
|
||||
export { InputModal } from './inputModal';
|
||||
export { MenuRow } from './menuRow';
|
||||
@@ -12,5 +11,5 @@ export { PermissionsModal } from './permissionsModal';
|
||||
export { PrivilegesAuthModal } from './privilegesAuthModal';
|
||||
export { PrivilegesManagementModal } from './privilegesManagementModal';
|
||||
export { RevisionPreviewModal } from './revisionPreviewModal';
|
||||
export { SessionHistoryMenu } from './sessionHistoryMenu';
|
||||
export { HistoryMenu } from './historyMenu';
|
||||
export { SyncResolutionMenu } from './syncResolutionMenu';
|
||||
@@ -1,37 +0,0 @@
|
||||
import template from '%/directives/input-modal.pug';
|
||||
|
||||
class InputModalCtrl {
|
||||
|
||||
/* @ngInject */
|
||||
constructor($scope, $element) {
|
||||
this.$element = $element;
|
||||
this.formData = {};
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
this.$scope.$destroy();
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.callback()(this.formData.input);
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
export class InputModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = InputModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
type: '=',
|
||||
title: '=',
|
||||
message: '=',
|
||||
placeholder: '=',
|
||||
callback: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
53
app/assets/javascripts/directives/views/inputModal.ts
Normal file
53
app/assets/javascripts/directives/views/inputModal.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/input-modal.pug';
|
||||
|
||||
export interface InputModalScope extends Partial<ng.IScope> {
|
||||
type: string
|
||||
title: string
|
||||
message: string
|
||||
callback: (value: string) => void
|
||||
}
|
||||
|
||||
class InputModalCtrl implements InputModalScope {
|
||||
|
||||
$element: JQLite
|
||||
type!: string
|
||||
title!: string
|
||||
message!: string
|
||||
callback!: (value: string) => void
|
||||
formData = { input: '' }
|
||||
|
||||
/* @ngInject */
|
||||
constructor($element: JQLite) {
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.callback(this.formData.input);
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
export class InputModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = InputModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
type: '=',
|
||||
title: '=',
|
||||
message: '=',
|
||||
callback: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/menu-row.pug';
|
||||
|
||||
class MenuRowCtrl {
|
||||
|
||||
onClick($event) {
|
||||
disabled!: boolean
|
||||
action!: () => void
|
||||
buttonAction!: () => void
|
||||
|
||||
onClick($event: Event) {
|
||||
if(this.disabled) {
|
||||
return;
|
||||
}
|
||||
@@ -10,7 +15,7 @@ class MenuRowCtrl {
|
||||
this.action();
|
||||
}
|
||||
|
||||
clickAccessoryButton($event) {
|
||||
clickAccessoryButton($event: Event) {
|
||||
if(this.disabled) {
|
||||
return;
|
||||
}
|
||||
@@ -19,8 +24,9 @@ class MenuRowCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export class MenuRow {
|
||||
export class MenuRow extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.transclude = true;
|
||||
this.template = template;
|
||||
@@ -1,71 +1,141 @@
|
||||
import { PanelPuppet, WebDirective } from './../../types';
|
||||
import angular from 'angular';
|
||||
import template from '%/directives/panel-resizer.pug';
|
||||
import { debounce } from '@/utils';
|
||||
|
||||
const PANEL_SIDE_RIGHT = 'right';
|
||||
const PANEL_SIDE_LEFT = 'left';
|
||||
|
||||
const MOUSE_EVENT_MOVE = 'mousemove';
|
||||
const MOUSE_EVENT_DOWN = 'mousedown';
|
||||
const MOUSE_EVENT_UP = 'mouseup';
|
||||
|
||||
enum PanelSide {
|
||||
Right = 'right',
|
||||
Left = 'left'
|
||||
};
|
||||
enum MouseEventType {
|
||||
Move = 'mousemove',
|
||||
Down = 'mousedown',
|
||||
Up = 'mouseup'
|
||||
};
|
||||
enum CssClass {
|
||||
Hoverable = 'hoverable',
|
||||
AlwaysVisible = 'always-visible',
|
||||
Dragging = 'dragging',
|
||||
NoSelection = 'no-selection',
|
||||
Collapsed = 'collapsed',
|
||||
AnimateOpacity = 'animate-opacity',
|
||||
};
|
||||
const WINDOW_EVENT_RESIZE = 'resize';
|
||||
|
||||
const PANEL_CSS_CLASS_HOVERABLE = 'hoverable';
|
||||
const PANEL_CSS_CLASS_ALWAYS_VISIBLE = 'always-visible';
|
||||
const PANEL_CSS_CLASS_DRAGGING = 'dragging';
|
||||
const PANEL_CSS_CLASS_NO_SELECTION = 'no-selection';
|
||||
const PANEL_CSS_CLASS_COLLAPSED = 'collapsed';
|
||||
const PANEL_CSS_CLASS_ANIMATE_OPACITY = 'animate-opacity';
|
||||
type ResizeFinishCallback = (
|
||||
lastWidth: number,
|
||||
lastLeft: number,
|
||||
isMaxWidth: boolean,
|
||||
isCollapsed: boolean
|
||||
) => void
|
||||
|
||||
interface PanelResizerScope {
|
||||
alwaysVisible: boolean
|
||||
collapsable: boolean
|
||||
control: PanelPuppet
|
||||
defaultWidth: number
|
||||
hoverable: boolean
|
||||
index: number
|
||||
minWidth: number
|
||||
onResizeFinish: () => ResizeFinishCallback
|
||||
panelId: string
|
||||
property: PanelSide
|
||||
}
|
||||
|
||||
class PanelResizerCtrl implements PanelResizerScope {
|
||||
|
||||
/** @scope */
|
||||
alwaysVisible!: boolean
|
||||
collapsable!: boolean
|
||||
control!: PanelPuppet
|
||||
defaultWidth!: number
|
||||
hoverable!: boolean
|
||||
index!: number
|
||||
minWidth!: number
|
||||
onResizeFinish!: () => ResizeFinishCallback
|
||||
panelId!: string
|
||||
property!: PanelSide
|
||||
|
||||
$compile: ng.ICompileService
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
panel!: HTMLElement
|
||||
resizerColumn!: HTMLElement
|
||||
currentMinWidth = 0
|
||||
pressed = false
|
||||
startWidth = 0
|
||||
lastDownX = 0
|
||||
collapsed = false
|
||||
lastWidth = 0
|
||||
startLeft = 0
|
||||
lastLeft = 0
|
||||
appFrame?: DOMRect
|
||||
widthBeforeLastDblClick = 0
|
||||
overlay?: JQLite
|
||||
|
||||
class PanelResizerCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$compile,
|
||||
$element,
|
||||
$scope,
|
||||
$timeout,
|
||||
$compile: ng.ICompileService,
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
this.$compile = $compile;
|
||||
this.$element = $element;
|
||||
this.$scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
|
||||
/** To allow for registering events */
|
||||
this.handleResize = debounce(this.handleResize.bind(this), 250);
|
||||
this.onMouseMove = this.onMouseMove.bind(this);
|
||||
this.onMouseUp = this.onMouseUp.bind(this);
|
||||
this.onMouseDown = this.onMouseDown.bind(this);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.configureControl();
|
||||
this.configureDefaults();
|
||||
this.addDoubleClickHandler();
|
||||
this.reloadDefaultValues();
|
||||
this.addMouseDownListener();
|
||||
this.addMouseMoveListener();
|
||||
this.addMouseUpListener();
|
||||
this.configureControl();
|
||||
this.addDoubleClickHandler();
|
||||
this.resizerColumn.addEventListener(MouseEventType.Down, this.onMouseDown);
|
||||
document.addEventListener(MouseEventType.Move, this.onMouseMove);
|
||||
document.addEventListener(MouseEventType.Up, this.onMouseUp);
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
(this.onResizeFinish as any) = undefined;
|
||||
(this.control as any) = undefined;
|
||||
window.removeEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
|
||||
document.removeEventListener(MouseEventType.Move, this.onMouseMove);
|
||||
document.removeEventListener(MouseEventType.Up, this.onMouseUp);
|
||||
this.resizerColumn.removeEventListener(MouseEventType.Down, this.onMouseDown);
|
||||
(this.handleResize as any) = undefined;
|
||||
(this.onMouseMove as any) = undefined;
|
||||
(this.onMouseUp as any) = undefined;
|
||||
(this.onMouseDown as any) = undefined;
|
||||
}
|
||||
|
||||
configureControl() {
|
||||
this.control.setWidth = (value) => {
|
||||
this.setWidth(value, true);
|
||||
};
|
||||
|
||||
this.control.setLeft = (value) => {
|
||||
this.setLeft(value);
|
||||
};
|
||||
|
||||
this.control.flash = () => {
|
||||
this.flash();
|
||||
};
|
||||
|
||||
this.control.isCollapsed = () => {
|
||||
return this.isCollapsed();
|
||||
};
|
||||
this.control.ready = true;
|
||||
this.control.onReady!();
|
||||
}
|
||||
|
||||
configureDefaults() {
|
||||
this.panel = document.getElementById(this.panelId);
|
||||
this.panel = document.getElementById(this.panelId)!;
|
||||
if (!this.panel) {
|
||||
console.error('Panel not found for', this.panelId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.resizerColumn = this.$element[0];
|
||||
this.currentMinWidth = this.minWidth || this.resizerColumn.offsetWidth;
|
||||
this.pressed = false;
|
||||
@@ -75,36 +145,34 @@ class PanelResizerCtrl {
|
||||
this.lastWidth = this.startWidth;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.lastLeft = this.startLeft;
|
||||
this.appFrame = null;
|
||||
this.appFrame = undefined;
|
||||
this.widthBeforeLastDblClick = 0;
|
||||
|
||||
if (this.property === PANEL_SIDE_RIGHT) {
|
||||
if (this.property === PanelSide.Right) {
|
||||
this.configureRightPanel();
|
||||
}
|
||||
if (this.alwaysVisible) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ALWAYS_VISIBLE);
|
||||
this.resizerColumn.classList.add(CssClass.AlwaysVisible);
|
||||
}
|
||||
if (this.hoverable) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_HOVERABLE);
|
||||
this.resizerColumn.classList.add(CssClass.Hoverable);
|
||||
}
|
||||
}
|
||||
|
||||
configureRightPanel() {
|
||||
const handleResize = debounce(event => {
|
||||
this.reloadDefaultValues();
|
||||
this.handleWidthEvent();
|
||||
this.$timeout(() => {
|
||||
this.finishSettingWidth();
|
||||
});
|
||||
}, 250);
|
||||
window.addEventListener(WINDOW_EVENT_RESIZE, handleResize);
|
||||
this.$scope.$on('$destroy', () => {
|
||||
window.removeEventListener(WINDOW_EVENT_RESIZE, handleResize);
|
||||
window.addEventListener(WINDOW_EVENT_RESIZE, this.handleResize);
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.reloadDefaultValues();
|
||||
this.handleWidthEvent();
|
||||
this.$timeout(() => {
|
||||
this.finishSettingWidth();
|
||||
});
|
||||
}
|
||||
|
||||
getParentRect() {
|
||||
return this.panel.parentNode.getBoundingClientRect();
|
||||
const node = this.panel!.parentNode! as HTMLElement;
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
|
||||
reloadDefaultValues() {
|
||||
@@ -112,7 +180,7 @@ class PanelResizerCtrl {
|
||||
? this.getParentRect().width
|
||||
: this.panel.scrollWidth;
|
||||
this.lastWidth = this.startWidth;
|
||||
this.appFrame = document.getElementById('app').getBoundingClientRect();
|
||||
this.appFrame = document.getElementById('app')!.getBoundingClientRect();
|
||||
}
|
||||
|
||||
addDoubleClickHandler() {
|
||||
@@ -125,9 +193,7 @@ class PanelResizerCtrl {
|
||||
this.widthBeforeLastDblClick = this.lastWidth;
|
||||
this.setWidth(this.currentMinWidth);
|
||||
}
|
||||
|
||||
this.finishSettingWidth();
|
||||
|
||||
const newCollapseState = !preClickCollapseState;
|
||||
this.onResizeFinish()(
|
||||
this.lastWidth,
|
||||
@@ -139,53 +205,65 @@ class PanelResizerCtrl {
|
||||
};
|
||||
}
|
||||
|
||||
addMouseDownListener() {
|
||||
this.resizerColumn.addEventListener(MOUSE_EVENT_DOWN, (event) => {
|
||||
this.addInvisibleOverlay();
|
||||
this.pressed = true;
|
||||
this.lastDownX = event.clientX;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.panel.classList.add(PANEL_CSS_CLASS_NO_SELECTION);
|
||||
if (this.hoverable) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_DRAGGING);
|
||||
}
|
||||
});
|
||||
onMouseDown(event: MouseEvent) {
|
||||
this.addInvisibleOverlay();
|
||||
this.pressed = true;
|
||||
this.lastDownX = event.clientX;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.panel.classList.add(CssClass.NoSelection);
|
||||
if (this.hoverable) {
|
||||
this.resizerColumn.classList.add(CssClass.Dragging);
|
||||
}
|
||||
}
|
||||
|
||||
addMouseMoveListener() {
|
||||
document.addEventListener(MOUSE_EVENT_MOVE, (event) => {
|
||||
if (!this.pressed) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (this.property && this.property === PANEL_SIDE_LEFT) {
|
||||
this.handleLeftEvent(event);
|
||||
} else {
|
||||
this.handleWidthEvent(event);
|
||||
}
|
||||
});
|
||||
onMouseUp() {
|
||||
this.removeInvisibleOverlay();
|
||||
if (!this.pressed) {
|
||||
return;
|
||||
}
|
||||
this.pressed = false;
|
||||
this.resizerColumn.classList.remove(CssClass.Dragging);
|
||||
this.panel.classList.remove(CssClass.NoSelection);
|
||||
const isMaxWidth = this.isAtMaxWidth();
|
||||
if (this.onResizeFinish) {
|
||||
this.onResizeFinish()(
|
||||
this.lastWidth,
|
||||
this.lastLeft,
|
||||
isMaxWidth,
|
||||
this.isCollapsed()
|
||||
);
|
||||
}
|
||||
this.finishSettingWidth();
|
||||
}
|
||||
|
||||
handleWidthEvent(event) {
|
||||
onMouseMove(event: MouseEvent) {
|
||||
if (!this.pressed) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (this.property && this.property === PanelSide.Left) {
|
||||
this.handleLeftEvent(event);
|
||||
} else {
|
||||
this.handleWidthEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleWidthEvent(event?: MouseEvent) {
|
||||
let x;
|
||||
if (event) {
|
||||
x = event.clientX;
|
||||
x = event!.clientX;
|
||||
} else {
|
||||
/** Coming from resize event */
|
||||
x = 0;
|
||||
this.lastDownX = 0;
|
||||
}
|
||||
|
||||
const deltaX = x - this.lastDownX;
|
||||
const newWidth = this.startWidth + deltaX;
|
||||
this.setWidth(newWidth, false);
|
||||
if (this.onResize()) {
|
||||
this.onResize()(this.lastWidth, this.panel);
|
||||
}
|
||||
}
|
||||
|
||||
handleLeftEvent(event) {
|
||||
handleLeftEvent(event: MouseEvent) {
|
||||
const panelRect = this.panel.getBoundingClientRect();
|
||||
const x = event.clientX || panelRect.x;
|
||||
let deltaX = x - this.lastDownX;
|
||||
@@ -205,34 +283,13 @@ class PanelResizerCtrl {
|
||||
if (newLeft + newWidth > parentRect.width) {
|
||||
newLeft = parentRect.width - newWidth;
|
||||
}
|
||||
this.setLeft(newLeft, false);
|
||||
this.setLeft(newLeft);
|
||||
this.setWidth(newWidth, false);
|
||||
}
|
||||
|
||||
addMouseUpListener() {
|
||||
document.addEventListener(MOUSE_EVENT_UP, event => {
|
||||
this.removeInvisibleOverlay();
|
||||
if (this.pressed) {
|
||||
this.pressed = false;
|
||||
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_DRAGGING);
|
||||
this.panel.classList.remove(PANEL_CSS_CLASS_NO_SELECTION);
|
||||
const isMaxWidth = this.isAtMaxWidth();
|
||||
if (this.onResizeFinish) {
|
||||
this.onResizeFinish()(
|
||||
this.lastWidth,
|
||||
this.lastLeft,
|
||||
isMaxWidth,
|
||||
this.isCollapsed()
|
||||
);
|
||||
}
|
||||
this.finishSettingWidth();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isAtMaxWidth() {
|
||||
return (
|
||||
Math.round(this.lastWidth + this.lastLeft) ===
|
||||
Math.round(this.lastWidth + this.lastLeft) ===
|
||||
Math.round(this.getParentRect().width)
|
||||
);
|
||||
}
|
||||
@@ -241,7 +298,7 @@ class PanelResizerCtrl {
|
||||
return this.lastWidth <= this.currentMinWidth;
|
||||
}
|
||||
|
||||
setWidth(width, finish) {
|
||||
setWidth(width: number, finish = false) {
|
||||
if (width < this.currentMinWidth) {
|
||||
width = this.currentMinWidth;
|
||||
}
|
||||
@@ -250,7 +307,7 @@ class PanelResizerCtrl {
|
||||
width = parentRect.width;
|
||||
}
|
||||
|
||||
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
|
||||
const maxWidth = this.appFrame!.width - this.panel.getBoundingClientRect().x;
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
@@ -267,7 +324,7 @@ class PanelResizerCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
setLeft(left) {
|
||||
setLeft(left: number) {
|
||||
this.panel.style.left = left + 'px';
|
||||
this.lastLeft = left;
|
||||
}
|
||||
@@ -279,9 +336,9 @@ class PanelResizerCtrl {
|
||||
|
||||
this.collapsed = this.isCollapsed();
|
||||
if (this.collapsed) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_COLLAPSED);
|
||||
this.resizerColumn.classList.add(CssClass.Collapsed);
|
||||
} else {
|
||||
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_COLLAPSED);
|
||||
this.resizerColumn.classList.remove(CssClass.Collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,28 +352,29 @@ class PanelResizerCtrl {
|
||||
if (this.overlay) {
|
||||
return;
|
||||
}
|
||||
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this.$scope);
|
||||
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this as any);
|
||||
angular.element(document.body).prepend(this.overlay);
|
||||
}
|
||||
|
||||
removeInvisibleOverlay() {
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
this.overlay = null;
|
||||
this.overlay = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
flash() {
|
||||
const FLASH_DURATION = 3000;
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ANIMATE_OPACITY);
|
||||
this.resizerColumn.classList.add(CssClass.AnimateOpacity);
|
||||
this.$timeout(() => {
|
||||
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_ANIMATE_OPACITY);
|
||||
this.resizerColumn.classList.remove(CssClass.AnimateOpacity);
|
||||
}, FLASH_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelResizer {
|
||||
export class PanelResizer extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PanelResizerCtrl;
|
||||
@@ -330,7 +388,6 @@ export class PanelResizer {
|
||||
hoverable: '=',
|
||||
index: '=',
|
||||
minWidth: '=',
|
||||
onResize: '&',
|
||||
onResizeFinish: '&',
|
||||
panelId: '=',
|
||||
property: '='
|
||||
@@ -1,302 +0,0 @@
|
||||
import { protocolManager } from 'snjs';
|
||||
import template from '%/directives/password-wizard.pug';
|
||||
import { STRING_FAILED_PASSWORD_CHANGE } from '@/strings';
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||
const Steps = {
|
||||
IntroStep: 0,
|
||||
BackupStep: 1,
|
||||
SignoutStep: 2,
|
||||
PasswordStep: 3,
|
||||
SyncStep: 4,
|
||||
FinishStep: 5
|
||||
};
|
||||
|
||||
class PasswordWizardCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element,
|
||||
$scope,
|
||||
$timeout,
|
||||
alertManager,
|
||||
archiveManager,
|
||||
authManager,
|
||||
modelManager,
|
||||
syncManager,
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
this.$scope = $scope;
|
||||
this.alertManager = alertManager;
|
||||
this.archiveManager = archiveManager;
|
||||
this.authManager = authManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.registerWindowUnloadStopper();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.syncStatus = this.syncManager.syncStatus;
|
||||
this.formData = {};
|
||||
this.configureDefaults();
|
||||
}
|
||||
|
||||
configureDefaults() {
|
||||
if (this.type === 'change-pw') {
|
||||
this.title = "Change Password";
|
||||
this.changePassword = true;
|
||||
} else if (this.type === 'upgrade-security') {
|
||||
this.title = "Security Update";
|
||||
this.securityUpdate = true;
|
||||
}
|
||||
this.continueTitle = DEFAULT_CONTINUE_TITLE;
|
||||
this.step = Steps.IntroStep;
|
||||
}
|
||||
|
||||
/** Confirms with user before closing tab */
|
||||
registerWindowUnloadStopper() {
|
||||
window.onbeforeunload = (e) => {
|
||||
return true;
|
||||
};
|
||||
this.$scope.$on("$destroy", () => {
|
||||
window.onbeforeunload = null;
|
||||
});
|
||||
}
|
||||
|
||||
titleForStep(step) {
|
||||
switch (step) {
|
||||
case Steps.BackupStep:
|
||||
return "Download a backup of your data";
|
||||
case Steps.SignoutStep:
|
||||
return "Sign out of all your devices";
|
||||
case Steps.PasswordStep:
|
||||
return this.changePassword
|
||||
? "Password information"
|
||||
: "Enter your current password";
|
||||
case Steps.SyncStep:
|
||||
return "Encrypt and sync data with new keys";
|
||||
case Steps.FinishStep:
|
||||
return "Sign back in to your devices";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async nextStep() {
|
||||
if (this.lockContinue || this.isContinuing) {
|
||||
return;
|
||||
}
|
||||
this.isContinuing = true;
|
||||
if (this.step === Steps.FinishStep) {
|
||||
this.dismiss();
|
||||
return;
|
||||
}
|
||||
const next = () => {
|
||||
this.step++;
|
||||
this.initializeStep(this.step);
|
||||
this.isContinuing = false;
|
||||
};
|
||||
const preprocessor = this.preprocessorForStep(this.step);
|
||||
if (preprocessor) {
|
||||
await preprocessor().then((success) => {
|
||||
if(success) {
|
||||
next();
|
||||
} else {
|
||||
this.$timeout(() => {
|
||||
this.isContinuing = false;
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
this.isContinuing = false;
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
preprocessorForStep(step) {
|
||||
if (step === Steps.PasswordStep) {
|
||||
return async () => {
|
||||
this.showSpinner = true;
|
||||
this.continueTitle = "Generating Keys...";
|
||||
const success = await this.validateCurrentPassword();
|
||||
this.showSpinner = false;
|
||||
this.continueTitle = DEFAULT_CONTINUE_TITLE;
|
||||
return success;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async initializeStep(step) {
|
||||
if (step === Steps.SyncStep) {
|
||||
await this.initializeSyncingStep();
|
||||
} else if (step === Steps.FinishStep) {
|
||||
this.continueTitle = "Finish";
|
||||
}
|
||||
}
|
||||
|
||||
async initializeSyncingStep() {
|
||||
this.lockContinue = true;
|
||||
this.formData.status = "Processing encryption keys...";
|
||||
this.formData.processing = true;
|
||||
|
||||
const passwordSuccess = await this.processPasswordChange();
|
||||
this.formData.statusError = !passwordSuccess;
|
||||
this.formData.processing = passwordSuccess;
|
||||
if(!passwordSuccess) {
|
||||
this.formData.status = "Unable to process your password. Please try again.";
|
||||
return;
|
||||
}
|
||||
this.formData.status = "Encrypting and syncing data with new keys...";
|
||||
|
||||
const syncSuccess = await this.resyncData();
|
||||
this.formData.statusError = !syncSuccess;
|
||||
this.formData.processing = !syncSuccess;
|
||||
if (syncSuccess) {
|
||||
this.lockContinue = false;
|
||||
if (this.changePassword) {
|
||||
this.formData.status = "Successfully changed password and synced all items.";
|
||||
} else if (this.securityUpdate) {
|
||||
this.formData.status = "Successfully performed security update and synced all items.";
|
||||
}
|
||||
} else {
|
||||
this.formData.status = STRING_FAILED_PASSWORD_CHANGE;
|
||||
}
|
||||
}
|
||||
|
||||
async validateCurrentPassword() {
|
||||
const currentPassword = this.formData.currentPassword;
|
||||
const newPass = this.securityUpdate ? currentPassword : this.formData.newPassword;
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
this.alertManager.alert({
|
||||
text: "Please enter your current password."
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (this.changePassword) {
|
||||
if (!newPass || newPass.length === 0) {
|
||||
this.alertManager.alert({
|
||||
text: "Please enter a new password."
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (newPass !== this.formData.newPasswordConfirmation) {
|
||||
this.alertManager.alert({
|
||||
text: "Your new password does not match its confirmation."
|
||||
});
|
||||
this.formData.status = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!this.authManager.user.email) {
|
||||
this.alertManager.alert({
|
||||
text: "We don't have your email stored. Please log out then log back in to fix this issue."
|
||||
});
|
||||
this.formData.status = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const minLength = this.authManager.getMinPasswordLength();
|
||||
if (!this.securityUpdate && newPass.length < minLength) {
|
||||
const message = `Your password must be at least ${minLength} characters in length. For your security, please choose a longer password or, ideally, a passphrase, and try again.`;
|
||||
this.alertManager.alert({
|
||||
text: message
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Validate current password */
|
||||
const authParams = await this.authManager.getAuthParams();
|
||||
const password = this.formData.currentPassword;
|
||||
const keys = await protocolManager.computeEncryptionKeysForUser(
|
||||
password,
|
||||
authParams
|
||||
);
|
||||
const success = keys.mk === (await this.authManager.keys()).mk;
|
||||
if (success) {
|
||||
this.currentServerPw = keys.pw;
|
||||
} else {
|
||||
this.alertManager.alert({
|
||||
text: "The current password you entered is not correct. Please try again."
|
||||
});
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
async resyncData() {
|
||||
await this.modelManager.setAllItemsDirty();
|
||||
const response = await this.syncManager.sync();
|
||||
if (!response || response.error) {
|
||||
this.alertManager.alert({
|
||||
text: STRING_FAILED_PASSWORD_CHANGE
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async processPasswordChange() {
|
||||
const newUserPassword = this.securityUpdate
|
||||
? this.formData.currentPassword
|
||||
: this.formData.newPassword;
|
||||
|
||||
const currentServerPw = this.currentServerPw;
|
||||
const results = await protocolManager.generateInitialKeysAndAuthParamsForUser(
|
||||
this.authManager.user.email,
|
||||
newUserPassword
|
||||
);
|
||||
const newKeys = results.keys;
|
||||
const newAuthParams = results.authParams;
|
||||
/**
|
||||
* Perform a sync beforehand to pull in any last minutes changes before we change
|
||||
* the encryption key (and thus cant decrypt new changes).
|
||||
*/
|
||||
await this.syncManager.sync();
|
||||
const response = await this.authManager.changePassword(
|
||||
await this.syncManager.getServerURL(),
|
||||
this.authManager.user.email,
|
||||
currentServerPw,
|
||||
newKeys,
|
||||
newAuthParams
|
||||
);
|
||||
if (response.error) {
|
||||
this.alertManager.alert({
|
||||
text: response.error.message
|
||||
? response.error.message
|
||||
: "There was an error changing your password. Please try again."
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
downloadBackup(encrypted) {
|
||||
this.archiveManager.downloadBackup(encrypted);
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
if (this.lockContinue) {
|
||||
this.alertManager.alert({
|
||||
text: "Cannot close window until pending tasks are complete."
|
||||
});
|
||||
} else {
|
||||
this.$element.remove();
|
||||
this.$scope.$destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordWizard {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PasswordWizardCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
type: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
239
app/assets/javascripts/directives/views/passwordWizard.ts
Normal file
239
app/assets/javascripts/directives/views/passwordWizard.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { PasswordWizardScope, PasswordWizardType, WebDirective } from './../../types';
|
||||
import template from '%/directives/password-wizard.pug';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
|
||||
const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
FinishStep = 2
|
||||
};
|
||||
|
||||
type FormData = {
|
||||
currentPassword?: string,
|
||||
newPassword?: string,
|
||||
newPasswordConfirmation?: string,
|
||||
status?: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
lockContinue: boolean
|
||||
formData: FormData,
|
||||
continueTitle: string,
|
||||
step: Steps,
|
||||
title: string,
|
||||
showSpinner: boolean
|
||||
processing: boolean
|
||||
}
|
||||
|
||||
type Props = {
|
||||
type: PasswordWizardType,
|
||||
changePassword: boolean,
|
||||
securityUpdate: boolean
|
||||
}
|
||||
|
||||
class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordWizardScope {
|
||||
$element: JQLite
|
||||
application!: WebApplication
|
||||
type!: PasswordWizardType
|
||||
isContinuing = false
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService,
|
||||
) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
this.registerWindowUnloadStopper();
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.initProps({
|
||||
type: this.type,
|
||||
changePassword: this.type === PasswordWizardType.ChangePassword,
|
||||
securityUpdate: this.type === PasswordWizardType.AccountUpgrade
|
||||
});
|
||||
this.setState({
|
||||
formData: {},
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE,
|
||||
step: Steps.PasswordStep,
|
||||
title: this.props.changePassword ? 'Change Password' : 'Account Update'
|
||||
});
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
super.$onDestroy();
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
/** Confirms with user before closing tab */
|
||||
registerWindowUnloadStopper() {
|
||||
window.onbeforeunload = () => {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
resetContinueState() {
|
||||
this.setState({
|
||||
showSpinner: false,
|
||||
continueTitle: DEFAULT_CONTINUE_TITLE
|
||||
});
|
||||
this.isContinuing = false;
|
||||
}
|
||||
|
||||
async nextStep() {
|
||||
if (this.state.lockContinue || this.isContinuing) {
|
||||
return;
|
||||
}
|
||||
if (this.state.step === Steps.FinishStep) {
|
||||
this.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isContinuing = true;
|
||||
await this.setState({
|
||||
showSpinner: true,
|
||||
continueTitle: "Generating Keys..."
|
||||
});
|
||||
const valid = await this.validateCurrentPassword();
|
||||
if (!valid) {
|
||||
this.resetContinueState();
|
||||
return;
|
||||
}
|
||||
const success = await this.processPasswordChange();
|
||||
if (!success) {
|
||||
this.resetContinueState();
|
||||
return;
|
||||
}
|
||||
this.isContinuing = false;
|
||||
this.setState({
|
||||
showSpinner: false,
|
||||
continueTitle: "Finish",
|
||||
step: Steps.FinishStep
|
||||
});
|
||||
}
|
||||
|
||||
async setFormDataState(formData: Partial<FormData>) {
|
||||
return this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
...formData
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async validateCurrentPassword() {
|
||||
const currentPassword = this.state.formData.currentPassword;
|
||||
const newPass = this.props.securityUpdate ? currentPassword : this.state.formData.newPassword;
|
||||
if (!currentPassword || currentPassword.length === 0) {
|
||||
this.application.alertService!.alert(
|
||||
"Please enter your current password."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this.props.changePassword) {
|
||||
if (!newPass || newPass.length === 0) {
|
||||
this.application.alertService!.alert(
|
||||
"Please enter a new password."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (newPass !== this.state.formData.newPasswordConfirmation) {
|
||||
this.application.alertService!.alert(
|
||||
"Your new password does not match its confirmation."
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: undefined
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!this.application.getUser()?.email) {
|
||||
this.application.alertService!.alert(
|
||||
"We don't have your email stored. Please log out then log back in to fix this issue."
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: undefined
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Validate current password */
|
||||
const success = await this.application.validateAccountPassword(
|
||||
this.state.formData.currentPassword!
|
||||
);
|
||||
if (!success) {
|
||||
this.application.alertService!.alert(
|
||||
"The current password you entered is not correct. Please try again."
|
||||
);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
async processPasswordChange() {
|
||||
await this.setState({
|
||||
lockContinue: true,
|
||||
processing: true
|
||||
});
|
||||
await this.setFormDataState({
|
||||
status: "Processing encryption keys..."
|
||||
});
|
||||
const newPassword = this.props.securityUpdate
|
||||
? this.state.formData.currentPassword
|
||||
: this.state.formData.newPassword;
|
||||
const response = await this.application.changePassword(
|
||||
this.state.formData.currentPassword!,
|
||||
newPassword!
|
||||
);
|
||||
const success = !response.error;
|
||||
await this.setState({
|
||||
processing: false,
|
||||
lockContinue: false,
|
||||
});
|
||||
if (!success) {
|
||||
this.setFormDataState({
|
||||
status: "Unable to process your password. Please try again."
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
status: this.props.changePassword
|
||||
? "Successfully changed password."
|
||||
: "Successfully performed account update."
|
||||
}
|
||||
});
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
if (this.state.lockContinue) {
|
||||
this.application.alertService!.alert(
|
||||
"Cannot close window until pending tasks are complete."
|
||||
);
|
||||
} else {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PasswordWizard extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PasswordWizardCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
type: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/permissions-modal.pug';
|
||||
|
||||
class PermissionsModalCtrl {
|
||||
|
||||
$element: JQLite
|
||||
callback!: (success: boolean) => void
|
||||
|
||||
/* @ngInject */
|
||||
constructor($element) {
|
||||
constructor($element: JQLite) {
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
|
||||
accept() {
|
||||
@@ -21,8 +29,9 @@ class PermissionsModalCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionsModal {
|
||||
export class PermissionsModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PermissionsModalCtrl;
|
||||
@@ -1,101 +0,0 @@
|
||||
import template from '%/directives/privileges-auth-modal.pug';
|
||||
|
||||
class PrivilegesAuthModalCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element,
|
||||
$timeout,
|
||||
privilegesManager,
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
this.privilegesManager = privilegesManager;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.authParameters = {};
|
||||
this.sessionLengthOptions = this.privilegesManager.getSessionLengthOptions();
|
||||
this.privilegesManager.getSelectedSessionLength().then((length) => {
|
||||
this.$timeout(() => {
|
||||
this.selectedSessionLength = length;
|
||||
});
|
||||
});
|
||||
this.privilegesManager.netCredentialsForAction(this.action).then((credentials) => {
|
||||
this.$timeout(() => {
|
||||
this.requiredCredentials = credentials.sort();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectSessionLength(length) {
|
||||
this.selectedSessionLength = length;
|
||||
}
|
||||
|
||||
promptForCredential(credential) {
|
||||
return this.privilegesManager.displayInfoForCredential(credential).prompt;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dismiss();
|
||||
this.onCancel && this.onCancel();
|
||||
}
|
||||
|
||||
isCredentialInFailureState(credential) {
|
||||
if (!this.failedCredentials) {
|
||||
return false;
|
||||
}
|
||||
return this.failedCredentials.find((candidate) => {
|
||||
return candidate === credential;
|
||||
}) != null;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const failed = [];
|
||||
for (const cred of this.requiredCredentials) {
|
||||
const value = this.authParameters[cred];
|
||||
if (!value || value.length === 0) {
|
||||
failed.push(cred);
|
||||
}
|
||||
}
|
||||
this.failedCredentials = failed;
|
||||
return failed.length === 0;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
const result = await this.privilegesManager.authenticateAction(
|
||||
this.action,
|
||||
this.authParameters
|
||||
);
|
||||
this.$timeout(() => {
|
||||
if (result.success) {
|
||||
this.privilegesManager.setSessionLength(this.selectedSessionLength);
|
||||
this.onSuccess();
|
||||
this.dismiss();
|
||||
} else {
|
||||
this.failedCredentials = result.failedCredentials;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivilegesAuthModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PrivilegesAuthModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
action: '=',
|
||||
onSuccess: '=',
|
||||
onCancel: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
128
app/assets/javascripts/directives/views/privilegesAuthModal.ts
Normal file
128
app/assets/javascripts/directives/views/privilegesAuthModal.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ProtectedAction, PrivilegeCredential, PrivilegeSessionLength } from 'snjs';
|
||||
import template from '%/directives/privileges-auth-modal.pug';
|
||||
|
||||
type PrivilegesAuthModalScope = {
|
||||
application: WebApplication
|
||||
action: ProtectedAction
|
||||
onSuccess: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
class PrivilegesAuthModalCtrl implements PrivilegesAuthModalScope {
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
application!: WebApplication
|
||||
action!: ProtectedAction
|
||||
onSuccess!: () => void
|
||||
onCancel!: () => void
|
||||
authParameters: Partial<Record<PrivilegeCredential, string>> = {}
|
||||
sessionLengthOptions!: { value: PrivilegeSessionLength, label: string }[]
|
||||
selectedSessionLength!: PrivilegeSessionLength
|
||||
requiredCredentials!: PrivilegeCredential[]
|
||||
failedCredentials!: PrivilegeCredential[]
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.sessionLengthOptions = this.application!.privilegesService!
|
||||
.getSessionLengthOptions();
|
||||
this.application.privilegesService!.getSelectedSessionLength()
|
||||
.then((length) => {
|
||||
this.$timeout(() => {
|
||||
this.selectedSessionLength = length;
|
||||
});
|
||||
});
|
||||
this.application.privilegesService!.netCredentialsForAction(this.action)
|
||||
.then((credentials) => {
|
||||
this.$timeout(() => {
|
||||
this.requiredCredentials = credentials.sort();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectSessionLength(length: PrivilegeSessionLength) {
|
||||
this.selectedSessionLength = length;
|
||||
}
|
||||
|
||||
promptForCredential(credential: PrivilegeCredential) {
|
||||
return this.application.privilegesService!.displayInfoForCredential(credential).prompt;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dismiss();
|
||||
this.onCancel && this.onCancel();
|
||||
}
|
||||
|
||||
isCredentialInFailureState(credential: PrivilegeCredential) {
|
||||
if (!this.failedCredentials) {
|
||||
return false;
|
||||
}
|
||||
return this.failedCredentials.find((candidate) => {
|
||||
return candidate === credential;
|
||||
}) != null;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const failed = [];
|
||||
for (const cred of this.requiredCredentials) {
|
||||
const value = this.authParameters[cred];
|
||||
if (!value || value.length === 0) {
|
||||
failed.push(cred);
|
||||
}
|
||||
}
|
||||
this.failedCredentials = failed;
|
||||
return failed.length === 0;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
const result = await this.application.privilegesService!.authenticateAction(
|
||||
this.action,
|
||||
this.authParameters
|
||||
);
|
||||
this.$timeout(() => {
|
||||
if (result.success) {
|
||||
this.application.privilegesService!.setSessionLength(this.selectedSessionLength);
|
||||
this.onSuccess();
|
||||
this.dismiss();
|
||||
} else {
|
||||
this.failedCredentials = result.failedCredentials;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivilegesAuthModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PrivilegesAuthModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
action: '=',
|
||||
onSuccess: '=',
|
||||
onCancel: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { PrivilegesManager } from '@/services/privilegesManager';
|
||||
import template from '%/directives/privileges-management-modal.pug';
|
||||
|
||||
class PrivilegesManagementModalCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
$element,
|
||||
privilegesManager,
|
||||
authManager,
|
||||
passcodeManager,
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
this.privilegesManager = privilegesManager;
|
||||
this.hasPasscode = passcodeManager.hasPasscode();
|
||||
this.hasAccount = !authManager.offline();
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
displayInfoForCredential(credential) {
|
||||
const info = this.privilegesManager.displayInfoForCredential(credential);
|
||||
if (credential === PrivilegesManager.CredentialLocalPasscode) {
|
||||
info.availability = this.hasPasscode;
|
||||
} else if (credential === PrivilegesManager.CredentialAccountPassword) {
|
||||
info.availability = this.hasAccount;
|
||||
} else {
|
||||
info.availability = true;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
displayInfoForAction(action) {
|
||||
return this.privilegesManager.displayInfoForAction(action).label;
|
||||
}
|
||||
|
||||
isCredentialRequiredForAction(action, credential) {
|
||||
if (!this.privileges) {
|
||||
return false;
|
||||
}
|
||||
return this.privileges.isCredentialRequiredForAction(action, credential);
|
||||
}
|
||||
|
||||
async clearSession() {
|
||||
await this.privilegesManager.clearSession();
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
async reloadPrivileges() {
|
||||
this.availableActions = this.privilegesManager.getAvailableActions();
|
||||
this.availableCredentials = this.privilegesManager.getAvailableCredentials();
|
||||
const sessionEndDate = await this.privilegesManager.getSessionExpirey();
|
||||
this.sessionExpirey = sessionEndDate.toLocaleString();
|
||||
this.sessionExpired = new Date() >= sessionEndDate;
|
||||
this.credentialDisplayInfo = {};
|
||||
for (const cred of this.availableCredentials) {
|
||||
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
|
||||
}
|
||||
const privs = await this.privilegesManager.getPrivileges();
|
||||
this.$timeout(() => {
|
||||
this.privileges = privs;
|
||||
});
|
||||
}
|
||||
|
||||
checkboxValueChanged(action, credential) {
|
||||
this.privileges.toggleCredentialForAction(action, credential);
|
||||
this.privilegesManager.savePrivileges();
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dismiss();
|
||||
this.onCancel && this.onCancel();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivilegesManagementModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PrivilegesManagementModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import template from '%/directives/privileges-management-modal.pug';
|
||||
import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from 'snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { PrivilegeMutator } from 'snjs/dist/@types/models';
|
||||
|
||||
type DisplayInfo = {
|
||||
label: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
class PrivilegesManagementModalCtrl extends PureViewCtrl {
|
||||
|
||||
hasPasscode = false
|
||||
hasAccount = false
|
||||
$element: JQLite
|
||||
application!: WebApplication
|
||||
privileges!: SNPrivileges
|
||||
availableActions!: ProtectedAction[]
|
||||
availableCredentials!: PrivilegeCredential[]
|
||||
sessionExpirey!: string
|
||||
sessionExpired = true
|
||||
credentialDisplayInfo: Partial<Record<PrivilegeCredential, DisplayInfo>> = {}
|
||||
onCancel!: () => void
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
$element: JQLite
|
||||
) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.hasPasscode = this.application.hasPasscode();
|
||||
this.hasAccount = !this.application.noAccount();
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
displayInfoForCredential(credential: PrivilegeCredential) {
|
||||
const info: any = this.application.privilegesService!.displayInfoForCredential(credential);
|
||||
if (credential === PrivilegeCredential.LocalPasscode) {
|
||||
info.availability = this.hasPasscode;
|
||||
} else if (credential === PrivilegeCredential.AccountPassword) {
|
||||
info.availability = this.hasAccount;
|
||||
} else {
|
||||
info.availability = true;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
displayInfoForAction(action: ProtectedAction) {
|
||||
return this.application.privilegesService!.displayInfoForAction(action).label;
|
||||
}
|
||||
|
||||
isCredentialRequiredForAction(action: ProtectedAction, credential: PrivilegeCredential) {
|
||||
if (!this.privileges) {
|
||||
return false;
|
||||
}
|
||||
return this.privileges.isCredentialRequiredForAction(action, credential);
|
||||
}
|
||||
|
||||
async clearSession() {
|
||||
await this.application.privilegesService!.clearSession();
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
async reloadPrivileges() {
|
||||
this.availableActions = this.application.privilegesService!.getAvailableActions();
|
||||
this.availableCredentials = this.application.privilegesService!.getAvailableCredentials();
|
||||
const sessionEndDate = await this.application.privilegesService!.getSessionExpirey();
|
||||
this.sessionExpirey = sessionEndDate.toLocaleString();
|
||||
this.sessionExpired = new Date() >= sessionEndDate;
|
||||
for (const cred of this.availableCredentials) {
|
||||
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
|
||||
}
|
||||
const privs = await this.application.privilegesService!.getPrivileges();
|
||||
this.$timeout(() => {
|
||||
this.privileges = privs;
|
||||
});
|
||||
}
|
||||
|
||||
checkboxValueChanged(action: ProtectedAction, credential: PrivilegeCredential) {
|
||||
this.application.changeAndSaveItem(this.privileges.uuid, (m) => {
|
||||
const mutator = m as PrivilegeMutator;
|
||||
mutator.toggleCredentialForAction(action, credential);
|
||||
})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dismiss();
|
||||
this.onCancel && this.onCancel();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivilegesManagementModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PrivilegesManagementModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { protocolManager, SNComponent, SFItem, SFModelManager } from 'snjs';
|
||||
import template from '%/directives/revision-preview-modal.pug';
|
||||
|
||||
class RevisionPreviewModalCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element,
|
||||
$scope,
|
||||
$timeout,
|
||||
alertManager,
|
||||
componentManager,
|
||||
modelManager,
|
||||
syncManager,
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
this.alertManager = alertManager;
|
||||
this.componentManager = componentManager;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
this.createNote();
|
||||
this.configureEditor();
|
||||
$scope.$on('$destroy', () => {
|
||||
if (this.identifier) {
|
||||
this.componentManager.deregisterHandler(this.identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createNote() {
|
||||
this.note = new SFItem({
|
||||
content: this.content,
|
||||
content_type: "Note"
|
||||
});
|
||||
}
|
||||
|
||||
configureEditor() {
|
||||
/**
|
||||
* Set UUID so editoForNote can find proper editor, but then generate new uuid
|
||||
* for note as not to save changes to original, if editor makes changes.
|
||||
*/
|
||||
this.note.uuid = this.uuid;
|
||||
const editorForNote = this.componentManager.editorForNote(this.note);
|
||||
this.note.uuid = protocolManager.crypto.generateUUIDSync();
|
||||
if (editorForNote) {
|
||||
/**
|
||||
* Create temporary copy, as a lot of componentManager is uuid based, so might
|
||||
* interfere with active editor. Be sure to copy only the content, as the top level
|
||||
* editor object has non-copyable properties like .window, which cannot be transfered
|
||||
*/
|
||||
const editorCopy = new SNComponent({
|
||||
content: editorForNote.content
|
||||
});
|
||||
editorCopy.readonly = true;
|
||||
editorCopy.lockReadonly = true;
|
||||
this.identifier = editorCopy.uuid;
|
||||
this.componentManager.registerHandler({
|
||||
identifier: this.identifier,
|
||||
areas: ['editor-editor'],
|
||||
contextRequestHandler: (component) => {
|
||||
if (component === this.editor) {
|
||||
return this.note;
|
||||
}
|
||||
},
|
||||
componentForSessionKeyHandler: (key) => {
|
||||
if (key === this.editor.sessionKey) {
|
||||
return this.editor;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.editor = editorCopy;
|
||||
}
|
||||
}
|
||||
|
||||
restore(asCopy) {
|
||||
const run = () => {
|
||||
let item;
|
||||
if (asCopy) {
|
||||
const contentCopy = Object.assign({}, this.content);
|
||||
if (contentCopy.title) {
|
||||
contentCopy.title += " (copy)";
|
||||
}
|
||||
item = this.modelManager.createItem({
|
||||
content_type: 'Note',
|
||||
content: contentCopy
|
||||
});
|
||||
this.modelManager.addItem(item);
|
||||
} else {
|
||||
const uuid = this.uuid;
|
||||
item = this.modelManager.findItem(uuid);
|
||||
item.content = Object.assign({}, this.content);
|
||||
this.modelManager.mapResponseItemsToLocalModels(
|
||||
[item],
|
||||
SFModelManager.MappingSourceRemoteActionRetrieved
|
||||
);
|
||||
}
|
||||
this.modelManager.setItemDirty(item);
|
||||
this.syncManager.sync();
|
||||
this.dismiss();
|
||||
};
|
||||
|
||||
if (!asCopy) {
|
||||
this.alertManager.confirm({
|
||||
text: "Are you sure you want to replace the current note's contents with what you see in this preview?",
|
||||
destructive: true,
|
||||
onConfirm: run
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
this.$scope.$destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export class RevisionPreviewModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = RevisionPreviewModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
uuid: '=',
|
||||
content: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
145
app/assets/javascripts/directives/views/revisionPreviewModal.ts
Normal file
145
app/assets/javascripts/directives/views/revisionPreviewModal.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { PureViewCtrl } from './../../views/abstract/pure_view_ctrl';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import {
|
||||
ContentType,
|
||||
PayloadSource,
|
||||
SNComponent,
|
||||
SNNote,
|
||||
ComponentArea
|
||||
} from 'snjs';
|
||||
import template from '%/directives/revision-preview-modal.pug';
|
||||
import { PayloadContent } from 'snjs/dist/@types/protocol/payloads/generator';
|
||||
import { confirmDialog } from '@/services/alertService';
|
||||
|
||||
interface RevisionPreviewScope {
|
||||
uuid: string
|
||||
content: PayloadContent
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
class RevisionPreviewModalCtrl extends PureViewCtrl implements RevisionPreviewScope {
|
||||
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
uuid!: string
|
||||
content!: PayloadContent
|
||||
application!: WebApplication
|
||||
unregisterComponent?: any
|
||||
note!: SNNote
|
||||
private originalNote!: SNNote;
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.configure();
|
||||
}
|
||||
|
||||
$onDestroy() {
|
||||
if (this.unregisterComponent) {
|
||||
this.unregisterComponent();
|
||||
this.unregisterComponent = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
get componentManager() {
|
||||
return this.application.componentManager!;
|
||||
}
|
||||
|
||||
async configure() {
|
||||
this.note = await this.application.createTemplateItem(
|
||||
ContentType.Note,
|
||||
this.content
|
||||
) as SNNote;
|
||||
this.originalNote = this.application.findItem(this.uuid) as SNNote;
|
||||
const editorForNote = this.componentManager.editorForNote(this.originalNote);
|
||||
if (editorForNote) {
|
||||
/**
|
||||
* Create temporary copy, as a lot of componentManager is uuid based, so might
|
||||
* interfere with active editor. Be sure to copy only the content, as the top level
|
||||
* editor object has non-copyable properties like .window, which cannot be transfered
|
||||
*/
|
||||
const editorCopy = await this.application.createTemplateItem(
|
||||
ContentType.Component,
|
||||
editorForNote.safeContent
|
||||
) as SNComponent;
|
||||
this.componentManager.setReadonlyStateForComponent(editorCopy, true, true);
|
||||
this.unregisterComponent = this.componentManager.registerHandler({
|
||||
identifier: editorCopy.uuid,
|
||||
areas: [ComponentArea.Editor],
|
||||
contextRequestHandler: (componentUuid) => {
|
||||
if (componentUuid === this.state.editor?.uuid) {
|
||||
return this.note;
|
||||
}
|
||||
},
|
||||
componentForSessionKeyHandler: (key) => {
|
||||
if (key === this.componentManager.sessionKeyForComponent(this.state.editor!)) {
|
||||
return this.state.editor;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({editor: editorCopy});
|
||||
}
|
||||
}
|
||||
|
||||
restore(asCopy: boolean) {
|
||||
const run = async () => {
|
||||
if (asCopy) {
|
||||
await this.application.duplicateItem(this.originalNote, {
|
||||
...this.content,
|
||||
title: this.content.title ? this.content.title + ' (copy)' : undefined
|
||||
});
|
||||
} else {
|
||||
this.application.changeAndSaveItem(this.uuid, (mutator) => {
|
||||
mutator.setContent(this.content);
|
||||
}, true, PayloadSource.RemoteActionRetrieved);
|
||||
}
|
||||
this.dismiss();
|
||||
};
|
||||
|
||||
if (!asCopy) {
|
||||
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) {
|
||||
run();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class RevisionPreviewModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = RevisionPreviewModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
uuid: '=',
|
||||
content: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import template from '%/directives/session-history-menu.pug';
|
||||
|
||||
class SessionHistoryMenuCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
actionsManager,
|
||||
alertManager,
|
||||
sessionHistory,
|
||||
) {
|
||||
this.$timeout = $timeout;
|
||||
this.alertManager = alertManager;
|
||||
this.actionsManager = actionsManager;
|
||||
this.sessionHistory = sessionHistory;
|
||||
this.diskEnabled = this.sessionHistory.diskEnabled;
|
||||
this.autoOptimize = this.sessionHistory.autoOptimize;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.reloadHistory();
|
||||
}
|
||||
|
||||
reloadHistory() {
|
||||
const history = this.sessionHistory.historyForItem(this.item);
|
||||
this.entries = history.entries.slice(0).sort((a, b) => {
|
||||
return a.item.updated_at < b.item.updated_at ? 1 : -1;
|
||||
});
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
openRevision(revision) {
|
||||
this.actionsManager.presentRevisionPreviewModal(
|
||||
revision.item.uuid,
|
||||
revision.item.content
|
||||
);
|
||||
}
|
||||
|
||||
classForRevision(revision) {
|
||||
const vector = revision.operationVector();
|
||||
if (vector === 0) {
|
||||
return 'default';
|
||||
} else if (vector === 1) {
|
||||
return 'success';
|
||||
} else if (vector === -1) {
|
||||
return 'danger';
|
||||
}
|
||||
}
|
||||
|
||||
clearItemHistory() {
|
||||
this.alertManager.confirm({
|
||||
text: "Are you sure you want to delete the local session history for this note?",
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.sessionHistory.clearHistoryForItem(this.item).then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadHistory();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAllHistory() {
|
||||
this.alertManager.confirm({
|
||||
text: "Are you sure you want to delete the local session history for all notes?",
|
||||
destructive: true,
|
||||
onConfirm: () => {
|
||||
this.sessionHistory.clearAllHistory().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadHistory();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleDiskSaving() {
|
||||
const run = () => {
|
||||
this.sessionHistory.toggleDiskSaving().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.diskEnabled = this.sessionHistory.diskEnabled;
|
||||
});
|
||||
});
|
||||
};
|
||||
if (!this.sessionHistory.diskEnabled) {
|
||||
this.alertManager.confirm({
|
||||
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.`,
|
||||
destructive: true,
|
||||
onConfirm: run
|
||||
});
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
toggleAutoOptimize() {
|
||||
this.sessionHistory.toggleAutoOptimize().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.autoOptimize = this.sessionHistory.autoOptimize;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionHistoryMenu {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = SessionHistoryMenuCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
item: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,30 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { WebDirective } from './../../types';
|
||||
import template from '%/directives/sync-resolution-menu.pug';
|
||||
|
||||
class SyncResolutionMenuCtrl {
|
||||
|
||||
closeFunction!: () => void
|
||||
application!: WebApplication
|
||||
|
||||
$timeout: ng.ITimeoutService
|
||||
status: Partial<{
|
||||
backupFinished: boolean,
|
||||
resolving: boolean,
|
||||
attemptedResolution: boolean,
|
||||
success: boolean
|
||||
fail: boolean
|
||||
}> = {}
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
archiveManager,
|
||||
syncManager,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
this.$timeout = $timeout;
|
||||
this.archiveManager = archiveManager;
|
||||
this.syncManager = syncManager;
|
||||
this.status = {};
|
||||
}
|
||||
|
||||
downloadBackup(encrypted) {
|
||||
this.archiveManager.downloadBackup(encrypted);
|
||||
downloadBackup(encrypted: boolean) {
|
||||
this.application.getArchiveService().downloadBackup(encrypted);
|
||||
this.status.backupFinished = true;
|
||||
}
|
||||
|
||||
@@ -24,11 +34,11 @@ class SyncResolutionMenuCtrl {
|
||||
|
||||
async performSyncResolution() {
|
||||
this.status.resolving = true;
|
||||
await this.syncManager.resolveOutOfSync();
|
||||
await this.application.resolveOutOfSync();
|
||||
this.$timeout(() => {
|
||||
this.status.resolving = false;
|
||||
this.status.attemptedResolution = true;
|
||||
if (this.syncManager.isOutOfSync()) {
|
||||
if (this.application.isOutOfSync()) {
|
||||
this.status.fail = true;
|
||||
} else {
|
||||
this.status.success = true;
|
||||
@@ -38,20 +48,22 @@ class SyncResolutionMenuCtrl {
|
||||
|
||||
close() {
|
||||
this.$timeout(() => {
|
||||
this.closeFunction()();
|
||||
this.closeFunction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncResolutionMenu {
|
||||
export class SyncResolutionMenu extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = SyncResolutionMenuCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
closeFunction: '&'
|
||||
closeFunction: '&',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user