@schandlergarcia/sf-web-components 2.3.17 → 2.4.0
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.
- package/CHANGELOG.md +16 -0
- package/CLAUDE.md +12 -13
- package/README.md +0 -15
- package/dist/styles/global.css +44 -57
- package/package.json +1 -2
- package/scripts/apply-brand.mjs +47 -30
- package/scripts/postinstall.mjs +1 -11
- package/src/styles/global.css +44 -57
- package/brands/engine/PARTNER_HUB_PRD.md +0 -584
- package/brands/engine/agentApiConfig.ts +0 -36
- package/brands/engine/app/api/graphql-operations-types.ts +0 -11260
- package/brands/engine/app/api/graphqlClient.ts +0 -25
- package/brands/engine/app/api/partnerQueries.ts +0 -212
- package/brands/engine/app/appLayout.tsx +0 -5
- package/brands/engine/app/components/AgentPanel.tsx +0 -541
- package/brands/engine/app/components/AgentforceConversationClient.tsx +0 -201
- package/brands/engine/app/components/Data360Widget.tsx +0 -301
- package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +0 -3
- package/brands/engine/app/components/alerts/status-alert.tsx +0 -49
- package/brands/engine/app/components/layouts/card-layout.tsx +0 -29
- package/brands/engine/app/components/workspace/CommandCenter.tsx +0 -16
- package/brands/engine/app/config/agentApi.ts +0 -36
- package/brands/engine/app/data/partner-hub-sample-data.js +0 -297
- package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +0 -46
- package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
- package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
- package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
- package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
- package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -357
- package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +0 -312
- package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +0 -34
- package/brands/engine/app/features/object-search/api/objectSearchService.ts +0 -84
- package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +0 -89
- package/brands/engine/app/features/object-search/components/FilterContext.tsx +0 -83
- package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
- package/brands/engine/app/features/object-search/components/PaginationControls.tsx +0 -109
- package/brands/engine/app/features/object-search/components/SearchBar.tsx +0 -41
- package/brands/engine/app/features/object-search/components/SortControl.tsx +0 -143
- package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +0 -78
- package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +0 -128
- package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
- package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -33
- package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +0 -97
- package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +0 -163
- package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +0 -50
- package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +0 -97
- package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +0 -91
- package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +0 -54
- package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +0 -184
- package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +0 -34
- package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +0 -252
- package/brands/engine/app/features/object-search/utils/debounce.ts +0 -25
- package/brands/engine/app/features/object-search/utils/fieldUtils.ts +0 -29
- package/brands/engine/app/features/object-search/utils/filterUtils.ts +0 -404
- package/brands/engine/app/features/object-search/utils/sortUtils.ts +0 -38
- package/brands/engine/app/hooks/useEngineLiveData.ts +0 -49
- package/brands/engine/app/hooks/useEvaAgent.ts +0 -288
- package/brands/engine/app/hooks/usePartnerDashboardData.ts +0 -141
- package/brands/engine/app/navigationMenu.tsx +0 -80
- package/brands/engine/app/pages/AccountObjectDetailPage.tsx +0 -361
- package/brands/engine/app/pages/AccountSearch.tsx +0 -305
- package/brands/engine/app/pages/BlankDashboard.tsx +0 -15
- package/brands/engine/app/pages/DataTest.tsx +0 -78
- package/brands/engine/app/pages/Home.tsx +0 -5
- package/brands/engine/app/pages/NotFound.tsx +0 -19
- package/brands/engine/app/pages/PartnerHubDashboard.tsx +0 -2760
- package/brands/engine/app/pages/Search.tsx +0 -13
- package/brands/engine/app/router-utils.tsx +0 -35
- package/brands/engine/app/routes.tsx +0 -39
- package/brands/engine/app/styles/global.css +0 -269
- package/brands/engine/brand.css +0 -40
- package/brands/engine/engine-command-center-prd.md +0 -575
- package/brands/engine/engine-live-data.js +0 -135
- package/brands/engine/engine-sample-data.js +0 -378
- package/brands/engine/engine_logo.png +0 -0
- package/brands/engine/global.css +0 -269
- package/brands/engine/partner-hub-sample-data.js +0 -281
- package/brands/engine/schema.graphql +0 -292
- package/brands/engine/useEngineLiveData.ts +0 -49
- package/brands/engine/useEvaAgent.ts +0 -288
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { useNavigate } from "react-router";
|
|
3
|
-
import { SearchBar } from "../../components/SearchBar";
|
|
4
|
-
import { Button } from "../../../../components/ui/button";
|
|
5
|
-
|
|
6
|
-
export default function HomePage() {
|
|
7
|
-
const navigate = useNavigate();
|
|
8
|
-
const [text, setText] = useState("");
|
|
9
|
-
|
|
10
|
-
const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
|
|
11
|
-
e.preventDefault();
|
|
12
|
-
const params = text ? `?q=${encodeURIComponent(text)}` : "";
|
|
13
|
-
navigate(`/accounts${params}`);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
18
|
-
<div className="flex items-center gap-6 mb-6">
|
|
19
|
-
<h1 className="text-2xl font-bold">Account Search</h1>
|
|
20
|
-
<Button variant="outline" size="sm" onClick={() => navigate("/accounts")}>
|
|
21
|
-
Browse All Accounts
|
|
22
|
-
</Button>
|
|
23
|
-
</div>
|
|
24
|
-
<form onSubmit={handleSubmit} className="flex gap-2">
|
|
25
|
-
<SearchBar
|
|
26
|
-
placeholder="Search by name, phone, or industry..."
|
|
27
|
-
value={text}
|
|
28
|
-
handleChange={setText}
|
|
29
|
-
/>
|
|
30
|
-
<Button type="submit">Search</Button>
|
|
31
|
-
</form>
|
|
32
|
-
</div>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { createDataSDK } from "@salesforce/sdk-data";
|
|
2
|
-
|
|
3
|
-
export interface ObjectSearchOptions<TWhere, TOrderBy> {
|
|
4
|
-
where?: TWhere;
|
|
5
|
-
orderBy?: TOrderBy;
|
|
6
|
-
first?: number;
|
|
7
|
-
after?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export type PicklistOption = { value: string; label: string };
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Executes a GraphQL search query and extracts the result for the given object name
|
|
14
|
-
* from the standard `uiapi.query.<ObjectName>` response shape.
|
|
15
|
-
*/
|
|
16
|
-
export async function searchObjects<TResult, TQuery, TVariables>(
|
|
17
|
-
query: string,
|
|
18
|
-
objectName: string,
|
|
19
|
-
options: ObjectSearchOptions<unknown, unknown> = {},
|
|
20
|
-
): Promise<TResult> {
|
|
21
|
-
const { where, orderBy, first = 20, after } = options;
|
|
22
|
-
|
|
23
|
-
const data = await createDataSDK();
|
|
24
|
-
const response = await data.graphql?.<TQuery, TVariables>(query, {
|
|
25
|
-
first,
|
|
26
|
-
after,
|
|
27
|
-
where,
|
|
28
|
-
orderBy,
|
|
29
|
-
} as TVariables);
|
|
30
|
-
|
|
31
|
-
if (response?.errors?.length) {
|
|
32
|
-
throw new Error(response.errors.map((e) => e.message).join("; "));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
|
|
36
|
-
| Record<string, unknown>
|
|
37
|
-
| undefined;
|
|
38
|
-
const queryResult = (result?.query as Record<string, unknown> | undefined)?.[objectName] as
|
|
39
|
-
| TResult
|
|
40
|
-
| undefined;
|
|
41
|
-
|
|
42
|
-
if (!queryResult) {
|
|
43
|
-
throw new Error(`No ${objectName} data returned`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return queryResult;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Executes a GraphQL aggregate/groupBy query and extracts picklist options
|
|
51
|
-
* from the standard `uiapi.aggregate.<ObjectName>` response shape.
|
|
52
|
-
*/
|
|
53
|
-
export async function fetchDistinctValues<TQuery>(
|
|
54
|
-
query: string,
|
|
55
|
-
objectName: string,
|
|
56
|
-
fieldName: string,
|
|
57
|
-
): Promise<PicklistOption[]> {
|
|
58
|
-
const data = await createDataSDK();
|
|
59
|
-
const response = await data.graphql?.<TQuery>(query);
|
|
60
|
-
const errors = response?.errors;
|
|
61
|
-
|
|
62
|
-
if (errors?.length) {
|
|
63
|
-
throw new Error(errors.map((e) => e.message).join("; "));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
|
|
67
|
-
| Record<string, unknown>
|
|
68
|
-
| undefined;
|
|
69
|
-
const aggregate = (result?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
|
|
70
|
-
| { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
|
|
71
|
-
| undefined;
|
|
72
|
-
|
|
73
|
-
const edges = aggregate?.edges ?? [];
|
|
74
|
-
return edges
|
|
75
|
-
.map((edge) => {
|
|
76
|
-
const field = edge?.node?.aggregate?.[fieldName] as
|
|
77
|
-
| { value?: string | null; displayValue?: string | null; label?: string | null }
|
|
78
|
-
| undefined;
|
|
79
|
-
const value = field?.value;
|
|
80
|
-
if (!value) return null;
|
|
81
|
-
return { value, label: field.label ?? field.displayValue ?? value };
|
|
82
|
-
})
|
|
83
|
-
.filter((opt): opt is PicklistOption => opt !== null);
|
|
84
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { X } from "lucide-react";
|
|
2
|
-
import { Button } from "../../../components/ui/button";
|
|
3
|
-
import { cn } from "../../../lib/utils";
|
|
4
|
-
import type { ActiveFilterValue } from "../utils/filterUtils";
|
|
5
|
-
|
|
6
|
-
function formatFilterLabel(filter: ActiveFilterValue): string {
|
|
7
|
-
const { label, type, value, min, max } = filter;
|
|
8
|
-
|
|
9
|
-
switch (type) {
|
|
10
|
-
case "search":
|
|
11
|
-
return `Search: ${value}`;
|
|
12
|
-
case "text":
|
|
13
|
-
case "picklist":
|
|
14
|
-
return `${label}: ${value}`;
|
|
15
|
-
case "multipicklist": {
|
|
16
|
-
const values = value ? value.split(",") : [];
|
|
17
|
-
if (values.length <= 2) return `${label}: ${values.join(", ")}`;
|
|
18
|
-
return `${label}: ${values.length} selected`;
|
|
19
|
-
}
|
|
20
|
-
case "boolean":
|
|
21
|
-
return `${label}: ${value === "true" ? "Yes" : "No"}`;
|
|
22
|
-
case "numeric": {
|
|
23
|
-
if (min && max) return `${label}: ${min} - ${max}`;
|
|
24
|
-
if (min) return `${label}: >= ${min}`;
|
|
25
|
-
return `${label}: <= ${max}`;
|
|
26
|
-
}
|
|
27
|
-
case "date": {
|
|
28
|
-
if (min && max) return `${label}: ${min} to ${max}`;
|
|
29
|
-
if (min) return `${label}: from ${min}`;
|
|
30
|
-
return `${label}: until ${max}`;
|
|
31
|
-
}
|
|
32
|
-
default:
|
|
33
|
-
return label;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ActiveFiltersProps extends React.ComponentProps<"div"> {
|
|
38
|
-
filters: ActiveFilterValue[];
|
|
39
|
-
onRemove: (field: string) => void;
|
|
40
|
-
buttonProps?: Omit<React.ComponentProps<typeof ActiveFilterButton>, "filter" | "onRemove">;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function ActiveFilters({
|
|
44
|
-
filters,
|
|
45
|
-
onRemove,
|
|
46
|
-
className,
|
|
47
|
-
buttonProps,
|
|
48
|
-
...props
|
|
49
|
-
}: ActiveFiltersProps) {
|
|
50
|
-
if (filters.length === 0) return null;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<div className={cn("flex flex-wrap gap-2", className)} {...props}>
|
|
54
|
-
{filters.map((filter) => (
|
|
55
|
-
<ActiveFilterButton
|
|
56
|
-
key={filter.field}
|
|
57
|
-
filter={filter}
|
|
58
|
-
onRemove={onRemove}
|
|
59
|
-
{...buttonProps}
|
|
60
|
-
/>
|
|
61
|
-
))}
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface ActiveFilterButtonProps extends React.ComponentProps<typeof Button> {
|
|
67
|
-
filter: ActiveFilterValue;
|
|
68
|
-
onRemove: (field: string) => void;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function ActiveFilterButton({
|
|
72
|
-
filter,
|
|
73
|
-
onRemove,
|
|
74
|
-
className,
|
|
75
|
-
...props
|
|
76
|
-
}: ActiveFilterButtonProps) {
|
|
77
|
-
return (
|
|
78
|
-
<Button
|
|
79
|
-
variant="outline"
|
|
80
|
-
size="sm"
|
|
81
|
-
className={cn("gap-1 h-7 text-xs", className)}
|
|
82
|
-
onClick={() => onRemove(filter.field)}
|
|
83
|
-
{...props}
|
|
84
|
-
>
|
|
85
|
-
{formatFilterLabel(filter)}
|
|
86
|
-
<X className="h-3 w-3" />
|
|
87
|
-
</Button>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useCallback, type ReactNode } from "react";
|
|
2
|
-
import { Button } from "../../../components/ui/button";
|
|
3
|
-
import type { ActiveFilterValue } from "../utils/filterUtils";
|
|
4
|
-
|
|
5
|
-
interface FilterContextValue {
|
|
6
|
-
filters: ActiveFilterValue[];
|
|
7
|
-
onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
|
|
8
|
-
onFilterRemove: (field: string) => void;
|
|
9
|
-
onReset: () => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const FilterContext = createContext<FilterContextValue | null>(null);
|
|
13
|
-
|
|
14
|
-
interface FilterProviderProps {
|
|
15
|
-
filters: ActiveFilterValue[];
|
|
16
|
-
onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
|
|
17
|
-
onFilterRemove: (field: string) => void;
|
|
18
|
-
onReset: () => void;
|
|
19
|
-
children: ReactNode;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function FilterProvider({
|
|
23
|
-
filters,
|
|
24
|
-
onFilterChange,
|
|
25
|
-
onFilterRemove,
|
|
26
|
-
onReset,
|
|
27
|
-
children,
|
|
28
|
-
}: FilterProviderProps) {
|
|
29
|
-
return (
|
|
30
|
-
<FilterContext.Provider
|
|
31
|
-
value={{
|
|
32
|
-
filters,
|
|
33
|
-
onFilterChange,
|
|
34
|
-
onFilterRemove,
|
|
35
|
-
onReset,
|
|
36
|
-
}}
|
|
37
|
-
>
|
|
38
|
-
{children}
|
|
39
|
-
</FilterContext.Provider>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function useFilterContext() {
|
|
44
|
-
const ctx = useContext(FilterContext);
|
|
45
|
-
if (!ctx) throw new Error("useFilterField must be used within a FilterProvider");
|
|
46
|
-
return ctx;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function useFilterField(field: string) {
|
|
50
|
-
const { filters, onFilterChange, onFilterRemove } = useFilterContext();
|
|
51
|
-
const value = filters.find((f) => f.field === field);
|
|
52
|
-
const onChange = useCallback(
|
|
53
|
-
(next: ActiveFilterValue | undefined) => {
|
|
54
|
-
if (next) {
|
|
55
|
-
onFilterChange(field, next);
|
|
56
|
-
} else {
|
|
57
|
-
onFilterRemove(field);
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
[field, onFilterChange, onFilterRemove],
|
|
61
|
-
);
|
|
62
|
-
return { value, onChange };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function useFilterPanel() {
|
|
66
|
-
const { filters, onReset } = useFilterContext();
|
|
67
|
-
return {
|
|
68
|
-
hasActiveFilters: filters.length > 0,
|
|
69
|
-
resetAll: onReset,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
type FilterResetButtonProps = Omit<React.ComponentProps<typeof Button>, "onClick">;
|
|
74
|
-
|
|
75
|
-
export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
|
|
76
|
-
const { hasActiveFilters, resetAll } = useFilterPanel();
|
|
77
|
-
if (!hasActiveFilters) return null;
|
|
78
|
-
return (
|
|
79
|
-
<Button onClick={resetAll} aria-label="Reset filters" variant="destructive" {...props}>
|
|
80
|
-
{children ?? "Reset"}
|
|
81
|
-
</Button>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { Link } from "react-router";
|
|
2
|
-
import {
|
|
3
|
-
Breadcrumb,
|
|
4
|
-
BreadcrumbList,
|
|
5
|
-
BreadcrumbItem,
|
|
6
|
-
BreadcrumbLink,
|
|
7
|
-
BreadcrumbSeparator,
|
|
8
|
-
BreadcrumbPage,
|
|
9
|
-
} from "../../../components/ui/breadcrumb";
|
|
10
|
-
import { Skeleton } from "../../../components/ui/skeleton";
|
|
11
|
-
|
|
12
|
-
interface ObjectBreadcrumbProps {
|
|
13
|
-
listPath: string;
|
|
14
|
-
listLabel: string;
|
|
15
|
-
recordName?: string;
|
|
16
|
-
loading?: boolean;
|
|
17
|
-
includeHome?: boolean; // default is true
|
|
18
|
-
homeLabel?: string; // default is "Home"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function ObjectBreadcrumb({
|
|
22
|
-
listPath,
|
|
23
|
-
listLabel,
|
|
24
|
-
recordName,
|
|
25
|
-
loading,
|
|
26
|
-
includeHome = true,
|
|
27
|
-
homeLabel = "Home",
|
|
28
|
-
}: ObjectBreadcrumbProps) {
|
|
29
|
-
const isDetailView = loading || recordName;
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Breadcrumb className="mb-3">
|
|
33
|
-
<BreadcrumbList>
|
|
34
|
-
{includeHome && (
|
|
35
|
-
<BreadcrumbItem>
|
|
36
|
-
<BreadcrumbLink asChild>
|
|
37
|
-
<Link to="/">{homeLabel}</Link>
|
|
38
|
-
</BreadcrumbLink>
|
|
39
|
-
</BreadcrumbItem>
|
|
40
|
-
)}
|
|
41
|
-
<BreadcrumbSeparator />
|
|
42
|
-
{isDetailView ? (
|
|
43
|
-
<>
|
|
44
|
-
<BreadcrumbItem>
|
|
45
|
-
<BreadcrumbLink asChild>
|
|
46
|
-
<Link to={listPath}>{listLabel}</Link>
|
|
47
|
-
</BreadcrumbLink>
|
|
48
|
-
</BreadcrumbItem>
|
|
49
|
-
<BreadcrumbSeparator />
|
|
50
|
-
<BreadcrumbItem>
|
|
51
|
-
{loading && !recordName ? (
|
|
52
|
-
<Skeleton className="h-4 w-32" />
|
|
53
|
-
) : (
|
|
54
|
-
<BreadcrumbPage>{recordName}</BreadcrumbPage>
|
|
55
|
-
)}
|
|
56
|
-
</BreadcrumbItem>
|
|
57
|
-
</>
|
|
58
|
-
) : (
|
|
59
|
-
<BreadcrumbItem>
|
|
60
|
-
<BreadcrumbPage>{listLabel}</BreadcrumbPage>
|
|
61
|
-
</BreadcrumbItem>
|
|
62
|
-
)}
|
|
63
|
-
</BreadcrumbList>
|
|
64
|
-
</Breadcrumb>
|
|
65
|
-
);
|
|
66
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Pagination,
|
|
3
|
-
PaginationContent,
|
|
4
|
-
PaginationItem,
|
|
5
|
-
PaginationPrevious,
|
|
6
|
-
PaginationNext,
|
|
7
|
-
} from "../../../components/ui/pagination";
|
|
8
|
-
import {
|
|
9
|
-
Select,
|
|
10
|
-
SelectContent,
|
|
11
|
-
SelectItem,
|
|
12
|
-
SelectTrigger,
|
|
13
|
-
SelectValue,
|
|
14
|
-
} from "../../../components/ui/select";
|
|
15
|
-
import { Label } from "../../../components/ui/label";
|
|
16
|
-
|
|
17
|
-
interface PaginationControlsProps {
|
|
18
|
-
pageIndex: number;
|
|
19
|
-
hasNextPage: boolean;
|
|
20
|
-
hasPreviousPage: boolean;
|
|
21
|
-
pageSize: number;
|
|
22
|
-
pageSizeOptions: readonly number[];
|
|
23
|
-
onNextPage: () => void;
|
|
24
|
-
onPreviousPage: () => void;
|
|
25
|
-
onPageSizeChange: (newPageSize: number) => void;
|
|
26
|
-
disabled?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default function PaginationControls({
|
|
30
|
-
pageIndex,
|
|
31
|
-
hasNextPage,
|
|
32
|
-
hasPreviousPage,
|
|
33
|
-
pageSize,
|
|
34
|
-
pageSizeOptions,
|
|
35
|
-
onNextPage,
|
|
36
|
-
onPreviousPage,
|
|
37
|
-
onPageSizeChange,
|
|
38
|
-
disabled = false,
|
|
39
|
-
}: PaginationControlsProps) {
|
|
40
|
-
const handlePageSizeChange = (newValue: string) => {
|
|
41
|
-
const newSize = parseInt(newValue, 10);
|
|
42
|
-
if (!isNaN(newSize) && newSize !== pageSize) {
|
|
43
|
-
onPageSizeChange(newSize);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
const currentPage = pageIndex + 1;
|
|
47
|
-
const prevDisabled = disabled || !hasPreviousPage;
|
|
48
|
-
const nextDisabled = disabled || !hasNextPage;
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
|
|
52
|
-
<div
|
|
53
|
-
className="flex justify-center sm:justify-start items-center gap-2 shrink-0 row-2 sm:row-1"
|
|
54
|
-
role="group"
|
|
55
|
-
aria-label="Page size selector"
|
|
56
|
-
>
|
|
57
|
-
<Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
|
|
58
|
-
Results per page:
|
|
59
|
-
</Label>
|
|
60
|
-
<Select
|
|
61
|
-
value={pageSize.toString()}
|
|
62
|
-
onValueChange={handlePageSizeChange}
|
|
63
|
-
disabled={disabled}
|
|
64
|
-
>
|
|
65
|
-
<SelectTrigger
|
|
66
|
-
id="page-size-select"
|
|
67
|
-
className="w-16"
|
|
68
|
-
aria-label="Select number of results per page"
|
|
69
|
-
>
|
|
70
|
-
<SelectValue />
|
|
71
|
-
</SelectTrigger>
|
|
72
|
-
<SelectContent>
|
|
73
|
-
{pageSizeOptions.map((size) => (
|
|
74
|
-
<SelectItem key={size} value={size.toString()}>
|
|
75
|
-
{size}
|
|
76
|
-
</SelectItem>
|
|
77
|
-
))}
|
|
78
|
-
</SelectContent>
|
|
79
|
-
</Select>
|
|
80
|
-
</div>
|
|
81
|
-
<Pagination className="w-full mx-0 sm:justify-end">
|
|
82
|
-
<PaginationContent>
|
|
83
|
-
<PaginationItem>
|
|
84
|
-
<PaginationPrevious
|
|
85
|
-
onClick={prevDisabled ? undefined : onPreviousPage}
|
|
86
|
-
aria-disabled={prevDisabled}
|
|
87
|
-
className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
88
|
-
/>
|
|
89
|
-
</PaginationItem>
|
|
90
|
-
<PaginationItem>
|
|
91
|
-
<span
|
|
92
|
-
className="min-w-16 text-center text-sm text-muted-foreground px-2"
|
|
93
|
-
aria-current="page"
|
|
94
|
-
>
|
|
95
|
-
Page {currentPage}
|
|
96
|
-
</span>
|
|
97
|
-
</PaginationItem>
|
|
98
|
-
<PaginationItem>
|
|
99
|
-
<PaginationNext
|
|
100
|
-
onClick={nextDisabled ? undefined : onNextPage}
|
|
101
|
-
aria-disabled={nextDisabled}
|
|
102
|
-
className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
103
|
-
/>
|
|
104
|
-
</PaginationItem>
|
|
105
|
-
</PaginationContent>
|
|
106
|
-
</Pagination>
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { Search } from "lucide-react";
|
|
2
|
-
import { Input } from "../../../components/ui/input";
|
|
3
|
-
import { cn } from "../../../lib/utils";
|
|
4
|
-
|
|
5
|
-
interface SearchBarProps extends React.ComponentProps<"div"> {
|
|
6
|
-
value: string;
|
|
7
|
-
handleChange: (value: string) => void;
|
|
8
|
-
placeholder?: string;
|
|
9
|
-
iconProps?: React.ComponentProps<typeof Search>;
|
|
10
|
-
inputProps?: Omit<React.ComponentProps<typeof Input>, "value">;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function SearchBar({
|
|
14
|
-
value,
|
|
15
|
-
handleChange,
|
|
16
|
-
placeholder,
|
|
17
|
-
className,
|
|
18
|
-
iconProps,
|
|
19
|
-
inputProps,
|
|
20
|
-
...props
|
|
21
|
-
}: SearchBarProps) {
|
|
22
|
-
return (
|
|
23
|
-
<div className={cn("relative flex-1", className)} title={placeholder} {...props}>
|
|
24
|
-
<Search
|
|
25
|
-
{...iconProps}
|
|
26
|
-
className={cn(
|
|
27
|
-
"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground",
|
|
28
|
-
iconProps?.className,
|
|
29
|
-
)}
|
|
30
|
-
/>
|
|
31
|
-
<Input
|
|
32
|
-
type="text"
|
|
33
|
-
value={value}
|
|
34
|
-
onChange={(e) => handleChange(e.target.value)}
|
|
35
|
-
placeholder={placeholder}
|
|
36
|
-
{...inputProps}
|
|
37
|
-
className={cn("pl-9", inputProps?.className)}
|
|
38
|
-
/>
|
|
39
|
-
</div>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { ArrowUp, ArrowDown } from "lucide-react";
|
|
2
|
-
import {
|
|
3
|
-
Select,
|
|
4
|
-
SelectContent,
|
|
5
|
-
SelectItem,
|
|
6
|
-
SelectTrigger,
|
|
7
|
-
SelectValue,
|
|
8
|
-
} from "../../../components/ui/select";
|
|
9
|
-
import { Button } from "../../../components/ui/button";
|
|
10
|
-
import { cn } from "../../../lib/utils";
|
|
11
|
-
import type { SortFieldConfig, SortState } from "../utils/sortUtils";
|
|
12
|
-
|
|
13
|
-
const NONE_VALUE = "__none__";
|
|
14
|
-
|
|
15
|
-
interface SortControlProps extends React.ComponentProps<"div"> {
|
|
16
|
-
configs: SortFieldConfig[];
|
|
17
|
-
sort: SortState | null;
|
|
18
|
-
onSortChange: (sort: SortState | null) => void;
|
|
19
|
-
labelProps?: React.ComponentProps<"span">;
|
|
20
|
-
selectProps?: Omit<
|
|
21
|
-
React.ComponentProps<typeof SortControlSelect>,
|
|
22
|
-
"configs" | "sort" | "onSortChange"
|
|
23
|
-
>;
|
|
24
|
-
directionButtonProps?: Omit<
|
|
25
|
-
React.ComponentProps<typeof SortDirectionButton>,
|
|
26
|
-
"sort" | "onSortChange"
|
|
27
|
-
>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function SortControl({
|
|
31
|
-
configs,
|
|
32
|
-
sort,
|
|
33
|
-
onSortChange,
|
|
34
|
-
className,
|
|
35
|
-
labelProps,
|
|
36
|
-
selectProps,
|
|
37
|
-
directionButtonProps,
|
|
38
|
-
...props
|
|
39
|
-
}: SortControlProps) {
|
|
40
|
-
return (
|
|
41
|
-
<div className={cn("flex items-center gap-2", className)} {...props}>
|
|
42
|
-
<span
|
|
43
|
-
{...labelProps}
|
|
44
|
-
className={cn("text-sm text-muted-foreground whitespace-nowrap", labelProps?.className)}
|
|
45
|
-
>
|
|
46
|
-
{labelProps?.children ?? "Sort by"}
|
|
47
|
-
</span>
|
|
48
|
-
<SortControlSelect
|
|
49
|
-
configs={configs}
|
|
50
|
-
sort={sort}
|
|
51
|
-
onSortChange={onSortChange}
|
|
52
|
-
{...selectProps}
|
|
53
|
-
/>
|
|
54
|
-
{sort && (
|
|
55
|
-
<SortDirectionButton sort={sort} onSortChange={onSortChange} {...directionButtonProps} />
|
|
56
|
-
)}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface SortControlSelectProps {
|
|
62
|
-
configs: SortFieldConfig[];
|
|
63
|
-
sort: SortState | null;
|
|
64
|
-
onSortChange: (sort: SortState | null) => void;
|
|
65
|
-
triggerProps?: React.ComponentProps<typeof SelectTrigger>;
|
|
66
|
-
contentProps?: React.ComponentProps<typeof SelectContent>;
|
|
67
|
-
selectValueProps?: React.ComponentProps<typeof SelectValue>;
|
|
68
|
-
selectItemProps?: Omit<React.ComponentProps<typeof SelectItem>, "value">;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function SortControlSelect({
|
|
72
|
-
configs,
|
|
73
|
-
sort,
|
|
74
|
-
onSortChange,
|
|
75
|
-
triggerProps,
|
|
76
|
-
contentProps,
|
|
77
|
-
selectValueProps,
|
|
78
|
-
selectItemProps,
|
|
79
|
-
}: SortControlSelectProps) {
|
|
80
|
-
return (
|
|
81
|
-
<Select
|
|
82
|
-
value={sort?.field ?? NONE_VALUE}
|
|
83
|
-
onValueChange={(v) => {
|
|
84
|
-
if (v === NONE_VALUE) {
|
|
85
|
-
onSortChange(null);
|
|
86
|
-
} else {
|
|
87
|
-
onSortChange({
|
|
88
|
-
field: v,
|
|
89
|
-
direction: sort?.direction ?? "ASC",
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}}
|
|
93
|
-
>
|
|
94
|
-
<SelectTrigger
|
|
95
|
-
size="sm"
|
|
96
|
-
{...triggerProps}
|
|
97
|
-
className={cn("w-[160px]", triggerProps?.className)}
|
|
98
|
-
>
|
|
99
|
-
<SelectValue placeholder="Default" {...selectValueProps} />
|
|
100
|
-
</SelectTrigger>
|
|
101
|
-
<SelectContent {...contentProps}>
|
|
102
|
-
<SelectItem value={NONE_VALUE} {...selectItemProps}>
|
|
103
|
-
Default
|
|
104
|
-
</SelectItem>
|
|
105
|
-
{configs.map((c) => (
|
|
106
|
-
<SelectItem key={c.field} value={c.field} {...selectItemProps}>
|
|
107
|
-
{c.label}
|
|
108
|
-
</SelectItem>
|
|
109
|
-
))}
|
|
110
|
-
</SelectContent>
|
|
111
|
-
</Select>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface SortDirectionButtonProps extends React.ComponentProps<typeof Button> {
|
|
116
|
-
sort: SortState;
|
|
117
|
-
onSortChange: (sort: SortState) => void;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function SortDirectionButton({
|
|
121
|
-
sort,
|
|
122
|
-
onSortChange,
|
|
123
|
-
className,
|
|
124
|
-
...props
|
|
125
|
-
}: SortDirectionButtonProps) {
|
|
126
|
-
return (
|
|
127
|
-
<Button
|
|
128
|
-
variant="ghost"
|
|
129
|
-
size="icon-sm"
|
|
130
|
-
className={cn(className)}
|
|
131
|
-
onClick={() =>
|
|
132
|
-
onSortChange({
|
|
133
|
-
...sort,
|
|
134
|
-
direction: sort.direction === "ASC" ? "DESC" : "ASC",
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
aria-label={`Sort ${sort.direction === "ASC" ? "descending" : "ascending"}`}
|
|
138
|
-
{...props}
|
|
139
|
-
>
|
|
140
|
-
{sort.direction === "ASC" ? <ArrowUp /> : <ArrowDown />}
|
|
141
|
-
</Button>
|
|
142
|
-
);
|
|
143
|
-
}
|