Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fe:FSADT1-1523): Apply design styling to Search screen #1282

Merged
merged 13 commits into from
Oct 29, 2024
Merged
34 changes: 33 additions & 1 deletion frontend/cypress/e2e/pages/SearchPage.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ describe("Search Page", () => {
.should("have.length.greaterThan", 0)
.should("be.visible");

cy.get("#search-box").find(`cds-combo-box-item[data-value^="${clientNumber}"]`).click();
cy.get("#search-box").find(`cds-combo-box-item[data-id="${clientNumber}"]`).click();
});
it("navigates to the client details", () => {
const greenDomain = "green-domain.com";
Expand Down Expand Up @@ -396,4 +396,36 @@ describe("Search Page", () => {
});
});
});

describe("when the API finds no matching results", () => {
beforeEach(() => {
// The "empty" value actually triggers the empty array response
cy.fillFormEntry("#search-box", "empty");
});
describe("and user clicks the Search button", () => {
beforeEach(() => {
cy.get("#search-button").click();
});

it('displays a "No results" message that includes the submitted search value', () => {
cy.wait("@fullSearch");

cy.contains("No results for “empty”");
});

describe("and the user types something else in the search box but does not re-submit the full search", () => {
beforeEach(() => {
cy.wait("@fullSearch");

cy.fillFormEntry("#search-box", "other");
});

it('still displays the "No results" message with the value that was previously submitted', () => {
cy.wait(200);

cy.contains("No results for “empty”");
});
});
});
});
});
30 changes: 29 additions & 1 deletion frontend/src/assets/styles/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1641,14 +1641,42 @@ div.internal-grouping-01:has(svg.warning) span.body-compact-01 {
}

#search-line {
--lg-size: 3rem;
--padding-size: 2.5rem;

display: flex;
align-items: flex-end;
align-items: flex-start;
min-height: calc(var(--lg-size) + var(--padding-size));
padding: 0 var(--padding-size);

.grouping-02 {
flex-grow: 1;
}
}

#search-box {
--row-height: 2.875rem;

cds-combo-box-item {
block-size: var(--row-height);
}

&::part(cds--combo-box), &::part(input) {
border-bottom-width: 0;
}

&::part(input)::placeholder {
color: var(--light-theme-text-text-primary);
}
}

.search-result-item {
display: flex;
gap: 1rem;
align-items: center;
margin: -0.0625rem 0;
}

#search-button {
width: 7.875rem;
}
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/components/forms/AutoCompleteInputComponent.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="ts" generic="T">
import { ref, computed, watch, nextTick } from "vue";
// Carbon
import "@carbon/web-components/es/components/combo-box/index";
Expand All @@ -7,8 +7,9 @@ import type { CDSComboBox } from "@carbon/web-components";
// Composables
import { useEventBus } from "@vueuse/core";
// Types
import type { BusinessSearchResult, CodeNameType } from "@/dto/CommonTypesDto";
import type { BusinessSearchResult, CodeNameType, CodeNameValue } from "@/dto/CommonTypesDto";
import { isEmpty, type ValidationMessageType } from "@/dto/CommonTypesDto";
import type { DROPDOWN_SIZE } from "@carbon/web-components/es/components/dropdown/defs";

