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
1 change: 1 addition & 0 deletions packages/app-elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@commercelayer/js-auth": "^7.4.2",
"@commercelayer/provisioning-sdk": "2.10.2",
"@commercelayer/sdk": "8.0.0-beta.11",
"@date-fns/tz": "^1.5.0",
"@monaco-editor/react": "~4.7.0",
Expand Down
1 change: 1 addition & 0 deletions packages/app-elements/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ export {
refreshResourceLists,
removeFromResourceLists,
type UseResourceListConfig,
type UseResourceListReturn,
useResourceList,
} from "#ui/resources/useResourceList"
export {
Expand Down
5 changes: 4 additions & 1 deletion packages/app-elements/src/ui/atoms/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ export const CodeBlock = withSkeletonTemplate<CodeBlockProps>(

return (
<InputWrapper {...rest} label={label} hint={hint}>
<div className="flex group w-full rounded bg-gray-50 in-[.overlay-container]:bg-gray-200">
{/* A shade darker only on a gray overlay, where `bg-gray-50` would
disappear into the surface. A white overlay — every drawer, unless it
asks for `backgroundColor="light"` — keeps the page's own shade. */}
<div className="flex group w-full rounded bg-gray-50 in-[.overlay-container-light]:bg-gray-200">
<div
className={cn(
"flex flex-col w-full px-4 py-2.5 text-primary font-mono font-medium marker:font-semibold leading-5 border-none break-all",
Expand Down
32 changes: 32 additions & 0 deletions packages/app-elements/src/ui/internals/Overlay.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,40 @@
import { render, waitFor } from "@testing-library/react"
import { CodeBlock } from "#ui/atoms/CodeBlock"
import { Modal } from "#ui/composite/Modal"
import { Input } from "#ui/forms/Input"
import { Overlay } from "./Overlay"

describe("Overlay surface", () => {
/**
* The class is a contract with the blocks rendered on the surface: `CodeBlock`
* reads it to go a shade darker, because its own `bg-gray-50` would be
* invisible on a gray-50 overlay. A white overlay must not carry it.
*/
test("Should announce a light surface, so blocks on it can darken", () => {
const { getByTestId } = render(
<Overlay backgroundColor="light">
<CodeBlock>secret</CodeBlock>
</Overlay>,
)

expect(getByTestId("overlay").className).toContain(
"overlay-container-light",
)
})

test("Should not announce it when the overlay is white", () => {
const { getByTestId } = render(
<Overlay drawer onBackdropClick={() => {}}>
<CodeBlock>secret</CodeBlock>
</Overlay>,
)

expect(getByTestId("overlay").className).not.toContain(
"overlay-container-light",
)
})
})

describe("Overlay body scroll lock", () => {
beforeEach(() => {
document.body.style.overflow = ""
Expand Down
4 changes: 3 additions & 1 deletion packages/app-elements/src/ui/internals/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export const Overlay: React.FC<OverlayProps> = ({
"overlay-container",
"fixed z-50 h-full overflow-y-auto outline-hidden",
{
"bg-gray-50": backgroundColor === "light",
// the class is what a block sitting on this surface reads to pick its
// own shade: `bg-gray-50` alone is invisible on a gray-50 overlay
"bg-gray-50 overlay-container-light": backgroundColor === "light",
"bg-white": backgroundColor == null,
"inset-0 w-full": !drawer,
// Full width on mobile. There used to be a 95vw max-width here, but it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,15 +343,27 @@ function ResourceListComponent<TResource extends ListableResourceType>({
metricsQuery,
type,
query,
filters,
paginationType = "infinite",
preProcess,
...listProps
}: UseResourceListConfig<TResource> & {
}: Omit<UseResourceListConfig<TResource>, "query"> & {
paginationType?: "infinite" | "pagination"
query?: Omit<
NonNullable<UseResourceListConfig<TResource>["query"]>,
"filters"
>
/** The filters the bar computed, merged into the query below. */
filters?: QueryFilter
} & ResourceListProps<TResource>): JSX.Element {
const result = useResourceList<TResource>({
type,
query,
// the cast is nameable here, where the resource type is in scope: a query
// spread produces a fresh object type that a deferred lookup will not accept
query: {
...query,
filters,
} as UseResourceListConfig<TResource>["query"],
metricsQuery,
paginationType,
preProcess,
Expand Down Expand Up @@ -427,10 +439,10 @@ const makeFilteredList: (options: {
? undefined
: (resourceListProps.title ?? t("common.all"))
}
query={{
...query,
filters: sdkFilters,
}}
query={query}
// merged into the query by the component below, where the resource type is
// in scope and the merge can be typed
filters={sdkFilters}
metricsQuery={
metricsQuery == null
? undefined
Expand All @@ -448,6 +460,7 @@ function ResourceTableComponent<TResource extends ListableResourceType>({
type,
columns,
query,
filters,
metricsQuery,
preProcess,
paginationType = "pagination",
Expand All @@ -458,11 +471,23 @@ function ResourceTableComponent<TResource extends ListableResourceType>({
onSortChange,
defaultSort,
...tableProps
}: UseResourceTableConfig<TResource> & ResourceTableProps): JSX.Element {
}: Omit<UseResourceTableConfig<TResource>, "query"> & {
query?: Omit<
NonNullable<UseResourceTableConfig<TResource>["query"]>,
"filters"
>
/** The filters the bar computed, merged into the query below. */
filters?: QueryFilter
} & ResourceTableProps): JSX.Element {
const { ResourceTable, Pagination } = useResourceTable<TResource>({
type,
columns,
query,
// as in `ResourceListComponent`: the merge is typed here, where the resource
// type is in scope
query: {
...query,
filters,
} as UseResourceTableConfig<TResource>["query"],
metricsQuery,
preProcess,
paginationType,
Expand Down Expand Up @@ -502,10 +527,10 @@ const makeFilteredTable: (options: {
title={
hideTitle === true ? undefined : (tableProps.title ?? t("common.all"))
}
query={{
...query,
filters: sdkFilters,
}}
query={query}
// merged into the query by the component below, where the resource type is
// in scope and the merge can be typed
filters={sdkFilters}
metricsQuery={
metricsQuery == null
? undefined
Expand Down
133 changes: 133 additions & 0 deletions packages/app-elements/src/ui/resources/useResourceList/apiFlavour.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Which Commerce Layer API a list speaks to, and the types that follow from it.
*
* The Core and Provisioning SDKs are structurally parallel where a list touches
* them — same `ListableResourceType` / `ResourceFields` / `ResourceSortFields` /
* `QueryParamsList` names, a `client[type].list({ ...query, pageNumber })` call, and
* a `meta` of `pageCount / recordCount / currentPage / recordsPerPage`. So a list
* needs no new logic to serve both, only a map from the flavour to each SDK's types.
*
* Both SDKs are imported for their types only. A runtime import of either would be
* bundled into every app that renders a list, so keep every import in this file
* `import type`.
*
* See `docs/adr/0001-provisioning-api-in-resource-list.md` for why the Provisioning
* client is passed in by the caller rather than built here.
*/

import type {
CommerceLayerProvisioningClient,
ListableResourceType as ProvisioningListableResourceType,
QueryParamsList as ProvisioningQueryParamsList,
ResourceFields as ProvisioningResourceFields,
ResourceSortFields as ProvisioningResourceSortFields,
} from "@commercelayer/provisioning-sdk"
import type {
CommerceLayerBundle,
ListableResourceType as CoreListableResourceType,
QueryParamsList as CoreQueryParamsList,
ResourceFields as CoreResourceFields,
ResourceSortFields as CoreResourceSortFields,
} from "@commercelayer/sdk"

export type { ProvisioningListableResourceType }

/** The API a list speaks to. Defaults to `core` wherever it is optional. */
export type ApiFlavour = "core" | "provisioning"

/**
* Any resource type either API can list.
*
* The conditional below distributes over the `ApiFlavour` union, so this is Core's
* listable union plus Provisioning's. Used where code holds a resource type without
* knowing its flavour — a signal key, or the metrics guard — and a plain `string`
* would give up typo checking.
*/
export type AnyListableResourceType = ListableResourceTypeFor<ApiFlavour>

/** The resource types that flavour can list. */
export type ListableResourceTypeFor<TApi extends ApiFlavour> =
TApi extends "provisioning"
? ProvisioningListableResourceType
: CoreListableResourceType

/** The SDK client that flavour is reached through. */
export type ClientFor<TApi extends ApiFlavour> = TApi extends "provisioning"
? CommerceLayerProvisioningClient
: CommerceLayerBundle

/**
* Per-flavour maps from resource type to what that resource is.
*
* Written as mapped types indexed by the resource, rather than as nested
* conditionals: a deferred indexed access (`CoreResources[TResource]`) stays
* assignable in both directions while `TResource` is still generic, whereas a
* conditional does not reduce until it is resolved — which made every caller that
* builds a query (the filters stack) fail to typecheck.
*/
type CoreResources = {
[K in CoreListableResourceType]: Awaited<
ReturnType<CommerceLayerBundle[K]["list"]>
>[number]
}

type ProvisioningResources = {
[K in ProvisioningListableResourceType]: Awaited<
ReturnType<CommerceLayerProvisioningClient[K]["list"]>
>[number]
}

type CoreQueries = {
[K in CoreListableResourceType]: Omit<
CoreQueryParamsList<CoreResourceFields[K]>,
"pageNumber"
>
}

type ProvisioningQueries = {
[K in ProvisioningListableResourceType]: Omit<
ProvisioningQueryParamsList<ProvisioningResourceFields[K]>,
"pageNumber"
>
}

type CoreSortables = {
[K in CoreListableResourceType]: Extract<
keyof CoreResourceSortFields[K],
string
>
}

type ProvisioningSortables = {
[K in ProvisioningListableResourceType]: Extract<
keyof ProvisioningResourceSortFields[K],
string
>
}

/** One record of `TResource`, as that flavour's SDK returns it. */
export type ResourceFor<
TApi extends ApiFlavour,
TResource extends ListableResourceTypeFor<TApi>,
> = TApi extends "provisioning"
? ProvisioningResources[TResource & ProvisioningListableResourceType]
: CoreResources[TResource & CoreListableResourceType]

/** The list query that flavour accepts, minus the page the list itself drives. */
export type QueryParamsListFor<
TApi extends ApiFlavour,
TResource extends ListableResourceTypeFor<TApi>,
> = TApi extends "provisioning"
? ProvisioningQueries[TResource & ProvisioningListableResourceType]
: CoreQueries[TResource & CoreListableResourceType]

/**
* The attributes that flavour's API can sort `TResource` by — the single source of
* truth for whether a column may be sortable, since the API rejects anything else.
*/
export type SortableAttributeFor<
TApi extends ApiFlavour,
TResource extends ListableResourceTypeFor<TApi>,
> = TApi extends "provisioning"
? ProvisioningSortables[TResource & ProvisioningListableResourceType]
: CoreSortables[TResource & CoreListableResourceType]
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export {
type ResourceListItemTemplateProps,
type ResourceListProps,
type UseResourceListConfig,
type UseResourceListReturn,
useResourceList,
} from "./useResourceList"
Loading
Loading