mirror of
https://github.com/datahub-project/datahub.git
synced 2025-08-29 11:35:56 +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 { 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<boolean>}
|
||||
* @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
|
||||
|
@ -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<Array<IOwner>>}
|
||||
* @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
|
||||
@ -129,9 +131,7 @@ export default class DatasetAuthors extends Component {
|
||||
* @type {ComputedProperty<Array<IOwner>>}
|
||||
* @memberof DatasetAuthors
|
||||
*/
|
||||
systemGeneratedOwners: ComputedProperty<Array<IOwner>> = filter('owners', function({ source, idType }: IOwner) {
|
||||
return source !== OwnerSource.Ui && idType === OwnerIdType.User;
|
||||
});
|
||||
systemGeneratedOwners: ComputedProperty<Array<IOwner>> = filter('owners', isSystemGeneratedOwner);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {Array<IOwner>} owners the list of owners to check
|
||||
@ -145,5 +166,8 @@ export {
|
||||
isRequiredMinOwnersNotConfirmed,
|
||||
ownerAlreadyExists,
|
||||
updateOwner,
|
||||
confirmOwner
|
||||
confirmOwner,
|
||||
isConfirmedOwner,
|
||||
confirmedOwners,
|
||||
isSystemGeneratedOwner
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@
|
||||
body {
|
||||
color: $text-color;
|
||||
font: normal 300 150% / 1.4 $text-font-stack;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
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 {
|
||||
|
@ -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'}}
|
||||
|
||||
<p>
|
||||
{{if hasValidExpiration (concat "Expires in " (moment-from-now calendar.selected)) ""}}
|
||||
{{if hasValidExpiration (concat "Expires in " (moment-from-now calendar.selected))}}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
@ -0,0 +1 @@
|
||||
{{avatars/stacked-avatars-list avatars=avatars avatarType="owner"}}
|
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{dataset-owner-list owners=owners datasetName=model.nativeName}}
|
||||
{{datasets/containers/dataset-owner-list urn=encodedUrn}}
|
||||
</div>
|
||||
|
||||
{{#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' {
|
||||
export const isPolicyExpectedShape: (policy: object) => boolean;
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user