@vtex/faststore-plugin-buyer-portal 1.0.42 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/src/features/addresses/clients/AddressesClient.ts +10 -0
  3. package/src/features/addresses/components/AddressLine/AddressLine.tsx +1 -1
  4. package/src/features/addresses/components/AddressLine/address-line.scss +0 -8
  5. package/src/features/addresses/hooks/useDebouncedFilterAddress.ts +27 -0
  6. package/src/features/addresses/hooks/useDebouncedSearchAddressByUnitId.ts +26 -0
  7. package/src/features/addresses/hooks/useSearchAddressByUnitId.ts +20 -0
  8. package/src/features/addresses/layouts/AddressesLayout/AddressesLayout.tsx +101 -71
  9. package/src/features/addresses/layouts/AddressesLayout/addresses-layout.scss +5 -2
  10. package/src/features/addresses/services/get-addresses-by-unit-id.service.ts +47 -0
  11. package/src/features/addresses/services/index.ts +8 -0
  12. package/src/features/addresses/services/search-address-by-unit-id.service.ts +30 -0
  13. package/src/features/buying-policies/layouts/BuyingPoliciesLayout/BuyingPoliciesLayout.tsx +22 -19
  14. package/src/features/buying-policies/layouts/BuyingPoliciesLayout/buying-policies-layout.scss +1 -1
  15. package/src/features/shared/components/EmptyState/EmptyState.tsx +28 -0
  16. package/src/features/shared/components/EmptyState/empty-state.scss +19 -0
  17. package/src/features/shared/components/InternalSearch/InternalSearch.tsx +49 -9
  18. package/src/features/shared/components/InternalSearch/internal-search.scss +29 -2
  19. package/src/features/shared/components/SearchHighlight/SearchHighlight.tsx +25 -0
  20. package/src/features/shared/components/SearchHighlight/search-highlight.scss +5 -0
  21. package/src/features/shared/components/Table/Table.tsx +17 -0
  22. package/src/features/shared/components/Table/TableBody/TableBody.tsx +3 -0
  23. package/src/features/shared/components/Table/TableHead/TableHead.tsx +36 -0
  24. package/src/features/shared/components/Table/TableHead/table-head.scss +41 -0
  25. package/src/features/shared/components/Table/TableLoading/TableLoading.tsx +15 -0
  26. package/src/features/shared/components/Table/TableLoading/table-loading.scss +11 -0
  27. package/src/features/shared/components/Table/TableRow/TableRow.tsx +71 -0
  28. package/src/features/shared/components/Table/TableRow/table-row.scss +64 -0
  29. package/src/features/shared/components/Table/table.scss +13 -0
  30. package/src/features/shared/components/Table/utils/tableColumns.ts +40 -0
  31. package/src/features/shared/components/index.ts +1 -1
  32. package/src/features/shared/utils/search.tsx +0 -12
  33. package/src/pages/addresses.tsx +13 -13
  34. package/src/features/shared/components/ListLine/ListLine.tsx +0 -52
  35. package/src/features/shared/components/ListLine/list-line.scss +0 -49
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/faststore-plugin-buyer-portal",
3
- "version": "1.0.42",
3
+ "version": "1.0.43",
4
4
  "description": "A plugin for faststore with buyer portal",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -19,6 +19,16 @@ export default class AddressesClient extends Client {
19
19
  });
20
20
  }
21
21
 
