Merge branch '004' into develop
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 */
|
||||
setImmediate(() => {
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
589
app/assets/javascripts/directives/views/accountMenu.ts
Normal file
589
app/assets/javascripts/directives/views/accountMenu.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { isDesktopApplication, isNullOrUndefined } 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
|
||||
} 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 } from '@/services/alertService';
|
||||
|
||||
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
|
||||
mfa: { payload: any }
|
||||
userMfaCode?: string
|
||||
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
|
||||
}
|
||||
|
||||
class AccountMenuCtrl extends PureViewCtrl {
|
||||
|
||||
public appVersion: string
|
||||
private syncStatus?: SyncOpStatus
|
||||
private closeFunction?: () => void
|
||||
|
||||
/* @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!.getLockService().getAutoLockIntervalOptions(),
|
||||
user: this.application!.getUser(),
|
||||
formData: {
|
||||
mergeLocal: true,
|
||||
ephemeral: false
|
||||
},
|
||||
mutable: {}
|
||||
} 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.loadBackupsAvailability();
|
||||
}
|
||||
|
||||
refreshedCredentialState() {
|
||||
return {
|
||||
user: this.application!.getUser(),
|
||||
canAddPasscode: !this.application!.isEphemeralSession(),
|
||||
hasPasscode: this.application!.hasPasscode(),
|
||||
showPasscodeForm: false
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.initProps({
|
||||
closeFunction: this.closeFunction
|
||||
});
|
||||
this.syncStatus = this.application!.getSyncStatus();
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$timeout(() => {
|
||||
this.props.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);
|
||||
}
|
||||
|
||||
async loadBackupsAvailability() {
|
||||
const hasUser = !isNullOrUndefined(this.application!.getUser());
|
||||
const hasPasscode = this.application!.hasPasscode();
|
||||
const encryptedAvailable = hasUser || hasPasscode;
|
||||
|
||||
function encryptionStatusString() {
|
||||
if (hasUser) {
|
||||
return STRING_E2E_ENABLED;
|
||||
} else if (hasPasscode) {
|
||||
return STRING_LOCAL_ENC_ENABLED;
|
||||
} else {
|
||||
return STRING_ENC_NOT_ENABLED;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
encryptionStatusString: encryptionStatusString(),
|
||||
encryptionEnabled: encryptedAvailable,
|
||||
mutable: {
|
||||
...this.getState().mutable,
|
||||
backupEncrypted: encryptedAvailable
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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.mfa && formData.mfa.payload.mfa_key,
|
||||
formData.userMfaCode,
|
||||
formData.mergeLocal
|
||||
);
|
||||
const hasError = !response || response.error;
|
||||
if (!hasError) {
|
||||
await this.setFormDataState({
|
||||
authenticating: false,
|
||||
user_password: undefined
|
||||
});
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
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,
|
||||
status: undefined
|
||||
});
|
||||
} else {
|
||||
await this.setFormDataState({
|
||||
showLogin: true,
|
||||
mfa: undefined,
|
||||
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
|
||||
);
|
||||
if (!response || response.error) {
|
||||
await this.setFormDataState({
|
||||
status: undefined
|
||||
});
|
||||
const error = response
|
||||
? response.error
|
||||
: { message: "An unknown error occured." };
|
||||
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) {
|
||||
if (await confirmDialog({
|
||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
this.setFormDataState({
|
||||
mergeLocal: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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.auth_params || data.keyParams) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.getState().importData,
|
||||
requestPassword: true,
|
||||
data: data
|
||||
}
|
||||
});
|
||||
const element = document.getElementById(
|
||||
ELEMENT_ID_IMPORT_PASSWORD_INPUT
|
||||
);
|
||||
if (element) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
} 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 errorCount = await this.importJSONData(data, password);
|
||||
this.setState({
|
||||
importData: null
|
||||
});
|
||||
if (errorCount > 0) {
|
||||
const message = StringImportError(errorCount);
|
||||
this.application!.alertService!.alert(
|
||||
message
|
||||
);
|
||||
} else {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_IMPORT_SUCCESS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async importJSONData(data: BackupFile, password?: string) {
|
||||
const { errorCount } = await this.application!.importData(
|
||||
data,
|
||||
password
|
||||
);
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
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!.getLockService().getAutoLockInterval();
|
||||
this.setState({
|
||||
selectedAutoLockInterval: interval
|
||||
});
|
||||
}
|
||||
|
||||
async selectAutoLockInterval(interval: number) {
|
||||
const run = async () => {
|
||||
await this.application!.getLockService().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
|
||||
});
|
||||
}
|
||||
|
||||
submitPasscodeForm() {
|
||||
const passcode = this.getState().formData.passcode!;
|
||||
if (passcode !== this.getState().formData.confirmPasscode!) {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_NON_MATCHING_PASSCODES
|
||||
);
|
||||
return;
|
||||
}
|
||||
(this.getState().formData.changingPasscode
|
||||
? this.application!.changePasscode(passcode)
|
||||
: this.application!.setPasscode(passcode)
|
||||
).then(() => {
|
||||
this.setFormDataState({
|
||||
passcode: undefined,
|
||||
confirmPasscode: undefined,
|
||||
showPasscodeForm: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 = !isNullOrUndefined(await this.application!.getUser());
|
||||
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
|
||||
if (!signedIn) {
|
||||
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
|
||||
}
|
||||
if (await confirmDialog({
|
||||
text: message,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
this.application!.removePasscode();
|
||||
}
|
||||
};
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
199
app/assets/javascripts/directives/views/actionsMenu.ts
Normal file
199
app/assets/javascripts/directives/views/actionsMenu.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
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 } from 'snjs/dist/@types';
|
||||
import { ActionResponse } from 'snjs/dist/@types/services/actions_service';
|
||||
import { ActionsExtensionMutator } from 'snjs/dist/@types/models/app/extension';
|
||||
|
||||
type ActionsMenuScope = {
|
||||
application: WebApplication
|
||||
item: SNItem
|
||||
}
|
||||
|
||||
type ActionSubRow = {
|
||||
onClick: () => void
|
||||
label: string
|
||||
subtitle: string
|
||||
spinnerClass: string | undefined
|
||||
}
|
||||
|
||||
type UpdateActionParams = {
|
||||
running?: boolean
|
||||
error?: boolean
|
||||
subrows?: ActionSubRow[]
|
||||
}
|
||||
|
||||
type UpdateExtensionParams = {
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
class ActionsMenuCtrl extends PureViewCtrl implements ActionsMenuScope {
|
||||
|
||||
application!: WebApplication
|
||||
item!: SNItem
|
||||
public loadingExtensions: boolean = true
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
super($timeout);
|
||||
this.state = {
|
||||
extensions: []
|
||||
};
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
super.$onInit();
|
||||
this.initProps({
|
||||
item: this.item
|
||||
});
|
||||
this.loadExtensions();
|
||||
};
|
||||
|
||||
async loadExtensions() {
|
||||
const actionExtensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
const extensionsForItem = await Promise.all(actionExtensions.map((extension) => {
|
||||
return this.application.actionsManager!.loadExtensionInContextOfItem(
|
||||
extension,
|
||||
this.props.item
|
||||
);
|
||||
}));
|
||||
if (extensionsForItem.length == 0) {
|
||||
this.loadingExtensions = false;
|
||||
}
|
||||
await this.setState({
|
||||
extensions: extensionsForItem
|
||||
});
|
||||
}
|
||||
|
||||
async executeAction(action: Action, extension: SNActionsExtension) {
|
||||
if (action.verb === 'nested') {
|
||||
if (!action.subrows) {
|
||||
const subrows = this.subRowsForAction(action, extension);
|
||||
await this.updateAction(action, extension, { subrows });
|
||||
}
|
||||
return;
|
||||
}
|
||||
await this.updateAction(action, extension, { running: true });
|
||||
const response = await this.application.actionsManager!.runAction(
|
||||
action,
|
||||
this.props.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: SNActionsExtension): ActionSubRow[] | undefined {
|
||||
if (!parentAction.subactions) {
|
||||
return undefined;
|
||||
}
|
||||
return parentAction.subactions.map((subaction) => {
|
||||
return {
|
||||
onClick: () => {
|
||||
this.executeAction(subaction, extension);
|
||||
},
|
||||
label: subaction.label,
|
||||
subtitle: subaction.desc,
|
||||
spinnerClass: subaction.running ? 'info' : undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async updateAction(
|
||||
action: Action,
|
||||
extension: SNActionsExtension,
|
||||
params: UpdateActionParams
|
||||
) {
|
||||
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,
|
||||
subrows: params?.subrows || act?.subrows,
|
||||
};
|
||||
}
|
||||
return act;
|
||||
});
|
||||
}) as SNActionsExtension;
|
||||
await this.updateExtension(updatedExtension);
|
||||
}
|
||||
|
||||
private async updateExtension(
|
||||
extension: SNActionsExtension,
|
||||
params?: UpdateExtensionParams
|
||||
) {
|
||||
const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => {
|
||||
const extensionMutator = mutator as ActionsExtensionMutator;
|
||||
extensionMutator.hidden = params && params.hidden;
|
||||
}) as SNActionsExtension;
|
||||
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
|
||||
if (extension.uuid === ext.uuid) {
|
||||
return updatedExtension;
|
||||
}
|
||||
return ext;
|
||||
});
|
||||
await this.setState({
|
||||
extensions: extensions
|
||||
});
|
||||
}
|
||||
|
||||
private async reloadExtension(extension: SNActionsExtension) {
|
||||
const extensionInContext = await this.application.actionsManager!.loadExtensionInContextOfItem(
|
||||
extension,
|
||||
this.props.item
|
||||
);
|
||||
const extensions = this.state.extensions.map((ext: SNActionsExtension) => {
|
||||
if (extension.uuid === ext.uuid) {
|
||||
return extensionInContext;
|
||||
}
|
||||
return ext;
|
||||
});
|
||||
this.setState({
|
||||
extensions: extensions
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
250
app/assets/javascripts/directives/views/componentView.ts
Normal file
250
app/assets/javascripts/directives/views/componentView.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
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
|
||||
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() {
|
||||
this.cleanUpOn();
|
||||
(this.cleanUpOn as any) = undefined;
|
||||
this.unregisterComponentHandler();
|
||||
(this.unregisterComponentHandler as any) = undefined;
|
||||
this.unregisterDesktopObserver();
|
||||
(this.unregisterDesktopObserver as any) = undefined;
|
||||
this.liveComponent.deinit();
|
||||
(this.liveComponent as any) = undefined;
|
||||
(this.application as any) = undefined;
|
||||
(this.onVisibilityChange as any) = undefined;
|
||||
this.onLoad = undefined;
|
||||
document.removeEventListener(
|
||||
VisibilityChangeKey,
|
||||
this.onVisibilityChange
|
||||
);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.liveComponent = new LiveItem(this.componentUuid, this.application);
|
||||
this.registerComponentHandlers();
|
||||
this.registerPackageUpdateObserver();
|
||||
}
|
||||
|
||||
get component() {
|
||||
return this.liveComponent?.item;
|
||||
}
|
||||
|
||||
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 'Component view is missing component';
|
||||
}
|
||||
if (!this.component.active) {
|
||||
throw 'Component view component must be active';
|
||||
}
|
||||
const iframe = this.application.componentManager!.iframeForComponent(
|
||||
this.componentUuid
|
||||
);
|
||||
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('reload-ext-dat');
|
||||
}
|
||||
}
|
||||
|
||||
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: '=',
|
||||
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 '@node_modules/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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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: '='
|
||||
218
app/assets/javascripts/directives/views/passwordWizard.ts
Normal file
218
app/assets/javascripts/directives/views/passwordWizard.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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";
|
||||
const Steps = {
|
||||
PasswordStep: 1,
|
||||
FinishStep: 2
|
||||
};
|
||||
|
||||
class PasswordWizardCtrl extends PureViewCtrl 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;
|
||||
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: any) {
|
||||
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.state.formData.status = null;
|
||||
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.state.formData.status = null;
|
||||
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() {
|
||||
this.setState({
|
||||
lockContinue: true,
|
||||
processing: true
|
||||
});
|
||||
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 || !response.error;
|
||||
this.setFormDataState({
|
||||
statusError: !success,
|
||||
processing: success
|
||||
});
|
||||
if (!success) {
|
||||
this.application.alertService!.alert(
|
||||
response!.error.message
|
||||
? response!.error.message
|
||||
: "There was an error changing your password. Please try again."
|
||||
);
|
||||
this.setFormDataState({
|
||||
status: "Unable to process your password. Please try again."
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
lockContinue: false,
|
||||
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 '@node_modules/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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
148
app/assets/javascripts/directives/views/revisionPreviewModal.ts
Normal file
148
app/assets/javascripts/directives/views/revisionPreviewModal.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
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 '@node_modules/snjs/dist/@types/protocol/payloads/generator';
|
||||
|
||||
interface RevisionPreviewScope {
|
||||
uuid: string
|
||||
content: PayloadContent
|
||||
application: WebApplication
|
||||
}
|
||||
|
||||
class RevisionPreviewModalCtrl implements RevisionPreviewScope {
|
||||
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
uuid!: string
|
||||
content!: PayloadContent
|
||||
application!: WebApplication
|
||||
unregisterComponent?: any
|
||||
note!: SNNote
|
||||
editor?: SNComponent
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
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;
|
||||
const originalNote = this.application.findItem(this.uuid) as SNNote;
|
||||
const editorForNote = this.componentManager.editorForNote(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.editor?.uuid) {
|
||||
return this.note;
|
||||
}
|
||||
},
|
||||
componentForSessionKeyHandler: (key) => {
|
||||
if (key === this.componentManager.sessionKeyForComponent(this.editor!)) {
|
||||
return this.editor;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.editor = editorCopy;
|
||||
}
|
||||
}
|
||||
|
||||
restore(asCopy: boolean) {
|
||||
const run = async () => {
|
||||
if (asCopy) {
|
||||
const contentCopy = Object.assign({}, this.content);
|
||||
if (contentCopy.title) {
|
||||
contentCopy.title += " (copy)";
|
||||
}
|
||||
await this.application.createManagedItem(
|
||||
ContentType.Note,
|
||||
contentCopy,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
this.application.changeAndSaveItem(this.uuid, (mutator) => {
|
||||
mutator.setContent(this.content);
|
||||
}, true, PayloadSource.RemoteActionRetrieved);
|
||||
}
|
||||
this.dismiss();
|
||||
};
|
||||
|
||||
if (!asCopy) {
|
||||
this.application.alertService!.confirm(
|
||||
"Are you sure you want to replace the current note's contents with what you see in this preview?",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
run,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
} 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: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
143
app/assets/javascripts/directives/views/sessionHistoryMenu.ts
Normal file
143
app/assets/javascripts/directives/views/sessionHistoryMenu.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import template from '%/directives/session-history-menu.pug';
|
||||
import { SNItem, ItemHistoryEntry, ItemHistory } from '@node_modules/snjs/dist/@types';
|
||||
|
||||
interface SessionHistoryScope {
|
||||
application: WebApplication
|
||||
item: SNItem
|
||||
}
|
||||
|
||||
class SessionHistoryMenuCtrl implements SessionHistoryScope {
|
||||
|
||||
$timeout: ng.ITimeoutService
|
||||
diskEnabled = false
|
||||
autoOptimize = false
|
||||
application!: WebApplication
|
||||
item!: SNItem
|
||||
entries!: ItemHistoryEntry[]
|
||||
history!: ItemHistory
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.reloadHistory();
|
||||
this.diskEnabled = this.application.historyManager!.isDiskEnabled();
|
||||
this.autoOptimize = this.application.historyManager!.isAutoOptimizeEnabled();
|
||||
}
|
||||
|
||||
reloadHistory() {
|
||||
const history = this.application.historyManager!.historyForItem(this.item);
|
||||
this.entries = history.entries.slice(0).sort((a, b) => {
|
||||
return a.payload.updated_at! < b.payload.updated_at! ? 1 : -1;
|
||||
});
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
openRevision(revision: ItemHistoryEntry) {
|
||||
this.application.presentRevisionPreviewModal(
|
||||
revision.payload.uuid,
|
||||
revision.payload.content
|
||||
);
|
||||
}
|
||||
|
||||
classForRevision(revision: ItemHistoryEntry) {
|
||||
const vector = revision.operationVector();
|
||||
if (vector === 0) {
|
||||
return 'default';
|
||||
} else if (vector === 1) {
|
||||
return 'success';
|
||||
} else if (vector === -1) {
|
||||
return 'danger';
|
||||
}
|
||||
}
|
||||
|
||||
clearItemHistory() {
|
||||
this.application.alertService!.confirm(
|
||||
"Are you sure you want to delete the local session history for this note?",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {
|
||||
this.application.historyManager!.clearHistoryForItem(this.item).then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadHistory();
|
||||
});
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
clearAllHistory() {
|
||||
this.application.alertService!.confirm(
|
||||
"Are you sure you want to delete the local session history for all notes?",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
() => {
|
||||
this.application.historyManager!.clearAllHistory().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.reloadHistory();
|
||||
});
|
||||
});
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
toggleDiskSaving() {
|
||||
const run = () => {
|
||||
this.application.historyManager!.toggleDiskSaving().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.diskEnabled = this.application.historyManager!.isDiskEnabled();
|
||||
});
|
||||
});
|
||||
};
|
||||
if (!this.application.historyManager!.isDiskEnabled()) {
|
||||
this.application.alertService!.confirm(
|
||||
`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.`,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
run,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
toggleAutoOptimize() {
|
||||
this.application.historyManager!.toggleAutoOptimize().then(() => {
|
||||
this.$timeout(() => {
|
||||
this.autoOptimize = this.application.historyManager!.autoOptimize;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionHistoryMenu extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = SessionHistoryMenuCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
item: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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