mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-31 12:52:13 +00:00
Merge pull request #1077 from theseyi/avatar-list
adds avatar interfaces ,components and styling. replaces scrolling avatar view with modal dialog
This commit is contained in:
commit
2912d38fec
33
wherehows-web/app/components/avatars/avatar-image.ts
Normal file
33
wherehows-web/app/components/avatars/avatar-image.ts
Normal file
@ -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<string>}
|
||||||
|
*/
|
||||||
|
src: ComputedProperty<string | void> = 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<IAvatar['name']>}
|
||||||
|
*/
|
||||||
|
name: ComputedProperty<IAvatar['name']> = alias('avatar.name');
|
||||||
|
}
|
20
wherehows-web/app/components/avatars/avatar-metadata.ts
Normal file
20
wherehows-web/app/components/avatars/avatar-metadata.ts
Normal file
@ -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';
|
||||||
|
}
|
28
wherehows-web/app/components/avatars/avatars-detail.ts
Normal file
28
wherehows-web/app/components/avatars/avatars-detail.ts
Normal file
@ -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')();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
wherehows-web/app/components/avatars/rollup-avatars.ts
Normal file
63
wherehows-web/app/components/avatars/rollup-avatars.ts
Normal file
@ -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<IAvatar>}
|
||||||
|
*/
|
||||||
|
avatars: Array<IAvatar>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<string>}
|
||||||
|
*/
|
||||||
|
header: ComputedProperty<string> = 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);
|
||||||
|
}
|
||||||
|
}
|
61
wherehows-web/app/components/avatars/stacked-avatars-list.ts
Normal file
61
wherehows-web/app/components/avatars/stacked-avatars-list.ts
Normal file
@ -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<IAvatar>}
|
||||||
|
*/
|
||||||
|
avatars: Array<IAvatar>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the max number of avatars to render
|
||||||
|
* @type {ComputedProperty<number>}
|
||||||
|
* @memberof StackedAvatarsList
|
||||||
|
*/
|
||||||
|
maxAvatarLength: ComputedProperty<number> = 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<StackedAvatarsList['avatars']>}
|
||||||
|
* @memberof StackedAvatarsList
|
||||||
|
*/
|
||||||
|
maxAvatars: ComputedProperty<StackedAvatarsList['avatars']> = 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<StackedAvatarsList['avatars']>}
|
||||||
|
* @memberof StackedAvatarsList
|
||||||
|
*/
|
||||||
|
rollupAvatars: ComputedProperty<StackedAvatarsList['avatars']> = computed('maxAvatars', function(
|
||||||
|
this: StackedAvatarsList
|
||||||
|
): StackedAvatarsList['avatars'] {
|
||||||
|
const { avatars, maxAvatarLength } = getProperties(this, ['avatars', 'maxAvatarLength']);
|
||||||
|
return avatars.slice(maxAvatarLength);
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import { get, set } from '@ember/object';
|
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 { TaskInstance, TaskProperty } from 'ember-concurrency';
|
||||||
import { action } from 'ember-decorators/object';
|
import { action } from 'ember-decorators/object';
|
||||||
import { IAccessControlAccessTypeOption } from 'wherehows-web/typings/api/datasets/aclaccess';
|
import { IAccessControlAccessTypeOption } from 'wherehows-web/typings/api/datasets/aclaccess';
|
||||||
@ -59,7 +59,7 @@ export default class DatasetAclAccess extends Component {
|
|||||||
* @type {ComputedProperty<boolean>}
|
* @type {ComputedProperty<boolean>}
|
||||||
* @memberof DatasetAclAccess
|
* @memberof DatasetAclAccess
|
||||||
*/
|
*/
|
||||||
hasValidExpiration = gte('selectedDate', minSelectableExpirationDate.getTime());
|
hasValidExpiration: ComputedProperty<boolean> = gte('selectedDate', minSelectableExpirationDate.getTime());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External task to remove the logged in user from the related dataset's acl
|
* External task to remove the logged in user from the related dataset's acl
|
||||||
|
@ -14,9 +14,11 @@ import {
|
|||||||
updateOwner,
|
updateOwner,
|
||||||
minRequiredConfirmedOwners,
|
minRequiredConfirmedOwners,
|
||||||
validConfirmedOwners,
|
validConfirmedOwners,
|
||||||
isRequiredMinOwnersNotConfirmed
|
isRequiredMinOwnersNotConfirmed,
|
||||||
|
isConfirmedOwner,
|
||||||
|
isSystemGeneratedOwner
|
||||||
} from 'wherehows-web/constants/datasets/owner';
|
} 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';
|
import Notifications, { NotificationEvent } from 'wherehows-web/services/notifications';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,7 +103,7 @@ export default class DatasetAuthors extends Component {
|
|||||||
* @type {ComputedProperty<Array<IOwner>>}
|
* @type {ComputedProperty<Array<IOwner>>}
|
||||||
* @memberof DatasetAuthors
|
* @memberof DatasetAuthors
|
||||||
*/
|
*/
|
||||||
confirmedOwners: ComputedProperty<Array<IOwner>> = filter('owners', ({ source }) => source === OwnerSource.Ui);
|
confirmedOwners: ComputedProperty<Array<IOwner>> = filter('owners', isConfirmedOwner);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intersection of confirmed owners and suggested owners
|
* Intersection of confirmed owners and suggested owners
|
||||||
@ -129,9 +131,7 @@ export default class DatasetAuthors extends Component {
|
|||||||
* @type {ComputedProperty<Array<IOwner>>}
|
* @type {ComputedProperty<Array<IOwner>>}
|
||||||
* @memberof DatasetAuthors
|
* @memberof DatasetAuthors
|
||||||
*/
|
*/
|
||||||
systemGeneratedOwners: ComputedProperty<Array<IOwner>> = filter('owners', function({ source, idType }: IOwner) {
|
systemGeneratedOwners: ComputedProperty<Array<IOwner>> = filter('owners', isSystemGeneratedOwner);
|
||||||
return source !== OwnerSource.Ui && idType === OwnerIdType.User;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invokes the external action as a dropping task
|
* Invokes the external action as a dropping task
|
||||||
|
@ -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<IOwner>}
|
||||||
|
* @memberof DatasetOwnerListContainer
|
||||||
|
*/
|
||||||
|
owners: Array<IOwner>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the avatar objects based off the dataset owners
|
||||||
|
* @type {ComputedProperty<Array<IAvatar>>}
|
||||||
|
* @memberof DatasetOwnerListContainer
|
||||||
|
*/
|
||||||
|
avatars: ComputedProperty<Array<IAvatar>> = computed('owners', function(): Array<IAvatar> {
|
||||||
|
return arrayMap(getAvatarProps)(get(this, 'owners'));
|
||||||
|
});
|
||||||
|
|
||||||
|
didInsertElement() {
|
||||||
|
get(this, 'getOwnersTask').perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
didUpdateAttrs() {
|
||||||
|
get(this, 'getOwnersTask').perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the owners for this dataset
|
||||||
|
* @type {Task<Promise<Array<IOwnerResponse>>, () => TaskInstance<Promise<IOwnerResponse>>>}
|
||||||
|
*/
|
||||||
|
getOwnersTask = task(function*(this: DatasetOwnerListContainer): IterableIterator<Promise<IOwnerResponse>> {
|
||||||
|
const { owners = [] }: IOwnerResponse = yield readDatasetOwnersByUrn(get(this, 'urn'));
|
||||||
|
|
||||||
|
set(this, 'owners', confirmedOwners(owners));
|
||||||
|
}).restartable();
|
||||||
|
}
|
33
wherehows-web/app/constants/avatars/avatars.ts
Normal file
33
wherehows-web/app/constants/avatars/avatars.ts
Normal file
@ -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<IAvatar>;
|
||||||
|
|
||||||
|
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 };
|
@ -129,6 +129,27 @@ const isValidConfirmedOwner = ({ confirmedBy, type, idType, isActive }: IOwner):
|
|||||||
*/
|
*/
|
||||||
const validConfirmedOwners = arrayFilter(isValidConfirmedOwner);
|
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<IOwner>) => Array<IOwner>}
|
||||||
|
*/
|
||||||
|
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
|
* Checks that the required minimum number of confirmed users is met with the type Owner and idType User
|
||||||
* @param {Array<IOwner>} owners the list of owners to check
|
* @param {Array<IOwner>} owners the list of owners to check
|
||||||
@ -145,5 +166,8 @@ export {
|
|||||||
isRequiredMinOwnersNotConfirmed,
|
isRequiredMinOwnersNotConfirmed,
|
||||||
ownerAlreadyExists,
|
ownerAlreadyExists,
|
||||||
updateOwner,
|
updateOwner,
|
||||||
confirmOwner
|
confirmOwner,
|
||||||
|
isConfirmedOwner,
|
||||||
|
confirmedOwners,
|
||||||
|
isSystemGeneratedOwner
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,8 @@ body {
|
|||||||
padding-top: $nav-min-height;
|
padding-top: $nav-min-height;
|
||||||
padding-bottom: $nav-min-height;
|
padding-bottom: $nav-min-height;
|
||||||
background-color: set-color(white, base);
|
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
|
* making all elements inheriting from the root box-sizing value
|
||||||
* See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/
|
* See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/
|
||||||
*/
|
*/
|
||||||
*, *::before, *::after {
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
body {
|
body {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
font: normal 300 150% / 1.4 $text-font-stack;
|
font: normal 300 150% / 1.4 $text-font-stack;
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
@import 'avatar';
|
|
||||||
@import 'navbar';
|
@import 'navbar';
|
||||||
@import 'hero';
|
@import 'hero';
|
||||||
|
@import 'avatar/all';
|
||||||
@import 'dataset-author/all';
|
@import 'dataset-author/all';
|
||||||
@import 'dataset-compliance/all';
|
@import 'dataset-compliance/all';
|
||||||
@import 'browse-nav/all';
|
@import 'browse-nav/all';
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
$avatar-size: 18px;
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
@include round-image($avatar-size);
|
|
||||||
}
|
|
2
wherehows-web/app/styles/components/avatar/_all.scss
Normal file
2
wherehows-web/app/styles/components/avatar/_all.scss
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
@import 'avatar';
|
||||||
|
@import 'avatars-detail';
|
116
wherehows-web/app/styles/components/avatar/_avatar.scss
Normal file
116
wherehows-web/app/styles/components/avatar/_avatar.scss
Normal file
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -26,7 +26,9 @@ $secondary-border-color: set-color(white, base);
|
|||||||
text-overflow: ellipsis,
|
text-overflow: ellipsis,
|
||||||
white-space: nowrap,
|
white-space: nowrap,
|
||||||
display: block
|
display: block
|
||||||
)) @include restyle-define(grain, (
|
));
|
||||||
|
|
||||||
|
@include restyle-define(grain, (
|
||||||
restyle-var(primary-border-color): $primary-border-color,
|
restyle-var(primary-border-color): $primary-border-color,
|
||||||
restyle-var(secondary-border-color): $secondary-border-color,
|
restyle-var(secondary-border-color): $secondary-border-color,
|
||||||
float: left,
|
float: left,
|
||||||
@ -59,7 +61,9 @@ $secondary-border-color: set-color(white, base);
|
|||||||
margin-top: -50px,
|
margin-top: -50px,
|
||||||
left: 100%,
|
left: 100%,
|
||||||
)
|
)
|
||||||
)) .nacho-breadcrumbs {
|
));
|
||||||
|
|
||||||
|
.nacho-breadcrumbs {
|
||||||
@include restyle(breadcrumbs);
|
@include restyle(breadcrumbs);
|
||||||
|
|
||||||
&__crumb {
|
&__crumb {
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
{{yield}}
|
@ -0,0 +1,6 @@
|
|||||||
|
{{yield (hash
|
||||||
|
userName=avatar.userName
|
||||||
|
email=avatar.email
|
||||||
|
name=avatar.name
|
||||||
|
team=team
|
||||||
|
)}}
|
@ -0,0 +1,16 @@
|
|||||||
|
{{#modal-dialog
|
||||||
|
overlayClass="avatars-detail-modal-overlay"
|
||||||
|
containerClassNames=containerClassNames
|
||||||
|
onClose=onClose
|
||||||
|
}}
|
||||||
|
<header class="avatars-detail-modal__header">
|
||||||
|
<h2>
|
||||||
|
{{header}}
|
||||||
|
</h2>
|
||||||
|
<button class="avatars-detail-modal__close" onclick={{action onClose}}>×</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="avatars-detail-modal__content">
|
||||||
|
{{yield}}
|
||||||
|
</section>
|
||||||
|
{{/modal-dialog}}
|
@ -0,0 +1,37 @@
|
|||||||
|
{{yield}}
|
||||||
|
|
||||||
|
{{#if isShowingAvatars}}
|
||||||
|
{{#avatars/avatars-detail
|
||||||
|
header=header
|
||||||
|
onClose=(action "dismissAvatars")}}
|
||||||
|
|
||||||
|
<ul class="avatar-detail-container">
|
||||||
|
{{#each avatars as |avatar|}}
|
||||||
|
<li class="avatar-detail">
|
||||||
|
|
||||||
|
<div class="avatar-detail__entity">
|
||||||
|
<img src="{{avatar.imageUrl}}" alt="{{avatar.name}}" class="avatar">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#avatars/avatar-metadata class="avatar-detail__meta" avatar=avatar as |aviMeta|}}
|
||||||
|
<h4>{{aviMeta.name}}</h4>
|
||||||
|
|
||||||
|
{{#if aviMeta.email}}
|
||||||
|
<a href="mailto:{{aviMeta.email}}" class="avatar-meta-action" target="_blank">
|
||||||
|
<strong><i class="fa fa-at"></i></strong> {{aviMeta.email}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if aviMeta.userName}}
|
||||||
|
<a href="slack://user?team={{aviMeta.team}}&id={{aviMeta.userName}}" class="avatar-meta-action">
|
||||||
|
<i class="fa fa-slack"></i> {{aviMeta.userName}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/avatars/avatar-metadata}}
|
||||||
|
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{/avatars/avatars-detail}}
|
||||||
|
{{/if}}
|
@ -0,0 +1,44 @@
|
|||||||
|
{{#if hasBlock}}
|
||||||
|
|
||||||
|
{{yield
|
||||||
|
(hash
|
||||||
|
image=(component "avatars/avatar-image")
|
||||||
|
meta=(component "avatars/avatar-metadata")
|
||||||
|
maxAvatars=maxAvatars
|
||||||
|
rollupAvatars=rollupAvatars
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{else}}
|
||||||
|
|
||||||
|
<ul class="avatar-container__list">
|
||||||
|
{{#each maxAvatars as |avatar|}}
|
||||||
|
<li class="avatar-item avatar-item--stacked">
|
||||||
|
|
||||||
|
{{avatars/avatar-image avatar=avatar class="avatar--stacked"}}
|
||||||
|
|
||||||
|
{{#avatars/avatar-metadata class="avatar-item--stacked__meta" avatar=avatar as |aviMeta|}}
|
||||||
|
{{#if aviMeta.email}}
|
||||||
|
<a href="mailto:{{aviMeta.email}}" class="avatar-meta-action" target="_blank">
|
||||||
|
<strong><i class="fa fa-at"></i></strong> {{aviMeta.email}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if aviMeta.userName}}
|
||||||
|
<a href="slack://user?team={{aviMeta.team}}&id={{aviMeta.userName}}" class="avatar-meta-action">
|
||||||
|
<i class="fa fa-slack"></i> {{aviMeta.userName}}
|
||||||
|
</a>
|
||||||
|
{{/if}}
|
||||||
|
{{/avatars/avatar-metadata}}
|
||||||
|
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{#if rollupAvatars}}
|
||||||
|
{{#avatars/rollup-avatars avatars=avatars avatarType=avatarType}}
|
||||||
|
{{rollupAvatars.length}}+
|
||||||
|
{{/avatars/rollup-avatars}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{/if}}
|
@ -84,7 +84,7 @@
|
|||||||
{{moment-format calendar.center 'MMMM YYYY'}}
|
{{moment-format calendar.center 'MMMM YYYY'}}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{{if hasValidExpiration (concat "Expires in " (moment-from-now calendar.selected)) ""}}
|
{{if hasValidExpiration (concat "Expires in " (moment-from-now calendar.selected))}}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
{{avatars/stacked-avatars-list avatars=avatars avatarType="owner"}}
|
@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{dataset-owner-list owners=owners datasetName=model.nativeName}}
|
{{datasets/containers/dataset-owner-list urn=encodedUrn}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#ivy-tabs selection=tabSelected as |tabs|}}
|
{{#ivy-tabs selection=tabSelected as |tabs|}}
|
||||||
|
13
wherehows-web/app/typings/app/avatars.d.ts
vendored
Normal file
13
wherehows-web/app/typings/app/avatars.d.ts
vendored
Normal file
@ -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 };
|
@ -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' {
|
declare module 'wherehows-web/utils/datasets/compliance-policy' {
|
||||||
export const isPolicyExpectedShape: (policy: object) => boolean;
|
export const isPolicyExpectedShape: (policy: object) => boolean;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
"ember-export-application-global": "^2.0.0",
|
"ember-export-application-global": "^2.0.0",
|
||||||
"ember-fetch": "^3.4.4",
|
"ember-fetch": "^3.4.4",
|
||||||
"ember-font-awesome": "^4.0.0-rc.2",
|
"ember-font-awesome": "^4.0.0-rc.2",
|
||||||
|
"ember-inflector": "^2.2.0",
|
||||||
"ember-load-initializers": "^1.0.0",
|
"ember-load-initializers": "^1.0.0",
|
||||||
"ember-math-helpers": "^2.4.0",
|
"ember-math-helpers": "^2.4.0",
|
||||||
"ember-metrics": "^0.12.1",
|
"ember-metrics": "^0.12.1",
|
||||||
|
@ -3431,6 +3431,12 @@ ember-inflector@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ember-cli-babel "^6.0.0"
|
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:
|
ember-load-initializers@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-1.0.0.tgz#4919eaf06f6dfeca7e134633d8c05a6c9921e6e7"
|
resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-1.0.0.tgz#4919eaf06f6dfeca7e134633d8c05a6c9921e6e7"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user