Merge pull request #1410 from theseyi/email-avatars-and-reversible

refactors implementation and design of stacked-avatars-list componen…
This commit is contained in:
Seyi Adebajo 2018-09-26 22:56:56 -07:00 committed by GitHub
commit b0037ccbc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 364 additions and 105 deletions

View File

@ -1,7 +1,8 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { get, getProperties, computed } from '@ember/object'; import { IAvatar, IAvatarDropDownAction } from 'wherehows-web/typings/app/avatars';
import ComputedProperty from '@ember/object/computed'; import { action, computed } from '@ember-decorators/object';
import { IAvatar } from 'wherehows-web/typings/app/avatars'; import { IDropDownOption } from 'wherehows-web/typings/app/dataset-compliance';
import { classNames } from '@ember-decorators/component';
/** /**
* Specifies the default maximum number of images to render before the more button * Specifies the default maximum number of images to render before the more button
@ -9,8 +10,13 @@ import { IAvatar } from 'wherehows-web/typings/app/avatars';
*/ */
const defaultMavAvatarLength = 6; const defaultMavAvatarLength = 6;
@classNames('avatar-container')
export default class StackedAvatarsList extends Component { export default class StackedAvatarsList extends Component {
classNames = ['avatar-container']; /**
* The list of avatar objects to render
* @type {Array<IAvatar>}
*/
avatars: Array<IAvatar>;
constructor() { constructor() {
super(...arguments); super(...arguments);
@ -18,44 +24,53 @@ export default class StackedAvatarsList extends Component {
this.avatars || (this.avatars = []); 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 * Calculates the max number of avatars to render
* @type {ComputedProperty<number>} * @type {ComputedProperty<number>}
* @memberof StackedAvatarsList * @memberof StackedAvatarsList
*/ */
maxAvatarLength: ComputedProperty<number> = computed('avatars.length', function(this: StackedAvatarsList): number { @computed('avatars.length')
const { length } = get(this, 'avatars'); get maxAvatarLength(): number {
const {
avatars: { length }
} = this;
return length ? Math.min(length, defaultMavAvatarLength) : defaultMavAvatarLength; return length ? Math.min(length, defaultMavAvatarLength) : defaultMavAvatarLength;
}); }
/** /**
* Build the list of avatars to render based on the max number * Build the list of avatars to render based on the max number
* @type {ComputedProperty<StackedAvatarsList['avatars']>} * @type {ComputedProperty<StackedAvatarsList['avatars']>}
* @memberof StackedAvatarsList * @memberof StackedAvatarsList
*/ */
maxAvatars: ComputedProperty<StackedAvatarsList['avatars']> = computed('maxAvatarLength', function( @computed('maxAvatarLength')
this: StackedAvatarsList get maxAvatars(): StackedAvatarsList['avatars'] {
): StackedAvatarsList['avatars'] { const { avatars, maxAvatarLength } = this;
const { avatars, maxAvatarLength } = getProperties(this, ['avatars', 'maxAvatarLength']);
return avatars.slice(0, maxAvatarLength); return avatars.slice(0, maxAvatarLength);
}); }
/** /**
* Determines the list of avatars that have not been rendered after the max has been ascertained * Determines the list of avatars that have not been rendered after the max has been ascertained
* @type {ComputedProperty<StackedAvatarsList['avatars']>} * @type {ComputedProperty<StackedAvatarsList['avatars']>}
* @memberof StackedAvatarsList * @memberof StackedAvatarsList
*/ */
rollupAvatars: ComputedProperty<StackedAvatarsList['avatars']> = computed('maxAvatars', function( @computed('maxAvatars')
this: StackedAvatarsList get rollupAvatars(): StackedAvatarsList['avatars'] {
): StackedAvatarsList['avatars'] { const { avatars, maxAvatarLength } = this;
const { avatars, maxAvatarLength } = getProperties(this, ['avatars', 'maxAvatarLength']);
return avatars.slice(maxAvatarLength); return avatars.slice(maxAvatarLength);
}); }
/**
* Handler to invoke IAvatarDropDownAction instance when the drop down option is selected
* @param {IAvatar} avatar the avatar item selected from the list
* @param {(IDropDownOption<IAvatarDropDownAction> | void)} selectedOption drop down option selected
* @memberof StackedAvatarsList
*/
@action
onAvatarOptionSelected(avatar: IAvatar, selectedOption: IDropDownOption<IAvatarDropDownAction> | void): void {
const { value } = selectedOption || { value: (a: IAvatar) => a };
value(avatar);
}
} }

View File

@ -1,18 +1,20 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { set } from '@ember/object'; import { set } from '@ember/object';
import { task } from 'ember-concurrency'; import { classNames } from '@ember-decorators/component';
import { assert } from '@ember/debug';
import { computed } from '@ember-decorators/object'; import { computed } from '@ember-decorators/object';
import { assert } from '@ember/debug';
import { task } from 'ember-concurrency';
import { readDatasetOwnersByUrn } from 'wherehows-web/utils/api/datasets/owners'; import { readDatasetOwnersByUrn } from 'wherehows-web/utils/api/datasets/owners';
import { arrayMap } from 'wherehows-web/utils/array'; import { arrayMap, arrayPipe } from 'wherehows-web/utils/array';
import { IAvatar } from 'wherehows-web/typings/app/avatars'; import { IAvatar } from 'wherehows-web/typings/app/avatars';
import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners'; import { IOwner, IOwnerResponse } from 'wherehows-web/typings/api/datasets/owners';
import { getAvatarProps } from 'wherehows-web/constants/avatars/avatars'; import { getAvatarProps } from 'wherehows-web/constants/avatars/avatars';
import { confirmedOwners } from 'wherehows-web/constants/datasets/owner'; import { confirmedOwners, avatarWithDropDownOption } from 'wherehows-web/constants/datasets/owner';
import { containerDataSource } from 'wherehows-web/utils/components/containers/data-source'; import { containerDataSource } from 'wherehows-web/utils/components/containers/data-source';
import { isLiUrn } from 'wherehows-web/utils/validators/urn'; import { decodeUrn, isLiUrn } from 'wherehows-web/utils/validators/urn';
import { IAppConfig } from 'wherehows-web/typings/api/configurator/configurator'; import { IAppConfig } from 'wherehows-web/typings/api/configurator/configurator';
@classNames('dataset-owner-list')
@containerDataSource('getOwnersTask') @containerDataSource('getOwnersTask')
export default class DatasetOwnerListContainer extends Component { export default class DatasetOwnerListContainer extends Component {
constructor() { constructor() {
@ -57,7 +59,12 @@ export default class DatasetOwnerListContainer extends Component {
@computed('owners') @computed('owners')
get avatars(): Array<IAvatar> { get avatars(): Array<IAvatar> {
const { avatarEntityProps, owners } = this; const { avatarEntityProps, owners } = this;
return arrayMap(getAvatarProps(avatarEntityProps))(owners); const [getAvatarProperties, augmentAvatarsWithDropDownOption] = [
arrayMap(getAvatarProps(avatarEntityProps)),
arrayMap(avatarWithDropDownOption)
];
return arrayPipe(getAvatarProperties, augmentAvatarsWithDropDownOption)(owners);
} }
/** /**
@ -67,7 +74,7 @@ export default class DatasetOwnerListContainer extends Component {
getOwnersTask = task(function*(this: DatasetOwnerListContainer): IterableIterator<Promise<IOwnerResponse>> { getOwnersTask = task(function*(this: DatasetOwnerListContainer): IterableIterator<Promise<IOwnerResponse>> {
const { urn } = this; const { urn } = this;
if (isLiUrn(urn)) { if (isLiUrn(decodeUrn(urn))) {
const { owners = [] }: IOwnerResponse = yield readDatasetOwnersByUrn(urn); const { owners = [] }: IOwnerResponse = yield readDatasetOwnersByUrn(urn);
set(this, 'owners', confirmedOwners(owners)); set(this, 'owners', confirmedOwners(owners));

View File

@ -2,6 +2,8 @@ import { set } from '@ember/object';
import { IOwner } from 'wherehows-web/typings/api/datasets/owners'; import { IOwner } from 'wherehows-web/typings/api/datasets/owners';
import { OwnerIdType, OwnerSource, OwnerType, OwnerUrnNamespace } from 'wherehows-web/utils/api/datasets/owners'; import { OwnerIdType, OwnerSource, OwnerType, OwnerUrnNamespace } from 'wherehows-web/utils/api/datasets/owners';
import { arrayFilter, isListUnique } from 'wherehows-web/utils/array'; import { arrayFilter, isListUnique } from 'wherehows-web/utils/array';
import { IAvatar } from 'wherehows-web/typings/app/avatars';
import { buildMailToUrl } from 'wherehows-web/utils/helpers/email';
/** /**
* Initial user name for candidate owners * Initial user name for candidate owners
@ -150,6 +152,27 @@ const confirmedOwners = arrayFilter(isConfirmedOwner);
const isRequiredMinOwnersNotConfirmed = (owners: Array<IOwner> = []): boolean => const isRequiredMinOwnersNotConfirmed = (owners: Array<IOwner> = []): boolean =>
validConfirmedOwners(owners).length < minRequiredConfirmedOwners; validConfirmedOwners(owners).length < minRequiredConfirmedOwners;
/**
* Augments an owner instance of IAvatar requiring property avatarOptions to exist on the returned instance
* @param {IAvatar} avatar the avatar object to augment
* @returns {(IAvatar & Required<Pick<IAvatar, 'avatarOptions'>>)}
*/
const avatarWithDropDownOption = (avatar: IAvatar): IAvatar & Required<Pick<IAvatar, 'avatarOptions'>> => {
const email = avatar.email || '';
return {
...avatar,
avatarOptions: [
{
// if the owner avatar does not have an email then a null value is returned with no action performed
value: ({ email }: IAvatar): Window | null =>
email ? window.open(buildMailToUrl({ to: email || '' }), '_blank') : null,
label: email
}
]
};
};
export { export {
defaultOwnerProps, defaultOwnerProps,
defaultOwnerUserName, defaultOwnerUserName,
@ -160,5 +183,6 @@ export {
updateOwner, updateOwner,
confirmOwner, confirmOwner,
isConfirmedOwner, isConfirmedOwner,
confirmedOwners confirmedOwners,
avatarWithDropDownOption
}; };

View File

@ -29,6 +29,7 @@
@import 'dataset-health/all'; @import 'dataset-health/all';
@import 'search/all'; @import 'search/all';
@import 'hotkey/all'; @import 'hotkey/all';
@import 'dataset-owner/all';
@import 'nacho/nacho-button'; @import 'nacho/nacho-button';
@import 'nacho/nacho-global-search'; @import 'nacho/nacho-global-search';

View File

@ -5,11 +5,27 @@
.avatar-container { .avatar-container {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-start;
padding: item-spacing(3 0);
/// --rtl modifier allows avatars to be animated from right-to-left
/// avatar-container element should ideally be positioned against the right edge of it's container
&--rtl {
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;
}
}
}
&:hover { &:hover {
.avatar-item--stacked { .avatar-item--stacked {
margin-left: item-spacing(7) / 2; margin-right: item-spacing(7) / 2;
transition-duration: 167ms; transition-duration: 167ms;
transition-timing-function: cubic-bezier(0, 0, 0.2, 1); transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
transition-delay: 0s; transition-delay: 0s;
@ -18,6 +34,8 @@
&__list { &__list {
list-style-type: none; list-style-type: none;
padding: 0;
margin: 0;
} }
} }
@ -52,6 +70,10 @@
} }
} }
} }
&__trigger {
outline: none;
}
} }
.avatar { .avatar {
@ -71,6 +93,10 @@
margin-left: -(item-spacing(7) / 2); margin-left: -(item-spacing(7) / 2);
} }
} }
&--focused {
border-color: get-color(blue5);
}
} }
.avatar-rollup { .avatar-rollup {

View File

@ -0,0 +1 @@
@import 'owners';

View File

@ -0,0 +1,5 @@
.dataset-owner-list {
display: inline-flex;
align-items: center;
margin: item-spacing(1 0);
}

View File

@ -22,12 +22,4 @@
right: item-spacing(5); right: item-spacing(5);
bottom: 0; bottom: 0;
} }
.dataset-owner-list {
margin-left: item-spacing(4);
.avatar-container {
justify-content: flex-start;
}
}
} }

