Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/31267-marketplace-server-facets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@object-ui/app-shell': patch
---

feat(console): the marketplace catalog is filtered and paged by the server

Clause-②: no
Metadata-protocol: none

The Console's marketplace page read the first 100 listings once and filtered them in the browser, so a catalog of more than 100 listings could never show the rest. It now reads one page at a time from `GET /api/v1/marketplace/packages`, and the search text, the facet selection and the page window travel with the read:

- `businessFunctions`, `industries` and `solutionTypes`, each one comma-joined parameter (`?businessFunctions=sales,marketing`), OR inside a facet and AND across facets, as the list door applies them. A facet with nothing selected is left out, never sent empty, and the retired `category` is never sent.
- The search box sends its text as `q` once typing pauses, and the list door does the matching.
- `limit` / `offset`: a pager under the cards walks the door's `total`, 24 listings a page. A new search or selection starts again from the first page.

`listMarketplacePackages` takes the four filters, `maturity` included, which the page itself does not select.

A drawn facet now lists every value the marketplace declares, in the spec's order, and no longer prints a count beside each value: one page of the filtered catalog cannot count the catalog, and the list door answers no counts. A facet appears once some listing the door answered carries it, and stays while a selection narrows the page, so a catalog that serves no taxonomy still draws no filter.
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
/**
* The marketplace catalog's facet filters: solution type as a segment (one
* value or All), business function and industry as toggle rows (any number of
* values, OR inside the row). The rows combine with AND. Each value carries
* the count of loaded listings that hold it under the other facets' selection.
* values, OR inside the row). The rows combine with AND. The server applies
* the selection: the page sends it with the list read and draws the answer.
*
* `facetOptions` (`marketplaceTaxonomy.ts`) decides what is drawn: a facet no
* loaded listing carries is empty and its row is not drawn, so a catalog that
* A drawn facet offers every value the spec declares, in the spec's order: the
* page holds one page of the filtered catalog, so the values its rows carry
* are not the values a selection can add. `ServedFacets`
* (`marketplaceTaxonomy.ts`) decides which facets are drawn, so a catalog that
* does not serve the taxonomy yet draws no filter at all.
*/

import type { ReactNode } from 'react';
import { Button } from '@object-ui/components';
import { useObjectTranslation } from '@object-ui/i18n';
import type { FacetOptions, FacetSelection } from './marketplaceTaxonomy.js';
import {
BUSINESS_FUNCTIONS,
INDUSTRIES,
SOLUTION_TYPES,
type FacetSelection,
type ServedFacets,
} from './marketplaceTaxonomy.js';

function toggled<T>(set: ReadonlySet<T>, value: T): Set<T> {
const next = new Set(set);
Expand All @@ -30,9 +38,8 @@ function FacetRow({ label, testId, children }: { label: string; testId: string;
);
}

function FacetButton({ label, count, pressed, onClick, testId }: {
function FacetButton({ label, pressed, onClick, testId }: {
label: string;
count?: number;
pressed: boolean;
onClick: () => void;
testId: string;
Expand All @@ -46,65 +53,57 @@ function FacetButton({ label, count, pressed, onClick, testId }: {
data-testid={testId}
>
{label}
{count !== undefined && (
<span className="ml-1.5 text-xs tabular-nums opacity-70" data-testid={`${testId}-count`}>{count}</span>
)}
</Button>
);
}

export function MarketplaceFacetFilters({ options, selection, onChange }: {
options: FacetOptions;
export function MarketplaceFacetFilters({ served, selection, onChange }: {
served: ServedFacets;
selection: FacetSelection;
onChange: (next: FacetSelection) => void;
}) {
const { t } = useObjectTranslation();
if (options.solutionType.length === 0 && options.businessFunctions.length === 0 && options.industries.length === 0) {
return null;
}
if (!served.solutionType && !served.businessFunctions && !served.industries) return null;
return (
<div className="flex flex-col gap-2" data-testid="marketplace-facets">
{options.solutionType.length > 0 && (
{served.solutionType && (
<FacetRow label={t('marketplace.facet.solutionType')} testId="marketplace-facet-solutionType">
<FacetButton
label={t('marketplace.all')}
pressed={selection.solutionType === null}
onClick={() => onChange({ ...selection, solutionType: null })}
testId="marketplace-facet-solutionType-all"
/>
{options.solutionType.map(({ value, count }) => (
{SOLUTION_TYPES.map((value) => (
<FacetButton
key={value}
label={t(`marketplace.solutionType.${value}`)}
count={count}
pressed={selection.solutionType === value}
onClick={() => onChange({ ...selection, solutionType: value })}
testId={`marketplace-facet-solutionType-${value}`}
/>
))}
</FacetRow>
)}
{options.businessFunctions.length > 0 && (
{served.businessFunctions && (
<FacetRow label={t('marketplace.facet.businessFunction')} testId="marketplace-facet-businessFunction">
{options.businessFunctions.map(({ value, count }) => (
{BUSINESS_FUNCTIONS.map((value) => (
<FacetButton
key={value}
label={t(`marketplace.businessFunction.${value}`)}
count={count}
pressed={selection.businessFunctions.has(value)}
onClick={() => onChange({ ...selection, businessFunctions: toggled(selection.businessFunctions, value) })}
testId={`marketplace-facet-businessFunction-${value}`}
/>
))}
</FacetRow>
)}
{options.industries.length > 0 && (
{served.industries && (
<FacetRow label={t('marketplace.facet.industry')} testId="marketplace-facet-industry">
{options.industries.map(({ value, count }) => (
{INDUSTRIES.map((value) => (
<FacetButton
key={value}
label={t(`marketplace.industry.${value}`)}
count={count}
pressed={selection.industries.has(value)}
onClick={() => onChange({ ...selection, industries: toggled(selection.industries, value) })}
testId={`marketplace-facet-industry-${value}`}
Expand Down
Loading
Loading