22
+
23
+ getAddressesByUnitId(orgUnitId: string, cookie: string, name?: string) {
24
+ const url = `unit/addresses/${orgUnitId}` + (name ? `?name=${name}` : "");
25
+ return this.get<AddressList>(url, {
26
+ headers: {
27
+ Cookie: cookie,
28
+ },
29
+ });
30
+ }
31
+
22
32
  getAddressById(addressId: string, cookie: string) {
23
33
  return this.get<AddressData>(`address/${addressId}`, {
24
34
  headers: {
@@ -1,4 +1,4 @@
1
- import { Dropdown } from "@faststore/ui";
1
+ import { Toggle, Dropdown, DropdownButton } from "@faststore/ui";
2
2
  import Link from "next/link";
3
3
  import { BasicDropdownMenu, Icon, Tag } from "../../../shared/components";
4
4
  import type { AddressData } from "../../types";
@@ -1,15 +1,8 @@
1
1
  [data-fs-addresses-line] {
2
2
  width: 100%;
3
- display: flex;
4
- align-items: center;
5
- justify-content: space-between;
6
-
7
3
  padding: var(--fs-spacing-2) 0;
8
4
  border-top: var(--fs-border-width) solid #e5e5e5;
9
5
 
10
- display: flex;
11
- align-items: center;
12
-
13
6
  &:hover {
14
7
  background-color: #f5f5f5;
15
8
  }
@@ -32,7 +25,6 @@
32
25
  }
33
26
 
34
27
  [data-fs-addresses-line-name] {
35
- width: 25%;
36
28
  margin-right: var(--fs-spacing-3);
37
29
  font-weight: var(--fs-text-weight-medium);
38
30
  font-size: var(--fs-text-size-1);
@@ -0,0 +1,27 @@
1
+ import { useMemo } from "react";
2
+ import { useDebounce } from "../../shared/hooks";
3
+ import { AddressData } from "../types";
4
+
5
+ export const useDebouncedFilterAddress = (
6
+ searchTerm = "",
7
+ addressData: AddressData[]
8
+ ) => {
9
+ const debouncedSearchTerm = useDebounce(searchTerm, 200);
10
+
11
+ const filteredAddresses = useMemo(() => {
12
+ if (debouncedSearchTerm === "") {
13
+ return addressData;
14
+ }
15
+
16
+ return addressData.filter((address) => {
17
+ const addressName = address.name.toLowerCase() || "";
18
+
19
+ return addressName
20
+ .includes(debouncedSearchTerm.toLowerCase());
21
+ });
22
+ }, [debouncedSearchTerm, addressData]);
23
+
24
+ return {
25
+ filteredAddresses,
26
+ };
27
+ };
@@ -0,0 +1,26 @@
1
+ import { useDebounce } from "../../shared/hooks";
2
+ import { AddressData } from "../types";
3
+ import { useSearchAddressByUnitId } from "./useSearchAddressByUnitId";
4
+
5
+ export const useDebouncedSearchAddressByUnitId = (
6
+ searchTerm = "",
7
+ unitId: string
8
+ ) => {
9
+ const debouncedSearchTerm = useDebounce(searchTerm, 500);
10
+ const { searchedAddresses, isLoading } = useSearchAddressByUnitId(
11
+ debouncedSearchTerm,
12
+ unitId
13
+ );
14
+
15
+ if (searchTerm === "") {
16
+ return {
17
+ searchedAddresses: [] as AddressData[],
18
+ isLoading: false,
19
+ };
20
+ }
21
+
22
+ return {
23
+ searchedAddresses: searchedAddresses?.addresses ?? [],
24
+ isLoading,
25
+ };
26
+ };
@@ -0,0 +1,20 @@
1
+ import { type QueryOptions, useQuery } from "../../shared/hooks";
2
+ import { searchAddressByUnitIdService } from "../services";
3
+
4
+ export const useSearchAddressByUnitId = (
5
+ name: string,
6
+ unitId: string,
7
+ options?: QueryOptions<AwaitedType<typeof searchAddressByUnitIdService>>
8
+ ) => {
9
+ const { data, error, isLoading, refetch } = useQuery(
10
+ `api/search/addresses/${name}`,
11
+ ({ cookie }) => searchAddressByUnitIdService({ name, unitId }, cookie),
12
+ options
13
+ );
14
+ return {
15
+ searchedAddresses: data,
16
+ hasError: error,
17
+ isLoading,
18
+ refetchSearchedAddresses: refetch,
19
+ };
20
+ };
@@ -1,45 +1,63 @@
1
- import {
2
- InternalTopBar,
3
- MainLinksDropdownMenu,
4
- InternalSearch,
5
- DropdownFilter,
6
- SortFilter,
7
- Icon,
8
- HeaderInside,
9
- } from "../../../shared/components";
1
+ import { use, useEffect, useState } from "react";
2
+ import { InternalSearch, HeaderInside } from "../../../shared/components";
10
3
  import {
11
4
  useQueryParams,
12
5
  useDrawerProps,
13
6
  useBuyerPortal,
14
7
  } from "../../../shared/hooks";
15
8
  import { ContractTabsLayout, GlobalLayout } from "../../../shared/layouts";
16
- import { statusFilters } from "../../../shared/utils";
17
9
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
18
- import { AddressLine, CreateAddressDrawer } from "../../components";
10
+ import { AddressDropdownMenu, CreateAddressDrawer } from "../../components";
19
11
  import type { AddressData } from "../../types";
12
+ import { useDebouncedSearchAddressByUnitId } from "../../hooks/useDebouncedSearchAddressByUnitId";
13
+ import { Table } from "../../../shared/components/Table/Table";
14
+ import { EmptyState } from "../../../shared/components/EmptyState/EmptyState";
15
+ import { getTableColumns } from "../../../shared/components/Table/utils/tableColumns";
20
16
 
21
17
  export type AddressLayoutProps = {
22
18
  data: AddressData[];
19
+ search?: string;
23
20
  };
24
21
 
25
- export const AddressLayout = ({ data }: AddressLayoutProps) => {
22
+ export const AddressLayout = ({ data, search }: AddressLayoutProps) => {
23
+ const [addressesData, setAddressesData] = useState<AddressData[]>(data);
24
+ const [querySearch, setQuerySearch] = useState(search ?? "");
26
25
  const { setQueryString, removeQueryString } = useQueryParams();
26
+
27
27
  const {
28
28
  open: openCreateDrawer,
29
29
  isOpen: isCreateAddressDrawerOpen,
30
30
  ...createDrawerProps
31
31
  } = useDrawerProps();
32
32
 
33
- const types = data
34
- .flatMap((address) => address.types)
35
- .filter((tag, index, self) => self.indexOf(tag) === index);
36
-
37
33
  const {
38
34
  currentOrgUnit: orgUnit,
39
35
  currentUser: user,
40
36
  currentContract: contract,
41
37
  } = useBuyerPortal();
42
38
 
39
+ const { searchedAddresses, isLoading } = useDebouncedSearchAddressByUnitId(
40
+ querySearch,
41
+ orgUnit?.id ?? ""
42
+ );
43
+
44
+ useEffect(() => {
45
+ if (!isLoading && querySearch.length > 0) {
46
+ setAddressesData(searchedAddresses);
47
+ }
48
+ }, [searchedAddresses, querySearch, isLoading]);
49
+
50
+ const handleSearch = (searchTerm: string) => {
51
+ setQuerySearch(searchTerm);
52
+
53
+ if (searchTerm) {
54
+ setQueryString("search", searchTerm);
55
+ } else {
56
+ removeQueryString("search");
57
+ setAddressesData(data);
58
+ }
59
+ };
60
+
43
61
  return (
44
62
  <GlobalLayout>
45
63
  <ContractTabsLayout
@@ -58,63 +76,75 @@ export const AddressLayout = ({ data }: AddressLayoutProps) => {
58
76
  <HeaderInside title="Address">
59
77
  <HeaderInside.Button onClick={openCreateDrawer} />
60
78
  </HeaderInside>
61
- <div data-fs-buyer-portal-address-filter>
62
- <div data-fs-buyer-portal-address-filter-search-container>
63
- <InternalSearch
64
- textSearch={(searchTerm) => {
65
- searchTerm
66
- ? setQueryString("search", searchTerm)
67
- : removeQueryString("search");
68
- }}
69
- />
70
-
71
- <DropdownFilter
72
- label="Type"
73
- filterOptions={types}
74
- updateFilter={(inputFilter) => {
75
- setQueryString("type", inputFilter);
76
- }}
77
- clearFilter={() => {
78
- removeQueryString("type");
79
- }}
80
- />
81
-
82
- <DropdownFilter
83
- label="Status"
84
- filterOptions={Object.keys(statusFilters)}
85
- updateFilter={(inputFilter) => {
86
- setQueryString("status", inputFilter);
87
- }}
88
- clearFilter={() => {
89
- removeQueryString("status");
90
- }}
91
- />
92
- </div>
93
79
 
94
- <SortFilter updateSort={(sort) => setQueryString("sort", sort)} />
95
- </div>
80
+ {data.length === 0 ? (
81
+ <EmptyState title="No addresses yet" iconName="LocalPostOffice" />
82
+ ) : (
83
+ <>
84
+ <div data-fs-buyer-portal-address-filter>
85
+ <InternalSearch defaultValue={querySearch} textSearch={handleSearch} />
86
+ </div>
87
+ {!isLoading &&
88
+ addressesData.length === 0 &&
89
+ querySearch.length > 0 ? (
90
+ <EmptyState
91
+ title="No results found"
92
+ description="Try using different terms or filters"
93
+ />
94
+ ) : (
95
+ <div data-fs-addresses-table>
96
+ <Table>
97
+ <Table.Head columns={getTableColumns({ withType: true })} />
98
+ {isLoading ? (
99
+ <Table.Body>
100
+ <Table.Loading />
101
+ </Table.Body>
102
+ ) : (
103
+ <Table.Body>
104
+ {addressesData.map(({ id, ...address }) => (
105
+ <Table.Row
106
+ key={id}
107
+ title={address.name}
108
+ searchTerm={querySearch}
109
+ iconName="LocalPostOffice"
110
+ iconSize={20}
111
+ children={
112
+ Array.isArray(address.types) &&
113
+ address.types.length > 0
114
+ ? address.types.map((type) => (
115
+ <span key={type}>{type}</span>
116
+ ))
117
+ : null
118
+ }
119
+ href={buyerPortalRoutes.addressDetails({
120
+ orgUnitId: orgUnit?.id ?? "",
121
+ contractId: contract?.id ?? "",
122
+ addressId: id,
123
+ })}
124
+ dropdownMenu={
125
+ <AddressDropdownMenu
126
+ currentAddress={{
127
+ id,
128
+ ...address,
129
+ }}
130
+ />
131
+ }
132
+ />
133
+ ))}
134
+ </Table.Body>
135
+ )}
136
+ </Table>
96
137
 
97
- <ul data-fs-addresses-list>
98
- {data.map(({ id, ...address }) => (
99
- <AddressLine
100
- currentAddress={{ id, ...address }}
101
- href={buyerPortalRoutes.addressDetails({
102
- orgUnitId: orgUnit?.id ?? "",
103
- contractId: contract?.id ?? "",
104
- addressId: id,
105
- })}
106
- id={`${id}`}
107
- key={id}
108
- {...address}
109
- />
110
- ))}
111
- </ul>
112
- {isCreateAddressDrawerOpen && (
113
- <CreateAddressDrawer
114
- readonly
115
- isOpen={isCreateAddressDrawerOpen}
116
- {...createDrawerProps}
117
- />
138
+ {isCreateAddressDrawerOpen && (
139
+ <CreateAddressDrawer
140
+ readonly
141
+ isOpen={isCreateAddressDrawerOpen}
142
+ {...createDrawerProps}
143
+ />
144
+ )}
145
+ </div>
146
+ )}
147
+ </>
118
148
  )}
119
149
  </section>
120
150
  </ContractTabsLayout>
@@ -4,16 +4,19 @@
4
4
 
5
5
  [data-fs-addresses-section] {
6
6
  @import "@faststore/ui/src/components/molecules/Toggle/styles.scss";
7
-
8
7
  @import "../../components/AddressLine/address-line.scss";
9
-
10
8
  @import "../../../shared/components/InternalSearch/internal-search.scss";
11
9
  @import "../../../shared/components/DropdownFilter/dropdown-filter.scss";
12
10
  @import "../../../shared/components/Tag/tag.scss";
13
11
  @import "../../../shared/components/SortFilter/sort-filter.scss";
14
12
  @import "../../../shared/components/HeaderInside/header-inside.scss";
13
+ @import "../../../shared/components/Table/table.scss";
14
+ @import "../../../shared/components/EmptyState/empty-state.scss";
15
15
 
16
16
  padding: 0 calc(var(--fs-spacing-9) - var(--fs-spacing-0));
17
+ display: flex;
18
+ flex-direction: column;
19
+ height: 100%;
17
20
 
18
21
  [data-fs-topbar-actions-wrapper] {
19
22
  display: flex;
@@ -0,0 +1,47 @@
1
+ import type { AddressData } from "../types/AddressData";
2
+ import { compareItems, statusFilters } from "../../shared/utils";
3
+ import { addressesClient } from "../clients/AddressesClient";
4
+
5
+ export type GetAddressesByUnitIdServiceProps = Partial<{
6
+ search: string;
7
+ orgUnitId: string;
8
+ status: string;
9
+ type: string;
10
+ sort: string;
11
+ }> & {
12
+ cookie: string;
13
+ };
14
+
15
+ export const getAddressesByUnitIdService = async ({
16
+ orgUnitId,
17
+ status,
18
+ type,
19
+ sort,
20
+ cookie,
21
+ }: GetAddressesByUnitIdServiceProps): Promise<AddressData[]> => {
22
+ if (!orgUnitId) {
23
+ return [];
24
+ }
25
+
26
+ const addressesData: AddressData[] = [];
27
+
28
+ try {
29
+ const { addresses = [] } = await addressesClient.getAddressesByUnitId(
30
+ orgUnitId,
31
+ cookie
32
+ );
33
+ addressesData.push(...addresses);
34
+ } catch (err) {
35
+ throw new Error(JSON.stringify(err));
36
+ }
37
+
38
+ return addressesData
39
+ ?.filter((address) => {
40
+ const matchesStatus =
41
+ !status || address.isActive === statusFilters[status];
42
+ const matchesType = !type || address.types.includes(type);
43
+
44
+ return matchesStatus && matchesType;
45
+ })
46
+ .sort((a, b) => compareItems(a, b, sort));
47
+ };
@@ -3,6 +3,10 @@ export {
3
3
  getAddressesService,
4
4
  type GetAddressesServiceProps,
5
5
  } from "./get-addresses.service";
6
+ export {
7
+ getAddressesByUnitIdService,
8
+ type GetAddressesByUnitIdServiceProps,
9
+ } from "./get-addresses-by-unit-id.service";
6
10
  export {
7
11
  createNewAddressService,
8
12
  type CreateNewAddressServiceProps,
@@ -15,6 +19,10 @@ export {
15
19
  searchAddressByNameService,
16
20
  type SearchAddressByNameProps,
17
21
  } from "./search-address-by-name.service";
22
+ export {
23
+ searchAddressByUnitIdService,
24
+ type SearchAddressByUnitIdProps,
25
+ } from "./search-address-by-unit-id.service";
18
26
  export {
19
27
  editAddressService,
20
28
  type EditAddressServiceProps,
@@ -0,0 +1,30 @@
1
+ import { addressesClient } from "../clients/AddressesClient";
2
+ import type { AddressData } from "../types";
3
+
4
+ export type SearchAddressByUnitIdProps = {
5
+ name: string;
6
+ unitId: string;
7
+ };
8
+
9
+ export const searchAddressByUnitIdService = async (
10
+ { name, unitId }: SearchAddressByUnitIdProps,
11
+ cookie: string
12
+ ): Promise<{ addresses: AddressData[]; total: number }> => {
13
+ if (!name || !unitId) {
14
+ return {
15
+ addresses: [],
16
+ total: 0,
17
+ };
18
+ }
19
+
20
+ const { addresses } = await addressesClient.getAddressesByUnitId(
21
+ unitId,
22
+ cookie,
23
+ name
24
+ );
25
+
26
+ return {
27
+ addresses: addresses,
28
+ total: addresses?.length ?? 0,
29
+ };
30
+ };
@@ -1,13 +1,11 @@
1
1
  import { FinanceTabsLayout, GlobalLayout } from "../../../shared/layouts";
2
- import {
3
- HeaderInside,
4
- InternalSearch,
5
- ListLine,
6
- } from "../../../shared/components";
2
+ import { HeaderInside, InternalSearch } from "../../../shared/components";
7
3
  import { useBuyerPortal, useQueryParams } from "../../../shared/hooks";
8
4
  import type { BuyingPolicy } from "../../types";
9
5
  import { buyerPortalRoutes } from "../../../shared/utils/buyerPortalRoutes";
10
6
  import { BuyingPolicyDropdownMenu } from "../../components";
7
+ import { Table } from "../../../shared/components/Table/Table";
8
+ import { getTableColumns } from "../../../shared/components/Table/utils/tableColumns";
11
9
 
12
10
  export type BuyingPoliciesLayoutProps = {
13
11
  data: { buyingPolicies: BuyingPolicy[] } | null;
@@ -41,20 +39,25 @@ export const BuyingPoliciesLayout = ({
41
39
  />
42
40
  </div>
43
41
 
44
- <span data-fs-buying-policies-heading>Name</span>
45
- {data?.buyingPolicies.map((buyingPolicy) => (
46
- <ListLine
47
- key={buyingPolicy.id}
48
- title={buyingPolicy.name}
49
- iconName="Rebase"
50
- href={buyerPortalRoutes.buyingPolicyDetails({
51
- contractId: currentContract?.id ?? "",
52
- orgUnitId: currentOrgUnit?.id ?? "",
53
- buyingPolicyId: buyingPolicy.id,
54
- })}
55
- dropdownMenu={<BuyingPolicyDropdownMenu />}
56
- />
57
- ))}
42
+ <Table>
43
+ <Table.Head columns={getTableColumns({ withType: false })} />
44
+ <Table.Body>
45
+ {data?.buyingPolicies.map((buyingPolicy) => (
46
+ <Table.Row
47
+ key={buyingPolicy.id}
48
+ title={buyingPolicy.name}
49
+ iconName="Rebase"
50
+ iconSize={20}
51
+ href={buyerPortalRoutes.buyingPolicyDetails({
52
+ contractId: currentContract?.id ?? "",
53
+ orgUnitId: currentOrgUnit?.id ?? "",
54
+ buyingPolicyId: buyingPolicy.id,
55
+ })}
56
+ dropdownMenu={<BuyingPolicyDropdownMenu />}
57
+ />
58
+ ))}
59
+ </Table.Body>
60
+ </Table>
58
61
  </section>
59
62
  </FinanceTabsLayout>
60
63
  </GlobalLayout>
@@ -5,7 +5,7 @@
5
5
  [data-fs-buying-policies-section] {
6
6
  @import "../../../shared/components/HeaderInside/header-inside.scss";
7
7
  @import "../../../shared/components/InternalSearch/internal-search.scss";
8
- @import "../../../shared/components/ListLine/list-line.scss";
8
+ @import "../../../shared/components/Table/table.scss";
9
9
 
10
10
  [data-fs-buying-policies-filter] {
11
11
  display: flex;
@@ -0,0 +1,28 @@
1
+ import { Icon } from "../Icon";
2
+
3
+ type EmptyStateProps = {
4
+ title: string;
5
+ description?: string;
6
+ iconName?: string;
7
+ iconSize?: number;
8
+ };
9
+
10
+ export const EmptyState = ({
11
+ title,
12
+ iconSize = 40,
13
+ iconName,
14
+ description,
15
+ }: EmptyStateProps) => (
16
+ <section data-fs-empty-state-section>
17
+ {iconName ? (
18
+ <Icon
19
+ data-fs-bp-line-icon
20
+ name={iconName}
21
+ width={iconSize}
22
+ height={iconSize}
23
+ />
24
+ ) : null}
25
+ <h2 data-fs-empty-state-title>{title}</h2>
26
+ {description ? <p data-fs-empty-state-description>{description}</p> : null}
27
+ </section>
28
+ );
@@ -0,0 +1,19 @@
1
+ [data-fs-empty-state-section] {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ justify-content: center;
6
+ flex: 1;
7
+ gap: var(--fs-spacing-1);
8
+ color: #5c5c5c;
9
+
10
+ [data-fs-empty-state-title] {
11
+ font-size: var(--fs-text-size-3);
12
+ font-weight: var(--fs-text-weight-semibold);
13
+ line-height: calc(var(--fs-text-size-4) + var(--fs-scale));
14
+ }
15
+
16
+ [data-fs-empty-state-description] {
17
+ font-size: var(--fs-text-size-1);
18
+ }
19
+ }
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import { useState } from "react";
2
2
  import { Icon as UIIcon } from "@faststore/ui";
3
3
 
4
4
  type InternalSearchProps = {
@@ -7,20 +7,60 @@ type InternalSearchProps = {
7
7
  };
8
8
 
9
9
  const InternalSearch = ({ textSearch, defaultValue }: InternalSearchProps) => {
10
+ const [searchTerm, setSearchTerm] = useState(defaultValue || "");
11
+ const [isFocused, setIsFocused] = useState(false);
12
+
13
+ const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
14
+ const value = event.target.value;
15
+ setSearchTerm(value);
16
+ textSearch(value);
17
+ };
18
+
10
19
  return (
11
- <div data-fs-buyer-portal-internal-search>
12
- <UIIcon
13
- name="MagnifyingGlass"
14
- height={20}
15
- width={20}
16
- data-fs-buyer-portal-search-icon
17
- />
20
+ <div
21
+ data-fs-buyer-portal-internal-search
22
+ data-fs-search-input-focused={isFocused}
23
+ >
18
24
  <input
19
25
  data-fs-buyer-portal-internal-search-input
26
+ value={searchTerm}
20
27
  placeholder="Search"
21
28
  defaultValue={defaultValue}
22
- onChange={(event) => textSearch(event.target.value)}
29
+ onChange={handleInputChange}
30
+ onFocus={() => setIsFocused(true)}
31
+ onBlur={() => setIsFocused(false)}
23
32
  />
33
+
34
+ <div data-fs-buyer-portal-search-icon>
35
+ {searchTerm.length > 0 ? (
36
+ <>
37
+ <div data-fs-icon-button>
38
+ <UIIcon
39
+ name="X"
40
+ width={20}
41
+ height={20}
42
+ onClick={() => {
43
+ setSearchTerm("");
44
+ textSearch("");
45
+ }}
46
+ />
47
+ </div>
48
+ <div data-fs-divider />
49
+ </>
50
+ ) : null}
51
+
52
+ <div data-fs-icon-button>
53
+ <UIIcon
54
+ name="MagnifyingGlass"
55
+ width={20}
56
+ height={20}
57
+ onClick={() => {
58
+ setSearchTerm("");
59
+ textSearch("");
60
+ }}
61
+ />
62
+ </div>
63
+ </div>
24
64
  </div>
25
65
  );
26
66
  };
@@ -6,14 +6,41 @@
6
6
  height: var(--fs-spacing-6);
7
7
  gap: var(--fs-spacing-4);
8
8
  border-radius: var(--fs-border-radius-pill);
9
- margin-right: var(--fs-spacing-0);
9
+ padding-left: var(--fs-spacing-3);
10
+ padding-right: var(--fs-spacing-0);
11
+ padding-top: var(--fs-spacing-0);
12
+ padding-bottom: var(--fs-spacing-0);
13
+ position: relative;
14
+
15
+
16
+ &[data-fs-search-input-focused="true"] {
17
+ border: var(--fs-border-width) solid #0366DD;
18
+ }
19
+
20
+ [data-fs-icon-button] {
21
+ display: flex;
22
+ padding: var(--fs-spacing-0);
23
+ align-items: center;
24
+ cursor: pointer;
25
+ }
26
+
27
+ [data-fs-divider] {
28
+ height: 24px;
29
+ width: 1px;
30
+ border: calc(var(--fs-border-width) / 2) solid #e0e0e0;
31
+ }
10
32
 
11
33
  [data-fs-buyer-portal-internal-search-input] {
34
+ width: 100%;
12
35
  border: none;
36
+ size: var(--fs-text-size-1);
13
37
  outline: none;
14
38
  }
15
39
 
16
40
  [data-fs-buyer-portal-search-icon] {
17
- margin-left: var(--fs-spacing-2);
41
+ display: flex;
42
+ gap: var(--fs-spacing-0);
43
+ display: flex;
44
+ align-items: center;
18
45
  }
19
46
  }
@@ -0,0 +1,25 @@
1
+ type SearchHighlightProps = {
2
+ text: string;
3
+ highlight: string;
4
+ };
5
+
6
+ export const SearchHighlight: React.FC<SearchHighlightProps> = ({ text, highlight }) => {
7
+ if (!highlight) return <>{text}</>;
8
+
9
+ const regex = new RegExp(`(${highlight})`, 'gi');
10
+ const parts = text.split(regex);
11
+
12
+ return (
13
+ <>
14
+ {parts.map((part, i) =>
15
+ regex.test(part) ? (
16
+ <span key={i} data-fs-search-highlight>
17
+ {part}
18
+ </span>
19
+ ) : (
20
+ <span key={i}>{part}</span>
21
+ )
22
+ )}
23
+ </>
24
+ );
25
+ };
@@ -0,0 +1,5 @@
1
+ [data-fs-search-highlight] {
2
+ background-color: #ffedcd;
3
+ border-radius: var(--fs-spacing-0);
4
+ padding: var(--fs-spacing-0) 0;
5
+ }
@@ -0,0 +1,17 @@
1
+ import { TableRow } from "./TableRow/TableRow";
2
+ import { TableBody } from "./TableBody/TableBody";
3
+ import TableHead from "./TableHead/TableHead";
4
+ import { TableLoading } from "./TableLoading/TableLoading";
5
+
6
+ export type TableProps = {
7
+ children: React.ReactNode;
8
+ };
9
+
10
+ export const Table = ({ children }: TableProps) => {
11
+ return <table data-fs-bp-table>{children}</table>;
12
+ };
13
+
14
+ Table.Head = TableHead;
15
+ Table.Body = TableBody;
16
+ Table.Row = TableRow;
17
+ Table.Loading = TableLoading;
@@ -0,0 +1,3 @@
1
+ export const TableBody = ({ children }: { children: React.ReactNode }) => {
2
+ return <tbody data-fs-bp-table-body>{children}</tbody>;
3
+ };
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+
3
+ export type TableColumn<T extends string = string> = {
4
+ key: T;
5
+ label: React.ReactNode;
6
+ align?: "left" | "center" | "right";
7
+ size?: "small" | "medium" | "large";
8
+ colspan?: number;
9
+ };
10
+
11
+ export interface TableHeadProps<T extends string | number = string> {
12
+ columns: Array<TableColumn>;
13
+ rowProps?: React.HTMLAttributes<HTMLTableRowElement>;
14
+ }
15
+
16
+ export const TableHead = ({ columns, rowProps }: TableHeadProps) => {
17
+ return (
18
+ <thead data-fs-bp-table-head>
19
+ <tr {...rowProps} data-fs-bp-table-head-row>
20
+ {columns.map((col) => (
21
+ <th
22
+ key={col.key}
23
+ data-fs-bp-table-head-column
24
+ data-fs-bp-table-head-column-size={col.size}
25
+ data-fs-bp-table-head-align={col.align || undefined}
26
+ colSpan={col.colspan}
27
+ >
28
+ {col.label}
29
+ </th>
30
+ ))}
31
+ </tr>
32
+ </thead>
33
+ );
34
+ };
35
+
36
+ export default TableHead;
@@ -0,0 +1,41 @@
1
+ [data-fs-bp-table-head] {
2
+ &[data-fs-bp-table-cell-align="left"] {
3
+ justify-content: flex-start;
4
+ }
5
+ &[data-fs-bp-table-cell-align="right"] {
6
+ justify-content: flex-end;
7
+ }
8
+
9
+ [data-fs-bp-table-head-column] {
10
+ color: #5c5c5c;
11
+ font-size: var(--fs-text-size-1);
12
+ font-weight: var(--fs-text-weight-regular);
13
+ padding-top: var(--fs-spacing-2);
14
+ padding-bottom: var(--fs-spacing-2);
15
+ padding-left: 0;
16
+ }
17
+
18
+ [data-fs-bp-table-head-align="left"] {
19
+ text-align: left;
20
+ }
21
+
22
+ [data-fs-bp-table-head-align="right"] {
23
+ text-align: right;
24
+ }
25
+
26
+ [data-fs-bp-table-head-align="center"] {
27
+ text-align: center;
28
+ }
29
+
30
+ [data-fs-bp-table-head-column-size="small"] {
31
+ width: 15%;
32
+ }
33
+
34
+ [data-fs-bp-table-head-column-size="medium"] {
35
+ width: 25%;
36
+ }
37
+
38
+ [data-fs-bp-table-head-column-size="large"] {
39
+ width: 40%;
40
+ }
41
+ }
@@ -0,0 +1,15 @@
1
+ import { Skeleton } from "@faststore/ui";
2
+
3
+ export const TableLoading = () => {
4
+ return (
5
+ <>
6
+ {Array.from({ length: 5 }).map((_, index) => (
7
+ <tr data-fs-bp-table-row-loading key={index}>
8
+ <td colSpan={4}>
9
+ <Skeleton size={{ width: "100%", height: "1.5rem" }} />
10
+ </td>
11
+ </tr>
12
+ ))}
13
+ </>
14
+ );
15
+ };
@@ -0,0 +1,11 @@
1
+ [data-fs-bp-table-row-loading] {
2
+ @import "@faststore/ui/src/components/atoms/Skeleton/styles.scss";
3
+ border-top: var(--fs-border-width) solid #e0e0e0;
4
+ padding: var(--fs-spacing-2) 0;
5
+
6
+ td {
7
+ padding: var(--fs-spacing-2) 0;
8
+ font-size: var(--fs-text-size-1);
9
+ color: #1f1f1f;
10
+ }
11
+ }
@@ -0,0 +1,71 @@
1
+ import type { ReactNode } from "react";
2
+ import { Icon } from "../../Icon";
3
+ import { BasicDropdownMenu } from "../../BasicDropdownMenu/BasicDropdownMenu";
4
+ import { Dropdown } from "@faststore/components";
5
+ import Link from "next/link";
6
+ import { SearchHighlight } from "../../SearchHighlight/SearchHighlight";
7
+
8
+ export type TableRowProps = {
9
+ children?: ReactNode;
10
+ title: string;
11
+ iconName?: string;
12
+ iconSize?: number;
13
+ dropdownMenu?: ReactNode;
14
+ href?: string;
15
+ searchTerm?: string;
16
+ onClick?: () => void;
17
+ };
18
+
19
+ export const TableRow = ({
20
+ iconName,
21
+ iconSize = 24,
22
+ title,
23
+ dropdownMenu,
24
+ children,
25
+ href,
26
+ searchTerm = "",
27
+ onClick,
28
+ ...otherProps
29
+ }: TableRowProps) => {
30
+ const ClickComponent = href ? Link : "button";
31
+
32
+ return (
33
+ <tr data-fs-bp-table-row {...otherProps}>
34
+ <td data-fs-bp-table-row-icon-cell>
35
+ {iconName && (
36
+ <Icon
37
+ data-fs-bp-table-row-icon
38
+ name={iconName}
39
+ width={iconSize}
40
+ height={iconSize}
41
+ />
42
+ )}
43
+ </td>
44
+
45
+ <td>
46
+ <span data-fs-bp-table-row-title>
47
+ <SearchHighlight text={title} highlight={searchTerm} />
48
+ </span>
49
+ </td>
50
+ <td>{children}</td>
51
+ <td data-fs-bp-table-row-dropdown>
52
+ {dropdownMenu && (
53
+ <Dropdown>
54
+ <BasicDropdownMenu.Trigger />
55
+ {dropdownMenu}
56
+ </Dropdown>
57
+ )}
58
+ </td>
59
+
60
+ <td data-fs-bp-table-row-actions>
61
+ {href && (
62
+ <ClickComponent
63
+ data-fs-bp-table-row-link
64
+ href={href}
65
+ onClick={onClick}
66
+ />
67
+ )}
68
+ </td>
69
+ </tr>
70
+ );
71
+ };
@@ -0,0 +1,64 @@
1
+ @import "../../BasicDropdownMenu/basic-dropdown-menu.scss";
2
+
3
+ [data-fs-bp-table-row] {
4
+ @import "../../SearchHighlight/search-highlight.scss";
5
+
6
+ border-top: var(--fs-border-width) solid #e0e0e0;
7
+ cursor: pointer;
8
+ position: relative;
9
+
10
+ &:hover {
11
+ background-color: #f5f5f5;
12
+ }
13
+
14
+ td {
15
+ padding: var(--fs-spacing-2) 0;
16
+ font-size: var(--fs-text-size-1);
17
+ color: #1f1f1f;
18
+ }
19
+
20
+ [data-fs-bp-table-row-link] {
21
+ position: absolute;
22
+ left: 0;
23
+ right: 0;
24
+ top: 0;
25
+ bottom: 0;
26
+ }
27
+
28
+ [data-fs-bp-table-row-icon-cell] {
29
+ width: var(--fs-spacing-7);
30
+
31
+ [data-fs-bp-table-row-icon] {
32
+ color: #0366dd;
33
+ margin: calc(var(--fs-spacing-2) - var(--fs-spacing-0));
34
+ margin-right: var(--fs-spacing-2);
35
+ }
36
+ }
37
+
38
+ [data-fs-bp-table-row-dropdown] {
39
+ position: relative;
40
+ cursor: pointer;
41
+
42
+ [data-fs-bp-basic-dropdown-menu-trigger] {
43
+ position: absolute;
44
+ right: 0;
45
+ bottom: 0;
46
+ top: 0;
47
+ margin: auto;
48
+ z-index: 1;
49
+
50
+ [data-fs-icon] {
51
+ margin-right: 0;
52
+ }
53
+ }
54
+ }
55
+
56
+ [data-fs-bp-table-row-title] {
57
+ font-weight: 500;
58
+ font-size: var(--fs-text-size-1);
59
+ line-height: calc(var(--fs-spacing-4) - var(--fs-spacing-0));
60
+ color: #1f1f1f;
61
+ z-index: 1;
62
+ pointer-events: none;
63
+ }
64
+ }
@@ -0,0 +1,13 @@
1
+ @import "./TableRow/table-row.scss";
2
+ @import "./TableHead/table-head.scss";
3
+ @import "./TableLoading/table-loading.scss";
4
+
5
+ [data-fs-bp-table] {
6
+ width: 100%;
7
+ border-collapse: collapse;
8
+
9
+ [data-fs-bp-icon] {
10
+ width: auto;
11
+ height: auto;
12
+ }
13
+ }
@@ -0,0 +1,40 @@
1
+ import { TableColumn } from "../TableHead/TableHead";
2
+
3
+ type GetColumnsOptions = {
4
+ withType?: boolean;
5
+ };
6
+
7
+ export const getTableColumns = <T extends string = string>(
8
+ options: GetColumnsOptions = {}
9
+ ): TableColumn<T>[] => {
10
+ const base: TableColumn<T>[] = [
11
+ {
12
+ key: "name" as T,
13
+ label: "Name",
14
+ align: "left",
15
+ colspan: 2,
16
+ },
17
+ ];
18
+
19
+ const typeColumn: TableColumn<T>[] = options.withType
20
+ ? [
21
+ {
22
+ key: "type" as T,
23
+ label: "Type",
24
+ align: "left",
25
+ size: "medium",
26
+ },
27
+ ]
28
+ : [];
29
+
30
+ const actions: TableColumn<T>[] = [
31
+ {
32
+ key: "actions" as T,
33
+ label: "",
34
+ align: "right",
35
+ size: "large",
36
+ },
37
+ ];
38
+
39
+ return [...base, ...typeColumn, ...actions];
40
+ };
@@ -66,4 +66,4 @@ export {
66
66
  HeaderInside,
67
67
  type HeaderInsideProps,
68
68
  } from "./HeaderInside/HeaderInside";
69
- export { ListLine, type ListLineProps } from "./ListLine/ListLine";
69
+ export { Table, type TableProps } from "./Table/Table";
@@ -12,8 +12,6 @@ export const userTypesFilters = ["Admin", "Buyer"];
12
12
  export const sortingOptions: Dictionary<string> = {
13
13
  "Alphabetical Order": "name",
14
14
  "Alphabetical Order Desc": "name",
15
- "Active First": "status",
16
- "Inactive First": "status",
17
15
  };
18
16
 
19
17
  export const updateSort = (arrayToSort: any[], selectedOption: string) => {
@@ -24,16 +22,6 @@ export const updateSort = (arrayToSort: any[], selectedOption: string) => {
24
22
  case "Alphabetical Order Desc": {
25
23
  return sortDescending(arrayToSort, sortingOptions[selectedOption]);
26
24
  }
27
- case "Active First": {
28
- return arrayToSort.sort((a, b) =>
29
- a.isActive === b.isActive ? 0 : a.isActive ? -1 : 1
30
- );
31
- }
32
- case "Inactive First": {
33
- return arrayToSort.sort((a, b) =>
34
- a.isActive === b.isActive ? 0 : a.isActive ? 1 : -1
35
- );
36
- }
37
25
  default:
38
26
  return arrayToSort;
39
27
  }
@@ -1,13 +1,10 @@
1
1
  import {
2
- getAddressesService,
2
+ getAddressesByUnitIdService,
3
+ searchAddressByUnitIdService,
3
4
  type GetAddressesServiceProps,
4
5
  } from "../features/addresses/services";
5
6
 
6
- import {
7
- type ClientContext,
8
- getClientContext,
9
- getCustomerIdFromCookieServerSide,
10
- } from "../features/shared/utils";
7
+ import { type ClientContext, getClientContext } from "../features/shared/utils";
11
8
  import type { AddressData } from "../features/addresses/types";
12
9
  import { AddressLayout } from "../features/addresses/layouts";
13
10
  import { BuyerPortalProvider } from "../features/shared/components";
@@ -21,6 +18,7 @@ import { getContractDetailsService } from "../features/contracts/services";
21
18
 
22
19
  export type AddressesPageData = {
23
20
  data: AddressData[];
21
+ search: string;
24
22
  context: {
25
23
  currentContract: ContractData | null;
26
24
  clientContext: ClientContext;
@@ -37,9 +35,10 @@ export type AddressesPageQuery = GetAddressesServiceProps & {
37
35
  export async function loader(
38
36
  data: LoaderData<AddressesPageQuery>
39
37
  ): Promise<AddressesPageData> {
40
- const { contractId, orgUnitId } = data.query;
38
+ const { contractId, orgUnitId, search } = data.query;
41
39
 
42
- const { cookie, userId, ...clientContext } = await getClientContext(data);
40
+ const { cookie, userId, customerId, ...clientContext } =
41
+ await getClientContext(data);
43
42
 
44
43
  const currentOrgUnit = await getOrgUnitBasicDataService({
45
44
  id: orgUnitId,
@@ -54,12 +53,13 @@ export async function loader(
54
53
  });
55
54
 
56
55
  return {
57
- data: await getAddressesService({
58
- customerId: getCustomerIdFromCookieServerSide(data),
56
+ data: await getAddressesByUnitIdService({
57
+ orgUnitId: currentOrgUnit.id,
59
58
  cookie,
60
59
  }),
60
+ search: search ?? "",
61
61
  context: {
62
- clientContext: { cookie, userId, ...clientContext },
62
+ clientContext: { cookie, userId, customerId, ...clientContext },
63
63
  currentOrgUnit,
64
64
  currentUser: user,
65
65
  currentContract: contract,
@@ -67,9 +67,9 @@ export async function loader(
67
67
  };
68
68
  }
69
69
 
70
- const AddressPage = ({ data, context }: AddressesPageData) => (
70
+ const AddressPage = ({ data, search, context }: AddressesPageData) => (
71
71
  <BuyerPortalProvider {...context}>
72
- <AddressLayout data={data} />
72
+ <AddressLayout data={data} search={search} />
73
73
  </BuyerPortalProvider>
74
74
  );
75
75
 
@@ -1,52 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { Icon } from "../Icon";
3
- import { BasicDropdownMenu } from "../BasicDropdownMenu/BasicDropdownMenu";
4
- import { Dropdown } from "@faststore/components";
5
- import Link from "next/link";
6
-
7
- export type ListLineProps = {
8
- children?: ReactNode;
9
- title: string;
10
- iconName?: string;
11
- iconSize?: number;
12
- dropdownMenu?: ReactNode;
13
- href?: string;
14
- onClick?: () => void;
15
- };
16
-
17
- export const ListLine = ({
18
- iconName,
19
- iconSize = 24,
20
- title,
21
- dropdownMenu,
22
- children,
23
- href,
24
- onClick,
25
- ...otherProps
26
- }: ListLineProps) => {
27
- const ClickComponent = href ? Link : "button";
28
-
29
- return (
30
- <li data-fs-bp-line {...otherProps}>
31
- {href && (
32
- <ClickComponent data-fs-bp-line-link href={href} onClick={onClick} />
33
- )}
34
- {iconName && (
35
- <Icon
36
- data-fs-bp-line-icon
37
- name={iconName}
38
- width={iconSize}
39
- height={iconSize}
40
- />
41
- )}
42
- <h3 data-fs-bp-line-title>{title}</h3>
43
- {children}
44
- {dropdownMenu && (
45
- <Dropdown>
46
- <BasicDropdownMenu.Trigger />
47
- {dropdownMenu}
48
- </Dropdown>
49
- )}
50
- </li>
51
- );
52
- };
@@ -1,49 +0,0 @@
1
- @import "../BasicDropdownMenu/basic-dropdown-menu.scss";
2
-
3
- [data-fs-bp-line] {
4
- position: relative;
5
- list-style: none;
6
- border-top: var(--fs-border-width) solid #e0e0e0;
7
- padding: var(--fs-spacing-1) var(--fs-spacing-1);
8
- align-items: center;
9
-
10
- display: flex;
11
- width: 100%;
12
-
13
- &:last-of-type {
14
- border-bottom: var(--fs-border-width) solid #e0e0e0;
15
- }
16
-
17
- [data-fs-bp-line-link] {
18
- position: absolute;
19
- left: 0;
20
- right: 0;
21
- top: 0;
22
- bottom: 0;
23
-
24
- &:hover {
25
- background-color: #f5f5f5;
26
- }
27
- }
28
-
29
- [data-fs-bp-line-icon] {
30
- color: #0366dd;
31
- margin-right: var(--fs-spacing-2);
32
- pointer-events: none;
33
- z-index: 1;
34
- }
35
-
36
- [data-fs-bp-line-title] {
37
- font-weight: 500;
38
- font-size: var(--fs-text-size-1);
39
- line-height: calc(var(--fs-spacing-4) - var(--fs-spacing-0));
40
- color: #1f1f1f;
41
- z-index: 1;
42
- pointer-events: none;
43
- }
44
-
45
- [data-fs-bp-basic-dropdown-menu-trigger] {
46
- z-index: 1;
47
- margin-left: auto;
48
- }
49
- }