diff --git a/wherehows-web/app/components/dataset-authors.ts b/wherehows-web/app/components/dataset-authors.ts index db9549f76e..dd3997a4b0 100644 --- a/wherehows-web/app/components/dataset-authors.ts +++ b/wherehows-web/app/components/dataset-authors.ts @@ -7,8 +7,18 @@ 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 { ownerAlreadyExists, confirmOwner, updateOwner } from 'wherehows-web/constants/datasets/owner'; -import { isRequiredMinOwnersNotConfirmed, OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners'; +import { + ownerAlreadyExists, + confirmOwner, + updateOwner, + minRequiredConfirmedOwners +} from 'wherehows-web/constants/datasets/owner'; +import { + isRequiredMinOwnersNotConfirmed, + OwnerSource, + OwnerType, + validConfirmedOwners +} from 'wherehows-web/utils/api/datasets/owners'; import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications'; /** @@ -79,6 +89,15 @@ export default class DatasetAuthors extends Component { return isRequiredMinOwnersNotConfirmed(get(this, 'confirmedOwners')); }); + /** + * Counts the number of valid confirmed owners needed to make changes to the dataset + * @type {ComputedProperty} + * @memberof DatasetAuthors + */ + ownersRequiredCount: ComputedProperty = computed('confirmedOwners.[]', function(this: DatasetAuthors) { + return minRequiredConfirmedOwners - validConfirmedOwners(get(this, 'confirmedOwners')).length; + }); + /** * Lists the owners that have be confirmed view the client ui * @type {ComputedProperty>} diff --git a/wherehows-web/app/constants/datasets/owner.ts b/wherehows-web/app/constants/datasets/owner.ts index a97b746af1..114e8bf2fd 100644 --- a/wherehows-web/app/constants/datasets/owner.ts +++ b/wherehows-web/app/constants/datasets/owner.ts @@ -86,9 +86,9 @@ function updateOwner( * Sets the `confirmedBy` attribute to the currently logged in user * @param {IOwner} owner the owner to be updated * @param {string} confirmedBy the userName of the confirming user - * @returns {(Array | void)} + * @returns {IOwner.confirmedBy} */ -const confirmOwner = (owner: IOwner, confirmedBy: string): null | string => +const confirmOwner = (owner: IOwner, confirmedBy: string): IOwner['confirmedBy'] => set(owner, 'confirmedBy', confirmedBy || null); /** * Defines the default properties for a newly created IOwner instance diff --git a/wherehows-web/app/styles/components/dataset-author/_dataset-author.scss b/wherehows-web/app/styles/components/dataset-author/_dataset-author.scss index 7e4ecabb19..23aa697ac2 100644 --- a/wherehows-web/app/styles/components/dataset-author/_dataset-author.scss +++ b/wherehows-web/app/styles/components/dataset-author/_dataset-author.scss @@ -8,6 +8,10 @@ $owner-list-issue-color: set-color(red, maroonflush); font-size: 20px; font-weight: fw(normal, 4); } + + &__required-count { + height: item-spacing(4); + } } .dataset-author-record { diff --git a/wherehows-web/app/templates/components/dataset-authors.hbs b/wherehows-web/app/templates/components/dataset-authors.hbs index 71a9030041..8cc725ad5c 100644 --- a/wherehows-web/app/templates/components/dataset-authors.hbs +++ b/wherehows-web/app/templates/components/dataset-authors.hbs @@ -10,14 +10,14 @@ -{{#if requiredMinNotConfirmed}} +

+ {{#if requiredMinNotConfirmed}} -

- Add at least {{sub 2 confirmedOwners.length}} confirmed owner(s) with ID Type - USER + Add at least {{ownersRequiredCount}} owner(s) with ID Type - USER and Owner Type - Owner -

-{{/if}} + {{/if}} +

@@ -92,7 +92,7 @@
diff --git a/wherehows-web/app/typings/ember-concurrency.d.ts b/wherehows-web/app/typings/ember-concurrency.d.ts new file mode 100644 index 0000000000..de0d70dfd5 --- /dev/null +++ b/wherehows-web/app/typings/ember-concurrency.d.ts @@ -0,0 +1,91 @@ +declare module 'ember-concurrency' { + export function timeout(delay: number): Promise; + import ComputedProperty from '@ember/object/computed'; + import RSVP from 'rsvp'; + + export enum TaskInstanceState { + Dropped = 'dropped', + Canceled = 'canceled', + Finished = 'finished', + Running = 'running', + Waiting = 'waiting' + } + + export interface TaskProperty extends ComputedProperty { + cancelOn(eventNames: string[]): this; + debug(): this; + drop(): this; + enqueue(): this; + group(groupPath: string): this; + keepLatest(): this; + maxConcurrency(n: number): this; + on(eventNames: string[]): this; + restartable(): this; + } + + export interface TaskInstance extends PromiseLike { + readonly error?: any; + readonly hasStarted: ComputedProperty; + readonly isCanceled: ComputedProperty; + readonly isDropped: ComputedProperty; + readonly isError: ComputedProperty; + readonly isFinished: ComputedProperty; + readonly isRunning: ComputedProperty; + readonly isSuccessful: ComputedProperty; + readonly state: ComputedProperty; + readonly value?: T; + cancel(): void; + catch: () => RSVP.Promise; + finally: () => RSVP.Promise; + then( + onfulfilled?: ((value: T) => TResult1 | RSVP.Promise) | undefined | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null + ): RSVP.Promise; + } + + export enum TaskState { + Running = 'running', + Queued = 'queued', + Idle = 'idle' + } + + type Task = TaskProperty & + ComputedProperty<{ perform: P }> & { + readonly isIdle: boolean; + readonly isQueued: boolean; + readonly isRunning: boolean; + readonly last?: TaskInstance; + readonly lastCanceled?: TaskInstance; + readonly lastComplete?: TaskInstance; + readonly lastErrored?: TaskInstance; + readonly lastIncomplete?: TaskInstance; + readonly lastPerformed?: TaskInstance; + readonly lastRunning?: TaskInstance; + readonly lastSuccessful?: TaskInstance; + readonly performCount: number; + readonly state: TaskState; + cancelAll(): void; + }; + + export function task(generatorFn: (a: A) => Iterator): Task TaskInstance>; + + export function task( + generatorFn: (a1: A1, a2: A2) => Iterator + ): Task TaskInstance>; + + export function task( + generatorFn: (a1: A1, a2: A2, a3: A3) => Iterator + ): Task TaskInstance>; + + export function task( + generatorFn: (a1: A1, a2: A2, a3: A3, a4: A4) => Iterator + ): Task TaskInstance>; + + export function task( + generatorFn: (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5) => Iterator + ): Task TaskInstance>; + + export function task( + generatorFn: (a1: A1, a2: A2, a3: A3, a4: A4, a6: A6) => Iterator + ): Task TaskInstance>; +} diff --git a/wherehows-web/app/typings/untyped-js-module.d.ts b/wherehows-web/app/typings/untyped-js-module.d.ts index d7b9ef5257..8ef543215b 100644 --- a/wherehows-web/app/typings/untyped-js-module.d.ts +++ b/wherehows-web/app/typings/untyped-js-module.d.ts @@ -23,24 +23,6 @@ declare module 'wherehows-web/utils/datasets/compliance-policy'; declare module 'ember-cli-mirage'; -declare module 'ember-concurrency' { - class TaskInstance {} - class TaskProperty { - perform(...args: Array): TaskInstance; - on(): this; - cancelOn(eventNames: string): this; - debug(): this; - drop(): this; - restartable(): this; - enqueue(): this; - keepLatest(): this; - performs(): this; - maxConcurrency(n: number): this; - } - export function task(...args: Array): TaskProperty; - export function timeout(delay: number): Promise; -} - // https://github.com/ember-cli/ember-fetch/issues/72 // TS assumes the mapping btw ES modules and CJS modules is 1:1 // However, `ember-fetch` is the module name, but it's imported with `fetch` diff --git a/wherehows-web/app/utils/api/datasets/owners.ts b/wherehows-web/app/utils/api/datasets/owners.ts index 9f43f4a7ec..1a053086c9 100644 --- a/wherehows-web/app/utils/api/datasets/owners.ts +++ b/wherehows-web/app/utils/api/datasets/owners.ts @@ -12,7 +12,7 @@ import { getJSON, postJSON } from 'wherehows-web/utils/api/fetcher'; /** * Defines a string enum for valid owner types */ -export enum OwnerIdType { +enum OwnerIdType { User = 'USER', Group = 'GROUP' } @@ -21,7 +21,7 @@ export enum OwnerIdType { * Defines the string enum for the OwnerType attribute * @type {string} */ -export enum OwnerType { +enum OwnerType { Owner = 'Owner', Consumer = 'Consumer', Delegate = 'Delegate', @@ -32,12 +32,12 @@ export enum OwnerType { /** * Accepted string values for the namespace of a user */ -export enum OwnerUrnNamespace { +enum OwnerUrnNamespace { corpUser = 'urn:li:corpuser', groupUser = 'urn:li:corpGroup' } -export enum OwnerSource { +enum OwnerSource { Scm = 'SCM', Nuage = 'NUAGE', Sos = 'SOS', @@ -71,7 +71,7 @@ const partyEntitiesUrl = `${ApiRoot}/party/entities`; * @param {number} id the dataset Id * @return {Promise>} the current list of dataset owners */ -export const readDatasetOwners = async (id: number): Promise> => { +const readDatasetOwners = async (id: number): Promise> => { const { owners = [], status, msg } = await getJSON({ url: datasetOwnersUrlById(id) }); if (status === ApiStatus.OK) { return owners.map(owner => ({ @@ -90,7 +90,7 @@ export const readDatasetOwners = async (id: number): Promise> => { * @param {Array} updatedOwners the updated list of owners for this dataset * @return {Promise} */ -export const updateDatasetOwners = async ( +const updateDatasetOwners = async ( id: number, csrfToken: string, updatedOwners: Array @@ -115,7 +115,7 @@ export const updateDatasetOwners = async ( * Requests party entities and if the response status is OK, resolves with an array of entities * @return {Promise>} */ -export const readPartyEntities = async (): Promise> => { +const readPartyEntities = async (): Promise> => { const { status, userEntities = [], msg } = await getJSON({ url: partyEntitiesUrl }); return status === ApiStatus.OK ? userEntities : Promise.reject(msg); }; @@ -127,7 +127,7 @@ export const readPartyEntities = async (): Promise> => { * userEntitiesSource property is also lazy evaluated and cached for app lifetime. * @type {() => Promise} */ -export const getUserEntities: () => Promise = (() => { +const getUserEntities: () => Promise = (() => { /** * Memoized reference to the resolved value of a previous invocation to curried function in getUserEntities * @type {{result: IPartyProps | null}} @@ -177,18 +177,40 @@ export const getUserEntities: () => Promise = (() => { * @param {Array} partyEntities * @return {IUserEntityMap} */ -export const readPartyEntitiesMap = (partyEntities: Array): IUserEntityMap => +const readPartyEntitiesMap = (partyEntities: Array): IUserEntityMap => partyEntities.reduce( (map: { [label: string]: string }, { label, displayName }: IPartyEntity) => ((map[label] = displayName), map), {} ); +/** + * Filters out a list of valid confirmed owners in a list of owners + * @param {Array} [owners=[]] the owners to filter + * @returns {Array} + */ +const validConfirmedOwners = (owners: Array = []): Array => + owners.filter( + ({ confirmedBy, type, idType }) => confirmedBy && type === OwnerType.Owner && idType === OwnerIdType.User + ); + /** * Checks that the required minimum number of confirmed users is met with the type Owner and idType User * @param {Array} owners the list of owners to check * @return {boolean} */ -export const isRequiredMinOwnersNotConfirmed = (owners: Array = []): boolean => - owners.filter( - ({ confirmedBy, type, idType }) => confirmedBy && type === OwnerType.Owner && idType === OwnerIdType.User - ).length < minRequiredConfirmed; +const isRequiredMinOwnersNotConfirmed = (owners: Array = []): boolean => + validConfirmedOwners(owners).length < minRequiredConfirmed; + +export { + validConfirmedOwners, + isRequiredMinOwnersNotConfirmed, + readDatasetOwners, + readPartyEntities, + readPartyEntitiesMap, + getUserEntities, + updateDatasetOwners, + OwnerIdType, + OwnerType, + OwnerUrnNamespace, + OwnerSource +};