@wix/agent-skills 1.19.2 → 1.20.1
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 +1 -1
- package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- package/skills/wix-headless-fast/SKILL.md +200 -34
- package/skills/wix-headless-fast/entry/skill.md +10 -2
- package/skills/wix-headless-fast/install/attach.mjs +253 -0
- package/skills/wix-headless-fast/install/deploy.mjs +109 -9
- package/skills/wix-headless-fast/install/fast-path.mjs +28 -31
- package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
- package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
- package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
- package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
- package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
- package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
- package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
- package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
- package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
- package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
- package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
- package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
- package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
- package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
- package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
- package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
- package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
- package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
- package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
- package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
- package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
- package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
- package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
- package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
- package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
- package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
- package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
- package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
- package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
- package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
- package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
- package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
- package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
- package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
- package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
- package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
- package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
- package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
- package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
- package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
- package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
- package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
- package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
- package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
- package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
- package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
- package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
- package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
- package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
- package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
- package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
- package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
- package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
- package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
- package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
- package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
- package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
- package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
- package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
- package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
- package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
- package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
- package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +55 -1
- package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
- package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
- package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
- package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +371 -81
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +136 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -130
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -182
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +53 -136
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +73 -230
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
- package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -5
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +24 -0
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +42 -41
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
1
|
+
// React binding of the item store (wix/cms/item-store.ts) — one item by `_id` or by a field match
|
|
2
|
+
// (slug routing), the loader lives there, framework-free. SSR-friendly: pass the server-fetched
|
|
3
|
+
// item as `initialItem` and no client fetch happens; a SPA passes nothing.
|
|
4
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
5
|
+
import { createItemStore, type ItemRef, type ItemStore } from "../../wix/cms/item-store";
|
|
5
6
|
import type { CmsItem } from "../../wix/cms/types";
|
|
6
7
|
|
|
7
8
|
/** Exactly one of `id` / `by`. */
|
|
8
|
-
export
|
|
9
|
-
id?: string;
|
|
10
|
-
by?: { field: string; value: string | number };
|
|
11
|
-
}
|
|
9
|
+
export type UseItemRef = ItemRef;
|
|
12
10
|
|
|
13
11
|
export interface UseItemOptions {
|
|
14
12
|
initialItem?: CmsItem;
|
|
@@ -25,33 +23,15 @@ export interface UseItem {
|
|
|
25
23
|
|
|
26
24
|
export function useItem(collectionId: string, ref: UseItemRef, options: UseItemOptions = {}): UseItem {
|
|
27
25
|
const { initialItem, include } = options;
|
|
28
|
-
|
|
29
|
-
const [
|
|
30
|
-
const
|
|
31
|
-
|
|
26
|
+
// A new ref (another id or slug) is another item — a fresh store, as useProductDetail does per slug.
|
|
27
|
+
const key = JSON.stringify([collectionId, ref.id ?? null, ref.by?.field ?? null, ref.by?.value ?? null]);
|
|
28
|
+
const holder = useRef<{ key: string; store: ItemStore } | null>(null);
|
|
29
|
+
if (!holder.current || holder.current.key !== key) holder.current = { key, store: createItemStore({ collectionId, ref, initialItem, include }) };
|
|
30
|
+
const store = holder.current.store;
|
|
32
31
|
useEffect(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
? getItemBy(collectionId, ref.by.field, ref.by.value, { include })
|
|
39
|
-
: Promise.reject(new Error("useItem: pass ref.id or ref.by"));
|
|
40
|
-
fetching
|
|
41
|
-
.then((found) => {
|
|
42
|
-
if (!alive) return;
|
|
43
|
-
setItem(found);
|
|
44
|
-
setNotFound(found === null);
|
|
45
|
-
})
|
|
46
|
-
.catch((e) => {
|
|
47
|
-
if (!alive) return;
|
|
48
|
-
setError(e instanceof Error ? e.message : String(e));
|
|
49
|
-
});
|
|
50
|
-
return () => {
|
|
51
|
-
alive = false;
|
|
52
|
-
};
|
|
53
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
-
}, [collectionId, ref.id, ref.by?.field, ref.by?.value]);
|
|
55
|
-
|
|
56
|
-
return { item, notFound, error };
|
|
32
|
+
store.start();
|
|
33
|
+
return () => store.stop();
|
|
34
|
+
}, [store]);
|
|
35
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
36
|
+
return { item: state.item, notFound: state.notFound, error: state.error };
|
|
57
37
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// One collection as list state, framework-free — the logic behind useCollection, usable from React
|
|
2
|
+
// (useCollection wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe
|
|
3
|
+
// and render), or as the specification for a port. Same shape as the storefront stores: state,
|
|
4
|
+
// actions, subscribe/getState, emit after every change.
|
|
5
|
+
//
|
|
6
|
+
// The query (collection, filters, sort, page size, includes) is identified BY VALUE; a changed
|
|
7
|
+
// query refetches from the first page, a late response from a superseded query is dropped.
|
|
8
|
+
// loadMore() appends the next page (skip paging: skip = items shown so far).
|
|
9
|
+
//
|
|
10
|
+
// SSR-friendly: seed with `initialItems` from the SAME query and no client fetch happens for the
|
|
11
|
+
// first page. One store per mounted listing (a page can hold two collections): createCollectionStore(),
|
|
12
|
+
// not a singleton.
|
|
13
|
+
import { queryItems } from "./items";
|
|
14
|
+
import type { CmsFilter, CmsItem, CmsSort } from "./types";
|
|
15
|
+
|
|
16
|
+
export interface CollectionQuery {
|
|
17
|
+
collectionId?: string;
|
|
18
|
+
filters?: CmsFilter[];
|
|
19
|
+
sort?: CmsSort[];
|
|
20
|
+
/** Page size (default 20). */
|
|
21
|
+
limit?: number;
|
|
22
|
+
/** Reference field keys to inline as full items. */
|
|
23
|
+
include?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CollectionStoreOptions extends CollectionQuery {
|
|
27
|
+
collectionId: string;
|
|
28
|
+
/** Server-fetched first page — must come from the SAME query (filters/sort/limit/include). */
|
|
29
|
+
initialItems?: CmsItem[];
|
|
30
|
+
/** The server fetch's hasNext. Omitted → inferred (a full first page ⇒ assume more). */
|
|
31
|
+
initialHasNext?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Everything a listing surface renders from. Read it with getState() or through a subscription. */
|
|
35
|
+
export interface CollectionState {
|
|
36
|
+
/** null while the first load is in flight — render skeletons, not an empty state. */
|
|
37
|
+
items: CmsItem[] | null;
|
|
38
|
+
hasNext: boolean;
|
|
39
|
+
loadingMore: boolean;
|
|
40
|
+
error: string | null;
|
|
41
|
+
/** True while items is null and a load is running (the same condition, named). */
|
|
42
|
+
loading: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CollectionStore {
|
|
46
|
+
getState(): CollectionState;
|
|
47
|
+
subscribe(listener: () => void): () => void;
|
|
48
|
+
/** Run the first query unless the seed already answered it. Call once when mounted (a browser). */
|
|
49
|
+
start(): void;
|
|
50
|
+
/** Stop reacting; drop late responses. */
|
|
51
|
+
stop(): void;
|
|
52
|
+
/** Change the query by value — same values keep the page, a change refetches from the first page. */
|
|
53
|
+
setQuery(query: CollectionQuery): void;
|
|
54
|
+
/** Append the next page (skip = items shown so far). No-op while loading or when there is none. */
|
|
55
|
+
loadMore(): Promise<void>;
|
|
56
|
+
/** Re-run the current query after an error. */
|
|
57
|
+
retry(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const queryKey = (q: Required<Pick<CollectionQuery, "collectionId" | "limit">> & CollectionQuery): string =>
|
|
61
|
+
// Dates in filters serialize to ISO, so this is stable.
|
|
62
|
+
JSON.stringify([q.collectionId, q.filters ?? null, q.sort ?? null, q.limit, q.include ?? null]);
|
|
63
|
+
|
|
64
|
+
export function createCollectionStore(options: CollectionStoreOptions): CollectionStore {
|
|
65
|
+
let collectionId = options.collectionId;
|
|
66
|
+
let filters = options.filters;
|
|
67
|
+
let sort = options.sort;
|
|
68
|
+
let limit = options.limit ?? 20;
|
|
69
|
+
let include = options.include;
|
|
70
|
+
const key = () => queryKey({ collectionId, filters, sort, limit, include });
|
|
71
|
+
|
|
72
|
+
let items: CmsItem[] | null = options.initialItems ?? null;
|
|
73
|
+
let hasNext = options.initialHasNext ?? (options.initialItems ? options.initialItems.length >= limit : false);
|
|
74
|
+
let loadingMore = false;
|
|
75
|
+
let error: string | null = null;
|
|
76
|
+
// The query the current items answer (null = nothing answered yet).
|
|
77
|
+
let pageKey: string | null = options.initialItems ? key() : null;
|
|
78
|
+
let inflight = false;
|
|
79
|
+
let started = false;
|
|
80
|
+
let generation = 0;
|
|
81
|
+
const listeners = new Set<() => void>();
|
|
82
|
+
let snapshot: CollectionState | null = null;
|
|
83
|
+
const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
|
|
84
|
+
|
|
85
|
+
function getState(): CollectionState {
|
|
86
|
+
if (snapshot) return snapshot;
|
|
87
|
+
snapshot = { items, hasNext, loadingMore, error, loading: items === null };
|
|
88
|
+
return snapshot;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function run(): void {
|
|
92
|
+
if (!started) return;
|
|
93
|
+
const k = key();
|
|
94
|
+
const id = ++generation;
|
|
95
|
+
pageKey = k; inflight = true;
|
|
96
|
+
items = null; error = null; loadingMore = false;
|
|
97
|
+
emit();
|
|
98
|
+
queryItems(collectionId, { filters, sort, limit, include })
|
|
99
|
+
.then((page) => {
|
|
100
|
+
if (generation !== id) return; // superseded — drop it
|
|
101
|
+
items = page.items; hasNext = page.hasNext; inflight = false;
|
|
102
|
+
emit();
|
|
103
|
+
})
|
|
104
|
+
.catch((e) => {
|
|
105
|
+
if (generation !== id) return;
|
|
106
|
+
items = []; hasNext = false; inflight = false;
|
|
107
|
+
error = e instanceof Error ? e.message : String(e);
|
|
108
|
+
emit();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
getState,
|
|
114
|
+
subscribe(fn) {
|
|
115
|
+
listeners.add(fn);
|
|
116
|
+
return () => listeners.delete(fn);
|
|
117
|
+
},
|
|
118
|
+
start() {
|
|
119
|
+
if (started) return;
|
|
120
|
+
started = true;
|
|
121
|
+
// The SSR seed answered this exact query — no fetch; a dropped first load reruns.
|
|
122
|
+
if (pageKey !== key() || (items === null && !inflight)) run();
|
|
123
|
+
},
|
|
124
|
+
stop() {
|
|
125
|
+
started = false;
|
|
126
|
+
generation++;
|
|
127
|
+
inflight = false;
|
|
128
|
+
},
|
|
129
|
+
setQuery(q) {
|
|
130
|
+
if (q.collectionId !== undefined) collectionId = q.collectionId;
|
|
131
|
+
filters = q.filters; sort = q.sort; include = q.include;
|
|
132
|
+
if (q.limit !== undefined) limit = q.limit;
|
|
133
|
+
if (key() === pageKey) return; // same values — same page
|
|
134
|
+
if (started) run();
|
|
135
|
+
},
|
|
136
|
+
async loadMore() {
|
|
137
|
+
const current = items;
|
|
138
|
+
if (!current || loadingMore) return;
|
|
139
|
+
const id = generation;
|
|
140
|
+
loadingMore = true; emit();
|
|
141
|
+
try {
|
|
142
|
+
const page = await queryItems(collectionId, { filters, sort, limit, include, skip: current.length });
|
|
143
|
+
if (generation !== id) return;
|
|
144
|
+
items = [...(items ?? []), ...page.items];
|
|
145
|
+
hasNext = page.hasNext;
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (generation !== id) return;
|
|
148
|
+
error = e instanceof Error ? e.message : String(e);
|
|
149
|
+
} finally {
|
|
150
|
+
if (generation === id) { loadingMore = false; emit(); }
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
retry() {
|
|
154
|
+
pageKey = null;
|
|
155
|
+
run();
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// One item as a framework-free store — the logic behind useItem: by `_id` or by a field match
|
|
2
|
+
// (slug routing), usable from React (useItem wraps it), from a static page's item view, from
|
|
3
|
+
// Vue/Svelte, or as the specification for a port.
|
|
4
|
+
//
|
|
5
|
+
// SSR-friendly: pass the server-fetched item as `initialItem` and start() fetches nothing.
|
|
6
|
+
// One store per item surface: createItemStore(), not a singleton.
|
|
7
|
+
import { getItemBy, getItemById } from "./items";
|
|
8
|
+
import type { CmsItem } from "./types";
|
|
9
|
+
|
|
10
|
+
/** Exactly one of `id` / `by`. */
|
|
11
|
+
export interface ItemRef {
|
|
12
|
+
id?: string;
|
|
13
|
+
by?: { field: string; value: string | number };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ItemStoreOptions {
|
|
17
|
+
collectionId: string;
|
|
18
|
+
ref: ItemRef;
|
|
19
|
+
initialItem?: CmsItem;
|
|
20
|
+
/** Reference field keys to inline as full items. */
|
|
21
|
+
include?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ItemState {
|
|
25
|
+
/** null while loading OR when not found — branch on notFound for the miss state. */
|
|
26
|
+
item: CmsItem | null;
|
|
27
|
+
notFound: boolean;
|
|
28
|
+
error: string | null;
|
|
29
|
+
/** True until the fetch settles (false at once with an initialItem). */
|
|
30
|
+
loading: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ItemStore {
|
|
34
|
+
getState(): ItemState;
|
|
35
|
+
subscribe(listener: () => void): () => void;
|
|
36
|
+
/** Fetch by ref when no `initialItem` was given. Call once when mounted. */
|
|
37
|
+
start(): void;
|
|
38
|
+
stop(): void;
|
|
39
|
+
/** Fetch again (after an error, or when the item may have changed). */
|
|
40
|
+
retry(): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createItemStore({ collectionId, ref, initialItem, include }: ItemStoreOptions): ItemStore {
|
|
44
|
+
let item: CmsItem | null = initialItem ?? null;
|
|
45
|
+
let notFound = false;
|
|
46
|
+
let error: string | null = null;
|
|
47
|
+
let loading = !initialItem;
|
|
48
|
+
let started = false;
|
|
49
|
+
let generation = 0;
|
|
50
|
+
const listeners = new Set<() => void>();
|
|
51
|
+
let snapshot: ItemState | null = null;
|
|
52
|
+
const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
|
|
53
|
+
|
|
54
|
+
function getState(): ItemState {
|
|
55
|
+
if (snapshot) return snapshot;
|
|
56
|
+
snapshot = { item, notFound, error, loading };
|
|
57
|
+
return snapshot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function load(): void {
|
|
61
|
+
const id = ++generation;
|
|
62
|
+
loading = true; error = null; emit();
|
|
63
|
+
const fetching = ref.id
|
|
64
|
+
? getItemById(collectionId, ref.id, { include })
|
|
65
|
+
: ref.by
|
|
66
|
+
? getItemBy(collectionId, ref.by.field, ref.by.value, { include })
|
|
67
|
+
: Promise.reject(new Error("useItem: pass ref.id or ref.by"));
|
|
68
|
+
fetching
|
|
69
|
+
.then((found) => {
|
|
70
|
+
if (generation !== id || !started) return;
|
|
71
|
+
item = found; notFound = found === null; loading = false;
|
|
72
|
+
emit();
|
|
73
|
+
})
|
|
74
|
+
.catch((e) => {
|
|
75
|
+
if (generation !== id || !started) return;
|
|
76
|
+
error = e instanceof Error ? e.message : String(e); loading = false;
|
|
77
|
+
emit();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
getState,
|
|
83
|
+
subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
|
84
|
+
start() {
|
|
85
|
+
if (started) return;
|
|
86
|
+
started = true;
|
|
87
|
+
if (initialItem) return;
|
|
88
|
+
load();
|
|
89
|
+
},
|
|
90
|
+
stop() { started = false; generation++; },
|
|
91
|
+
retry() { if (started) load(); },
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Wix Data rules and DTO mapping — transport-agnostic, imported by BOTH transports: ./items.ts
|
|
2
|
+
// (the SDK, managed Astro and React) and the REST twin in references/cms/rest/items.ts (fetch, a
|
|
3
|
+
// static site or a port to another language). Every rule about normalizing an item, validating a
|
|
4
|
+
// filter, and spelling a query for the wire lives HERE, once. A raw item may come from the SDK
|
|
5
|
+
// (dates as Date objects) or from REST (dates as `{ "$date": iso }`); the mappers accept both.
|
|
6
|
+
// Imports are type-only so a strip to JS emits no imports.
|
|
7
|
+
import type { CmsFilter, CmsItem, CmsPage, CmsQuery, CmsSort } from "./types";
|
|
8
|
+
|
|
9
|
+
/** A raw data item's payload as either transport returns it (fields flat, `_id` included). */
|
|
10
|
+
export type Raw = Record<string, any>;
|
|
11
|
+
/** Media value + size → https URL. Injected: the SDK transport scales through @wix/sdk, REST through the URL form. */
|
|
12
|
+
export type ImgSrc = (value: any, width: number, height: number) => string;
|
|
13
|
+
|
|
14
|
+
/** Page size when a query names none. */
|
|
15
|
+
export const DEFAULT_LIMIT = 20;
|
|
16
|
+
/** The size every IMAGE field is resolved at — one URL per item, large enough for a detail page. */
|
|
17
|
+
export const IMAGE_WIDTH = 1200;
|
|
18
|
+
export const IMAGE_HEIGHT = 900;
|
|
19
|
+
|
|
20
|
+
// ---- DTO normalization ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** REST spells DATE/DATETIME fields as `{ "$date": "<iso>" }`; the SDK decodes them into Date objects. */
|
|
23
|
+
function isDateWrapper(v: unknown): v is { $date: string } {
|
|
24
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
25
|
+
const keys = Object.keys(v as Raw);
|
|
26
|
+
return keys.length === 1 && keys[0] === "$date" && typeof (v as Raw).$date === "string";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Raw → DTO value: dates become ISO strings (Date objects aren't serializable as island props;
|
|
31
|
+
* `{ $date }` wrappers aren't dates at all), IMAGE fields holding a `wix:image://` identifier
|
|
32
|
+
* become https URLs a browser can load. Recursive — included reference items too.
|
|
33
|
+
*/
|
|
34
|
+
export function toValue(v: unknown, imgSrc: ImgSrc): unknown {
|
|
35
|
+
if (v instanceof Date) return v.toISOString();
|
|
36
|
+
if (isDateWrapper(v)) {
|
|
37
|
+
// Same spelling as the SDK path (REST omits the milliseconds: "…T00:00:00Z").
|
|
38
|
+
const t = new Date(v.$date);
|
|
39
|
+
return Number.isNaN(t.getTime()) ? v.$date : t.toISOString();
|
|
40
|
+
}
|
|
41
|
+
if (typeof v === "string" && v.startsWith("wix:image://")) return imgSrc(v, IMAGE_WIDTH, IMAGE_HEIGHT);
|
|
42
|
+
if (Array.isArray(v)) return v.map((x) => toValue(x, imgSrc));
|
|
43
|
+
if (v && typeof v === "object") {
|
|
44
|
+
const out: Raw = {};
|
|
45
|
+
for (const [k, val] of Object.entries(v as Raw)) out[k] = toValue(val, imgSrc);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const toItem = (raw: Raw, imgSrc: ImgSrc): CmsItem => toValue(raw, imgSrc) as CmsItem;
|
|
52
|
+
|
|
53
|
+
export function toPage(items: Raw[], hasNext: boolean, total: number | null | undefined, imgSrc: ImgSrc): CmsPage {
|
|
54
|
+
return { items: items.map((r) => toItem(r, imgSrc)), hasNext, total: total ?? null };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A DATE/DATETIME field for display — the DTO carries an ISO string; this is the one place it
|
|
59
|
+
* becomes copy. "" for an absent or unparseable value (never "Invalid Date", never the raw ISO).
|
|
60
|
+
* formatDate(item.publishDate) → "August 20, 2026"
|
|
61
|
+
* formatDate(item.publishDate, "en-GB", { dateStyle: "medium" })
|
|
62
|
+
*/
|
|
63
|
+
export function formatDate(iso: unknown, locale = "en-US", options: Intl.DateTimeFormatOptions = { dateStyle: "long" }): string {
|
|
64
|
+
if (typeof iso !== "string" || !iso) return "";
|
|
65
|
+
const t = new Date(iso);
|
|
66
|
+
return Number.isNaN(t.getTime()) ? "" : new Intl.DateTimeFormat(locale, options).format(t);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---- filter rules --------------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* An undefined comparand silently changes what a query matches (every row, or none) with no
|
|
73
|
+
* server error — the classic "my items shows everyone's items" bug. Throw at the call site;
|
|
74
|
+
* omit the filter entirely when you don't hold a value yet.
|
|
75
|
+
*/
|
|
76
|
+
export function assertFilterValue(f: CmsFilter): void {
|
|
77
|
+
if (f.op !== "isEmpty" && f.op !== "isNotEmpty" && f.value === undefined) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`cms: filter on "${f.field}" (${f.op}) has an undefined value — pass a real value or omit the filter.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- the REST spelling of a query ---------------------------------------------------------------
|
|
85
|
+
// The SDK's query builder serializes these itself; the REST twin sends them literally. Proven live:
|
|
86
|
+
// a DATE comparand must travel as `{ "$date": iso }` (a plain ISO string compares as text and
|
|
87
|
+
// matches nothing); isEmpty is `{ field: null }` and isNotEmpty `{ field: { $ne: null } }` — the
|
|
88
|
+
// `$isEmpty` keyword is rejected (WDE0076).
|
|
89
|
+
|
|
90
|
+
/** A comparand on the wire: Date → `{ $date }`, arrays element-wise, everything else as-is. */
|
|
91
|
+
export function restValue(v: unknown): unknown {
|
|
92
|
+
if (v instanceof Date) return { $date: v.toISOString() };
|
|
93
|
+
if (Array.isArray(v)) return v.map(restValue);
|
|
94
|
+
return v;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const REST_OPS: Record<Exclude<CmsFilter["op"], "isEmpty" | "isNotEmpty">, string> = {
|
|
98
|
+
eq: "$eq",
|
|
99
|
+
ne: "$ne",
|
|
100
|
+
gt: "$gt",
|
|
101
|
+
ge: "$gte",
|
|
102
|
+
lt: "$lt",
|
|
103
|
+
le: "$lte",
|
|
104
|
+
contains: "$contains",
|
|
105
|
+
startsWith: "$startsWith",
|
|
106
|
+
hasSome: "$hasSome",
|
|
107
|
+
hasAll: "$hasAll",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** One predicate → one filter object `{ field: condition }`. */
|
|
111
|
+
export function restCondition(f: CmsFilter): Raw {
|
|
112
|
+
assertFilterValue(f);
|
|
113
|
+
if (f.op === "isEmpty") return { [f.field]: null };
|
|
114
|
+
if (f.op === "isNotEmpty") return { [f.field]: { $ne: null } };
|
|
115
|
+
return { [f.field]: { [REST_OPS[f.op]]: restValue(f.value) } };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** All predicates AND-ed: `{}` for none, the one object for one, `{ $and: [...] }` otherwise. */
|
|
119
|
+
export function restFilter(filters: CmsFilter[] = []): Raw {
|
|
120
|
+
const conditions = filters.map(restCondition);
|
|
121
|
+
if (conditions.length === 0) return {};
|
|
122
|
+
if (conditions.length === 1) return conditions[0];
|
|
123
|
+
return { $and: conditions };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function restSort(sort: CmsSort[] = []): { fieldName: string; order: "ASC" | "DESC" }[] {
|
|
127
|
+
return sort.map((s) => ({ fieldName: s.field, order: s.direction === "desc" ? "DESC" : "ASC" }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The full Query Data Items body. Offset paging (`skip` = page N × limit) so every page can carry
|
|
132
|
+
* its own filter/sort and `returnTotalCount` works; `includeReferences[].field` inlines reference
|
|
133
|
+
* items. POST /wix-data/v2/items/query
|
|
134
|
+
*/
|
|
135
|
+
export function queryBody(collectionId: string, query: CmsQuery = {}): Raw {
|
|
136
|
+
const { filters = [], sort = [], limit = DEFAULT_LIMIT, skip = 0, include = [], withTotal = false } = query;
|
|
137
|
+
return {
|
|
138
|
+
dataCollectionId: collectionId,
|
|
139
|
+
query: {
|
|
140
|
+
filter: restFilter(filters),
|
|
141
|
+
...(sort.length ? { sort: restSort(sort) } : {}),
|
|
142
|
+
paging: { limit, offset: skip },
|
|
143
|
+
},
|
|
144
|
+
...(include.length ? { includeReferences: include.map((field) => ({ field })) } : {}),
|
|
145
|
+
...(withTotal ? { returnTotalCount: true } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Whether another page follows — the response says so; the arithmetic is the fallback. */
|
|
150
|
+
export function restHasNext(pagingMetadata: Raw | undefined, offset: number, limit: number): boolean {
|
|
151
|
+
if (typeof pagingMetadata?.hasNext === "boolean") return pagingMetadata.hasNext;
|
|
152
|
+
if (typeof pagingMetadata?.total === "number") return offset + (pagingMetadata.count ?? 0) < pagingMetadata.total;
|
|
153
|
+
return (pagingMetadata?.count ?? 0) >= limit;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- the REST spelling of a write ---------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Field values for an insert/update body. A DATE field must arrive as `{ $date }` — JSON.stringify
|
|
160
|
+
* turns a Date into a plain ISO string, which Wix stores as TEXT and never matches a date query.
|
|
161
|
+
* The read-only system fields are Wix's to stamp, never sent.
|
|
162
|
+
*/
|
|
163
|
+
export function restWriteData(data: Record<string, unknown>): Raw {
|
|
164
|
+
const out: Raw = {};
|
|
165
|
+
for (const [k, v] of Object.entries(data)) {
|
|
166
|
+
if (k === "_createdDate" || k === "_updatedDate" || k === "_owner") continue;
|
|
167
|
+
out[k] = restValue(v);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The Patch Data Item modifications for "set these fields" — one SET_FIELD per key. PATCH /wix-data/v2/items/{id} */
|
|
173
|
+
export function patchModifications(fields: Record<string, unknown>): Raw[] {
|
|
174
|
+
const entries = Object.entries(fields);
|
|
175
|
+
if (!entries.length) throw new Error("cms: patchItemFields called with no fields.");
|
|
176
|
+
return entries.map(([fieldPath, value]) => ({ fieldPath, action: "SET_FIELD", setFieldOptions: { value: restValue(value) } }));
|
|
177
|
+
}
|
|
@@ -1,52 +1,24 @@
|
|
|
1
|
-
// Wix Data reads/writes (@wix/data `items`) — the only file that touches raw data
|
|
2
|
-
// CMS is schema-generic: every function takes the collection id from the
|
|
3
|
-
// (seed/SEED.md) and returns plain CmsItem DTOs from ./types.
|
|
4
|
-
//
|
|
1
|
+
// Wix Data reads/writes (@wix/data `items`) over the SDK — the only file that touches raw data
|
|
2
|
+
// items on this transport. CMS is schema-generic: every function takes the collection id from the
|
|
3
|
+
// seed plan (seed/SEED.md) and returns plain CmsItem DTOs from ./types. The rules and mappers live
|
|
4
|
+
// in ./items-core (shared with the REST twin in references/cms/rest/); this file is the transport
|
|
5
|
+
// only. Copy as-is; extend by adding functions, not by editing these.
|
|
5
6
|
// docs: https://dev.wix.com/docs/sdk/business-solutions/data/items/query.md
|
|
6
7
|
// docs: https://dev.wix.com/docs/sdk/business-solutions/data/items/update.md
|
|
7
8
|
import { items as itemsModule } from "@wix/data";
|
|
8
9
|
import { wixModule } from "../sdk";
|
|
9
10
|
import { imgSrc } from "../media";
|
|
11
|
+
import { DEFAULT_LIMIT, assertFilterValue, toItem, type Raw } from "./items-core";
|
|
10
12
|
import type { CmsFilter, CmsItem, CmsPage, CmsQuery } from "./types";
|
|
11
13
|
|
|
12
14
|
const items = wixModule(itemsModule);
|
|
13
15
|
|
|
14
|
-
type Raw = Record<string, any>;
|
|
15
|
-
|
|
16
|
-
// SDK → DTO: the SDK decodes date fields into Date objects (not serializable as island
|
|
17
|
-
// props) and IMAGE fields arrive as wix:image:// identifiers a browser can't load
|
|
18
|
-
// (ERR_UNKNOWN_URL_SCHEME). Normalize recursively — included reference items too.
|
|
19
|
-
function toValue(v: unknown): unknown {
|
|
20
|
-
if (v instanceof Date) return v.toISOString();
|
|
21
|
-
if (typeof v === "string" && v.startsWith("wix:image://")) return imgSrc(v, 1200, 900);
|
|
22
|
-
if (Array.isArray(v)) return v.map(toValue);
|
|
23
|
-
if (v && typeof v === "object") {
|
|
24
|
-
const out: Raw = {};
|
|
25
|
-
for (const [k, val] of Object.entries(v as Raw)) out[k] = toValue(val);
|
|
26
|
-
return out;
|
|
27
|
-
}
|
|
28
|
-
return v;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const toItem = (raw: Raw): CmsItem => toValue(raw) as CmsItem;
|
|
32
|
-
|
|
33
|
-
// An undefined comparand silently changes what a query matches (every row, or none) with no
|
|
34
|
-
// server error — the classic "my items shows everyone's items" bug. Throw at the call site;
|
|
35
|
-
// omit the filter entirely when you don't hold a value yet.
|
|
36
|
-
function assertFilterValue(f: CmsFilter): void {
|
|
37
|
-
if (f.op !== "isEmpty" && f.op !== "isNotEmpty" && f.value === undefined) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`cms: filter on "${f.field}" (${f.op}) has an undefined value — pass a real value or omit the filter.`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
16
|
/**
|
|
45
17
|
* Query one page of a collection. An empty result on a PUBLIC collection is a seed
|
|
46
18
|
* permissions bug (read must be "ANYONE"), not a query bug — never reach for auth.elevate.
|
|
47
19
|
*/
|
|
48
20
|
export async function queryItems(collectionId: string, query: CmsQuery = {}): Promise<CmsPage> {
|
|
49
|
-
const { filters = [], sort = [], limit =
|
|
21
|
+
const { filters = [], sort = [], limit = DEFAULT_LIMIT, skip = 0, include = [], withTotal = false } = query;
|
|
50
22
|
let q = items.query(collectionId);
|
|
51
23
|
for (const f of filters) {
|
|
52
24
|
assertFilterValue(f);
|
|
@@ -70,7 +42,7 @@ export async function queryItems(collectionId: string, query: CmsQuery = {}): Pr
|
|
|
70
42
|
if (include.length) q = q.include(...include);
|
|
71
43
|
const res = await q.limit(limit).skip(skip).find(withTotal ? { returnTotalCount: true } : undefined);
|
|
72
44
|
return {
|
|
73
|
-
items: (res.items ?? []).map((r: Raw) => toItem(r)),
|
|
45
|
+
items: (res.items ?? []).map((r: Raw) => toItem(r, imgSrc)),
|
|
74
46
|
hasNext: res.hasNext(),
|
|
75
47
|
total: res.totalCount ?? null,
|
|
76
48
|
};
|
|
@@ -85,7 +57,7 @@ export async function getItemById(
|
|
|
85
57
|
const raw = await items.get(collectionId, itemId, {
|
|
86
58
|
...(include.length ? { includeReferences: include.map((field) => ({ field })) } : {}),
|
|
87
59
|
});
|
|
88
|
-
return raw ? toItem(raw as Raw) : null;
|
|
60
|
+
return raw ? toItem(raw as Raw, imgSrc) : null;
|
|
89
61
|
}
|
|
90
62
|
|
|
91
63
|
/**
|
|
@@ -116,7 +88,7 @@ export async function countItems(collectionId: string, filters: CmsFilter[] = []
|
|
|
116
88
|
*/
|
|
117
89
|
export async function insertItem(collectionId: string, data: Record<string, unknown>): Promise<CmsItem> {
|
|
118
90
|
const created = await items.insert(collectionId, data as Raw);
|
|
119
|
-
return toItem(created as Raw);
|
|
91
|
+
return toItem(created as Raw, imgSrc);
|
|
120
92
|
}
|
|
121
93
|
|
|
122
94
|
/**
|
|
@@ -127,7 +99,7 @@ export async function insertItem(collectionId: string, data: Record<string, unkn
|
|
|
127
99
|
*/
|
|
128
100
|
export async function updateItem(collectionId: string, item: CmsItem): Promise<CmsItem> {
|
|
129
101
|
const updated = await items.update(collectionId, item as Raw & { _id: string });
|
|
130
|
-
return toItem(updated as Raw);
|
|
102
|
+
return toItem(updated as Raw, imgSrc);
|
|
131
103
|
}
|
|
132
104
|
|
|
133
105
|
/** Patch only the named fields — the safe partial change (no replace-wipes-fields footgun). */
|
|
@@ -141,11 +113,11 @@ export async function patchItemFields(
|
|
|
141
113
|
let p = items.patch(collectionId, itemId);
|
|
142
114
|
for (const [k, v] of entries) p = p.setField(k, v);
|
|
143
115
|
const patched = await p.run();
|
|
144
|
-
return toItem(patched as Raw);
|
|
116
|
+
return toItem(patched as Raw, imgSrc);
|
|
145
117
|
}
|
|
146
118
|
|
|
147
119
|
/** Remove an item by `_id`. Irreversible. Returns the removed item (null if it didn't exist). */
|
|
148
120
|
export async function removeItem(collectionId: string, itemId: string): Promise<CmsItem | null> {
|
|
149
121
|
const removed = await items.remove(collectionId, itemId);
|
|
150
|
-
return removed ? toItem(removed as Raw) : null;
|
|
122
|
+
return removed ? toItem(removed as Raw, imgSrc) : null;
|
|
151
123
|
}
|