diff --git a/wherehows-web/app/components/avatars/avatar-image.ts b/wherehows-web/app/components/avatars/avatar-image.ts new file mode 100644 index 0000000000..6c791f5d76 --- /dev/null +++ b/wherehows-web/app/components/avatars/avatar-image.ts @@ -0,0 +1,33 @@ +import Component from '@ember/component'; +import ComputedProperty, { alias } from '@ember/object/computed'; +import { computed, get } from '@ember/object'; +import { IAvatar } from 'wherehows-web/typings/app/avatars'; + +export default class AvatarImage extends Component { + tagName = 'img'; + + classNames = ['avatar']; + + attributeBindings = ['src', 'alt:name']; + + /** + * The avatar object to render + * @type {IAvatar} + */ + avatar: IAvatar; + + /** + * Returns the image url for the avatar + * @type {ComputedProperty} + */ + src: ComputedProperty = computed('avatar.imageUrl', function(this: AvatarImage): string | void { + //@ts-ignore dot notation property access + return get(this, 'avatar.imageUrl'); + }); + + /** + * Aliases the name property on the related avatar + * @type {ComputedProperty} + */ + name: ComputedProperty = alias('avatar.name'); +} diff --git a/wherehows-web/app/components/avatars/avatar-metadata.ts b/wherehows-web/app/components/avatars/avatar-metadata.ts new file mode 100644 index 0000000000..6311694dad --- /dev/null +++ b/wherehows-web/app/components/avatars/avatar-metadata.ts @@ -0,0 +1,20 @@ +import Component from '@ember/component'; +import { IAvatar } from 'wherehows-web/typings/app/avatars'; + +export default class extends Component { + tagName: 'span'; + + classNames = ['avatar-metadata']; + + /** + * Reference to avatar containing metadata + * @type {IAvatar} + */ + avatar: IAvatar; + + /** + * Slack team ID + * @type {string} + */ + team = 'T06BYN8F7'; +} diff --git a/wherehows-web/app/components/avatars/avatars-detail.ts b/wherehows-web/app/components/avatars/avatars-detail.ts new file mode 100644 index 0000000000..d14065bf5e --- /dev/null +++ b/wherehows-web/app/components/avatars/avatars-detail.ts @@ -0,0 +1,28 @@ +import Component from '@ember/component'; +import { get } from '@ember/object'; +import { action } from 'ember-decorators/object'; + +enum Key { + Escape = 27 +} + +export default class extends Component { + containerClassNames = ['avatars-detail-modal']; + + /** + * External action to close detail interface + */ + onClose: () => void; + + /** + * Handles key up event on interface + * @param {KeyboardEvent} { key, which } + */ + @action + onKeyUp({ key, which }: KeyboardEvent) { + // if escape key, close modal + if (which === Key.Escape || key === 'Escape') { + get(this, 'onClose')(); + } + } +} diff --git a/wherehows-web/app/components/avatars/rollup-avatars.ts b/wherehows-web/app/components/avatars/rollup-avatars.ts new file mode 100644 index 0000000000..addd401f66 --- /dev/null +++ b/wherehows-web/app/components/avatars/rollup-avatars.ts @@ -0,0 +1,63 @@ +import Component from '@ember/component'; +import { IAvatar } from 'wherehows-web/typings/app/avatars'; +import { set, get, computed } from '@ember/object'; +import ComputedProperty from '@ember/object/computed'; +import { action } from 'ember-decorators/object'; +import { singularize, pluralize } from 'ember-inflector'; + +export default class extends Component { + tagName = 'span'; + + classNames = ['avatar-rollup']; + + constructor() { + super(...arguments); + + this.avatars || (this.avatars = []); + this.avatarType || (this.avatarType = 'entity'); + } + + /** + * References the full list of avatars + * @type {Array} + */ + avatars: Array; + + /** + * Flag indicating if the avatars detail view should be rendered + * @type {boolean} + */ + isShowingAvatars = false; + + /** + * The type of avatars being shown + * @type {string} + */ + avatarType: string; + + /** + * Returns the text to be shown in the avatar detail page header + * @type {ComputedProperty} + */ + header: ComputedProperty = computed('avatars.length', function(): string { + const count = get(this, 'avatars').length; + const suffix = get(this, 'avatarType'); + + return `${count} ${count > 1 ? pluralize(suffix) : singularize(suffix)}`; + }); + + /** + * Handles the component click event + */ + click() { + set(this, 'isShowingAvatars', true); + } + + /** + * Updates the flag indicating if the view should be rendered + */ + @action + dismissAvatars() { + set(this, 'isShowingAvatars', false); + } +} diff --git a/wherehows-web/app/components/avatars/stacked-avatars-list.ts b/wherehows-web/app/components/avatars/stacked-avatars-list.ts new file mode 100644 index 0000000000..0e18663f39 --- /dev/null +++ b/wherehows-web/app/components/avatars/stacked-avatars-list.ts @@ -0,0 +1,61 @@ +import Component from '@ember/component'; +import { get, getProperties, computed } from '@ember/object'; +import ComputedProperty from '@ember/object/computed'; +import { IAvatar } from 'wherehows-web/typings/app/avatars'; + +/** + * Specifies the default maximum number of images to render before the more button + * @type {number} + */ +const defaultMavAvatarLength = 6; + +export default class StackedAvatarsList extends Component { + classNames = ['avatar-container']; + + constructor() { + super(...arguments); + + this.avatars || (this.avatars = []); + } + + /** + * The list of avatar objects to render + * @type {Array} + */ + avatars: Array; + + /** + * Calculates the max number of avatars to render + * @type {ComputedProperty} + * @memberof StackedAvatarsList + */ + maxAvatarLength: ComputedProperty = computed('avatars.length', function(this: StackedAvatarsList): number { + const { length } = get(this, 'avatars'); + return length ? Math.min(length, defaultMavAvatarLength) : defaultMavAvatarLength; + }); + + /** + * Build the list of avatars to render based on the max number + * @type {ComputedProperty} + * @memberof StackedAvatarsList + */ + maxAvatars: ComputedProperty = computed('maxAvatarLength', function( + this: StackedAvatarsList + ): StackedAvatarsList['avatars'] { + const { avatars, maxAvatarLength } = getProperties(this, ['avatars', 'maxAvatarLength']); + + return avatars.slice(0, maxAvatarLength); + }); + + /** + * Determines the list of avatars that have not been rendered after the max has been ascertained + * @type {ComputedProperty} + * @memberof StackedAvatarsList + */ + rollupAvatars: ComputedProperty = computed('maxAvatars', function( + this: StackedAvatarsList + ): StackedAvatarsList['avatars'] { + const { avatars, maxAvatarLength } = getProperties(this, ['avatars', 'maxAvatarLength']); + return avatars.slice(maxAvatarLength); + }); +} diff --git a/wherehows-web/app/components/dataset-aclaccess.ts b/wherehows-web/app/components/dataset-aclaccess.ts index 61c4bf6c46..b08afd1ba4 100644 --- a/wherehows-web/app/components/dataset-aclaccess.ts +++ b/wherehows-web/app/components/dataset-aclaccess.ts @@ -1,6 +1,6 @@ import Component from '@ember/component'; import { get, set } from '@ember/object'; -import { gte } from '@ember/object/computed'; +import ComputedProperty, { gte } from '@ember/object/computed'; import { TaskInstance, TaskProperty } from 'ember-concurrency'; import { action } from 'ember-decorators/object'; import { IAccessControlAccessTypeOption } from 'wherehows-web/typings/api/datasets/aclaccess'; @@ -59,7 +59,7 @@ export default class DatasetAclAccess extends Component { * @type {ComputedProperty} * @memberof DatasetAclAccess */ - hasValidExpiration = gte('selectedDate', minSelectableExpirationDate.getTime()); + hasValidExpiration: ComputedProperty = gte('selectedDate', minSelectableExpirationDate.getTime()); /** * External task to remove the logged in user from the related dataset's acl diff --git a/wherehows-web/app/components/dataset-authors.ts b/wherehows-web/app/components/dataset-authors.ts index d52f9d8a2a..e8783f1a65 100644 --- a/wherehows-web/app/components/dataset-authors.ts +++ b/wherehows-web/app/components/dataset-authors.ts @@ -14,9 +14,11 @@ import { updateOwner, minRequiredConfirmedOwners, validConfirmedOwners, - isRequiredMinOwnersNotConfirmed + isRequiredMinOwnersNotConfirmed, + isConfirmedOwner, + isSystemGeneratedOwner } from 'wherehows-web/constants/datasets/owner'; -import { OwnerIdType, OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners'; +import { OwnerSource, OwnerType } from 'wherehows-web/utils/api/datasets/owners'; import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications'; /** @@ -101,7 +103,7 @@ export default class DatasetAuthors extends Component { * @type {ComputedProperty>} * @memberof DatasetAuthors */ - confirmedOwners: ComputedProperty> = filter('owners', ({ source }) => source === OwnerSource.Ui); + confirmedOwners: ComputedProperty> = filter('owners', isConfirmedOwner); /** * Intersection of confirmed owners and suggested owners @@ -129,9 +131,7 @@ export default class DatasetAuthors extends Component { * @type {ComputedProperty>} * @memberof DatasetAuthors */ - systemGeneratedOwners: ComputedProperty> = filter('owners', function({ source, idType }: IOwner) { - return source !== OwnerSource.Ui && idType === OwnerIdType.User; - }); + systemGeneratedOwners: ComputedProperty> = filter('owners', isSystemGeneratedOwner); /** * Invokes the external action as a dropping task diff --git a/wherehows-web/app/components/datasets/containers/dataset-owner-list.ts b/wherehows-web/app/components/datasets/containers/dataset-owner-list.ts new file mode 100644 index 0000000000..6aa0c9c5e2 --- /dev/null +++ b/wherehows-web/app/components/datasets/containers/dataset-owner-list.ts @@ -0,0 +1,59 @@ +import Component from '@ember/component'; +import { get, set, computed } from '@ember/object'; +import ComputedProperty from '@ember/object/computed'; +import { IAvatar } from 'wherehows-web/typings/app/avatars'; +import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners'; +import { task } from 'ember-concurrency'; +import { readDatasetOwnersByUrn } from 'wherehows-web/utils/api/datasets/owners'; +import { arrayMap } from 'wherehows-web/utils/array'; +import { getAvatarProps } from 'wherehows-web/constants/avatars/avatars'; +import { confirmedOwners } from 'wherehows-web/constants/datasets/owner'; + +export default class DatasetOwnerListContainer extends Component { + constructor() { + super(...arguments); + + this.owners || (this.owners = []); + } + + /** + * Urn for the related dataset + * @type {string} + * @memberof DatasetOwnerListContainer + */ + urn: string; + + /** + * The owners for the dataset + * @type {Array} + * @memberof DatasetOwnerListContainer + */ + owners: Array; + + /** + * Lists the avatar objects based off the dataset owners + * @type {ComputedProperty>} + * @memberof DatasetOwnerListContainer + */ + avatars: ComputedProperty> = computed('owners', function(): Array { + return arrayMap(getAvatarProps)(get(this, 'owners')); + }); + + didInsertElement() { + get(this, 'getOwnersTask').perform(); + } + + didUpdateAttrs() { + get(this, 'getOwnersTask').perform(); + } + + /** + * Reads the owners for this dataset + * @type {Task>, () => TaskInstance>>} + */ + getOwnersTask = task(function*(this: DatasetOwnerListContainer): IterableIterator> { + const { owners = [] }: IOwnerResponse = yield readDatasetOwnersByUrn(get(this, 'urn')); + + set(this, 'owners', confirmedOwners(owners)); + }).restartable(); +} diff --git a/wherehows-web/app/constants/avatars/avatars.ts b/wherehows-web/app/constants/avatars/avatars.ts new file mode 100644 index 0000000000..c6261fe0da --- /dev/null +++ b/wherehows-web/app/constants/avatars/avatars.ts @@ -0,0 +1,33 @@ +import { IAvatar } from 'wherehows-web/typings/app/avatars'; +import { avatar } from 'wherehows-web/constants/application'; +import { pick } from 'lodash'; + +/** + * Defines a partial interface for an object that can be narrowed into an avatar + */ +type PartAvatar = Partial; + +const { url: avatarUrl } = avatar; + +/** + * Default image url if an avatar url cannot be determined + * @type {string} + */ +const defaultAvatarImage = '/assets/assets/images/default_avatar.png'; + +/** + * Takes a PartAvatar object and build an object confirming to an IAvatar + * @param {PartAvatar} object + * @return {IAvatar} + */ +const getAvatarProps = (object: PartAvatar): IAvatar => { + const props = pick(object, ['email', 'userName', 'name']); + const imageUrl = props.userName ? avatarUrl.replace('[username]', props.userName) : defaultAvatarImage; + + return { + imageUrl, + ...props + }; +}; + +export { getAvatarProps }; diff --git a/wherehows-web/app/constants/datasets/owner.ts b/wherehows-web/app/constants/datasets/owner.ts index eb884423a0..b20bbf076e 100644 --- a/wherehows-web/app/constants/datasets/owner.ts +++ b/wherehows-web/app/constants/datasets/owner.ts @@ -129,6 +129,27 @@ const isValidConfirmedOwner = ({ confirmedBy, type, idType, isActive }: IOwner): */ const validConfirmedOwners = arrayFilter(isValidConfirmedOwner); +/** + * Checks if an owner has been confirmed by a user, i.e. OwnerSource.Ui + * @param {IOwner} { source } + * @returns {boolean} + */ +const isConfirmedOwner = ({ source }: IOwner): boolean => source === OwnerSource.Ui; + +/** + * Takes a list of owners and returns those that are confirmed + * @type {(array: Array) => Array} + */ +const confirmedOwners = arrayFilter(isConfirmedOwner); + +/** + * Checks that an owner is a system generated owner + * @param {IOwner} { source, idType } + * @returns {boolean} + */ +const isSystemGeneratedOwner = ({ source, idType }: IOwner): boolean => + source !== OwnerSource.Ui && 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 @@ -145,5 +166,8 @@ export { isRequiredMinOwnersNotConfirmed, ownerAlreadyExists, updateOwner, - confirmOwner + confirmOwner, + isConfirmedOwner, + confirmedOwners, + isSystemGeneratedOwner }; diff --git a/wherehows-web/app/styles/base/_base.scss b/wherehows-web/app/styles/base/_base.scss index cd484c6353..a656e26885 100644 --- a/wherehows-web/app/styles/base/_base.scss +++ b/wherehows-web/app/styles/base/_base.scss @@ -14,6 +14,8 @@ body { padding-top: $nav-min-height; padding-bottom: $nav-min-height; background-color: set-color(white, base); + overflow-y: scroll; + overflow-x: hidden; } /** @@ -22,7 +24,9 @@ body { * making all elements inheriting from the root box-sizing value * See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */ -*, *::before, *::after { +*, +*::before, +*::after { box-sizing: inherit; } diff --git a/wherehows-web/app/styles/base/_typography.scss b/wherehows-web/app/styles/base/_typography.scss index 5c70081754..c95f3216c6 100644 --- a/wherehows-web/app/styles/base/_typography.scss +++ b/wherehows-web/app/styles/base/_typography.scss @@ -4,7 +4,6 @@ body { color: $text-color; font: normal 300 150% / 1.4 $text-font-stack; - overflow-y: scroll; } h1, diff --git a/wherehows-web/app/styles/components/_all.scss b/wherehows-web/app/styles/components/_all.scss index 170ee92982..7cbcb5afdc 100644 --- a/wherehows-web/app/styles/components/_all.scss +++ b/wherehows-web/app/styles/components/_all.scss @@ -1,6 +1,6 @@ -@import 'avatar'; @import 'navbar'; @import 'hero'; +@import 'avatar/all'; @import 'dataset-author/all'; @import 'dataset-compliance/all'; @import 'browse-nav/all'; diff --git a/wherehows-web/app/styles/components/_avatar.scss b/wherehows-web/app/styles/components/_avatar.scss deleted file mode 100644 index a5136eefbb..0000000000 --- a/wherehows-web/app/styles/components/_avatar.scss +++ /dev/null @@ -1,5 +0,0 @@ -$avatar-size: 18px; - -.user-avatar { - @include round-image($avatar-size); -} diff --git a/wherehows-web/app/styles/components/avatar/_all.scss b/wherehows-web/app/styles/components/avatar/_all.scss new file mode 100644 index 0000000000..303ae252f3 --- /dev/null +++ b/wherehows-web/app/styles/components/avatar/_all.scss @@ -0,0 +1,2 @@ +@import 'avatar'; +@import 'avatars-detail'; diff --git a/wherehows-web/app/styles/components/avatar/_avatar.scss b/wherehows-web/app/styles/components/avatar/_avatar.scss new file mode 100644 index 0000000000..db43288942 --- /dev/null +++ b/wherehows-web/app/styles/components/avatar/_avatar.scss @@ -0,0 +1,116 @@ +$avatar-size: 18px; + +.user-avatar { + @include round-image($avatar-size); +} + +.avatar-container { + display: flex; + justify-content: flex-end; + + &:hover { + .avatar-item--stacked { + margin-left: item-spacing(7) / 2; + transition-duration: 167ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-delay: 0s; + } + } + + &__list { + list-style-type: none; + } +} + +.avatar-item { + display: inline; + + &--stacked { + transition-duration: 167ms; + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); + transition-delay: 0s; + position: relative; + + &:nth-of-type(1n + 2) { + margin-left: -(item-spacing(7) / 2); + } + + &__meta { + position: absolute; + right: 0; + opacity: 0; + z-index: z(dropdown); + } + + &:hover { + .avatar-item--stacked__meta { + transition-duration: 167ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-delay: 0s; + opacity: 1; + } + } + } +} + +.avatar { + @include round-image(item-spacing(7)); + box-sizing: border-box; + background-clip: content-box; + border: 2px solid transparent; + + &--stacked { + border: 2px solid white; + transition-duration: 167ms; + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); + transition-delay: 0s; + + &:nth-of-type(1n + 2) { + margin-left: -(item-spacing(7) / 2); + } + } +} + +.avatar-rollup { + @include round-image(item-spacing(7)); + box-sizing: border-box; + background-clip: content-box; + border: 1px solid get-color(black, 0.5); + font-weight: 400; + color: get-color(black, 0.5); + font-size: 15px; + display: inline-block; + background: white; + text-align: center; + z-index: 2; + line-height: item-spacing(7); + cursor: pointer; +} + +.avatar-meta-action { + background-color: transparent; + border: 0; + border-radius: 2px; + box-sizing: border-box; + color: get-color(black, 0.7); + cursor: pointer; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: item-spacing(0 2); + display: block; + text-decoration: none; + vertical-align: middle; + + &:hover { + background-color: get-color(black, 0.05); + transition-duration: 167ms; + transition-property: background-color, box-shadow, color; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +} + +.avatar-metadata { + max-width: item-spacing(7) * 2; +} diff --git a/wherehows-web/app/styles/components/avatar/_avatars-detail.scss b/wherehows-web/app/styles/components/avatar/_avatars-detail.scss new file mode 100644 index 0000000000..b45f99f48f --- /dev/null +++ b/wherehows-web/app/styles/components/avatar/_avatars-detail.scss @@ -0,0 +1,84 @@ +.avatars-detail-modal { + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 12px 18px 1px rgba(0, 0, 0, 0.2); + position: relative; + border-radius: 2px; + background-color: white; + max-height: calc(100vh - 64px); + min-height: item-spacing(7); + width: 520px; + display: flex; + flex-direction: column; + + &__header, + &__content { + display: flex; + } + + &__header { + padding: item-spacing(4 4); + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(0, 0, 0, 0.15); + top: 0; + left: 0; + width: 100%; + min-height: 48px; + } + + &__close { + font-weight: fw(normal, 2); + font-size: 48px; + background-color: transparent; + text-align: center; + text-decoration: none; + vertical-align: middle; + cursor: pointer; + box-sizing: border-box; + color: get-color(black, 0.7); + border: 0; + } +} + +.avatars-detail-modal-overlay { + position: fixed; + top: 0; + left: 0; + z-index: z('modal'); + display: flex; + height: 100vh; + width: 100vw; + align-items: center; + justify-content: center; + background-color: rgba(41, 39, 36, 0.75); +} + +.avatar-detail-container { + margin: 0; + padding: item-spacing(0 0 0 3); + width: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +.avatar-detail { + display: flex; + align-items: center; + + &__entity { + flex-grow: 1; + } + + &__meta { + display: flex; + flex-direction: column; + flex-grow: 10; + border-bottom: 1px solid rgba(0, 0, 0, 0.15); + max-width: 100%; + } + + &:last-of-type { + .avatar-detail__meta { + border: 0; + } + } +} diff --git a/wherehows-web/app/styles/components/nacho/_nacho-breadcrumbs.scss b/wherehows-web/app/styles/components/nacho/_nacho-breadcrumbs.scss index 8ac122466a..9f4d55e91c 100644 --- a/wherehows-web/app/styles/components/nacho/_nacho-breadcrumbs.scss +++ b/wherehows-web/app/styles/components/nacho/_nacho-breadcrumbs.scss @@ -26,7 +26,9 @@ $secondary-border-color: set-color(white, base); text-overflow: ellipsis, white-space: nowrap, display: block -)) @include restyle-define(grain, ( +)); + +@include restyle-define(grain, ( restyle-var(primary-border-color): $primary-border-color, restyle-var(secondary-border-color): $secondary-border-color, float: left, @@ -59,7 +61,9 @@ $secondary-border-color: set-color(white, base); margin-top: -50px, left: 100%, ) -)) .nacho-breadcrumbs { +)); + +.nacho-breadcrumbs { @include restyle(breadcrumbs); &__crumb { diff --git a/wherehows-web/app/templates/components/avatars/avatar-image.hbs b/wherehows-web/app/templates/components/avatars/avatar-image.hbs new file mode 100644 index 0000000000..fb5c4b157d --- /dev/null +++ b/wherehows-web/app/templates/components/avatars/avatar-image.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/avatars/avatar-metadata.hbs b/wherehows-web/app/templates/components/avatars/avatar-metadata.hbs new file mode 100644 index 0000000000..e6269f0084 --- /dev/null +++ b/wherehows-web/app/templates/components/avatars/avatar-metadata.hbs @@ -0,0 +1,6 @@ +{{yield (hash + userName=avatar.userName + email=avatar.email + name=avatar.name + team=team + )}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/avatars/avatars-detail.hbs b/wherehows-web/app/templates/components/avatars/avatars-detail.hbs new file mode 100644 index 0000000000..8fef474059 --- /dev/null +++ b/wherehows-web/app/templates/components/avatars/avatars-detail.hbs @@ -0,0 +1,16 @@ +{{#modal-dialog + overlayClass="avatars-detail-modal-overlay" + containerClassNames=containerClassNames + onClose=onClose +}} +
+

+ {{header}} +

+ +
+ +
+ {{yield}} +
+{{/modal-dialog}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/avatars/rollup-avatars.hbs b/wherehows-web/app/templates/components/avatars/rollup-avatars.hbs new file mode 100644 index 0000000000..0da8caf929 --- /dev/null +++ b/wherehows-web/app/templates/components/avatars/rollup-avatars.hbs @@ -0,0 +1,37 @@ +{{yield}} + +{{#if isShowingAvatars}} + {{#avatars/avatars-detail + header=header + onClose=(action "dismissAvatars")}} + +
    + {{#each avatars as |avatar|}} +
  • + +
    + {{avatar.name}} +
    + + {{#avatars/avatar-metadata class="avatar-detail__meta" avatar=avatar as |aviMeta|}} +

    {{aviMeta.name}}

    + + {{#if aviMeta.email}} + + {{aviMeta.email}} + + {{/if}} + + {{#if aviMeta.userName}} + + {{aviMeta.userName}} + + {{/if}} + {{/avatars/avatar-metadata}} + +
  • + {{/each}} +
+ + {{/avatars/avatars-detail}} +{{/if}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/avatars/stacked-avatars-list.hbs b/wherehows-web/app/templates/components/avatars/stacked-avatars-list.hbs new file mode 100644 index 0000000000..a576b1abf4 --- /dev/null +++ b/wherehows-web/app/templates/components/avatars/stacked-avatars-list.hbs @@ -0,0 +1,44 @@ +{{#if hasBlock}} + + {{yield + (hash + image=(component "avatars/avatar-image") + meta=(component "avatars/avatar-metadata") + maxAvatars=maxAvatars + rollupAvatars=rollupAvatars + ) + }} + +{{else}} + +
    + {{#each maxAvatars as |avatar|}} +
  • + + {{avatars/avatar-image avatar=avatar class="avatar--stacked"}} + + {{#avatars/avatar-metadata class="avatar-item--stacked__meta" avatar=avatar as |aviMeta|}} + {{#if aviMeta.email}} + + {{aviMeta.email}} + + {{/if}} + + {{#if aviMeta.userName}} + + {{aviMeta.userName}} + + {{/if}} + {{/avatars/avatar-metadata}} + +
  • + {{/each}} +
+ + {{#if rollupAvatars}} + {{#avatars/rollup-avatars avatars=avatars avatarType=avatarType}} + {{rollupAvatars.length}}+ + {{/avatars/rollup-avatars}} + {{/if}} + +{{/if}} \ No newline at end of file diff --git a/wherehows-web/app/templates/components/dataset-aclaccess.hbs b/wherehows-web/app/templates/components/dataset-aclaccess.hbs index 5011b26e6e..04e0b80509 100644 --- a/wherehows-web/app/templates/components/dataset-aclaccess.hbs +++ b/wherehows-web/app/templates/components/dataset-aclaccess.hbs @@ -84,7 +84,7 @@ {{moment-format calendar.center 'MMMM YYYY'}}

- {{if hasValidExpiration (concat "Expires in " (moment-from-now calendar.selected)) ""}} + {{if hasValidExpiration (concat "Expires in " (moment-from-now calendar.selected))}}

diff --git a/wherehows-web/app/templates/components/datasets/containers/dataset-owner-list.hbs b/wherehows-web/app/templates/components/datasets/containers/dataset-owner-list.hbs new file mode 100644 index 0000000000..f75229c5e3 --- /dev/null +++ b/wherehows-web/app/templates/components/datasets/containers/dataset-owner-list.hbs @@ -0,0 +1 @@ +{{avatars/stacked-avatars-list avatars=avatars avatarType="owner"}} \ No newline at end of file diff --git a/wherehows-web/app/templates/datasets/dataset.hbs b/wherehows-web/app/templates/datasets/dataset.hbs index 578c6d9758..076289ed00 100644 --- a/wherehows-web/app/templates/datasets/dataset.hbs +++ b/wherehows-web/app/templates/datasets/dataset.hbs @@ -46,7 +46,7 @@ - {{dataset-owner-list owners=owners datasetName=model.nativeName}} + {{datasets/containers/dataset-owner-list urn=encodedUrn}} {{#ivy-tabs selection=tabSelected as |tabs|}} diff --git a/wherehows-web/app/typings/app/avatars.d.ts b/wherehows-web/app/typings/app/avatars.d.ts new file mode 100644 index 0000000000..1d04dbd662 --- /dev/null +++ b/wherehows-web/app/typings/app/avatars.d.ts @@ -0,0 +1,13 @@ +/** + * Describes the interface for an avatar object + * @interface IAvatar + */ +interface IAvatar { + imageUrl: string; + email?: null | string; + // Handle for the avatar + userName?: string; + name?: string; +} + +export { IAvatar }; diff --git a/wherehows-web/app/typings/untyped-js-module.d.ts b/wherehows-web/app/typings/untyped-js-module.d.ts index 3435ffa195..1336f8e04c 100644 --- a/wherehows-web/app/typings/untyped-js-module.d.ts +++ b/wherehows-web/app/typings/untyped-js-module.d.ts @@ -33,6 +33,12 @@ declare module 'ember-simple-auth/services/session' { } } +declare module 'ember-inflector' { + const singularize: (arg: string) => string; + const pluralize: (arg: string) => string; + export { singularize, pluralize }; +} + declare module 'wherehows-web/utils/datasets/compliance-policy' { export const isPolicyExpectedShape: (policy: object) => boolean; } diff --git a/wherehows-web/package.json b/wherehows-web/package.json index a98f1520db..a1b5c481d4 100644 --- a/wherehows-web/package.json +++ b/wherehows-web/package.json @@ -60,6 +60,7 @@ "ember-export-application-global": "^2.0.0", "ember-fetch": "^3.4.4", "ember-font-awesome": "^4.0.0-rc.2", + "ember-inflector": "^2.2.0", "ember-load-initializers": "^1.0.0", "ember-math-helpers": "^2.4.0", "ember-metrics": "^0.12.1", diff --git a/wherehows-web/yarn.lock b/wherehows-web/yarn.lock index 2fbfde0913..a6612c1c22 100644 --- a/wherehows-web/yarn.lock +++ b/wherehows-web/yarn.lock @@ -3431,6 +3431,12 @@ ember-inflector@^2.0.0: dependencies: ember-cli-babel "^6.0.0" +ember-inflector@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-2.2.0.tgz#edd273dfd1a29be27f14b195e2f0ed70e812d9e0" + dependencies: + ember-cli-babel "^6.0.0" + ember-load-initializers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-1.0.0.tgz#4919eaf06f6dfeca7e134633d8c05a6c9921e6e7"