From d91bd4f1d049dc9971b50e87b5232dd2bb365a20 Mon Sep 17 00:00:00 2001 From: Seyi Adebajo Date: Wed, 8 Nov 2017 23:21:07 -0800 Subject: [PATCH] adds the dataset-author and dataset-authors components --- .../app/components/dataset-author.ts | 144 ++++++++++ .../app/components/dataset-authors.ts | 250 ++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 wherehows-web/app/components/dataset-author.ts create mode 100644 wherehows-web/app/components/dataset-authors.ts diff --git a/wherehows-web/app/components/dataset-author.ts b/wherehows-web/app/components/dataset-author.ts new file mode 100644 index 0000000000..6d3c78d43e --- /dev/null +++ b/wherehows-web/app/components/dataset-author.ts @@ -0,0 +1,144 @@ +import Component from '@ember/component'; +import ComputedProperty, { equal } from '@ember/object/computed'; +import { getProperties, computed } from '@ember/object'; +import { assert } from '@ember/debug'; + +import { IOwner } from 'wherehows-web/typings/api/datasets/owners'; +import { OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners'; + +/** + * This component renders a single owner record and also provides functionality for interacting with the component + * in the ui or performing operations on a single owner record + * @export + * @class DatasetAuthor + * @extends {Component} + */ +export default class DatasetAuthor extends Component { + tagName = 'tr'; + + classNames = ['dataset-author-record']; + + classNameBindings = ['isConfirmedSuggestedOwner:dataset-author-record--disabled']; + + /** + * The owner record being rendered + * @type {IOwner} + * @memberof DatasetAuthor + */ + owner: IOwner; + + /** + * List of suggested owners that have been confirmed by a user + * @type {Array} + * @memberof DatasetAuthor + */ + commonOwners: Array; + + /** + * External action to handle owner removal from the confirmed list + * @param {IOwner} owner the owner to be removed + * @memberof DatasetAuthor + */ + removeOwner: (owner: IOwner) => IOwner | void; + + /** + * External action to handle owner addition to the confirmed list + * @param {IOwner} owner the suggested owner to be confirmed + * @return {Array | void} the list of owners or void if unsuccessful + * @memberof DatasetAuthor + */ + confirmSuggestedOwner: (owner: IOwner) => Array | void; + + /** + * External action to handle owner property updates, currently on the confirmed list + * @param {IOwner} owner the owner to update + * @param {OwnerType} type the type of the owner + * @memberof DatasetAuthor + */ + updateOwnerType: (owner: IOwner, type: OwnerType) => void; + + /** + * A list of available owner types retreived from the api + * @type {Array} + * @memberof DatasetAuthor + */ + ownerTypes: Array; + + /** + * Compares the source attribute on an owner, if it matches the OwnerSource.Ui type + * @type {ComputedProperty} + * @memberof DatasetAuthor + */ + isOwnerMutable: ComputedProperty = equal('owner.source', OwnerSource.Ui); + + /** + * Detemines if the owner record is a system suggested owner and if this record is confirmed by a user + * @type {ComputedProperty} + * @memberof DatasetAuthor + */ + isConfirmedSuggestedOwner: ComputedProperty = computed('commonOwners', function(this: DatasetAuthor) { + const { commonOwners, isOwnerMutable, owner: { userName } } = getProperties(this, [ + 'commonOwners', + 'isOwnerMutable', + 'owner' + ]); + + if (!isOwnerMutable) { + return commonOwners.findBy('userName', userName); + } + + return false; + }); + + constructor() { + super(...arguments); + const typeOfRemoveOwner = typeof this.removeOwner; + const typeOfConfirmSuggestedOwner = typeof this.confirmSuggestedOwner; + + // Checks that the expected external actions are provided + assert( + `Expected action removeOwner to be an function (Ember action), got ${typeOfRemoveOwner}`, + typeOfRemoveOwner === 'function' + ); + + assert( + `Expected action confirmOwner to be an function (Ember action), got ${typeOfConfirmSuggestedOwner}`, + typeOfConfirmSuggestedOwner === 'function' + ); + } + + actions = { + /** + * Invokes the external action removeOwner to remove an owner from the confirmed list + * @return {false | void | IOwner} + */ + removeOwner: () => { + const { owner, isOwnerMutable, removeOwner } = getProperties(this, ['owner', 'isOwnerMutable', 'removeOwner']); + return isOwnerMutable && removeOwner(owner); + }, + + /** + * @return {Array | void} + */ + confirmOwner: () => { + const { owner, confirmSuggestedOwner } = getProperties(this, ['owner', 'confirmSuggestedOwner']); + return confirmSuggestedOwner(owner); + }, + + /** + * Updates the type attribute on the owner record + * @param {HTMLSelectElement} {target} + * @return { void } + */ + updateOwnerType: ({ target }: Event) => { + const { value } = target; + const { owner, isOwnerMutable, updateOwnerType } = getProperties(this, [ + 'owner', + 'isOwnerMutable', + 'updateOwnerType' + ]); + + return isOwnerMutable && updateOwnerType(owner, value); + } + }; +} diff --git a/wherehows-web/app/components/dataset-authors.ts b/wherehows-web/app/components/dataset-authors.ts new file mode 100644 index 0000000000..af945a1d1b --- /dev/null +++ b/wherehows-web/app/components/dataset-authors.ts @@ -0,0 +1,250 @@ +import Component from '@ember/component'; +import { inject } from '@ember/service'; +import ComputedProperty, { or, lt, filter } from '@ember/object/computed'; +import { set, get, computed, getProperties } from '@ember/object'; +import { assert } from '@ember/debug'; + +import UserLookup from 'wherehows-web/services/user-lookup'; +import CurrentUser from 'wherehows-web/services/current-user'; +import { IOwner } from 'wherehows-web/typings/api/datasets/owners'; +import { + defaultOwnerProps, + defaultOwnerUserName, + minRequiredConfirmedOwners, + ownerAlreadyExists, + userNameEditableClass, + confirmOwner, + updateOwner +} from 'wherehows-web/constants/datasets/owner'; +import { OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners'; +import { objectDeepEqual } from 'wherehows-web/utils/object'; +import { ApiStatus } from 'wherehows-web/utils/api'; + +/** + * Defines properties for the component that renders a list of owners and provides functionality for + * interacting with the list items or the list as whole + * @export + * @class DatasetAuthors + * @extends {Component} + */ +export default class DatasetAuthors extends Component { + /** + * Invokes an external save action to persist the list of owners + * @return {Promise<{ status: ApiStatus }>} + * @memberof DatasetAuthors + */ + save: (owners: Array) => Promise<{ status: ApiStatus }>; + + /** + * The list of owners + * @type {Array} + * @memberof DatasetAuthors + */ + owners: Array; + + /** + * Current user service + * @type {ComputedProperty} + * @memberof DatasetAuthors + */ + currentUser: ComputedProperty = inject(); + + /** + * User look up service + * @type {ComputedProperty} + * @memberof DatasetAuthors + */ + userLookup: ComputedProperty = inject(); + + /** + * Reference to the userNamesResolver function to asynchronously match userNames + * @type {UserLookup['userNamesResolver']} + * @memberof DatasetAuthors + */ + userNamesResolver: UserLookup['userNamesResolver']; + + /** + * A list of valid owner type strings returned from the remote api endpoint + * @type {Array} + * @memberof DatasetAuthors + */ + ownerTypes: Array; + + /** + * Computed flag indicating that a set of negative flags is true + * e.g. if the userName is invalid or the required minimum users are not confirmed + * @type {ComputedProperty} + * @memberof DatasetAuthors + */ + ownershipIsInvalid: ComputedProperty = or('userNameInvalid', 'requiredMinNotConfirmed'); + + /** + * Checks that the list of owners does not contain a default user name + * @type {ComputedProperty} + * @memberof DatasetAuthors + */ + userNameInvalid: ComputedProperty = computed('owners.[]', function(this: DatasetAuthors) { + const owners = get(this, 'owners') || []; + + return owners.filter(({ userName }) => userName === defaultOwnerUserName).length > 0; + }); + + /** + * Flag that resolves in the affirmative if the number of confirmed owner is less the minimum required + * @type {ComputedProperty} + * @memberof DatasetAuthors + */ + requiredMinNotConfirmed: ComputedProperty = lt('confirmedOwners.length', minRequiredConfirmedOwners); + + /** + * Lists the owners that have be confirmed view the client ui + * @type {ComputedProperty>} + * @memberof DatasetAuthors + */ + confirmedOwners: ComputedProperty> = filter('owners', function({ source }: IOwner) { + return source === OwnerSource.Ui; + }); + + /** + * Intersection of confirmed owners and suggested owners + * @type {ComputedProperty>} + * @memberof DatasetAuthors + */ + commonOwners: ComputedProperty> = computed( + 'confirmedOwners.@each.userName', + 'systemGeneratedOwners.@each.userName', + function(this: DatasetAuthors) { + const { confirmedOwners = [], systemGeneratedOwners = [] } = getProperties(this, [ + 'confirmedOwners', + 'systemGeneratedOwners' + ]); + + return confirmedOwners.reduce((common, owner) => { + const { userName } = owner; + return systemGeneratedOwners.findBy('userName', userName) ? [...common, owner] : common; + }, []); + } + ); + + /** + * Lists owners that have been gleaned from dataset metadata + * @type {ComputedProperty>} + * @memberof DatasetAuthors + */ + systemGeneratedOwners: ComputedProperty> = filter('owners', function({ source }: IOwner) { + return source !== OwnerSource.Ui; + }); + + constructor() { + super(...arguments); + const typeOfSaveAction = typeof this.save; + + // on instantiation, sets a reference to the userNamesResolver async function + set(this, 'userNamesResolver', get(get(this, 'userLookup'), 'userNamesResolver')); + + assert( + `Expected action save to be an function (Ember action), got ${typeOfSaveAction}`, + typeOfSaveAction === 'function' + ); + } + + actions = { + /** + * Prepares component for updates to the userName attribute + * @param {IOwner} _ unused + * @param {HTMLElement} { currentTarget } + */ + willEditUserName(_: IOwner, { currentTarget }: Event) { + const { classList } = (currentTarget || {}); + if (classList instanceof HTMLElement) { + classList.add(userNameEditableClass); + } + }, + + /** + * Updates the owner instance userName property + * @param {IOwner} currentOwner an instance of an IOwner type to be updates + * @param {string} [userName] optional userName to update to + */ + editUserName: async (currentOwner: IOwner, userName?: string) => { + if (userName) { + const { getPartyEntityWithUserName } = get(this, 'userLookup'); + const partyEntity = await getPartyEntityWithUserName(userName); + + if (partyEntity) { + const { label, displayName, category } = partyEntity; + const isGroup = category === 'group'; + const updatedOwnerProps: IOwner = { + ...currentOwner, + isGroup, + source: OwnerSource.Ui, + userName: label, + name: displayName, + idType: isGroup ? OwnerType.Group : OwnerType.User + }; + + updateOwner(get(this, 'owners'), currentOwner, updatedOwnerProps); + } + } + }, + + /** + * Adds the component owner record to the list of owners with default props + * @returns {Array | void} + */ + addOwner: () => { + const owners = get(this, 'owners') || []; + const newOwner: IOwner = { ...defaultOwnerProps }; + + if (!ownerAlreadyExists(owners, { userName: newOwner.userName })) { + const { userName } = get(get(this, 'currentUser'), 'currentUser'); + let updatedOwners = [newOwner, ...owners]; + confirmOwner(get(this, 'owners'), newOwner, userName); + + return owners.setObjects(updatedOwners); + } + }, + + /** + * Updates the type attribute for a given owner in the owner list + * @param {IOwner} owner owner to be updates + * @param {OwnerType} type new value to be set on the type attribute + */ + updateOwnerType: (owner: IOwner, type: OwnerType) => { + const owners = get(this, 'owners') || []; + return updateOwner(owners, owner, 'type', type); + }, + + /** + * Adds the owner instance to the list of owners with the source set to ui + * @param {IOwner} owner the owner to add to the list of owner with the source set to OwnerSource.Ui + * @return {Array | void} + */ + confirmSuggestedOwner: (owner: IOwner) => { + const owners = get(this, 'owners') || []; + const suggestedOwner = { ...owner, source: OwnerSource.Ui }; + const hasSuggested = owners.find(owner => objectDeepEqual(owner, suggestedOwner)); + + if (!hasSuggested) { + return owners.setObjects([...owners, suggestedOwner]); + } + }, + + /** + * removes an owner instance from the list of owners + * @param {IOwner} owner the owner to be removed + */ + removeOwner: (owner: IOwner) => { + const owners = get(this, 'owners') || []; + return owners.removeObject(owner); + }, + + /** + * Persists the owners list by invoking the external action + */ + saveOwners: () => { + const { save } = this; + save(get(this, 'owners')); + } + }; +}