datahub/wherehows-web/app/components/dataset-author.js

360 lines
12 KiB
JavaScript

import Ember from 'ember';
const {
set,
get,
getWithDefault,
computed,
Component,
inject: { service }
} = Ember;
/**
* Array of source names to restrict from user updates
* @type {[String]}
*/
const restrictedSources = ['SCM', 'NUAGE'];
/**
* Pattern to look for in source strings
* Will case-insensitive find strings containing `scm` or `nuage`
* @type {RegExp}
*/
const restrictedSourcesPattern = new RegExp(`.*${restrictedSources.join('|')}.*`, 'i');
const removeFromSourceMessage = `Owners sourced from ${restrictedSources.join(', ')} should be removed directly from that source`;
// Class to toggle readonly mode vs edit mode
const userNameEditableClass = 'dataset-author-cell--editing';
// Required minimum confirmed owners needed to update the list of owners
const minRequiredConfirmed = 2;
/**
* Initial user name for candidate owners
* @type {string}
* @private
*/
const _defaultOwnerUserName = 'New Owner';
const _defaultOwnerProps = {
userName: _defaultOwnerUserName,
email: null,
name: '',
isGroup: false,
namespace: 'urn:li:griduser',
type: 'Producer',
subType: null,
sortId: 0,
source: ''
};
export default Component.extend({
/**
* Inject the currentUser service to retrieve logged in userName
* @type {Ember.Service}
*/
sessionUser: service('current-user'),
/**
* Lookup for userEntity objects
* @type {Ember.Service}
*/
ldapUsers: service('user-lookup'),
$ownerTable: null,
restrictedSources,
removeFromSourceMessage,
init() {
this._super(...arguments);
// Sets a reference to the userNamesResolver function on instantiation.
// Typeahead component uses this function to resolve matches for user
// input
set(this, 'userNamesResolver', get(this, 'ldapUsers.userNamesResolver'));
},
/**
* Helper function get the userName for the currently logged in user
* @return {String|*} the userName is found
*/
loggedInUserName() {
// Current user service provides the userName on one of two api
// TODO: DSS-6718 Refactor. merge this into one
return get(this, 'sessionUser.userName') ||
get(this, 'sessionUser.currentUser.userName');
},
// Combination macro to check that the entered username is valid
// i.e. not _defaultOwnerUserName and the requiredMinConfirmed
ownershipIsInvalid: computed.or('userNameInvalid', 'requiredMinNotConfirmed'),
// Returns a list of owners with truthy value for their confirmedBy attribute,
// i.e. they confirmedBy contains a userName and
// type is `Owner` and idType is `USER`.
confirmedOwners: computed('owners.[]', function() {
return getWithDefault(this, 'owners', []).filter(
({ confirmedBy, type, idType }) =>
confirmedBy && type === 'Owner' && idType === 'USER'
);
}),
/**
* Checks that the number of confirmedOwners is < minRequiredConfirmed
* @type {Ember.ComputedProperty}
* @requires minRequiredConfirmed
*/
requiredMinNotConfirmed: computed.lt(
'confirmedOwners.length',
minRequiredConfirmed
),
/**
* Checks that the list of owners does not contain an owner
* with the default userName `_defaultOwnerUserName`
* @type {Ember.ComputedProperty}
* @requires _defaultOwnerUserName
*/
userNameInvalid: computed('owners.[]', function() {
return getWithDefault(this, 'owners', []).filter(
({ userName }) => userName === _defaultOwnerUserName
).length > 0;
}),
didInsertElement() {
this._super(...arguments);
// Cache reference to element on component
this.set('$ownerTable', $('[data-attribute=owner-table]'));
// Apply jQuery sortable plugin to element
this.get('$ownerTable').sortable({
start: (e, { item }) => set(this, 'startPosition', item.index()),
update: (e, { item }) => {
const from = get(this, 'startPosition');
const to = item.index(); // New position where row was dropped
const currentOwners = getWithDefault(this, 'owners', []);
// Updates the owners array to reflect the UI position changes
if (currentOwners.length) {
const reArrangedOwners = [...currentOwners];
const travelingOwner = reArrangedOwners.splice(from, 1).pop();
reArrangedOwners.splice(to, 0, travelingOwner);
currentOwners.setObjects(reArrangedOwners);
}
}
});
},
willDestroyElement() {
this._super(...arguments);
// Removes the sortable functionality from the cached DOM element reference
get(this, '$ownerTable').sortable('destroy');
},
/**
* Non mutative update to an owner on the list of current owners.
* @example _updateOwner(currentOwner, isConfirmed, false);
* @example _updateOwner(currentOwner, {isConfirmed: false, isGroup: true});
*
* @param {Object} currentProps the current props on the currentProps
* to be updated
* @param {String|Object} props the property to update on the currentProps in
* the list of owners. Props can be also be an Object containing the
* properties mapped to updated values.
* @param {*} [value] optional value to set on the currentProps in
* the source list, required is props is map of key -> value pairs
* @private
*/
_updateOwner(currentProps, ...[props, value]) {
const sourceOwners = get(this, 'owners');
// Create a copy so in-flight mutations are not propagates to the ui
const updatingOwners = [...sourceOwners];
// Ensure that the provided currentProps is in the list of sourceOwners
if (updatingOwners.includes(currentProps)) {
const ownerPosition = updatingOwners.indexOf(currentProps);
let updatedOwner = props;
if (typeof props === 'string') {
updatedOwner = Object.assign({}, currentProps, { [props]: value });
}
// Non-mutative array insertion
const updatedOwners = [
...updatingOwners.slice(0, ownerPosition),
updatedOwner,
...updatingOwners.slice(ownerPosition + 1)
];
// Full reset of the `owners` list with the new list
sourceOwners.setObjects(updatedOwners);
}
},
actions: {
/**
* Handles the user intention to update the owner userName, by
* adding a class signifying edit to the DOM td classList.
* Only `owners` in this list who are not sourced from `restrictedSources` (see var above) should
* the user be allowed to update / edit.
* The click event is handled on the TD which is the parent
* of the target label.
* @param {String} source source of the currently interactive owner from
* the list
* @param {DOMTokenList} classList retrieve the classList from the
* wrapping TD element, which handles the bubbled Mouse click on the
* label
*/
willEditUserName({ source = '' }, { currentTarget: { classList } }) {
// Add the className cached in `userNameEditableClass`. This renders
// the input element in the DOM, and removes the label from layout
const disallowEdit = String(source).match(restrictedSourcesPattern);
if (!disallowEdit && typeof classList.add === 'function') {
classList.add(userNameEditableClass);
}
},
/**
* Updates the owner.userName property, setting it to the value entered
* in the table input field
* @param {Object} currentOwner the currentOwner object rendered
* for this table row
* @param {String} userName the userName returned from the typeahead
* @return {Promise.<void>}
*/
async editUserName(currentOwner, userName) {
if (userName) {
// getUser returns a promise, treat as such
const getUser = get(this, 'ldapUsers.getPartyEntityWithUserName');
const { label, displayName, category } = await getUser(userName);
const isGroup = category === 'group';
const updatedProps = Object.assign({}, currentOwner, {
isGroup,
source: 'UI',
userName: label,
name: displayName,
idType: isGroup ? 'GROUP' : 'USER'
});
this._updateOwner(currentOwner, updatedProps);
}
},
/**
* Adds a candidate owner to the list of updated owners to be potentially
* propagated to the server.
* If the owner already exists in the list of currentOwners, this is a no-op.
* Also, sets the errorMessage to reflect this occurrence.
* @return {Array.<Object>|void}
*/
addOwner() {
// Make a copy of the list of current owners
const currentOwners = [...getWithDefault(this, 'owners', [])];
// Make a shallow copy for the new user. A shallow copy is fine,
// since the properties are scalars.
const newOwner = Object.assign({}, _defaultOwnerProps);
// Case insensitive regular exp to check that the username is not in
// the current list of owners
const regex = new RegExp(`.*${newOwner.userName}.*`, 'i');
let updatedOwners = [];
const ownerAlreadyExists = currentOwners
.mapBy('userName')
.some(userName => regex.test(userName));
if (!ownerAlreadyExists) {
updatedOwners = [newOwner, ...currentOwners];
return set(this, 'owners', updatedOwners);
}
set(
this,
'errorMessage',
`Uh oh! There is already a user with the username ${newOwner.userName} in the list of owners.`
);
},
/**
* Removes the provided owner from the current owners array. Pretty simple.
* @param {Object} owner
* @returns {any|Ember.Array|{}}
*/
removeOwner(owner) {
return getWithDefault(this, 'owners', []).removeObject(owner);
},
/**
* Updates the ownerType prop in the list of owners
* @param {Object} owner props to be updated
* @param {String} value current owner value
*/
updateOwnerType(owner, { target: { value } }) {
this._updateOwner(owner, 'ownerType', value);
},
/**
* Sets the checked confirmedBy property on an owner, when the user
* indicates this thought the UI, and if their username is
* available.
* @param {Object} owner the owner object to update
* @prop {String|null|void} owner.confirmedBy flag indicating userName
* that confirmed ownership, or null or void otherwise
* @param {Boolean = false} checked flag for the current ui checked state
*/
confirmOwner(owner, { target: { checked } = false }) {
// Attempt to get the userName from the currentUser service
const userName = get(this, 'loggedInUserName').call(this);
// If checked, assign userName otherwise, null
const isConfirmedBy = checked ? userName : null;
// If we have a non blank userName in confirmedBy
// assign to owner, otherwise assign a toggled blank.
// Blanking will prevent the UI updating it's state, since this will
// reset to value to a falsey value.
// A toggled blank is necessary as a state change device, since
// Ember does a deep comparison of object properties when deciding
// to perform a re-render. We could also use a device such as a hidden
// field with a randomized value.
// This helps to ensure our UI checkbox is consistent with our world
// state.
const currentConfirmation = get(owner, 'confirmedBy');
const nextBlank = currentConfirmation === null ? void 0 : null;
const confirmedBy = isConfirmedBy ? isConfirmedBy : nextBlank;
this._updateOwner(owner, 'confirmedBy', confirmedBy);
},
/**
* Invokes the passed in `save` closure action with the current list of owners.
* @returns {boolean|Promise.<TResult>|*}
*/
updateOwners() {
const closureAction = get(this, 'attrs.save');
return typeof closureAction === 'function' &&
closureAction(get(this, 'owners'))
.then(({
status
} = {}) => {
if (status === 'ok') {
set(
this,
'actionMessage',
'Successfully updated ownership for this dataset'
);
}
})
.catch(({ msg }) =>
set(
this,
'errorMessage',
`An error occurred while saving. Please let us know of this incident. ${msg}`
));
}
}
});