//Define the input properties for this component
const props = withDefaults(
Expand All @@ -18,8 +19,9 @@ const props = withDefaults(
ariaLabel?: string;
tip?: string;
placeholder?: string;
size?: `${DROPDOWN_SIZE}`;
modelValue: string;
contents: Array<BusinessSearchResult>;
contents: Array<CodeNameValue<T>>;
validations: Array<Function>;
errorMessage?: string;
loading?: boolean;
Expand Down Expand Up @@ -82,15 +84,15 @@ watch(
);

// This is to make the input list contains the selected value to show when component render
const inputList = computed<Array<BusinessSearchResult>>(() => {
const inputList = computed<Array<CodeNameValue<T>>>(() => {
if (props.contents?.length > 0) {
return props.contents.filter((entry) => entry.name);
} else if (props.modelValue !== userValue.value) {
// Needed when the component mounts with a pre-filled value.
return [{ name: props.modelValue, code: "", status: "", legalType: "" }];
return [{ name: props.modelValue, code: "", value: undefined }];
} else if (props.modelValue && showLoading.value) {
// Just to give a "loading" feedback.
return [{ name: loadingName, code: "", status: "", legalType: "" }];
return [{ name: loadingName, code: "", value: undefined }];
}
return [];
});
Expand Down Expand Up @@ -313,6 +315,7 @@ const safeHelperText = computed(() => props.tip || " ");
<cds-combo-box
ref="cdsComboBoxRef"
:id="id"
:size="size"
:class="warning ? 'warning' : ''"
:autocomplete="autocomplete"
:title-text="label"
Expand Down Expand Up @@ -359,7 +362,9 @@ const safeHelperText = computed(() => props.tip || " ");
<cds-inline-loading />
</template>
<template v-else>
{{ item.name }}
<slot :value="item.value">
{{ item.name }}
</slot>
</template>
</cds-combo-box-item>
</cds-combo-box>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/dto/CommonTypesDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export interface CodeNameType {
name: string;
}

export interface CodeNameValue<T> extends CodeNameType {
value: T;
}

export interface BusinessSearchResult {
code: string;
name: string;
Expand Down
54 changes: 42 additions & 12 deletions frontend/src/pages/SearchPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import "@carbon/web-components/es/components/tag/index";
import { useFetchTo } from "@/composables/useFetch";
import { useEventBus } from "@vueuse/core";

import type { ClientSearchResult, CodeNameType } from "@/dto/CommonTypesDto";
import type { ClientSearchResult, CodeNameValue } from "@/dto/CommonTypesDto";
import { adminEmail, getObfuscatedEmailLink, toTitleCase } from "@/services/ForestClientService";
import summit from "@carbon/pictograms/es/summit";
import userSearch from "@carbon/pictograms/es/user--search";
import useSvg from "@/composables/useSvg";

// @ts-ignore
Expand All @@ -30,6 +31,7 @@ import {
const revalidateBus = useEventBus<string[] | undefined>("revalidate-bus");

const summitSvg = useSvg(summit);
const userSearchSvg = useSvg(userSearch);

const userhasAuthority = ["CLIENT_VIEWER", "CLIENT_EDITOR", "CLIENT_ADMIN", "CLIENT_SUSPEND"].some(authority => ForestClientUserSession.authorities.includes(authority));

Expand All @@ -42,6 +44,7 @@ const totalItems = ref(0);
const pageSize = ref(10);

const searchKeyword = ref("");
const lastSearchKeyword = ref("");

// empty is valid
const valid = ref(true);
Expand Down Expand Up @@ -75,6 +78,8 @@ const search = (skipResetPage = false) => {
if (!skipResetPage) {
pageNumber.value = 1;
}

lastSearchKeyword.value = searchKeyword.value;
fetchSearch();
};

Expand Down Expand Up @@ -116,18 +121,29 @@ const paginate = (event: any) => {
};

/**
* Converts a client search result to a code/name representation.
* Converts a client search result to a code/name/value representation.
* @param searchResult The client search result
*/
const searchResultToCodeName = (searchResult: ClientSearchResult): CodeNameType => {
const { clientNumber, clientFullName, clientType, city, clientStatus } = searchResult;
const searchResultToCodeNameValue = (
searchResult: ClientSearchResult,
): CodeNameValue<ClientSearchResult> => {
const { clientNumber, clientFullName } = searchResult;
const result = {
code: clientNumber,
name: `${toTitleCase(`${clientNumber}, ${clientFullName}, ${clientType}, ${city}`)} (${clientStatus})`,
name: clientFullName,
value: searchResult,
};
return result;
};

const searchResultToCodeNameValueList = (list: ClientSearchResult[]) => list.map(searchResultToCodeNameValue);

const searchResultToText = (searchResult: ClientSearchResult): string => {
const { clientNumber, clientFullName, clientType, city } = searchResult;
const result = toTitleCase(`${clientNumber}, ${clientFullName}, ${clientType}, ${city}`);
return result;
};

const openClientDetails = (clientCode: string) => {
if (clientCode) {
const url = `https://${greenDomain}/int/client/client02MaintenanceAction.do?bean.clientNumber=${clientCode}`;
Expand Down Expand Up @@ -160,7 +176,6 @@ onMounted(() => {

<template>
<div id="screen" class="table-list">

<div id="title" v-if="userhasAuthority">
<div>
<div class="form-header-title mg-sd-25">
Expand Down Expand Up @@ -202,8 +217,9 @@ onMounted(() => {
autocomplete="off"
tip=""
placeholder="Search by client number, name or acronym"
size="lg"
v-model="searchKeyword"
:contents="content?.map(searchResultToCodeName)"
:contents="searchResultToCodeNameValueList(content)"
:validations="validations"
:validations-on-change="validationsOnChange"
:loading="loading"
Expand All @@ -212,7 +228,15 @@ onMounted(() => {
@update:model-value="valid = false"
@error="valid = !$event"
@press:enter="search()"
/>
#="{ value }"
>
<div class="search-result-item" v-if="value">
{{ searchResultToText(value) }}
<cds-tag :type="tagColor(value.clientStatus)" title="">
<span>{{ value.clientStatus }}</span>
</cds-tag>
</div>
</AutoCompleteInputComponent>
</data-fetcher>
<cds-button kind="primary" @click.prevent="search()" id="search-button">
<span>Search</span>
Expand Down Expand Up @@ -265,16 +289,22 @@ onMounted(() => {
/>
</div>

<div
class="empty-table-list"
v-if="(!tableData || totalItems == 0) && userhasAuthority && !loadingSearch"
>
<div class="empty-table-list" v-if="!tableData && userhasAuthority && !loadingSearch">
<summit-svg alt="Summit pictogram" class="standard-svg" />
<p class="heading-02">Nothing to show yet!</p>
<p class="body-compact-01">Enter at least one criteria to start the search.</p>
<p class="body-compact-01">The list will display here.</p>
</div>

<div
class="empty-table-list"
v-if="tableData && totalItems == 0 && userhasAuthority && !loadingSearch"
>
<user-search-svg alt="User search pictogram" class="standard-svg" />
<p class="heading-02">No results for “{{ lastSearchKeyword }}”</p>
<p class="body-compact-01">Consider adjusting your search term(s) and try again.</p>
</div>

<div class="paginator" v-if="totalItems && userhasAuthority">
<cds-pagination
items-per-page-text="Clients per page"
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/services/ForestClientService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,18 @@ export const getContactDescription = (contact: Contact, index: number): string =

export const toTitleCase = (inputString: string): string => {
if (inputString === undefined) return "";
return inputString
.toLowerCase()
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");

const splitMapJoin = (currentString: string, separator: string) =>
currentString
.split(separator)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(separator);

let result = inputString.toLowerCase();
result = splitMapJoin(result, " ");
result = splitMapJoin(result, "(");
result = splitMapJoin(result, ".");
return result;
};

export const toSentenceCase = (inputString: string): string => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/stub/__files/response-client-search-page0.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
{
"clientNumber": "00191081",
"clientAcronym": null,
"clientFullName": "DFGDGDFG",
"clientFullName": "D.F.G. DGDFG",
"clientType": "Ministry of Forests and Range",
"city": "CALGARY",
"clientStatus": "Active",
Expand All @@ -74,7 +74,7 @@
{
"clientNumber": "00191080",
"clientAcronym": null,
"clientFullName": "EDMOND KAAUA",
"clientFullName": "EDMOND KAAUA (EDMOND'S PAINTING)",
"clientType": "Individual",
"city": "VICTORIA",
"clientStatus": "Active",
Expand Down
Loading
Loading