import Component from '@ember/component'; import { set } from '@ember/object'; import { action, computed } from '@ember/object'; import { filter, map, alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import UserLookup from 'wherehows-web/services/user-lookup'; import CurrentUser from '@datahub/shared/services/current-user'; import { IOwner } from 'wherehows-web/typings/api/datasets/owners'; import { ownerAlreadyExists, confirmOwner, updateOwner, minRequiredConfirmedOwners, validConfirmedOwners, isRequiredMinOwnersNotConfirmed, isConfirmedOwner } from 'wherehows-web/constants/datasets/owner'; import { OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners'; import Notifications from '@datahub/utils/services/notifications'; import { noop } from 'wherehows-web/utils/helpers/functions'; import { IAppConfig } from '@datahub/shared/types/configurator/configurator'; import { makeAvatar } from 'wherehows-web/constants/avatars/avatars'; import { OwnerWithAvatarRecord } from 'wherehows-web/typings/app/datasets/owners'; import { NotificationEvent } from '@datahub/utils/constants/notifications'; import { PersonEntity } from '@datahub/data-models/entity/person/person-entity'; import { task } from 'ember-concurrency'; import { ETaskPromise } from '@datahub/utils/types/concurrency'; import DataModelsService from '@datahub/data-models/services/data-models'; type Comparator = -1 | 0 | 1; /** * 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>} * @memberof DatasetAuthors */ save: (owners: Array) => Promise> = noop; /** * The list of owners * @type {Array} * @memberof DatasetAuthors */ owners: Array = []; /** * The list of suggested owners, used to populate the suggestions window below the owners table * @type {Array} * @memberof DatasetAuthors */ suggestedOwners: Array = []; /** * Avatar properties used to generate avatar images * @type {(IAppConfig['userEntityProps'] | undefined)} * @memberof DatasetAuthors */ avatarProperties: IAppConfig['userEntityProps'] | undefined; /** * Current user service * @type {ComputedProperty} * @memberof DatasetAuthors */ @service('current-user') currentUser: CurrentUser; /** * Application notifications service * @type {ComputedProperty} * @memberof DatasetAuthors */ @service notifications: Notifications; /** * User look up service * @type {ComputedProperty} * @memberof DatasetAuthors */ @service('user-lookup') userLookup: UserLookup; /** * Injected service for our data models getter to access the PersonEntity class */ @service('data-models') dataModels!: DataModelsService; /** * If there are no changes to the ownership tab, we want to keep the save button disabled. Rather than * try to compare two sets of prev vs new data, we just have a flag here that short stops the validation * function. * Note: changedState has 3 states: * -1 Component hasn't completed initialization yet, and no changes have been made. When * requiredMinNotConfirmed is run on init/render, this gets incremented to its neutral state at 0 * 0 No changes have been made yet * 1 At least one change has been made * @type {number} * @memberof DatasetAuthors */ changedState: Comparator = -1; /** * Reference to the userNamesResolver function to asynchronously match userNames * @type {UserLookup.userNamesResolver} * @memberof DatasetAuthors */ @alias('userLookup.userNamesResolver') userNamesResolver: UserLookup['userNamesResolver']; /** * A list of valid owner type strings returned from the remote api endpoint * @type {Array} * @memberof DatasetAuthors */ ownerTypes: Array; /** * Boolean that determines whether the user is currently using the typeahead box to add an * owner. This is triggered to true when the user clicks on Add an Owner in the table and * returns to false at the end of the addOwner action * @type {boolean} * @default false * @memberof DatasetAuthors */ isAddingOwner = false; setOwnershipRuleChange: (notConfirmed: boolean) => void = noop; /** * Flag that resolves in the affirmative if the number of confirmed owner is less the minimum required * @type {ComputedProperty} * @memberof DatasetAuthors */ @computed('confirmedOwners.@each.type') get requiredMinNotConfirmed(): boolean { const { changedState } = this; if (changedState < 1) { set(this, 'changedState', (changedState + 1) as Comparator); } // If there have been no changes, then we want to automatically set true in order to disable save button // when no changes have been made const requiredOwnersNotConfirmed = isRequiredMinOwnersNotConfirmed(this.confirmedOwners); this.setOwnershipRuleChange(requiredOwnersNotConfirmed); return changedState === -1 || requiredOwnersNotConfirmed; } /** * Counts the number of valid confirmed owners needed to make changes to the dataset * @type {ComputedProperty} * @memberof DatasetAuthors */ @computed('confirmedOwners.@each.type') get ownersRequiredCount(): number { return minRequiredConfirmedOwners - validConfirmedOwners(this.confirmedOwners).length; } /** * Lists the owners that have be confirmed view the client ui * @param {IOwner} owner the current owner to check if confirmed * @type {ComputedProperty>} * @memberof DatasetAuthors */ @filter('owners', function(owner: IOwner): boolean { return isConfirmedOwner(owner); }) _confirmedOwnersFilter: Array; /** * Aliases the _confirmedOwnersFilter. This is a workaround for * @link https://github.com/ember-decorators/ember-decorators/issues/407 * @type {Array} * @memberof DatasetAuthors */ @alias('_confirmedOwnersFilter') confirmedOwners: Array; /** * Augments an IOwner instance with an IAvatar Record keyed by 'avatar' * @this {DatasetAuthors} * @param owner * @memberof DatasetAuthors * @returns {OwnerWithAvatarRecord} */ datasetAuthorsOwnersAugmentedWithAvatars = (owner: IOwner): OwnerWithAvatarRecord => { const { avatarProperties, dataModels } = this; const PersonEntityClass = dataModels.getModel('people') as typeof PersonEntity; return { owner, avatar: avatarProperties ? makeAvatar(avatarProperties)(owner) : { imageUrl: '', imageUrlFallback: '/assets/images/default_avatar.png' }, profile: PersonEntityClass.urnFromUsername(owner.userName) }; }; /** * Augments each confirmedOwner IOwner instance with an avatar Record * @param {IOwner} owner the IOwner instance * @returns {OwnerWithAvatarRecord} * @memberof DatasetAuthors */ @map('confirmedOwners', function(this: DatasetAuthors, owner: IOwner): OwnerWithAvatarRecord { return this.datasetAuthorsOwnersAugmentedWithAvatars(owner); }) confirmedOwnersWithAvatars: Array; /** * Intersection of confirmed owners and suggested owners * @type {ComputedProperty>} * @memberof DatasetAuthors */ @computed('{confirmedOwners,systemGeneratedOwners}.@each.userName') get commonOwners(): Array { const { confirmedOwners, systemGeneratedOwners } = this; return confirmedOwners.reduce((common, owner): Array => { const { userName } = owner; return systemGeneratedOwners.findBy('userName', userName) ? [...common, owner] : common; }, []); } /** * Lists owners that have been gleaned from dataset metadata, * filters out owners that have a source that is NOT OwnerSource.Ui and idType that IS OwnerIdType.User * @type {ComputedProperty>} * @memberof DatasetAuthors */ @computed('suggestedOwners') get systemGeneratedOwners(): Array { // Creates a copy of suggested owners since using it directly seems to invoke a "modified twice in the // same render" error return (this.suggestedOwners || []).slice(0); } /** * Augments each systemGeneratedOwner IOwner instance with an avatar Record * @param {IOwner} owner the IOwner instance * @returns {OwnerWithAvatarRecord} * @memberof DatasetAuthors */ @map('systemGeneratedOwners', function(this: DatasetAuthors, owner: IOwner): OwnerWithAvatarRecord { return this.datasetAuthorsOwnersAugmentedWithAvatars(owner); }) systemGeneratedOwnersWithAvatars: Array; /** * Invokes the external action as a dropping task * @memberof DatasetAuthors */ @(task(function*(this: DatasetAuthors): IterableIterator>> { yield this.save(this.owners); }).drop()) saveOwners!: ETaskPromise>; /** * Adds the component owner record to the list of owners with default props * @returns {Array | void} */ @action addOwner(this: DatasetAuthors, newOwner: IOwner): Array | void { const { owners = [], notifications: { notify } } = this; if (ownerAlreadyExists(owners, { userName: newOwner.userName, source: newOwner.source })) { return void notify({ content: 'Owner has already been added to "confirmed" list', type: NotificationEvent.info }); } const { userName = '' } = this.currentUser ? this.currentUser.currentUser || {} : {}; const updatedOwners = [...owners, newOwner]; confirmOwner(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 */ @action updateOwnerType(this: DatasetAuthors, owner: IOwner, type: OwnerType): Array | void { const owners = 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} */ @action confirmSuggestedOwner(this: DatasetAuthors, owner: IOwner): Array | void { const suggestedOwner = { ...owner, source: OwnerSource.Ui }; return this.addOwner.call(this, suggestedOwner); } /** * removes an owner instance from the list of owners * @param {IOwner} owner the owner to be removed */ @action removeOwner(this: DatasetAuthors, owner: IOwner): IOwner { const owners = this.owners || []; return owners.removeObject(owner); } }