web/satellite: add button for resending expired project invitations

A button has been added to the Team page for resending expired project
member invitations. It appears when one or more of such invitations
have been selected.

Additionally, styling for certain search fields and the Team page's
header has been updated to align more closely with our designs.

Resolves #5752

Change-Id: I623fed5f50e60beca2f82136f8771dde5aa684f4
This commit is contained in:
Jeremy Wharton 2023-06-22 23:52:31 -05:00
parent 2ae75bcf4e
commit 99f4a34a1d
18 changed files with 198 additions and 465 deletions

View File

@ -89,6 +89,7 @@ const (
eventProjectInvitationAccepted = "Project Invitation Accepted"
eventProjectInvitationDeclined = "Project Invitation Declined"
eventGalleryViewClicked = "Gallery View Clicked"
eventResendInviteClicked = "Resend Invite Clicked"
)
var (
@ -158,7 +159,7 @@ func NewService(log *zap.Logger, config Config, satelliteName string) *Service {
eventApplyNewCouponClicked, eventCreditCardRemoved, eventCouponCodeApplied, eventInvoiceDownloaded, eventCreditCardAddedFromBilling,
eventStorjTokenAddedFromBilling, eventAddFundsClicked, eventProjectMembersInviteSent, eventError, eventProjectNameUpdated, eventProjectDescriptionUpdated,
eventProjectStorageLimitUpdated, eventProjectBandwidthLimitUpdated, eventProjectInvitationAccepted, eventProjectInvitationDeclined,
eventGalleryViewClicked,
eventGalleryViewClicked, eventResendInviteClicked,
} {
service.clientEvents[name] = true
}

View File

@ -110,12 +110,7 @@
<div class="access-grants__header-container">
<h3 class="access-grants__header-container__title">My Accesses</h3>
<div class="access-grants__header-container__divider" />
<VHeader
class="access-header-component"
placeholder="Accesses"
:search="fetch"
style-type="access"
/>
<VSearch :search="fetch" />
</div>
<VLoader v-if="areGrantsFetching" width="100px" height="100px" class="grants-loader" />
<div class="access-grants-items">
@ -175,8 +170,8 @@ import { MODALS } from '@/utils/constants/appStatePopUps';
import AccessGrantsItem from '@/components/accessGrants/AccessGrantsItem.vue';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import VHeader from '@/components/common/VHeader.vue';
import VTable from '@/components/common/VTable.vue';
import VSearch from '@/components/common/VSearch.vue';
import AccessGrantsIcon from '@/../static/images/accessGrants/accessGrantsIcon.svg';
import CLIIcon from '@/../static/images/accessGrants/cli.svg';
@ -465,10 +460,6 @@ onBeforeUnmount(() => {
.access-grants-items {
padding-bottom: 55px;
@media screen and (width <= 1150px) {
margin-top: -45px;
}
&__content {
margin-top: 20px;
}
@ -505,12 +496,7 @@ onBeforeUnmount(() => {
height: 1px;
width: auto;
background-color: #dadfe7;
margin-top: 10px;
}
&__access-header-component {
height: 55px !important;
margin-top: 15px;
margin: 13px 0 16px;
}
}
}

View File

@ -145,7 +145,7 @@ import EndDateSelection from '@/components/accessGrants/createFlow/components/En
import Toggle from '@/components/accessGrants/createFlow/components/Toggle.vue';
import VButton from '@/components/common/VButton.vue';
import SearchIcon from '@/../static/images/accessGrants/newCreateFlow/search.svg';
import SearchIcon from '@/../static/images/common/search.svg';
import CloseIcon from '@/../static/images/accessGrants/newCreateFlow/close.svg';
const props = withDefaults(defineProps<{

View File

@ -64,6 +64,7 @@ import DocumentIcon from '@/../static/images/common/documentIcon.svg';
import DownloadIcon from '@/../static/images/common/download.svg';
import FolderIcon from '@/../static/images/objects/newFolder.svg';
import ResourcesIcon from '@/../static/images/navigation/resources.svg';
import UploadIcon from '@/../static/images/common/upload.svg';
const props = withDefaults(defineProps<{
link?: string;
@ -119,6 +120,7 @@ const icons = new Map<string, string>([
['resources', ResourcesIcon],
['addcircle', AddCircleIcon],
['add', WhitePlusIcon],
['upload', UploadIcon],
]);
const iconComponent = computed((): string | undefined => icons.get(props.icon.toLowerCase()));

View File

@ -1,86 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="header-container">
<div class="header-container__buttons-area">
<slot />
</div>
<div v-if="styleType === 'common'" class="search-container">
<VSearch
ref="searchInput"
:placeholder="placeholder"
:search="search"
/>
</div>
<div v-if="styleType === 'access'">
<VSearchAlternateStyling
ref="searchInput"
:placeholder="placeholder"
:search="search"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import VSearch from '@/components/common/VSearch.vue';
import VSearchAlternateStyling from '@/components/common/VSearchAlternateStyling.vue';
type searchCallback = (search: string) => Promise<void>;
const props = withDefaults(defineProps<{
placeholder: string;
search: searchCallback;
styleType?: string;
}>(), {
placeholder: '',
styleType: 'common',
});
const searchInput = ref<{ clearSearch: () => void }>();
function clearSearch(): void {
searchInput.value?.clearSearch();
}
defineExpose({ clearSearch });
</script>
<style scoped lang="scss">
.header-container {
width: 100%;
height: 85px;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
&__buttons-area {
width: auto;
display: flex;
align-items: center;
justify-content: space-between;
}
.search-container {
position: relative;
}
}
@media screen and (width <= 1150px) {
.header-container {
flex-direction: column;
align-items: flex-start;
margin-bottom: 75px;
.search-container {
width: 100%;
margin-top: 30px;
}
}
}
</style>

View File

@ -2,76 +2,46 @@
// See LICENSE for copying information.
<template>
<input
ref="input"
v-model="searchQuery"
readonly
class="common-search-input"
:placeholder="`Search ${placeholder}`"
:style="style"
type="text"
autocomplete="off"
maxlength="72"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@input="processSearchQuery"
@focus="removeReadOnly"
@blur="addReadOnly"
>
<div class="search-container">
<SearchIcon class="search-container__icon" />
<input
v-model="searchQuery"
class="search-container__input"
placeholder="Search"
type="text"
autocomplete="off"
readonly
maxlength="72"
@input="processSearchQuery"
@focus="removeReadOnly"
@blur="addReadOnly"
>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { ref } from 'vue';
import { useDOM } from '@/composables/DOM';
type searchCallback = (search: string) => Promise<void>;
interface SearchStyle {
width: string;
}
import SearchIcon from '@/../static/images/common/search.svg';
const props = withDefaults(defineProps<{
search: searchCallback;
placeholder?: string;
}>(), {
placeholder: '',
});
declare type searchCallback = (search: string) => Promise<void>;
const props = defineProps<{
search: searchCallback,
}>();
const { removeReadOnly, addReadOnly } = useDOM();
const inputWidth = ref<string>('56px');
const searchQuery = ref<string>('');
const input = ref<HTMLInputElement>();
const style = computed((): SearchStyle => {
return { width: inputWidth.value };
});
/**
* Expands search input.
*/
function onMouseEnter(): void {
inputWidth.value = '540px';
input.value?.focus();
}
/**
* Collapses search input if no search query.
*/
function onMouseLeave(): void {
if (!searchQuery.value) {
inputWidth.value = '56px';
input.value?.blur();
}
}
/**
* Clears search query and collapses input.
* Clears search query.
*/
function clearSearch(): void {
searchQuery.value = '';
processSearchQuery();
inputWidth.value = '56px';
}
async function processSearchQuery(): Promise<void> {
@ -82,31 +52,37 @@ defineExpose({ clearSearch });
</script>
<style scoped lang="scss">
.common-search-input {
position: absolute;
right: 0;
bottom: 50%;
transform: translateY(50%);
padding: 0 38px 0 18px;
border: 1px solid #f2f2f2;
.search-container {
padding: 8px;
display: flex;
align-items: center;
box-sizing: border-box;
box-shadow: 0 4px 4px rgb(231 232 238 / 60%);
outline: none;
border-radius: 36px;
height: 56px;
font-family: 'font_regular', sans-serif;
font-size: 16px;
transition: all 0.4s ease-in-out;
background-image: url('../../../static/images/common/search.png');
background-repeat: no-repeat;
background-size: 22px 22px;
background-position: top 16px right 16px;
}
border: 1px solid var(--c-grey-3);
border-radius: 10px;
width: 250px;
background-color: #fff;
@media screen and (width <= 1150px) {
@media screen and (width <= 1150px) {
width: 100%;
}
.common-search-input {
width: 100% !important;
&__icon {
margin: 0 12px 0 4px;
}
&__input {
flex: 1;
background-color: transparent;
outline: none;
border: none;
font-family: 'font_regular', sans-serif;
font-size: 14px;
line-height: 20px;
}
}
::placeholder {
color: var(--c-grey-6);
opacity: 0.7;
}
</style>

View File

@ -1,78 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<input
v-model="searchQuery"
class="access-search-input"
:placeholder="`Search ${placeholder}`"
type="text"
autocomplete="off"
readonly
maxlength="72"
@input="processSearchQuery"
@focus="removeReadOnly"
@blur="addReadOnly"
>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useDOM } from '@/composables/DOM';
declare type searchCallback = (search: string) => Promise<void>;
const props = withDefaults(defineProps<{
placeholder?: string,
search: searchCallback,
}>(), { placeholder: '' });
const { removeReadOnly, addReadOnly } = useDOM();
const searchQuery = ref<string>('');
/**
* Clears search query.
*/
function clearSearch(): void {
searchQuery.value = '';
processSearchQuery();
}
async function processSearchQuery(): Promise<void> {
await props.search(searchQuery.value);
}
defineExpose({ clearSearch });
</script>
<style scoped lang="scss">
.access-search-input {
position: absolute;
left: 0;
bottom: 0;
padding: 0 10px 0 50px;
box-sizing: border-box;
outline: none;
border: 1px solid var(--c-grey-3);
border-radius: 10px;
height: 40px;
width: 250px;
font-family: 'font_regular', sans-serif;
font-size: 16px;
background-color: #fff;
background-image: url('../../../static/images/common/search-gray.png');
background-repeat: no-repeat;
background-size: 22px 22px;
background-position: top 8px left 14px;
@media screen and (width <= 1150px) {
width: 100%;
}
}
::placeholder {
color: #afb7c1;
}
</style>

View File

@ -3,12 +3,7 @@
<template>
<div class="buckets-table">
<VHeader
class="buckets-table__search"
placeholder="Buckets"
:search="searchBuckets"
style-type="access"
/>
<VSearch class="buckets-table__search" :search="searchBuckets" />
<VLoader
v-if="isLoading || searchLoading"
width="100px"
@ -84,8 +79,8 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
import VTable from '@/components/common/VTable.vue';
import BucketItem from '@/components/objects/BucketItem.vue';
import VLoader from '@/components/common/VLoader.vue';
import VHeader from '@/components/common/VHeader.vue';
import VOverallLoader from '@/components/common/VOverallLoader.vue';
import VSearch from '@/components/common/VSearch.vue';
import WhitePlusIcon from '@/../static/images/common/plusWhite.svg';
import EmptyBucketIcon from '@/../static/images/objects/emptyBucket.svg';
@ -245,7 +240,6 @@ onBeforeUnmount(() => {
&__search {
margin-bottom: 20px;
height: 56px;
}
&__loader {

View File

@ -36,28 +36,35 @@
<div class="team-header-container__divider" />
<div class="team-header-container__wrapper">
<VSearchAlternateStyling
<VSearch
ref="searchInput"
class="team-header-container__wrapper__search"
placeholder="members"
:search="processSearchQuery"
/>
<div>
<div v-if="areProjectMembersSelected" class="header-selected-members">
<div v-if="selectedEmailsLength" class="team-header-container__wrapper__right">
<span class="team-header-container__wrapper__right__selected-text">
{{ selectedEmailsLength }} user{{ selectedEmailsLength !== 1 ? 's' : '' }} selected
</span>
<div class="team-header-container__wrapper__right__buttons">
<VButton
class="button deletion"
class="team-header-container__wrapper__right__buttons__button"
label="Delete"
width="122px"
height="40px"
border-radius="8px"
font-size="12px"
is-white
icon="trash"
:on-press="toggleRemoveTeamMembersModal"
/>
<VButton
class="button"
label="Cancel"
width="122px"
height="40px"
:is-transparent="true"
:on-press="onClearSelection"
v-if="resendInvitesShown"
class="team-header-container__wrapper__right__buttons__button"
label="Resend invite"
border-radius="8px"
font-size="12px"
is-white
icon="upload"
:on-press="resendInvites"
:is-disabled="isLoading"
/>
</div>
</div>
@ -68,45 +75,40 @@
<script setup lang="ts">
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import { ProjectMemberHeaderState } from '@/types/projectMembers';
import { AnalyticsHttpApi } from '@/api/analytics';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { MODALS } from '@/utils/constants/appStatePopUps';
import { useNotify } from '@/utils/hooks';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
import { useAppStore } from '@/store/modules/appStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useConfigStore } from '@/store/modules/configStore';
import { useLoading } from '@/composables/useLoading';
import VInfo from '@/components/common/VInfo.vue';
import VButton from '@/components/common/VButton.vue';
import VSearchAlternateStyling from '@/components/common/VSearchAlternateStyling.vue';
import VSearch from '@/components/common/VSearch.vue';
import InfoIcon from '@/../static/images/team/infoTooltip.svg';
interface ClearSearch {
clearSearch(): void;
}
const configStore = useConfigStore();
const appStore = useAppStore();
const pmStore = useProjectMembersStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const { isLoading, withLoading } = useLoading();
const analytics = new AnalyticsHttpApi();
const props = withDefaults(defineProps<{
headerState: ProjectMemberHeaderState;
isAddButtonDisabled: boolean;
}>(), {
headerState: ProjectMemberHeaderState.DEFAULT,
isAddButtonDisabled: false,
});
const FIRST_PAGE = 1;
const analytics: AnalyticsHttpApi = new AnalyticsHttpApi();
const isDeleteClicked = ref<boolean>(false);
const searchInput = ref<typeof VSearchAlternateStyling & ClearSearch>();
const searchInput = ref<InstanceType<typeof VSearch> | null>(null);
/**
* Returns the name of the selected project from store.
@ -115,8 +117,15 @@ const projectName = computed((): string => {
return projectsStore.state.selectedProject.name;
});
const areProjectMembersSelected = computed((): boolean => {
return props.headerState === 1 && !isDeleteClicked.value;
const selectedEmailsLength = computed((): number => {
return pmStore.state.selectedProjectMembersEmails.length;
});
const resendInvitesShown = computed((): boolean => {
const expired = pmStore.state.page.projectInvitations.filter(invite => invite.expired);
return pmStore.state.selectedProjectMembersEmails.every(email => {
return expired.some(invite => invite.email === email);
});
});
/**
@ -130,14 +139,6 @@ function toggleRemoveTeamMembersModal(): void {
appStore.updateActiveModal(MODALS.removeTeamMember);
}
/**
* Clears selection and returns area state to default.
*/
function onClearSelection(): void {
pmStore.clearProjectMemberSelection();
isDeleteClicked.value = false;
}
/**
* Fetches team members of current project depends on search query.
* @param search
@ -158,6 +159,29 @@ async function processSearchQuery(search: string): Promise<void> {
}
}
/**
* resendInvites resends project member invitations.
* It expects that all of the selected project member emails belong to expired invitations.
*/
async function resendInvites(): Promise<void> {
await withLoading(async () => {
analytics.eventTriggered(AnalyticsEvent.RESEND_INVITE_CLICKED);
try {
await pmStore.inviteMembers(pmStore.state.selectedProjectMembersEmails, projectsStore.state.selectedProject.id);
notify.success('Invites re-sent!');
} catch (error) {
notify.error(`Unable to resend project invitations. ${error.message}`, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER);
}
try {
await pmStore.refresh();
} catch (error) {
notify.error(`Unable to fetch project members. ${error.message}`, AnalyticsErrorEventSource.PROJECT_MEMBERS_HEADER);
}
});
}
/**
* Lifecycle hook after initial render.
* Set up listener to clear search bar.
@ -177,10 +201,9 @@ onMounted((): void => {
/**
* Lifecycle hook before component destruction.
* Clears selection and search query for team members page.
* Clears search query for team members page.
*/
onBeforeUnmount((): void => {
onClearSelection();
pmStore.setSearchQuery('');
});
</script>
@ -230,17 +253,6 @@ onBeforeUnmount((): void => {
margin-left: 10px;
display: inline;
&:hover {
.team-header-svg-path {
fill: #fff;
}
.team-header-svg-rect {
fill: #2683ff;
}
}
&__message {
color: #586c86;
font-family: 'font_regular', sans-serif;
@ -257,103 +269,68 @@ onBeforeUnmount((): void => {
background: #dadfe7;
margin: 24px 0;
}
}
.header-default-state,
.header-after-delete-click {
display: flex;
flex-direction: column;
justify-content: center;
&__info-text {
font-family: 'font_medium', sans-serif;
font-size: 14px;
line-height: 28px;
}
&__delete-confirmation {
font-family: 'font_regular', sans-serif;
font-size: 14px;
line-height: 28px;
}
&__button-area {
&__wrapper {
position: relative;
display: flex;
}
}
.header-selected-members {
display: flex;
align-items: center;
justify-content: center;
&__info-text {
margin-left: 25px;
line-height: 48px;
}
}
.button {
margin-right: 12px;
}
.team-header-container__wrapper {
position: relative;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
@media screen and (width <= 1150px) {
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
row-gap: 10px;
}
&__search {
position: static;
}
.blur-content {
position: absolute;
top: 100%;
left: 0;
background-color: #f5f6fa;
width: 100%;
height: 70vh;
z-index: 100;
opacity: 0.3;
}
.blur-search {
position: absolute;
bottom: 0;
left: 0;
width: 300px;
height: 40px;
z-index: 100;
opacity: 0.3;
background-color: #f5f6fa;
align-items: center;
justify-content: space-between;
@media screen and (width <= 1150px) {
bottom: unset;
right: 0;
width: unset;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
row-gap: 10px;
}
}
}
.container.deletion {
background-color: #ff4f4d;
&__search {
position: static;
}
&.label {
color: #fff;
}
&__right {
display: flex;
align-items: center;
gap: 20px;
&:hover {
background-color: #de3e3d;
box-shadow: none;
@media screen and (width <= 1150px) {
width: 100%;
flex-direction: column-reverse;
align-items: flex-start;
gap: 8px;
}
&__selected-text {
color: rgb(0 0 0 / 60%);
font-family: 'font_regular', sans-serif;
font-size: 14px;
line-height: 24px;
}
&__buttons {
display: flex;
gap: 14px;
@media screen and (width <= 1150px) {
width: 100%;
}
&__button {
padding: 8px 12px;
@media screen and (width <= 1150px) {
padding: 12px;
}
:deep(.label) {
color: #56606D !important;
}
:deep(path) {
fill: #56606D !important;
}
}
}
}
}
}

View File

@ -3,13 +3,11 @@
<template>
<div class="team-area">
<div class="team-area__header">
<HeaderArea
:header-state="headerState"
:selected-project-members-count="selectedProjectMembersLength"
:is-add-button-disabled="areMembersFetching"
/>
</div>
<HeaderArea
class="team-area__header"
:selected-project-members-count="selectedProjectMembersLength"
:is-add-button-disabled="areMembersFetching"
/>
<VLoader v-if="areMembersFetching" width="100px" height="100px" />
<div v-if="isEmptySearchResultShown" class="team-area__empty-search-result-area">
@ -49,10 +47,7 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import {
ProjectMemberHeaderState,
ProjectMemberItemModel,
} from '@/types/projectMembers';
import { ProjectMemberItemModel } from '@/types/projectMembers';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useProjectMembersStore } from '@/store/modules/projectMembersStore';
@ -117,10 +112,6 @@ const selectedProjectMembersLength = computed((): number => {
return pmStore.state.selectedProjectMembersEmails.length;
});
const headerState = computed((): number => {
return selectedProjectMembersLength.value > 0 ? ProjectMemberHeaderState.ON_SELECT : ProjectMemberHeaderState.DEFAULT;
});
const isEmptySearchResultShown = computed((): boolean => {
return projectMembersCount.value === 0 && projectMembersTotalCount.value === 0;
});
@ -171,6 +162,7 @@ onMounted(async (): Promise<void> => {
&__header {
width: 100%;
margin-bottom: 20px;
background-color: #f5f6fa;
top: auto;
}

View File

@ -20,6 +20,7 @@ export class ProjectMembersState {
public cursor: ProjectMemberCursor = new ProjectMemberCursor();
public page: ProjectMembersPage = new ProjectMembersPage();
public selectedProjectMembersEmails: string[] = [];
public lastProjectID = '';
}
export const useProjectMembersStore = defineStore('projectMembers', () => {
@ -40,6 +41,7 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
async function getProjectMembers(page: number, projectID: string, limit = DEFAULT_PAGE_LIMIT): Promise<ProjectMembersPage> {
state.cursor.page = page;
state.cursor.limit = limit;
state.lastProjectID = projectID;
const projectMembersPage: ProjectMembersPage = await api.get(projectID, state.cursor);
@ -96,6 +98,11 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
state.page.getAllItems().forEach(member => member.setSelected(false));
}
async function refresh(): Promise<void> {
clearProjectMemberSelection();
await getProjectMembers(state.cursor.page, state.lastProjectID, state.cursor.limit);
}
function clear() {
state.cursor = new ProjectMemberCursor();
state.page = new ProjectMembersPage();
@ -115,6 +122,7 @@ export const useProjectMembersStore = defineStore('projectMembers', () => {
setPageNumber,
toggleProjectMemberSelection,
clearProjectMemberSelection,
refresh,
clear,
};
});

View File

@ -16,18 +16,6 @@ export enum ProjectMemberOrderBy {
CREATED_AT,
}
/**
* Contains values of project members header component state
* used in ProjectMembersArea and HeaderArea.
*/
export enum ProjectMemberHeaderState {
DEFAULT = 0,
/**
* Used when some project members selected
*/
ON_SELECT,
}
/**
* ProjectMembersApi is a graphql implementation of ProjectMembers API.
* Exposes all ProjectMembers-related functionality

View File

@ -55,6 +55,7 @@ export enum AnalyticsEvent {
PROJECT_INVITATION_ACCEPTED = 'Project Invitation Accepted',
PROJECT_INVITATION_DECLINED = 'Project Invitation Declined',
PASSPHRASE_CREATED = 'Passphrase Created',
RESEND_INVITE_CLICKED = 'Resend Invite Clicked',
}
export enum AnalyticsErrorEventSource {

View File

@ -1,4 +0,0 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.67582 12.7785C10.6736 12.7785 13.1037 10.3907 13.1037 7.44515C13.1037 4.49963 10.6736 2.11182 7.67582 2.11182C4.67808 2.11182 2.24792 4.49963 2.24792 7.44515C2.24792 10.3907 4.67808 12.7785 7.67582 12.7785Z" stroke="#444955" stroke-width="1.64731" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.4606 14.1109L11.5092 11.2109" stroke="#444955" stroke-width="1.64731" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.33333 12.6667C10.2789 12.6667 12.6667 10.2789 12.6667 7.33333C12.6667 4.38781 10.2789 2 7.33333 2C4.38781 2 2 4.38781 2 7.33333C2 10.2789 4.38781 12.6667 7.33333 12.6667Z" stroke="#444955" stroke-width="1.64731" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.9996 13.9996L11.0996 11.0996" stroke="#444955" stroke-width="1.64731" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.32568 0.975085L8.34476 0.993421L11.7271 4.37577C11.984 4.63262 11.984 5.04906 11.7271 5.30591C11.4765 5.5565 11.074 5.56261 10.816 5.32425L10.797 5.30591L8.51542 3.02433V10.4553C8.51542 10.8185 8.22095 11.113 7.85771 11.113C7.50332 11.113 7.2144 10.8327 7.20052 10.4817L7.2 10.4553V3.0681L4.96241 5.30591C4.71182 5.5565 4.30934 5.56261 4.05134 5.32425L4.03227 5.30591C3.78168 5.05532 3.77557 4.65284 4.01393 4.39484L4.03227 4.37577L7.41461 0.993421C7.6652 0.742833 8.06769 0.736721 8.32568 0.975085ZM15.1998 13.8755C15.1998 14.2387 14.9053 14.5332 14.5421 14.5332H1.50744C1.14419 14.5332 0.849725 14.2387 0.849725 13.8755C0.849725 13.5122 1.14419 13.2178 1.50744 13.2178H14.5421C14.9053 13.2178 15.1998 13.5122 15.1998 13.8755ZM1.45752 14.4833C1.09427 14.4833 0.799805 14.1888 0.799805 13.8256V11.1947C0.799805 10.8315 1.09427 10.537 1.45752 10.537C1.82076 10.537 2.11523 10.8315 2.11523 11.1947V13.8256C2.11523 14.1888 1.82076 14.4833 1.45752 14.4833ZM14.4922 14.4833C14.1289 14.4833 13.8345 14.1888 13.8345 13.8256V11.1947C13.8345 10.8315 14.1289 10.537 14.4922 10.537C14.8554 10.537 15.1499 10.8315 15.1499 11.1947V13.8256C15.1499 14.1888 14.8554 14.4833 14.4922 14.4833Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,31 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { mount, shallowMount } from '@vue/test-utils';
import HeaderComponent from '@/components/common/VHeader.vue';
describe('HeaderComponent.vue', () => {
it('renders correctly', () => {
const wrapper = shallowMount(HeaderComponent);
expect(wrapper).toMatchSnapshot();
});
it('renders correctly with default props', () => {
const wrapper = mount(HeaderComponent);
expect(wrapper.vm.$props.placeholder).toMatch('');
});
it('function clearSearch works correctly', () => {
const search = jest.fn();
const wrapper = mount(HeaderComponent, {
propsData: {
search: search,
},
});
wrapper.vm.clearSearch();
expect(search).toHaveBeenCalledTimes(1);
});
});