@aglyn/plugins-commerce 1.0.0-beta.199 → 1.0.0-beta.201
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/package.json +13 -13
- package/src/lib/components/console/catalog-organization-card.component.js +48 -3
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/gift-cards-card.component.d.ts +9 -0
- package/src/lib/components/console/gift-cards-card.component.js +53 -28
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.d.ts +26 -0
- package/src/lib/components/console/host-orders-card.component.js +264 -194
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +12 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-page.component.js +86 -45
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.js +30 -6
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-slugs.d.ts +43 -0
- package/src/lib/components/console/product-slugs.js +60 -0
- package/src/lib/components/console/product-slugs.js.map +1 -0
- package/src/lib/components/console/products-hub-card.component.d.ts +2 -2
- package/src/lib/components/console/products-hub-card.component.js +229 -282
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +10 -5
- package/src/lib/components/console/products-hub-zone.component.js +32 -9
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/recovery-queue-card.component.d.ts +37 -5
- package/src/lib/components/console/recovery-queue-card.component.js +107 -86
- package/src/lib/components/console/recovery-queue-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +15 -3
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/console/smart-collections.d.ts +32 -0
- package/src/lib/components/console/smart-collections.js +57 -0
- package/src/lib/components/console/smart-collections.js.map +1 -0
- package/src/lib/components/console/stock-movements-card.component.d.ts +13 -4
- package/src/lib/components/console/stock-movements-card.component.js +142 -180
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +5 -0
- package/src/lib/components/product-grid.js +95 -34
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/constants/orders-list-query.d.ts +40 -0
- package/src/lib/constants/orders-list-query.js +205 -0
- package/src/lib/constants/orders-list-query.js.map +1 -0
- package/src/lib/constants/product-list-query.d.ts +42 -0
- package/src/lib/constants/product-list-query.js +217 -0
- package/src/lib/constants/product-list-query.js.map +1 -0
- package/src/lib/constants/stock-movement-query.d.ts +27 -0
- package/src/lib/constants/stock-movement-query.js +110 -0
- package/src/lib/constants/stock-movement-query.js.map +1 -0
- package/src/lib/constants/storefront-catalog-query.d.ts +103 -0
- package/src/lib/constants/storefront-catalog-query.js +312 -0
- package/src/lib/constants/storefront-catalog-query.js.map +1 -0
- package/src/lib/model/checkout-recovery.d.ts +37 -0
- package/src/lib/model/checkout-recovery.js +44 -0
- package/src/lib/model/checkout-recovery.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +7 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +80 -0
- package/src/lib/model/commerce.js +67 -6
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/gift-card-search.d.ts +24 -0
- package/src/lib/model/gift-card-search.js +48 -0
- package/src/lib/model/gift-card-search.js.map +1 -0
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/order-list-fields.d.ts +80 -0
- package/src/lib/model/order-list-fields.js +112 -0
- package/src/lib/model/order-list-fields.js.map +1 -0
- package/src/lib/server/billing-webhook.js +180 -99
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cancel-order.js +6 -6
- package/src/lib/server/cancel-order.js.map +1 -1
- package/src/lib/server/cart-checkout.js +9 -2
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/catalog.d.ts +46 -30
- package/src/lib/server/catalog.js +507 -170
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout.js +3 -1
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/collection-membership.d.ts +35 -0
- package/src/lib/server/collection-membership.js +172 -0
- package/src/lib/server/collection-membership.js.map +1 -0
- package/src/lib/server/draft-order.js +5 -3
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/gift-cards.js +7 -1
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/member-name-search.d.ts +17 -0
- package/src/lib/server/member-name-search.js +23 -1
- package/src/lib/server/member-name-search.js.map +1 -1
- package/src/lib/server/membership-account.js +7 -3
- package/src/lib/server/membership-account.js.map +1 -1
- package/src/lib/server/membership-admin-password.js +8 -2
- package/src/lib/server/membership-admin-password.js.map +1 -1
- package/src/lib/server/membership-recover.js +4 -1
- package/src/lib/server/membership-recover.js.map +1 -1
- package/src/lib/server/membership-register.js +3 -1
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/pos-order.js +9 -5
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/process-abandoned.js +20 -7
- package/src/lib/server/process-abandoned.js.map +1 -1
- package/src/lib/server/process-restock.js +6 -2
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/reserve-stock.js +3 -4
- package/src/lib/server/reserve-stock.js.map +1 -1
- package/src/lib/server/reserve.js +3 -1
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/site-page-enricher.js +3 -3
- package/src/lib/server/site-page-enricher.js.map +1 -1
- package/src/lib/server.js +4 -0
- package/src/lib/server.js.map +1 -1
|
@@ -19,36 +19,41 @@ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_wi
|
|
|
19
19
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
20
20
|
import * as Aglyn from "@aglyn/aglyn";
|
|
21
21
|
import * as CommerceModel from "../../model/index.js";
|
|
22
|
-
import {
|
|
22
|
+
import { PRODUCT_LIST_BASE, PRODUCT_LIST_FIELDS, PRODUCT_LIST_HEADERS, PRODUCT_LIST_OPTIONS, PRODUCT_LIST_QUERY, PRODUCT_LIST_SELECT_FIELDS, productListRequestClauses } from "../../constants/product-list-query.js";
|
|
23
23
|
import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
|
|
24
24
|
import ListFilterChips from "@aglyn/shared-ui-jsx/components/list-filter-chips.component";
|
|
25
25
|
import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
|
|
26
|
+
import { ListQueryNotices, listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
|
|
26
27
|
import { ListTable, listActionsColumn } from "@aglyn/shared-ui-jsx/components/list-table.component";
|
|
27
28
|
import QuotaReadoutComponent from "@aglyn/shared-ui-jsx/components/quota-readout.component";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
29
|
+
import { hiddenFilterVisibility } from "@aglyn/shared-ui-jsx/const/list-filter";
|
|
30
|
+
import { listFilterGridColumns } from "@aglyn/shared-ui-jsx/const/list-grid-filter";
|
|
30
31
|
import { useListGridFilter } from "@aglyn/shared-ui-jsx/hooks/use-list-grid-filter";
|
|
31
32
|
import { useSnackbar } from "@aglyn/shared-ui-snackstack";
|
|
32
33
|
import { Timestamp } from "@aglyn/shared-util-timestamp";
|
|
33
|
-
import {
|
|
34
|
-
import { addDoc, collection, doc, documentId, getCountFromServer, limit, orderBy, query, updateDoc, where } from "firebase/firestore";
|
|
34
|
+
import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Typography } from "@mui/material";
|
|
35
|
+
import { addDoc, collection, doc, documentId, getCountFromServer, getDocs, limit, orderBy, query, startAfter, updateDoc, where } from "firebase/firestore";
|
|
35
36
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
36
37
|
import { useFirestore } from "@aglyn/tenant-feature-instance";
|
|
37
|
-
import {
|
|
38
|
+
import { useFirestoreCollection, writeGuardedBySeed } from "@aglyn/tenant-feature-instance";
|
|
39
|
+
import { listQueryConstraints, useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
|
|
38
40
|
import { useHostResourceApi } from "@aglyn/tenant-feature-instance";
|
|
39
41
|
import { useOrgPlan } from "@aglyn/tenant-feature-instance";
|
|
40
42
|
import ProductEditorDialog from "./product-editor-dialog.component.js";
|
|
43
|
+
import { productSlugLedger } from "./product-slugs.js";
|
|
44
|
+
import { productCollectionFields, readSmartCollections } from "./smart-collections.js";
|
|
41
45
|
import ProductsHubZone from "./products-hub-zone.component.js";
|
|
42
46
|
import { pluginDocsHelp } from "@aglyn/aglyn";
|
|
43
47
|
import { useConsoleWidgetSlot } from "@aglyn/aglyn/app-utils/console-widget-slot-context";
|
|
48
|
+
/** Products read per request of the CSV export's walk. */ const EXPORT_PAGE = 500;
|
|
44
49
|
/**
|
|
45
|
-
*
|
|
50
|
+
* The most products one CSV export writes (AGL-3321).
|
|
46
51
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*/ const
|
|
52
|
+
* The export walks the table's own query page by page, so it writes what the
|
|
53
|
+
* filters and search match across the whole catalog rather than the rows on
|
|
54
|
+
* screen. It stops here because the file is built in the browser, and says
|
|
55
|
+
* so when it does.
|
|
56
|
+
*/ const EXPORT_CEILING = 10000;
|
|
52
57
|
/**
|
|
53
58
|
* How many of ONE product's license keys the dialog reads.
|
|
54
59
|
*
|
|
@@ -60,42 +65,23 @@ const STATUS_COLOR = {
|
|
|
60
65
|
draft: 'warning',
|
|
61
66
|
archived: 'default'
|
|
62
67
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
/** The grid's own columns; every other declared field is a hidden filter column. */ const PRODUCT_VISIBLE_COLUMNS = [
|
|
69
|
+
'name',
|
|
70
|
+
'status',
|
|
71
|
+
'type',
|
|
72
|
+
'priceUsd',
|
|
73
|
+
'stock',
|
|
74
|
+
'variants'
|
|
69
75
|
];
|
|
70
|
-
const PRODUCT_GRID_FILTER_HEADERS = {
|
|
71
|
-
status: 'Status'
|
|
72
|
-
};
|
|
73
|
-
const PRODUCT_GRID_FILTER_OPTIONS = {
|
|
74
|
-
status: [
|
|
75
|
-
{
|
|
76
|
-
value: 'active',
|
|
77
|
-
label: 'Active'
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
value: 'draft',
|
|
81
|
-
label: 'Draft'
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
value: 'archived',
|
|
85
|
-
label: 'Archived'
|
|
86
|
-
}
|
|
87
|
-
]
|
|
88
|
-
};
|
|
89
|
-
const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
90
76
|
/**
|
|
91
77
|
* Products hub v1 (AGL-279): the catalog manager replacing the Commerce
|
|
92
|
-
* Starter card —
|
|
93
|
-
* full editor dialog, duplicate, archive/activate, soft delete (past
|
|
78
|
+
* Starter card — a table over `hosts/{hostId}/products` whose search and
|
|
79
|
+
* filters are its Firestore query (AGL-3321), full editor dialog, duplicate, archive/activate, soft delete (past
|
|
94
80
|
* order rows keep resolving). Product cap (`productsPerHost`, AGL-278)
|
|
95
81
|
* gated here on create/duplicate/import (AGL-471); server-side
|
|
96
82
|
* enforcement of the client-write path rides AGL-473.
|
|
97
83
|
*/ export function ProductsHubCard(props) {
|
|
98
|
-
var
|
|
84
|
+
var _ref, _ref1, _ref2, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10, _ref11, _ref12;
|
|
99
85
|
var _importing_parsed, _importing_parsed1, _importing_parsed2;
|
|
100
86
|
const { hostId } = props;
|
|
101
87
|
const firestore = useFirestore();
|
|
@@ -106,38 +92,13 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
106
92
|
const { org, ready: planReady } = useOrgPlan(hostId);
|
|
107
93
|
const { confirm } = useConfirmationContext();
|
|
108
94
|
/*
|
|
109
|
-
* The grid's
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
selectFields: PRODUCT_GRID_SELECT_FIELDS,
|
|
115
|
-
search: {
|
|
116
|
-
words: searchWords,
|
|
117
|
-
onChange: setSearchWords
|
|
118
|
-
}
|
|
95
|
+
* The grid's Filters panel and quick search, bound to the clauses the
|
|
96
|
+
* query below serves (AGL-3321). Every clause and the search word go on
|
|
97
|
+
* that query; see `constants/product-list-query.ts` for what is offered.
|
|
98
|
+
*/ const gridFilter = useListGridFilter({
|
|
99
|
+
selectFields: PRODUCT_LIST_SELECT_FIELDS
|
|
119
100
|
});
|
|
120
|
-
const
|
|
121
|
-
/*
|
|
122
|
-
* The Status clause the query can serve — only while no search is typed,
|
|
123
|
-
* because the query carries one predicate.
|
|
124
|
-
*/ const servedStatus = search ? null : (_filterClauses_find = filterClauses.find((clause)=>clause.field === 'status' && listFilterConstraints(PRODUCT_LIST_FILTER_FIELDS, clause) !== null)) != null ? _filterClauses_find : null;
|
|
125
|
-
/*
|
|
126
|
-
* WHICH field the search box searches (AGL-2501). A SCOPE of the search,
|
|
127
|
-
* not a filter: it picks which served query the search runs, so it stays
|
|
128
|
-
* beside the grid rather than in its panel.
|
|
129
|
-
*
|
|
130
|
-
* The box used to compare four fields at once — name, slug, tag, SKU — over
|
|
131
|
-
* the rows the listener had already fetched. Server-side that is not one
|
|
132
|
-
* query: Firestore allows a single `array-contains` per query and cannot OR
|
|
133
|
-
* across fields, so four fields at once means four queries and a merge, and
|
|
134
|
-
* a merged result cannot be paged or capped coherently.
|
|
135
|
-
*
|
|
136
|
-
* Naming the field is the honest trade. It buys a search that reaches the
|
|
137
|
-
* WHOLE catalog instead of an arbitrary five hundred rows of it, which is
|
|
138
|
-
* the case the old box got wrong — and a merchant looking for a SKU knows
|
|
139
|
-
* they are looking for a SKU.
|
|
140
|
-
*/ const [searchField, setSearchField] = useState('name');
|
|
101
|
+
const filtering = gridFilter.clauses.length > 0 || gridFilter.searchWords.some((word)=>word.trim());
|
|
141
102
|
const [editing, setEditing] = useState(null);
|
|
142
103
|
const [creating, setCreating] = useState(false);
|
|
143
104
|
const [adjusting, setAdjusting] = useState(null);
|
|
@@ -151,68 +112,48 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
151
112
|
*/ const [importOptions, setImportOptions] = useState({});
|
|
152
113
|
const [lastImport, setLastImport] = useState(null);
|
|
153
114
|
const WidgetSlot = useConsoleWidgetSlot();
|
|
154
|
-
const {
|
|
115
|
+
const { rows: productDocs, data: productWindow, hasMore, page, setPage, pageSize, setPageSize, plan, status: productsStatus, /**
|
|
155
116
|
* The product rows the stock dialog is seeded from are unconfirmed by
|
|
156
117
|
* the server (AGL-1358). A stock adjustment does not write the delta —
|
|
157
118
|
* it recomputes the WHOLE `variants` array from the seeded product and
|
|
158
119
|
* replaces it, so a cached seed silently reverts every sale, return and
|
|
159
120
|
* adjustment the server has recorded since that snapshot, on every
|
|
160
121
|
* variant and every location.
|
|
161
|
-
*/ fromCache: productsFromCache } =
|
|
122
|
+
*/ fromCache: productsFromCache } = useListQuery({
|
|
162
123
|
/*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
op: 'contains',
|
|
197
|
-
value: search
|
|
198
|
-
} : servedStatus);
|
|
199
|
-
return query(collection(firestore, 'hosts', hostId, 'products'), ...constraints != null ? constraints : [
|
|
200
|
-
orderBy(documentId())
|
|
201
|
-
], /*
|
|
202
|
-
* `CATALOG_CEILING + 1` is a PROBE (AGL-2501). One document past the
|
|
203
|
-
* ceiling turns "this catalog is larger than the table" into a fact.
|
|
204
|
-
* The probe row is dropped below and never rendered, exported or
|
|
205
|
-
* counted.
|
|
206
|
-
*/ limit(CATALOG_CEILING + 1));
|
|
207
|
-
}, [
|
|
208
|
-
firestore,
|
|
209
|
-
hostId,
|
|
210
|
-
search,
|
|
211
|
-
searchField,
|
|
212
|
-
servedStatus == null ? void 0 : servedStatus.op,
|
|
213
|
-
servedStatus == null ? void 0 : servedStatus.value
|
|
214
|
-
], {
|
|
215
|
-
idField: '$id'
|
|
124
|
+
* PAGED BY THE QUERY, and every filter ON it (AGL-3321).
|
|
125
|
+
*
|
|
126
|
+
* The table read a five-hundred-product window and put ONE predicate on
|
|
127
|
+
* it — the search or the Status clause — then matched Status beside a
|
|
128
|
+
* search, dropped soft-deleted products and sorted by name over the rows
|
|
129
|
+
* it held, and paged that. Past five hundred products, a match past the
|
|
130
|
+
* window answered "no products match", and a page came back short by
|
|
131
|
+
* however many deleted products it had held.
|
|
132
|
+
*
|
|
133
|
+
* Now the plan puts the search word and every clause on one query under
|
|
134
|
+
* the `deletedAt == null` scope, ordered by `nameLower`, and each page is
|
|
135
|
+
* a page of that query — one page plus a probe row, widened as the reader
|
|
136
|
+
* pages forward. Nothing is matched over the rows afterwards; a clause
|
|
137
|
+
* the query cannot hold is refused by name (`ListQueryNotices` below).
|
|
138
|
+
*/ collection: collection(firestore, 'hosts', hostId, 'products'),
|
|
139
|
+
declaration: PRODUCT_LIST_QUERY,
|
|
140
|
+
request: {
|
|
141
|
+
clauses: productListRequestClauses(gridFilter.clauses),
|
|
142
|
+
search: gridFilter.searchWords,
|
|
143
|
+
base: PRODUCT_LIST_BASE
|
|
144
|
+
},
|
|
145
|
+
deps: [
|
|
146
|
+
firestore,
|
|
147
|
+
hostId
|
|
148
|
+
],
|
|
149
|
+
idField: '$id',
|
|
150
|
+
/*
|
|
151
|
+
* The scope and the filters read MUTABLE fields — a delete sets
|
|
152
|
+
* `deletedAt`, Archive moves `status`, a rename moves the tokens — so a
|
|
153
|
+
* product can leave this query mid-session, and the SDK can cache that
|
|
154
|
+
* departure as a tombstone at the product's own path (AGL-1196). A
|
|
155
|
+
* disappearance is confirmed against the server before it is believed.
|
|
156
|
+
*/ confirmDisappearances: true
|
|
216
157
|
});
|
|
217
158
|
/*
|
|
218
159
|
* License key pool (AGL-308) for the open dialog's product — asked FOR that
|
|
@@ -250,69 +191,15 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
250
191
|
], {
|
|
251
192
|
idField: '$id'
|
|
252
193
|
});
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
* substring, so a two-word search would have been sent to the server as
|
|
260
|
-
* its first word and then dropped again here by a whole-string compare
|
|
261
|
-
* that the matching row never satisfies: rows found, then hidden.
|
|
262
|
-
*
|
|
263
|
-
* The panel's clauses are matched here, and Status only earns its keep
|
|
264
|
-
* when a search is also active — that is the one combination the single
|
|
265
|
-
* predicate above cannot express, and here it runs over rows the server
|
|
266
|
-
* already matched across the whole catalog rather than over an arbitrary
|
|
267
|
-
* window. No search words are passed: the query answered them.
|
|
268
|
-
*
|
|
269
|
-
* Soft-deleted products are still dropped here rather than in the query:
|
|
270
|
-
* `deletedAt` is absent on a live product, and Firestore cannot ask for
|
|
271
|
-
* documents that LACK a field.
|
|
272
|
-
*/ const live = (productDocs != null ? productDocs : []).slice(0, CATALOG_CEILING).filter((product)=>!product.deletedAt).map((product)=>_extends({}, CommerceModel.liftLegacyProduct(product), {
|
|
194
|
+
/*
|
|
195
|
+
* The PAGE on screen, lifted from legacy shapes. It is what the table
|
|
196
|
+
* draws, the reserved-stock clock watches and the zone hands its widgets,
|
|
197
|
+
* and nothing narrows it: the query already answered every filter, and
|
|
198
|
+
* soft-deleted products are outside its scope.
|
|
199
|
+
*/ const products = useMemo(()=>(productDocs != null ? productDocs : []).map((product)=>_extends({}, CommerceModel.liftLegacyProduct(product), {
|
|
273
200
|
$id: product.$id
|
|
274
|
-
}))
|
|
275
|
-
|
|
276
|
-
paths: [],
|
|
277
|
-
words: []
|
|
278
|
-
}).sort((a, b)=>a.name.localeCompare(b.name));
|
|
279
|
-
}, [
|
|
280
|
-
productDocs,
|
|
281
|
-
filterClauses
|
|
282
|
-
]);
|
|
283
|
-
/*==========================================
|
|
284
|
-
* THE TABLE PAGES, and the READ deliberately does not (AGL-2501).
|
|
285
|
-
*
|
|
286
|
-
* The card rendered every row of a five-hundred-document window in one wall
|
|
287
|
-
* with no footer under it — the shape this sweep is about. What it must not
|
|
288
|
-
* become is a server-paged query, because three other things in this file
|
|
289
|
-
* read the same window and every one of them needs it whole:
|
|
290
|
-
*
|
|
291
|
-
* * `handleExport` writes the CSV from these rows. A ten-row page would
|
|
292
|
-
* silently export ten products under a filename that claims the catalog.
|
|
293
|
-
* * the CSV importer builds `existingSlugs` from them to refuse a
|
|
294
|
-
* duplicate slug. Narrowed to a page, it would stop seeing the clash and
|
|
295
|
-
* create the duplicate it exists to prevent.
|
|
296
|
-
* * the reserved-stock clock arms off them.
|
|
297
|
-
*
|
|
298
|
-
* The window is also already correct: `orderBy(documentId())` unfiltered,
|
|
299
|
-
* `nameLower` under a name filter, and the filters themselves reach the
|
|
300
|
-
* whole collection through the query. So the fix here is the CONTROL, not
|
|
301
|
-
* the read — and the head-count beside it has been a server aggregate since
|
|
302
|
-
* AGL-1716.
|
|
303
|
-
*=========================================*/ const [page, setPage] = useState(0);
|
|
304
|
-
const [pageSize, setPageSize] = useState(TABLE_PAGE_SIZE_DEFAULT);
|
|
305
|
-
// A filter narrows the list under the reader's feet, and page four of the
|
|
306
|
-
// unfiltered catalog is not a position in the filtered one.
|
|
307
|
-
useEffect(()=>setPage(0), [
|
|
308
|
-
search,
|
|
309
|
-
searchField,
|
|
310
|
-
filterClauses
|
|
311
|
-
]);
|
|
312
|
-
const visibleProducts = useMemo(()=>products.slice(page * pageSize, page * pageSize + pageSize), [
|
|
313
|
-
products,
|
|
314
|
-
page,
|
|
315
|
-
pageSize
|
|
201
|
+
})), [
|
|
202
|
+
productDocs
|
|
316
203
|
]);
|
|
317
204
|
/**
|
|
318
205
|
* A CLOCK, because a hold lapses without anybody writing anything
|
|
@@ -326,7 +213,8 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
326
213
|
*
|
|
327
214
|
* Only runs while something is actually held, so an ordinary catalog costs
|
|
328
215
|
* no timer. A NEW hold arrives as a document change, which re-renders, which
|
|
329
|
-
* re-arms this.
|
|
216
|
+
* re-arms this. It watches the page on screen, which is every row whose
|
|
217
|
+
* caption it keeps true.
|
|
330
218
|
*/ const [nowMs, setNowMs] = useState(()=>Date.now());
|
|
331
219
|
const anyHeld = useMemo(()=>products.some((product)=>CommerceModel.heldProductUnits(product, nowMs) > 0), [
|
|
332
220
|
products,
|
|
@@ -360,10 +248,10 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
360
248
|
* excluded them. The card now asks the enforcing route's question, in the
|
|
361
249
|
* enforcing route's terms.
|
|
362
250
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* where a listener refreshed for free, so the count is
|
|
366
|
-
* mutation that moves it.
|
|
251
|
+
* The table pages its own query (AGL-3321) and answers a different
|
|
252
|
+
* question — which products match — so it never stands in for this. A
|
|
253
|
+
* one-shot goes stale where a listener refreshed for free, so the count is
|
|
254
|
+
* re-read after any mutation that moves it.
|
|
367
255
|
*
|
|
368
256
|
* No counting RULE moves: `checkQuota` is untouched and `report-usage`
|
|
369
257
|
* meters contacts, storage and API requests — never the catalog.
|
|
@@ -387,12 +275,11 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
387
275
|
hostId,
|
|
388
276
|
productCountEpoch
|
|
389
277
|
]);
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// overstate,
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
]);
|
|
278
|
+
// The fallback, pending or denied: the live products the table's query has
|
|
279
|
+
// read, probe row included. Each is a product the site has, so it can only
|
|
280
|
+
// UNDERSTATE, never overstate, and nothing it gates fires on a count larger
|
|
281
|
+
// than the truth (AGL-471).
|
|
282
|
+
const loadedProductCount = (_ref1 = productWindow == null ? void 0 : productWindow.length) != null ? _ref1 : 0;
|
|
396
283
|
const productCount = serverProductCount != null ? serverProductCount : loadedProductCount;
|
|
397
284
|
// Gate only once the org doc has loaded: an unresolved org reads as the
|
|
398
285
|
// free tier's 0-product cap, which swallowed every Add/Duplicate click.
|
|
@@ -419,17 +306,31 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
419
306
|
persist: false
|
|
420
307
|
});
|
|
421
308
|
}
|
|
422
|
-
|
|
423
|
-
|
|
309
|
+
// The source's SKU and barcode keys are not copied: the copy's own are
|
|
310
|
+
// derived below, and a copy of a product with none must have none.
|
|
311
|
+
const { $id: _sourceId, skus: _skus, barcodes: _barcodes } = product, copy = _object_without_properties_loose(product, [
|
|
312
|
+
"$id",
|
|
313
|
+
"skus",
|
|
314
|
+
"barcodes"
|
|
424
315
|
]);
|
|
425
316
|
try {
|
|
317
|
+
/*
|
|
318
|
+
* The copy's slug is asked of the store, so a second Duplicate of
|
|
319
|
+
* the same product does not mint a second `-copy` (AGL-3321), and
|
|
320
|
+
* its search keys are the COPY's: spreading the source's would sort
|
|
321
|
+
* and find it by the source's name.
|
|
322
|
+
*/ const slug = await productSlugLedger(firestore, hostId).claim(CommerceModel.commerceSlug(`${product.slug}-copy`));
|
|
426
323
|
// Duplicate is a create — rides the quota-enforcing API (AGL-473).
|
|
427
324
|
await createHostResource({
|
|
428
325
|
hostId,
|
|
429
326
|
resource: 'product',
|
|
430
|
-
data: _extends({}, copy, {
|
|
327
|
+
data: _extends({}, copy, CommerceModel.productSearchFields({
|
|
431
328
|
name: `${product.name} (copy)`,
|
|
432
|
-
|
|
329
|
+
variants: product.variants
|
|
330
|
+
}), CommerceModel.productStockFields(product), await productCollectionFields(firestore, hostId, _extends({}, product, {
|
|
331
|
+
name: `${product.name} (copy)`
|
|
332
|
+
})), {
|
|
333
|
+
slug,
|
|
433
334
|
status: 'draft',
|
|
434
335
|
createdAtMs: Date.now(),
|
|
435
336
|
updatedAtMs: Date.now()
|
|
@@ -451,6 +352,7 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
451
352
|
});
|
|
452
353
|
}
|
|
453
354
|
}, [
|
|
355
|
+
firestore,
|
|
454
356
|
hostId,
|
|
455
357
|
createHostResource,
|
|
456
358
|
enqueueSnackbar,
|
|
@@ -498,22 +400,72 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
498
400
|
hostId,
|
|
499
401
|
enqueueSnackbar
|
|
500
402
|
]);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
403
|
+
/*
|
|
404
|
+
* CSV import/export (AGL-282): Shopify-dialect columns, dry-run first.
|
|
405
|
+
*
|
|
406
|
+
* The export writes what the table's filters and search MATCH, across the
|
|
407
|
+
* whole catalog (AGL-3321): it walks the table's own query — the same plan,
|
|
408
|
+
* so the same scope, predicates and order — a page of `EXPORT_PAGE` at a
|
|
409
|
+
* time from a cursor, rather than writing the rows on screen. A walk that
|
|
410
|
+
* reaches `EXPORT_CEILING` stops, asks once whether anything is left, and
|
|
411
|
+
* says so when something is.
|
|
412
|
+
*/ const [exporting, setExporting] = useState(false);
|
|
413
|
+
const handleExport = useCallback(async ()=>{
|
|
414
|
+
setExporting(true);
|
|
415
|
+
try {
|
|
416
|
+
const productsRef = collection(firestore, 'hosts', hostId, 'products');
|
|
417
|
+
const constraints = listQueryConstraints(plan);
|
|
418
|
+
const exported = [];
|
|
419
|
+
let cursor = null;
|
|
420
|
+
let more = false;
|
|
421
|
+
for(;;){
|
|
422
|
+
const room = EXPORT_CEILING - exported.length;
|
|
423
|
+
const snapshot = await getDocs(query(productsRef, ...constraints, ...cursor ? [
|
|
424
|
+
startAfter(cursor)
|
|
425
|
+
] : [], limit(room > 0 ? Math.min(EXPORT_PAGE, room) : 1)));
|
|
426
|
+
if (room <= 0) {
|
|
427
|
+
more = !snapshot.empty;
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
for (const product of snapshot.docs){
|
|
431
|
+
exported.push(_extends({}, CommerceModel.liftLegacyProduct(product.data()), {
|
|
432
|
+
$id: product.id
|
|
433
|
+
}));
|
|
434
|
+
}
|
|
435
|
+
if (snapshot.docs.length < Math.min(EXPORT_PAGE, room)) break;
|
|
436
|
+
cursor = snapshot.docs[snapshot.docs.length - 1];
|
|
437
|
+
}
|
|
438
|
+
const csv = CommerceModel.productsToCsv(exported);
|
|
439
|
+
const blob = new Blob([
|
|
440
|
+
csv
|
|
441
|
+
], {
|
|
442
|
+
type: 'text/csv'
|
|
443
|
+
});
|
|
444
|
+
const anchor = document.createElement('a');
|
|
445
|
+
anchor.href = URL.createObjectURL(blob);
|
|
446
|
+
anchor.download = `products-${hostId}.csv`;
|
|
447
|
+
anchor.click();
|
|
448
|
+
URL.revokeObjectURL(anchor.href);
|
|
449
|
+
if (more) {
|
|
450
|
+
enqueueSnackbar(`Exported the first ${EXPORT_CEILING.toLocaleString()} matching ` + 'products — the most one file holds. Filter the list to export the rest.', {
|
|
451
|
+
variant: 'info',
|
|
452
|
+
persist: false
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
var _ref;
|
|
457
|
+
enqueueSnackbar((_ref = error == null ? void 0 : error.message) != null ? _ref : 'Could not export the products', {
|
|
458
|
+
variant: 'warning',
|
|
459
|
+
persist: false
|
|
460
|
+
});
|
|
461
|
+
} finally{
|
|
462
|
+
setExporting(false);
|
|
463
|
+
}
|
|
514
464
|
}, [
|
|
515
|
-
|
|
516
|
-
hostId
|
|
465
|
+
firestore,
|
|
466
|
+
hostId,
|
|
467
|
+
plan,
|
|
468
|
+
enqueueSnackbar
|
|
517
469
|
]);
|
|
518
470
|
const handleImportApply = useCallback(async ()=>{
|
|
519
471
|
const parsed = importing == null ? void 0 : importing.parsed;
|
|
@@ -530,24 +482,31 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
530
482
|
persist: false
|
|
531
483
|
});
|
|
532
484
|
}
|
|
533
|
-
|
|
485
|
+
/*
|
|
486
|
+
* The duplicate-slug check asks the STORE (AGL-3321): every slug the file
|
|
487
|
+
* names, by `slug in […]`, and any suffixed one only when it is needed.
|
|
488
|
+
* It asked the table's rows, which held five hundred products and now
|
|
489
|
+
* hold one page, so a slug past them was handed out a second time.
|
|
490
|
+
*/ const slugs = productSlugLedger(firestore, hostId);
|
|
534
491
|
const created = [];
|
|
535
492
|
try {
|
|
493
|
+
await slugs.ask(parsed.products.map((product)=>product.slug));
|
|
494
|
+
// One read of the host's smart collections for the whole file.
|
|
495
|
+
const smartCollections = await readSmartCollections(firestore, hostId);
|
|
536
496
|
// Each create rides the quota-enforcing API (AGL-473); the batch cap
|
|
537
497
|
// above short-circuits before we start, so this loop stays bounded.
|
|
538
498
|
for (const product of parsed.products){
|
|
539
499
|
var _ref, _ref1;
|
|
540
500
|
var _product_variants_, _product_mediaUrls;
|
|
541
|
-
|
|
542
|
-
while(existingSlugs.has(slug))slug = `${product.slug}-${Date.now() % 1000}`;
|
|
543
|
-
existingSlugs.add(slug);
|
|
501
|
+
const slug = await slugs.claim(product.slug);
|
|
544
502
|
const { id } = await createHostResource({
|
|
545
503
|
hostId,
|
|
546
504
|
resource: 'product',
|
|
547
|
-
data: _extends({}, product, CommerceModel.productSearchFields(product), {
|
|
505
|
+
data: _extends({}, product, CommerceModel.productSearchFields(product), CommerceModel.productStockFields(product), {
|
|
506
|
+
// Its smart collections (AGL-3321).
|
|
507
|
+
collectionIds: CommerceModel.productCollectionIds(product, smartCollections),
|
|
548
508
|
slug,
|
|
549
509
|
priceUsd: (_ref = (_product_variants_ = product.variants[0]) == null ? void 0 : _product_variants_.priceUsd) != null ? _ref : 0,
|
|
550
|
-
inventory: CommerceModel.productInventory(product),
|
|
551
510
|
imageUrl: (_ref1 = (_product_mediaUrls = product.mediaUrls) == null ? void 0 : _product_mediaUrls[0]) != null ? _ref1 : null,
|
|
552
511
|
createdAtMs: Date.now(),
|
|
553
512
|
updatedAtMs: Date.now()
|
|
@@ -576,7 +535,7 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
576
535
|
});
|
|
577
536
|
}, [
|
|
578
537
|
importing,
|
|
579
|
-
|
|
538
|
+
firestore,
|
|
580
539
|
hostId,
|
|
581
540
|
createHostResource,
|
|
582
541
|
enqueueSnackbar,
|
|
@@ -605,13 +564,13 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
605
564
|
unreadable: productsStatus === 'error',
|
|
606
565
|
fromCache: productsFromCache
|
|
607
566
|
}, async ()=>{
|
|
608
|
-
await updateDoc(doc(firestore, 'hosts', hostId, 'products', adjusting.product.$id), {
|
|
609
|
-
variants
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
567
|
+
await updateDoc(doc(firestore, 'hosts', hostId, 'products', adjusting.product.$id), _extends({
|
|
568
|
+
variants
|
|
569
|
+
}, CommerceModel.productStockFields(_extends({}, adjusting.product, {
|
|
570
|
+
variants
|
|
571
|
+
})), {
|
|
613
572
|
updatedAtMs: Date.now()
|
|
614
|
-
});
|
|
573
|
+
}));
|
|
615
574
|
// Adjustment history (AGL-281): the same log the sale webhook writes.
|
|
616
575
|
await addDoc(collection(firestore, 'hosts', hostId, 'inventoryAdjustments'), _extends({
|
|
617
576
|
productId: adjusting.product.$id,
|
|
@@ -813,7 +772,7 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
813
772
|
}), {
|
|
814
773
|
width: 470
|
|
815
774
|
})
|
|
816
|
-
],
|
|
775
|
+
], PRODUCT_LIST_FIELDS, PRODUCT_LIST_OPTIONS, PRODUCT_LIST_HEADERS);
|
|
817
776
|
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
818
777
|
/*
|
|
819
778
|
* The header count is the SITE'S catalog, not the rows in hand.
|
|
@@ -825,7 +784,7 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
825
784
|
* the aggregate counts the whole catalog and the view counts matches, so
|
|
826
785
|
* neither describes what the reader is looking at, and the footer's own
|
|
827
786
|
* count line answers it instead.
|
|
828
|
-
*/ header:
|
|
787
|
+
*/ header: filtering ? 'Products' : `Products${productCount ? ` (${productCount})` : ''}`,
|
|
829
788
|
help: pluginDocsHelp('commerce', {
|
|
830
789
|
anchor: '#products-hub'
|
|
831
790
|
}),
|
|
@@ -842,30 +801,6 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
842
801
|
},
|
|
843
802
|
spacing: 1,
|
|
844
803
|
children: [
|
|
845
|
-
/*#__PURE__*/ _jsxs(TextField, {
|
|
846
|
-
label: "Search in",
|
|
847
|
-
value: searchField,
|
|
848
|
-
onChange: (event)=>setSearchField(event.target.value),
|
|
849
|
-
size: "small",
|
|
850
|
-
select: true,
|
|
851
|
-
sx: {
|
|
852
|
-
minWidth: 170
|
|
853
|
-
},
|
|
854
|
-
children: [
|
|
855
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
856
|
-
value: "name",
|
|
857
|
-
children: 'Name'
|
|
858
|
-
}),
|
|
859
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
860
|
-
value: "skus",
|
|
861
|
-
children: 'SKU (whole)'
|
|
862
|
-
}),
|
|
863
|
-
/*#__PURE__*/ _jsx(MenuItem, {
|
|
864
|
-
value: "barcodes",
|
|
865
|
-
children: 'Barcode (whole)'
|
|
866
|
-
})
|
|
867
|
-
]
|
|
868
|
-
}),
|
|
869
804
|
/*#__PURE__*/ _jsx(Box, {
|
|
870
805
|
sx: {
|
|
871
806
|
flex: 1
|
|
@@ -902,9 +837,10 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
902
837
|
}),
|
|
903
838
|
/*#__PURE__*/ _jsx(Button, {
|
|
904
839
|
size: "small",
|
|
905
|
-
|
|
840
|
+
// Nothing matches: there is nothing for the walk to write.
|
|
841
|
+
disabled: exporting || products.length === 0 && page === 0,
|
|
906
842
|
onClick: handleExport,
|
|
907
|
-
children: 'Export'
|
|
843
|
+
children: exporting ? 'Exporting…' : 'Export'
|
|
908
844
|
})
|
|
909
845
|
]
|
|
910
846
|
}),
|
|
@@ -922,42 +858,53 @@ const PRODUCT_GRID_SELECT_FIELDS = Object.keys(PRODUCT_GRID_FILTER_OPTIONS);
|
|
|
922
858
|
onCreated: onCatalogCreated
|
|
923
859
|
}),
|
|
924
860
|
/*#__PURE__*/ _jsx(ListFilterChips, {
|
|
925
|
-
fields:
|
|
926
|
-
headers:
|
|
927
|
-
clauses:
|
|
861
|
+
fields: PRODUCT_LIST_FIELDS,
|
|
862
|
+
headers: PRODUCT_LIST_HEADERS,
|
|
863
|
+
clauses: gridFilter.clauses,
|
|
928
864
|
onChange: gridFilter.setClauses,
|
|
929
|
-
options:
|
|
930
|
-
|
|
865
|
+
options: PRODUCT_LIST_OPTIONS
|
|
866
|
+
}),
|
|
867
|
+
/*#__PURE__*/ _jsx(ListQueryNotices, {
|
|
868
|
+
refused: listQueryRefusals(plan.refused, {
|
|
869
|
+
fields: PRODUCT_LIST_FIELDS,
|
|
870
|
+
headers: PRODUCT_LIST_HEADERS,
|
|
871
|
+
options: PRODUCT_LIST_OPTIONS
|
|
872
|
+
}),
|
|
873
|
+
notices: plan.notices
|
|
931
874
|
}),
|
|
932
875
|
/*#__PURE__*/ _jsx(ListTable, {
|
|
933
876
|
"aria-label": "Products",
|
|
934
|
-
rows:
|
|
877
|
+
rows: products,
|
|
935
878
|
columns: productColumns,
|
|
879
|
+
loading: productsStatus === 'loading',
|
|
880
|
+
// One page of the query, turned by the footer below: the grid
|
|
881
|
+
// neither slices, filters nor sorts it.
|
|
882
|
+
hideFooter: true,
|
|
936
883
|
filterMode: "server",
|
|
937
884
|
filterModel: gridFilter.filterModel,
|
|
938
885
|
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
939
|
-
// The search box drives the served query above.
|
|
940
886
|
quickFilter: true,
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
887
|
+
// The one order the list offers, by name: the query's.
|
|
888
|
+
sortingMode: "server",
|
|
889
|
+
disableColumnSorting: true,
|
|
890
|
+
initialState: {
|
|
891
|
+
columns: {
|
|
892
|
+
columnVisibilityModel: hiddenFilterVisibility(PRODUCT_LIST_FIELDS, PRODUCT_VISIBLE_COLUMNS)
|
|
893
|
+
}
|
|
894
|
+
},
|
|
895
|
+
noRowsLabel: filtering ? 'No products match these filters' : 'No products yet',
|
|
896
|
+
noRowsDescription: filtering ? undefined : 'Build your catalog: add a product, then drop commerce ' + 'blocks on any screen in Besigner.'
|
|
945
897
|
}),
|
|
946
|
-
products.length === 0 ? null :
|
|
898
|
+
products.length === 0 && page === 0 && productsStatus !== 'loading' ? null : /* The count line is the PAGE's. `hasMore` is a fact from the probe
|
|
899
|
+
row; the site's total is `productCount` above, which counts the
|
|
900
|
+
whole catalog rather than the matches, so it is not this. */ /*#__PURE__*/ _jsx(ListPagination, {
|
|
947
901
|
page: page,
|
|
948
902
|
pageSize: pageSize,
|
|
949
|
-
rowCount:
|
|
950
|
-
|
|
951
|
-
// full below the ceiling. NOT the catalog total — that is
|
|
952
|
-
// `productCount`, and it answers a different question.
|
|
953
|
-
count: products.length,
|
|
903
|
+
rowCount: products.length,
|
|
904
|
+
hasMore: hasMore,
|
|
954
905
|
onPageChange: setPage,
|
|
955
906
|
onPageSizeChange: setPageSize
|
|
956
|
-
})
|
|
957
|
-
catalogTruncated ? /*#__PURE__*/ _jsx(Alert, {
|
|
958
|
-
severity: "info",
|
|
959
|
-
children: `This table holds ${CATALOG_CEILING} products at a time and this ` + 'catalog is larger. Search, and Status on its own, reach every ' + 'product; the CSV export covers what the table holds.'
|
|
960
|
-
}) : null
|
|
907
|
+
})
|
|
961
908
|
]
|
|
962
909
|
}),
|
|
963
910
|
/*#__PURE__*/ _jsxs(Dialog, {
|