View File

@ -14,30 +14,28 @@
<ul class="avatar-container__list"> <ul class="avatar-container__list">
{{#each maxAvatars as |avatar|}} {{#each maxAvatars as |avatar|}}
<li class="avatar-item avatar-item--stacked"> <li class="avatar-item avatar-item--stacked">
{{#nacho/dropdown/hover-dropdown
dropDownItems=avatar.avatarOptions
selectedDropDown=null
wrappedTriggerComponent=true
renderInPlace=true
onSelect=(action "onAvatarOptionSelected" avatar) as |dd|}}
{{avatars/avatar-image avatar=avatar class="avatar--stacked"}} {{#dd.trigger class="avatar-item__trigger"}}
{{avatars/avatar-image avatar=avatar
class=(if dd.isExpanded "avatar--stacked avatar--focused" "avatar--stacked")}}
{{/dd.trigger}}
{{#avatars/avatar-metadata class="avatar-item--stacked__meta" avatar=avatar as |aviMeta|}} {{dd.content}}
{{#if aviMeta.email}} {{/nacho/dropdown/hover-dropdown}}
<a href="mailto:{{aviMeta.email}}" class="avatar-meta-action" target="_blank" rel="noopener">
<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> </li>
{{/each}} {{/each}}
</ul> </ul>
{{#if rollupAvatars}} {{#if rollupAvatars}}
{{#avatars/rollup-avatars avatars=avatars avatarType=avatarType}} {{#avatars/rollup-avatars avatars=avatars avatarType=@avatarType}}
{{rollupAvatars.length}}+ +{{rollupAvatars.length}}
{{/avatars/rollup-avatars}} {{/avatars/rollup-avatars}}
{{/if}} {{/if}}

View File

@ -1,20 +1,28 @@
{{#basic-dropdown as |bd|}} {{#basic-dropdown renderInPlace=@renderInPlace as |bd|}}
{{#bd.trigger class="nacho-drop-down__trigger" {{#if (not-eq @wrappedTriggerComponent true)}}
onMouseDown=(action "prevent") {{#bd.trigger class="nacho-drop-down__trigger"
onMouseEnter=(action "showDropDown") onMouseDown=(action "prevent")
onMouseLeave=(action "hideDropDown")}} onMouseEnter=(action "showDropDown")
<strong class="nacho-drop-down__active"> onMouseLeave=(action "hideDropDown")}}
{{selectedDropDown.label}} <strong class="nacho-drop-down__active">
</strong> {{selectedDropDown.label}}
</strong>
{{#if selectedDropDown}} {{#if selectedDropDown}}
<span class="nacho-drop-down__active__toggle"> <span class="nacho-drop-down__active__toggle">
{{fa-icon (if isExpanded "caret-up" "caret-down")}} {{fa-icon (if isExpanded "caret-up" "caret-down")}}
</span> </span>
{{/if}} {{/if}}
{{/bd.trigger}} {{/bd.trigger}}
{{/if}}
{{yield (hash {{yield (hash
trigger=(component bd.trigger
onMouseDown=(action "prevent")
onMouseEnter=(action "showDropDown")
onMouseLeave=(action "hideDropDown")
)
isExpanded=isExpanded
content=(component 'nacho/dropdown/dropdown-content' content=(component 'nacho/dropdown/dropdown-content'
baseComponent=bd.content baseComponent=bd.content
onMouseEnter=(action "showDropDown") onMouseEnter=(action "showDropDown")

View File

@ -56,16 +56,14 @@
</div> </div>
{{/datasets/containers/dataset-fabrics}} {{/datasets/containers/dataset-fabrics}}
{{datasets/containers/dataset-owner-list
urn=encodedUrn
avatarEntityProps=avatarEntityProps
class="dataset-owner-list"}}
{{#link-to "datasets.dataset.health" encodedUrn}} {{#link-to "datasets.dataset.health" encodedUrn}}
{{datasets/containers/health-score-gauge urn=encodedUrn wikiLinks=wikiLinks}} {{datasets/containers/health-score-gauge urn=encodedUrn wikiLinks=wikiLinks}}
{{/link-to}} {{/link-to}}
</div> </div>
{{datasets/containers/dataset-owner-list
urn=encodedUrn
avatarEntityProps=avatarEntityProps}}
</div> </div>
</div> </div>

View File

@ -1,3 +1,13 @@
import { IDropDownOption } from 'wherehows-web/typings/app/dataset-compliance';
/**
* Defines the interface for functions that are supplied as options to IAvatar.avatarOptions
* @interface IAvatarDropDownAction
*/
interface IAvatarDropDownAction {
(avatar: IAvatar): any;
}
/** /**
* Describes the interface for an avatar object * Describes the interface for an avatar object
* @interface IAvatar * @interface IAvatar
@ -11,6 +21,8 @@ interface IAvatar {
// Handle for the avatar // Handle for the avatar
userName?: string; userName?: string;
name?: string; name?: string;
// Selection options for an avatar with dropdown
avatarOptions?: Array<IDropDownOption<IAvatarDropDownAction>>;
} }
export { IAvatar }; export { IAvatar, IAvatarDropDownAction };

14
wherehows-web/app/typings/app/core.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
/**
* Defines a generic type for a type T that could also be undefined
* @template T type which maybe is generic over
*/
type Maybe<T> = T | undefined;
/**
* Defines a generic type that recasts a type T as a union of U and an intersection of T and U
* @template T the type to be recast
* @template U the result of the type recast
*/
type Recast<T, U> = T & U | U;
export { Maybe, Recast };

View File

@ -0,0 +1,18 @@
import { Maybe } from 'wherehows-web/typings/app/core';
/**
* Union string literal of safe email headers and the body field
*/
type MailerHeaders = 'to' | 'cc' | 'bcc' | 'subject' | 'body';
/**
* Alias for a string of list of string email header values
* @alias
*/
export type MailerHeaderValue = Maybe<string>;
/**
* An optional record of email headers to it's value - MailerHeaderValue
* @alias
*/
export type IMailHeaderRecord = Partial<Record<MailerHeaders, MailerHeaderValue>>;

View File

@ -1,18 +1,24 @@
import { encode, decode } from 'wherehows-web/utils/encode-decode-uri-component-with-space'; import { encode, decode } from 'wherehows-web/utils/encode-decode-uri-component-with-space';
import { isBlank } from '@ember/utils'; import { isBlank } from '@ember/utils';
import { isObject } from 'wherehows-web/utils/object';
/** /**
* Construct a url by appending a query pair (?key=value | &key=value) to a base url and * Construct a url by appending a query pair (?key=value | &key=value) to a base url and
* encoding the query value in the pair * encoding the query value in the pair
* @param {String} baseUrl the base or original url that will be appended with a query string * @param baseUrl the base or original url that will be appended with a query string
* @param {String} queryParam * @param queryParamOrMap a map of query keys to values or a single query param key
* @param {String} queryValue * @param queryValue if a queryParam is supplied, then a queryValue can be expected
* @param useEncoding flag indicating if the query values should be encoded
* @returns {string} * @returns {string}
*/ */
function buildUrl(): string; function buildUrl(baseUrl: string, queryParamOrMap?: {}, useEncoding?: boolean): string;
function buildUrl(baseUrl: string, mapParams: Record<string, any>): string; function buildUrl(baseUrl: string, queryParamOrMap?: string, queryValue?: string, useEncoding?: boolean): string;
function buildUrl(baseUrl: string, queryKey: string, queryValue: string): string; function buildUrl(
function buildUrl(baseUrl?: string, queryParamOrMap?: string | Record<string, string>, queryValue?: string): string { baseUrl: string,
queryParamOrMap: string | Record<string, any> = {},
queryValue?: string | boolean,
useEncoding: boolean = true
): string {
if (!baseUrl) { if (!baseUrl) {
return ''; return '';
} }
@ -21,16 +27,24 @@ function buildUrl(baseUrl?: string, queryParamOrMap?: string | Record<string, st
return baseUrl; return baseUrl;
} }
let paramMap: { [x: string]: string }; let paramMap: Record<string, any> = {};
// queryParamOrMap is a string then, reify paramMap object with supplied value
if (typeof queryParamOrMap === 'string') { if (typeof queryParamOrMap === 'string') {
paramMap = { paramMap = {
[queryParamOrMap]: queryValue || '' [queryParamOrMap]: queryValue
}; };
} else {
paramMap = queryParamOrMap;
} }
return Object.keys(paramMap).reduce((url, paramKey) => { if (isObject(queryParamOrMap)) {
paramMap = queryParamOrMap;
if (typeof queryValue === 'boolean') {
useEncoding = queryValue;
}
}
return Object.keys(paramMap).reduce((url: string, paramKey: string): string => {
// If the query string already contains the initial question mark append // If the query string already contains the initial question mark append
// kv-pair with ampersand // kv-pair with ampersand
const separator = String(url).includes('?') ? '&' : '?'; const separator = String(url).includes('?') ? '&' : '?';
@ -44,21 +58,23 @@ function buildUrl(baseUrl?: string, queryParamOrMap?: string | Record<string, st
return url; return url;
} }
// Malformed URL will cause decodeURIComponent to throw if (useEncoding) {
// handle and encode queryValue in such instance // Malformed URL will cause decodeURIComponent to throw
try { // handle and encode queryValue in such instance
// Check if queryValue is already encoded, try {
// otherwise encode queryValue before composing url // Check if queryValue is already encoded,
// e.g. if user directly enters query in location bar // otherwise encode queryValue before composing url
if (decode(paramValue) === queryValue) { // e.g. if user directly enters query in location bar
paramValue = encode(paramValue); if (decode(paramValue) === paramValue) {
} paramValue = encode(paramValue);
} catch (err) { }
if (err instanceof URIError) { } catch (err) {
paramValue = encode(paramValue); if (err instanceof URIError) {
} paramValue = encode(paramValue);
}
throw err; throw err;
}
} }
return `${url}${separator}${paramKey}=${paramValue}`; return `${url}${separator}${paramKey}=${paramValue}`;

View File

@ -0,0 +1,16 @@
import buildUrl from 'wherehows-web/utils/build-url';
import { IMailHeaderRecord } from 'wherehows-web/typings/app/helpers/email';
/**
* Constructs a `mailto:` address with supplied email headers as query parameters
* @param {IMailHeaderRecord} [headers={}]
* @returns {string}
*/
const buildMailToUrl = (headers: IMailHeaderRecord = {}): string => {
const { to = '', ...otherHeaders } = headers;
const mailTo = `mailto:${encodeURIComponent(to)}`;
return buildUrl(mailTo, otherHeaders);
};
export { buildMailToUrl };

View File

@ -0,0 +1,59 @@
const emailAddress = 'nowhere@example.com';
const cc = 'nowhere1@example.com';
const bcc = 'nowhere-bcc@example.com';
const body = 'Message';
const subject = 'Email';
const base = 'mailto:';
const mailToEmail = `${base}${encodeURIComponent(emailAddress)}`;
const emailMailToAsserts = [
{
expected: base,
args: void 0,
assertMsg: 'it should return a basic mailto: string without an email when no arguments are passed'
},
{
expected: base,
args: {},
assertMsg: 'it should return a basic mailto: string without an email when an empty object is passed'
},
{
expected: base,
args: { to: '' },
assertMsg:
'it should return a basic mailto: string without an email when an object with only an empty string in the `to` field is passed'
},
{
expected: `${mailToEmail}`,
args: { to: emailAddress },
assertMsg: 'it should return a mailto: string with an email when an object with only the `to` field is passed'
},
{
expected: `${mailToEmail}?cc=${encodeURIComponent(cc)}`,
args: { to: emailAddress, cc },
assertMsg: 'it should return a mailto: string with an email and a cc query when to and cc are passed in'
},
{
expected: `${mailToEmail}?cc=${encodeURIComponent(cc)}&subject=${encodeURIComponent(subject)}`,
args: { to: emailAddress, cc, subject },
assertMsg:
'it should return a mailto: string with an email, subject, and a cc query when to, subject, and cc are passed in'
},
{
expected: `${mailToEmail}?cc=${encodeURIComponent(cc)}&subject=${encodeURIComponent(
subject
)}&bcc=${encodeURIComponent(bcc)}`,
args: { to: emailAddress, cc, subject, bcc },
assertMsg:
'it should return a mailto: string with an email, subject, bcc, and a cc query when to, subject, bcc, and cc are passed in'
},
{
expected: `${mailToEmail}?cc=${encodeURIComponent(cc)}&subject=${encodeURIComponent(
subject
)}&bcc=${encodeURIComponent(bcc)}&body=${encodeURIComponent(body)}`,
args: { to: emailAddress, cc, subject, bcc, body },
assertMsg:
'it should return a mailto: string with an email, subject, bcc, body, and a cc query when to, subject, bcc, body, and cc are passed in'
}
];
export { emailMailToAsserts };

View File

@ -3,10 +3,42 @@ import { module, test } from 'qunit';
module('Unit | Utility | build url', function() { module('Unit | Utility | build url', function() {
const baseUrl = 'https://www.linkedin.com'; const baseUrl = 'https://www.linkedin.com';
const unEncodedString = 'string@string';
const encodedString = encodeURIComponent(unEncodedString);
test('baseUrl', function(assert) { test('baseUrl', function(assert) {
let result = buildUrl(); let result = buildUrl(baseUrl);
assert.equal(result, '', 'returns an empty string when no arguments are passed'); assert.equal(result, baseUrl, 'it returns the base url when no other arguments are passed');
result = buildUrl(baseUrl, {});
assert.equal(result, baseUrl, 'it returns the base url when an empty object is supplied as query parameter');
result = buildUrl(baseUrl, '');
assert.equal(result, baseUrl, 'it returns the base url when an empty string is supplied as query parameter');
result = buildUrl(baseUrl, '', true);
assert.equal(
result,
baseUrl,
'it returns the base url when the queryParam is an empty string and the useEncoding is set'
);
result = buildUrl(baseUrl, 'query', true);
assert.equal(
result,
`${baseUrl}?query=true`,
'it returns the base url with a query key set to true when true is passed as a query value'
);
result = buildUrl(baseUrl, 'query', unEncodedString, true);
assert.equal(
result,
`${baseUrl}?query=${encodedString}`,
'it returns the encoded value when the useEncoding flag is true'
);
result = buildUrl(baseUrl, { query: unEncodedString }, true);
assert.equal(result, `${baseUrl}?query=${encodedString}`, 'it returns the encoded string on a queryParams object');
result = buildUrl(baseUrl, '', ''); result = buildUrl(baseUrl, '', '');
assert.equal(result, baseUrl, 'returns the baseUrl when no query parameter is supplied'); assert.equal(result, baseUrl, 'returns the baseUrl when no query parameter is supplied');

View File

@ -0,0 +1,17 @@
import { buildMailToUrl } from 'wherehows-web/utils/helpers/email';
import { module, test } from 'qunit';
import { emailMailToAsserts } from 'wherehows-web/tests/helpers/validators/email/email-test-fixture';
module('Unit | Utility | helpers/email', function(): void {
test('buildMailToUrl exists', function(assert) {
assert.equal(typeof buildMailToUrl, 'function', 'it is of function type');
});
test('buildMailToUrl generates expected url values', function(assert): void {
assert.expect(emailMailToAsserts.length);
emailMailToAsserts.forEach(({ expected, args, assertMsg }) => {
assert.equal(buildMailToUrl(args), expected, assertMsg);
});
});
});