Refactors most controllers and directives into classes for more organized and maintainable code
This commit is contained in:
16
app/assets/javascripts/directives/functional/autofocus.js
Normal file
16
app/assets/javascripts/directives/functional/autofocus.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* @ngInject */
|
||||
export function autofocus($timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
shouldFocus: '='
|
||||
},
|
||||
link: function($scope, $element) {
|
||||
$timeout(function() {
|
||||
if ($scope.shouldFocus) {
|
||||
$element[0].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/* @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;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
44
app/assets/javascripts/directives/functional/delay-hide.js
Normal file
44
app/assets/javascripts/directives/functional/delay-hide.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
14
app/assets/javascripts/directives/functional/elemReady.js
Normal file
14
app/assets/javascripts/directives/functional/elemReady.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* @ngInject */
|
||||
export function elemReady($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function($scope, elem, attrs) {
|
||||
elem.ready(function() {
|
||||
$scope.$apply(function() {
|
||||
var func = $parse(attrs.elemReady);
|
||||
func($scope);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
16
app/assets/javascripts/directives/functional/file-change.js
Normal file
16
app/assets/javascripts/directives/functional/file-change.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/* @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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
9
app/assets/javascripts/directives/functional/index.js
Normal file
9
app/assets/javascripts/directives/functional/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export { autofocus } from './autofocus';
|
||||
export { clickOutside } from './click-outside';
|
||||
export { delayHide } from './delay-hide';
|
||||
export { elemReady } from './elemReady';
|
||||
export { fileChange } from './file-change';
|
||||
export { infiniteScroll } from './infiniteScroll';
|
||||
export { lowercase } from './lowercase';
|
||||
export { selectOnClick } from './selectOnClick';
|
||||
export { snEnter } from './snEnter';
|
||||
@@ -0,0 +1,17 @@
|
||||
/* @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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
19
app/assets/javascripts/directives/functional/lowercase.js
Normal file
19
app/assets/javascripts/directives/functional/lowercase.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* @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]);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/* @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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
14
app/assets/javascripts/directives/functional/snEnter.js
Normal file
14
app/assets/javascripts/directives/functional/snEnter.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/* @ngInject */
|
||||
export function snEnter() {
|
||||
return function(scope, element, attrs) {
|
||||
element.bind('keydown keypress', function(event) {
|
||||
if (event.which === 13) {
|
||||
scope.$apply(function() {
|
||||
scope.$eval(attrs.snEnter, { event: event });
|
||||
});
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
563
app/assets/javascripts/directives/views/accountMenu.js
Normal file
563
app/assets/javascripts/directives/views/accountMenu.js
Normal file
@@ -0,0 +1,563 @@
|
||||
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';
|
||||
|
||||
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
submitAuthForm() {
|
||||
if (!this.state.formData.email || !this.state.formData.user_password) {
|
||||
return;
|
||||
}
|
||||
if (this.state.formData.showLogin) {
|
||||
this.login();
|
||||
} else {
|
||||
this.register();
|
||||
}
|
||||
}
|
||||
|
||||
async login(extraParams) {
|
||||
/** Prevent a timed sync from occuring while signing in. */
|
||||
this.syncManager.lockSyncing();
|
||||
this.state.formData.status = STRING_GENERATING_LOGIN_KEYS;
|
||||
this.state.formData.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) {
|
||||
await this.onAuthSuccess();
|
||||
this.syncManager.unlockSyncing();
|
||||
this.syncManager.sync({ performIntegrityCheck: true });
|
||||
return;
|
||||
}
|
||||
this.syncManager.unlockSyncing();
|
||||
this.state.formData.status = null;
|
||||
const error = response
|
||||
? response.error
|
||||
: { message: "An unknown error occured." }
|
||||
|
||||
if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') {
|
||||
this.state.formData.showLogin = false;
|
||||
this.state.formData.mfa = error;
|
||||
} else {
|
||||
this.state.formData.showLogin = true;
|
||||
this.state.formData.mfa = null;
|
||||
if (error.message) {
|
||||
this.alertManager.alert({
|
||||
text: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
this.state.formData.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;
|
||||
}
|
||||
this.state.formData.confirmPassword = false;
|
||||
this.state.formData.status = STRING_GENERATING_REGISTER_KEYS;
|
||||
this.state.formData.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) {
|
||||
this.state.formData.status = null;
|
||||
const error = response
|
||||
? response.error
|
||||
: { message: "An unknown error occured." };
|
||||
this.state.formData.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.state.formData.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();
|
||||
}
|
||||
this.state.formData.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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this.passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
addPasscodeClicked() {
|
||||
this.state.formData.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 () => {
|
||||
this.setState({
|
||||
formData: {
|
||||
...this.state.formData,
|
||||
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: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
104
app/assets/javascripts/directives/views/actionsMenu.js
Normal file
104
app/assets/javascripts/directives/views/actionsMenu.js
Normal file
@@ -0,0 +1,104 @@
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
34
app/assets/javascripts/directives/views/componentModal.js
Normal file
34
app/assets/javascripts/directives/views/componentModal.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
276
app/assets/javascripts/directives/views/componentView.js
Normal file
276
app/assets/javascripts/directives/views/componentView.js
Normal file
@@ -0,0 +1,276 @@
|
||||
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 MAX_LOAD_THRESHOLD = 4000;
|
||||
|
||||
const VISIBILITY_CHANGE_LISTENER_KEY = 'visibilitychange';
|
||||
|
||||
class ComponentViewCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$scope,
|
||||
$rootScope,
|
||||
$timeout,
|
||||
componentManager,
|
||||
desktopManager,
|
||||
themeManager
|
||||
) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.$timeout = $timeout;
|
||||
this.themeManager = themeManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.componentManager = componentManager;
|
||||
this.componentValid = true;
|
||||
|
||||
$scope.$watch('ctrl.component', (component, prevComponent) => {
|
||||
this.componentValueDidSet(component, prevComponent);
|
||||
});
|
||||
$scope.$on('ext-reload-complete', () => {
|
||||
this.reloadStatus(false);
|
||||
})
|
||||
$scope.$on('$destroy', () => {
|
||||
this.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.registerComponentHandlers();
|
||||
this.registerPackageUpdateObserver();
|
||||
};
|
||||
|
||||
registerPackageUpdateObserver() {
|
||||
this.updateObserver = this.desktopManager
|
||||
.registerUpdateObserver((component) => {
|
||||
if(component === this.component && component.active) {
|
||||
this.reloadComponent();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerComponentHandlers() {
|
||||
this.themeHandlerIdentifier = 'component-view-' + Math.random();
|
||||
this.componentManager.registerHandler({
|
||||
identifier: this.themeHandlerIdentifier,
|
||||
areas: ['themes'],
|
||||
activationHandler: (component) => {
|
||||
this.reloadThemeStatus();
|
||||
}
|
||||
});
|
||||
|
||||
this.identifier = 'component-view-' + Math.random();
|
||||
this.componentManager.registerHandler({
|
||||
identifier: this.identifier,
|
||||
areas: [this.component.area],
|
||||
activationHandler: (component) => {
|
||||
if(component !== this.component) {
|
||||
return;
|
||||
}
|
||||
this.$timeout(() => {
|
||||
this.handleActivation();
|
||||
})
|
||||
},
|
||||
actionHandler: (component, action, data) => {
|
||||
if(action === 'set-size') {
|
||||
this.componentManager.handleSetSizeEvent(component, data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onVisibilityChange() {
|
||||
if(document.visibilityState === 'hidden') {
|
||||
return;
|
||||
}
|
||||
if(this.issueLoading) {
|
||||
this.reloadComponent();
|
||||
}
|
||||
}
|
||||
|
||||
async reloadComponent() {
|
||||
this.componentValid = false;
|
||||
await this.componentManager.reloadComponent(this.component);
|
||||
this.reloadStatus();
|
||||
}
|
||||
|
||||
reloadStatus(doManualReload = true) {
|
||||
this.reloading = true;
|
||||
const component = this.component;
|
||||
const previouslyValid = this.componentValid;
|
||||
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();
|
||||
if(!component.lockReadonly) {
|
||||
component.readonly = 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 = null;
|
||||
}
|
||||
if(this.componentValid !== previouslyValid) {
|
||||
if(this.componentValid) {
|
||||
this.componentManager.reloadComponent(component, true);
|
||||
}
|
||||
}
|
||||
if(this.expired && doManualReload) {
|
||||
this.$rootScope.$broadcast('reload-ext-dat');
|
||||
}
|
||||
this.reloadThemeStatus();
|
||||
this.$timeout(() => {
|
||||
this.reloading = false;
|
||||
}, 500)
|
||||
}
|
||||
|
||||
handleActivation() {
|
||||
if(!this.component.active) {
|
||||
return;
|
||||
}
|
||||
const iframe = this.componentManager.iframeForComponent(
|
||||
this.component
|
||||
);
|
||||
if(!iframe) {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
if(this.loadTimeout) {
|
||||
this.$timeout.cancel(this.loadTimeout);
|
||||
}
|
||||
this.loadTimeout = this.$timeout(() => {
|
||||
this.handleIframeLoadTimeout();
|
||||
}, MAX_LOAD_THRESHOLD);
|
||||
|
||||
iframe.onload = (event) => {
|
||||
this.handleIframeLoad(iframe);
|
||||
};
|
||||
}
|
||||
|
||||
async handleIframeLoadTimeout() {
|
||||
if(this.loading) {
|
||||
this.loading = false;
|
||||
this.issueLoading = true;
|
||||
if(!this.didAttemptReload) {
|
||||
this.didAttemptReload = true;
|
||||
this.reloadComponent();
|
||||
} else {
|
||||
document.addEventListener(
|
||||
VISIBILITY_CHANGE_LISTENER_KEY,
|
||||
this.onVisibilityChange.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleIframeLoad(iframe) {
|
||||
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.componentManager.registerComponentWindow(
|
||||
this.component,
|
||||
iframe.contentWindow
|
||||
);
|
||||
const avoidFlickerTimeout = 7;
|
||||
this.$timeout(() => {
|
||||
this.loading = false;
|
||||
this.issueLoading = desktopError ? true : false;
|
||||
this.onLoad && this.onLoad(this.component);
|
||||
}, avoidFlickerTimeout)
|
||||
}
|
||||
|
||||
componentValueDidSet(component, prevComponent) {
|
||||
const dontSync = true;
|
||||
if(prevComponent && component !== prevComponent) {
|
||||
this.componentManager.deactivateComponent(
|
||||
prevComponent,
|
||||
dontSync
|
||||
);
|
||||
}
|
||||
if(component) {
|
||||
this.componentManager.activateComponent(
|
||||
component,
|
||||
dontSync
|
||||
);
|
||||
this.reloadStatus();
|
||||
}
|
||||
}
|
||||
|
||||
reloadThemeStatus() {
|
||||
if(this.component.acceptsThemes()) {
|
||||
return;
|
||||
}
|
||||
if(this.themeManager.hasActiveTheme()) {
|
||||
if(!this.dismissedNoThemesMessage) {
|
||||
this.showNoThemesMessage = true;
|
||||
}
|
||||
} else {
|
||||
this.showNoThemesMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
dismissNoThemesMessage() {
|
||||
this.showNoThemesMessage = false;
|
||||
this.dismissedNoThemesMessage = true;
|
||||
}
|
||||
|
||||
disableActiveTheme() {
|
||||
this.themeManager.deactivateAllThemes();
|
||||
this.dismissNoThemesMessage();
|
||||
}
|
||||
|
||||
getUrl() {
|
||||
const url = this.componentManager.urlForComponent(this.component);
|
||||
this.component.runningLocally = (url === this.component.local_url);
|
||||
return url;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.componentManager.deregisterHandler(this.themeHandlerIdentifier);
|
||||
this.componentManager.deregisterHandler(this.identifier);
|
||||
if(this.component && !this.manualDealloc) {
|
||||
const dontSync = true;
|
||||
this.componentManager.deactivateComponent(this.component, dontSync);
|
||||
}
|
||||
|
||||
this.desktopManager.deregisterUpdateObserver(this.updateObserver);
|
||||
document.removeEventListener(
|
||||
VISIBILITY_CHANGE_LISTENER_KEY,
|
||||
this.onVisibilityChange.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ComponentView {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.scope = {
|
||||
component: '=',
|
||||
onLoad: '=?',
|
||||
manualDealloc: '=?'
|
||||
};
|
||||
this.controller = ComponentViewCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
110
app/assets/javascripts/directives/views/editorMenu.js
Normal file
110
app/assets/javascripts/directives/views/editorMenu.js
Normal file
@@ -0,0 +1,110 @@
|
||||
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.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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
16
app/assets/javascripts/directives/views/index.js
Normal file
16
app/assets/javascripts/directives/views/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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';
|
||||
export { PanelResizer } from './panelResizer';
|
||||
export { PasswordWizard } from './passwordWizard';
|
||||
export { PermissionsModal } from './permissionsModal';
|
||||
export { PrivilegesAuthModal } from './privilegesAuthModal';
|
||||
export { PrivilegesManagementModal } from './privilegesManagementModal';
|
||||
export { RevisionPreviewModal } from './revisionPreviewModal';
|
||||
export { SessionHistoryMenu } from './sessionHistoryMenu';
|
||||
export { SyncResolutionMenu } from './syncResolutionMenu';
|
||||
37
app/assets/javascripts/directives/views/inputModal.js
Normal file
37
app/assets/javascripts/directives/views/inputModal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
48
app/assets/javascripts/directives/views/menuRow.js
Normal file
48
app/assets/javascripts/directives/views/menuRow.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import template from '%/directives/menu-row.pug';
|
||||
|
||||
class MenuRowCtrl {
|
||||
|
||||
onClick($event) {
|
||||
if(this.disabled) {
|
||||
return;
|
||||
}
|
||||
$event.stopPropagation();
|
||||
this.action();
|
||||
}
|
||||
|
||||
clickAccessoryButton($event) {
|
||||
if(this.disabled) {
|
||||
return;
|
||||
}
|
||||
$event.stopPropagation();
|
||||
this.buttonAction();
|
||||
}
|
||||
}
|
||||
|
||||
export class MenuRow {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.transclude = true;
|
||||
this.template = template;
|
||||
this.controller = MenuRowCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
action: '&',
|
||||
buttonAction: '&',
|
||||
buttonClass: '=',
|
||||
buttonText: '=',
|
||||
desc: '=',
|
||||
disabled: '=',
|
||||
circle: '=',
|
||||
circleAlign: '=',
|
||||
faded: '=',
|
||||
hasButton: '=',
|
||||
label: '=',
|
||||
spinnerClass: '=',
|
||||
stylekitClass: '=',
|
||||
subRows: '=',
|
||||
subtitle: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
339
app/assets/javascripts/directives/views/panelResizer.js
Normal file
339
app/assets/javascripts/directives/views/panelResizer.js
Normal file
@@ -0,0 +1,339 @@
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
class PanelResizerCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$compile,
|
||||
$element,
|
||||
$scope,
|
||||
$timeout,
|
||||
) {
|
||||
this.$compile = $compile;
|
||||
this.$element = $element;
|
||||
this.$scope = $scope;
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.configureControl();
|
||||
this.configureDefaults();
|
||||
this.addDoubleClickHandler();
|
||||
this.reloadDefaultValues();
|
||||
this.addMouseDownListener();
|
||||
this.addMouseMoveListener();
|
||||
this.addMouseUpListener();
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
}
|
||||
|
||||
configureDefaults() {
|
||||
this.panel = document.getElementById(this.panelId);
|
||||
if (!this.panel) {
|
||||
console.error('Panel not found for', this.panelId);
|
||||
}
|
||||
|
||||
this.resizerColumn = this.$element[0];
|
||||
this.currentMinWidth = this.minWidth || this.resizerColumn.offsetWidth;
|
||||
this.pressed = false;
|
||||
this.startWidth = this.panel.scrollWidth;
|
||||
this.lastDownX = 0;
|
||||
this.collapsed = false;
|
||||
this.lastWidth = this.startWidth;
|
||||
this.startLeft = this.panel.offsetLeft;
|
||||
this.lastLeft = this.startLeft;
|
||||
this.appFrame = null;
|
||||
this.widthBeforeLastDblClick = 0;
|
||||
|
||||
if (this.property === PANEL_SIDE_RIGHT) {
|
||||
this.configureRightPanel();
|
||||
}
|
||||
if (this.alwaysVisible) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ALWAYS_VISIBLE);
|
||||
}
|
||||
if (this.hoverable) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_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);
|
||||
});
|
||||
}
|
||||
|
||||
getParentRect() {
|
||||
return this.panel.parentNode.getBoundingClientRect();
|
||||
}
|
||||
|
||||
reloadDefaultValues() {
|
||||
this.startWidth = this.isAtMaxWidth()
|
||||
? this.getParentRect().width
|
||||
: this.panel.scrollWidth;
|
||||
this.lastWidth = this.startWidth;
|
||||
this.appFrame = document.getElementById('app').getBoundingClientRect();
|
||||
}
|
||||
|
||||
addDoubleClickHandler() {
|
||||
this.resizerColumn.ondblclick = () => {
|
||||
this.$timeout(() => {
|
||||
const preClickCollapseState = this.isCollapsed();
|
||||
if (preClickCollapseState) {
|
||||
this.setWidth(this.widthBeforeLastDblClick || this.defaultWidth);
|
||||
} else {
|
||||
this.widthBeforeLastDblClick = this.lastWidth;
|
||||
this.setWidth(this.currentMinWidth);
|
||||
}
|
||||
|
||||
this.finishSettingWidth();
|
||||
|
||||
const newCollapseState = !preClickCollapseState;
|
||||
this.onResizeFinish()(
|
||||
this.lastWidth,
|
||||
this.lastLeft,
|
||||
this.isAtMaxWidth(),
|
||||
newCollapseState
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleWidthEvent(event) {
|
||||
let x;
|
||||
if (event) {
|
||||
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) {
|
||||
const panelRect = this.panel.getBoundingClientRect();
|
||||
const x = event.clientX || panelRect.x;
|
||||
let deltaX = x - this.lastDownX;
|
||||
let newLeft = this.startLeft + deltaX;
|
||||
if (newLeft < 0) {
|
||||
newLeft = 0;
|
||||
deltaX = -this.startLeft;
|
||||
}
|
||||
const parentRect = this.getParentRect();
|
||||
let newWidth = this.startWidth - deltaX;
|
||||
if (newWidth < this.currentMinWidth) {
|
||||
newWidth = this.currentMinWidth;
|
||||
}
|
||||
if (newWidth > parentRect.width) {
|
||||
newWidth = parentRect.width;
|
||||
}
|
||||
if (newLeft + newWidth > parentRect.width) {
|
||||
newLeft = parentRect.width - newWidth;
|
||||
}
|
||||
this.setLeft(newLeft, false);
|
||||
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.getParentRect().width)
|
||||
);
|
||||
}
|
||||
|
||||
isCollapsed() {
|
||||
return this.lastWidth <= this.currentMinWidth;
|
||||
}
|
||||
|
||||
setWidth(width, finish) {
|
||||
if (width < this.currentMinWidth) {
|
||||
width = this.currentMinWidth;
|
||||
}
|
||||
const parentRect = this.getParentRect();
|
||||
if (width > parentRect.width) {
|
||||
width = parentRect.width;
|
||||
}
|
||||
|
||||
const maxWidth = this.appFrame.width - this.panel.getBoundingClientRect().x;
|
||||
if (width > maxWidth) {
|
||||
width = maxWidth;
|
||||
}
|
||||
if (Math.round(width + this.lastLeft) === Math.round(parentRect.width)) {
|
||||
this.panel.style.width = `calc(100% - ${this.lastLeft}px)`;
|
||||
this.panel.style.flexBasis = `calc(100% - ${this.lastLeft}px)`;
|
||||
} else {
|
||||
this.panel.style.flexBasis = width + 'px';
|
||||
this.panel.style.width = width + 'px';
|
||||
}
|
||||
this.lastWidth = width;
|
||||
if (finish) {
|
||||
this.finishSettingWidth();
|
||||
}
|
||||
}
|
||||
|
||||
setLeft(left) {
|
||||
this.panel.style.left = left + 'px';
|
||||
this.lastLeft = left;
|
||||
}
|
||||
|
||||
finishSettingWidth() {
|
||||
if (!this.collapsable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.collapsed = this.isCollapsed();
|
||||
if (this.collapsed) {
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_COLLAPSED);
|
||||
} else {
|
||||
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_COLLAPSED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If an iframe is displayed adjacent to our panel, and the mouse exits over the iframe,
|
||||
* document[onmouseup] is not triggered because the document is no longer the same over
|
||||
* the iframe. We add an invisible overlay while resizing so that the mouse context
|
||||
* remains in our main document.
|
||||
*/
|
||||
addInvisibleOverlay() {
|
||||
if (this.overlay) {
|
||||
return;
|
||||
}
|
||||
this.overlay = this.$compile(`<div id='resizer-overlay'></div>`)(this.$scope);
|
||||
angular.element(document.body).prepend(this.overlay);
|
||||
}
|
||||
|
||||
removeInvisibleOverlay() {
|
||||
if (this.overlay) {
|
||||
this.overlay.remove();
|
||||
this.overlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
flash() {
|
||||
const FLASH_DURATION = 3000;
|
||||
this.resizerColumn.classList.add(PANEL_CSS_CLASS_ANIMATE_OPACITY);
|
||||
this.$timeout(() => {
|
||||
this.resizerColumn.classList.remove(PANEL_CSS_CLASS_ANIMATE_OPACITY);
|
||||
}, FLASH_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
export class PanelResizer {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PanelResizerCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
alwaysVisible: '=',
|
||||
collapsable: '=',
|
||||
control: '=',
|
||||
defaultWidth: '=',
|
||||
hoverable: '=',
|
||||
index: '=',
|
||||
minWidth: '=',
|
||||
onResize: '&',
|
||||
onResizeFinish: '&',
|
||||
panelId: '=',
|
||||
property: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
284
app/assets/javascripts/directives/views/passwordWizard.js
Normal file
284
app/assets/javascripts/directives/views/passwordWizard.js
Normal file
@@ -0,0 +1,284 @@
|
||||
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(next).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;
|
||||
}
|
||||
|
||||
/** 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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
38
app/assets/javascripts/directives/views/permissionsModal.js
Normal file
38
app/assets/javascripts/directives/views/permissionsModal.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import template from '%/directives/permissions-modal.pug';
|
||||
|
||||
class PermissionsModalCtrl {
|
||||
/* @ngInject */
|
||||
constructor($element) {
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.$element.remove();
|
||||
}
|
||||
|
||||
accept() {
|
||||
this.callback(true);
|
||||
this.dismiss();
|
||||
}
|
||||
|
||||
deny() {
|
||||
this.callback(false);
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionsModal {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PermissionsModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
show: '=',
|
||||
component: '=',
|
||||
permissionsString: '=',
|
||||
callback: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
101
app/assets/javascripts/directives/views/privilegesAuthModal.js
Normal file
101
app/assets/javascripts/directives/views/privilegesAuthModal.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
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 = {};
|
||||
}
|
||||
}
|
||||
133
app/assets/javascripts/directives/views/revisionPreviewModal.js
Normal file
133
app/assets/javascripts/directives/views/revisionPreviewModal.js
Normal file
@@ -0,0 +1,133 @@
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
118
app/assets/javascripts/directives/views/sessionHistoryMenu.js
Normal file
118
app/assets/javascripts/directives/views/sessionHistoryMenu.js
Normal file
@@ -0,0 +1,118 @@
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import template from '%/directives/sync-resolution-menu.pug';
|
||||
|
||||
class SyncResolutionMenuCtrl {
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout,
|
||||
archiveManager,
|
||||
syncManager,
|
||||
) {
|
||||
this.$timeout = $timeout;
|
||||
this.archiveManager = archiveManager;
|
||||
this.syncManager = syncManager;
|
||||
this.status = {};
|
||||
}
|
||||
|
||||
downloadBackup(encrypted) {
|
||||
this.archiveManager.downloadBackup(encrypted);
|
||||
this.status.backupFinished = true;
|
||||
}
|
||||
|
||||
skipBackup() {
|
||||
this.status.backupFinished = true;
|
||||
}
|
||||
|
||||
async performSyncResolution() {
|
||||
this.status.resolving = true;
|
||||
await this.syncManager.resolveOutOfSync();
|
||||
this.$timeout(() => {
|
||||
this.status.resolving = false;
|
||||
this.status.attemptedResolution = true;
|
||||
if (this.syncManager.isOutOfSync()) {
|
||||
this.status.fail = true;
|
||||
} else {
|
||||
this.status.success = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$timeout(() => {
|
||||
this.closeFunction()();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncResolutionMenu {
|
||||
constructor() {
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = SyncResolutionMenuCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
closeFunction: '&'
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user