toga-ai 1.0.166 → 1.0.167
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/knowledge/2.0/apps/toga2-commerce/INDEX.md +3 -0
- package/knowledge/2.0/apps/toga2-commerce/architecture.md +246 -0
- package/knowledge/2.0/apps/toga2-commerce/features/client-fields.md +187 -0
- package/knowledge/2.0/apps/toga2-commerce/features/multi-tenant-theming.md +142 -0
- package/knowledge/INDEX.md +1 -1
- package/package.json +1 -1
|
@@ -2,5 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
|
+
| [TOGa Commerce (toga2-commerce / commerce2-react) Architecture](architecture.md) | `toga2-commerce` (npm package name **`commerce2-react`**, product name **TOGa Commerce**) is the customer-facing **B2B commerce storefront** of the 2.0 platform | src/main.tsx, src/App.tsx, src/routes.tsx, src/contexts/AuthContext.tsx, src/contexts/helpers/getLoginSettings.ts, src/api/axiosInstance.ts, src/stores/, src/themeConfig/ThemeContext.tsx, src/fieldsConfig/index.ts, src/hooks/useAssignClientFields.ts, vite.config.ts, package.json |
|
|
5
6
|
| [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
|
|
7
|
+
| [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx |
|
|
8
|
+
| [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. | src/themeConfig/themes.json, src/themeConfig/ThemeContext.tsx, src/themeConfig/types.ts, src/components/ThemeSwitcher/ThemeSwitcher.tsx, src/components/AuthLayout/AuthLayout.tsx, src/api/axiosInstance.ts, src/contexts/AuthContext.tsx, tailwind.config.js |
|
|
6
9
|
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. | toga2-commerce/amplify.yml, toga2-commerce/.gitattributes, toga2-commerce/package.json, toga2-commerce/.github/workflows/sync-stage-environments.yml |
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: TOGa Commerce (toga2-commerce / commerce2-react) Architecture
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: architecture
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- src/main.tsx
|
|
13
|
+
- src/App.tsx
|
|
14
|
+
- src/routes.tsx
|
|
15
|
+
- src/contexts/AuthContext.tsx
|
|
16
|
+
- src/contexts/helpers/getLoginSettings.ts
|
|
17
|
+
- src/api/axiosInstance.ts
|
|
18
|
+
- src/stores/
|
|
19
|
+
- src/themeConfig/ThemeContext.tsx
|
|
20
|
+
- src/fieldsConfig/index.ts
|
|
21
|
+
- src/hooks/useAssignClientFields.ts
|
|
22
|
+
- vite.config.ts
|
|
23
|
+
- package.json
|
|
24
|
+
related:
|
|
25
|
+
- 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
|
|
26
|
+
- 2.0/apps/toga2-commerce/features/client-fields.md
|
|
27
|
+
- 2.0/apps/toga2-commerce/workflows/amplify-build-and-deploy.md
|
|
28
|
+
- 2.0/apps/api2/architecture.md
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Summary
|
|
32
|
+
|
|
33
|
+
`toga2-commerce` (npm package name **`commerce2-react`**, product name **TOGa Commerce**) is the
|
|
34
|
+
customer-facing **B2B commerce storefront** of the 2.0 platform. It is a **React 18 + TypeScript +
|
|
35
|
+
Vite 5 single-page app** styled with **Tailwind**, and it is the React front end in the 2.0
|
|
36
|
+
"decoupled" architecture: it talks to **`api2`** (the TOGa v2 JSON API) for all data, and renders
|
|
37
|
+
nothing server-side.
|
|
38
|
+
|
|
39
|
+
It is **multi-tenant**: a single codebase serves several clients (currently **COMPASS**,
|
|
40
|
+
**COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from
|
|
41
|
+
the **hostname** and drives three independent systems — the API base URL, the theme, and the
|
|
42
|
+
per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and
|
|
43
|
+
[client-fields](features/client-fields.md) for those subsystems in depth.
|
|
44
|
+
|
|
45
|
+
> Note: this is a **2.0 *app* with no PHP**. Its registry `dependsOn` is `api2`; it consumes the
|
|
46
|
+
> `_underscore`/`api2` backend over HTTP but contains no framework PHP classes itself.
|
|
47
|
+
|
|
48
|
+
## Tech stack (verified versions, package.json)
|
|
49
|
+
|
|
50
|
+
| Concern | Library | Version |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| UI | react / react-dom | ^18.3.1 |
|
|
53
|
+
| Routing | react-router-dom | ^7.5.1 |
|
|
54
|
+
| Server state / cache | @tanstack/react-query (+ persist-client, sync-storage-persister) | ^5.59.19 |
|
|
55
|
+
| Client state | zustand | ^4.5.4 |
|
|
56
|
+
| HTTP | axios | 1.7.7 |
|
|
57
|
+
| Forms | react-hook-form | ^7.53.1 |
|
|
58
|
+
| Animation | framer-motion | ^11.0.18 |
|
|
59
|
+
| Styling | tailwindcss | ^3.4.4 |
|
|
60
|
+
| Errors / monitoring | @sentry/react | ^8.50.0 |
|
|
61
|
+
| Build / tooling | vite ^5.4.10, typescript ^5.8.2 |
|
|
62
|
+
| Tables | @tanstack/react-table, react-table (v7), react-table-sticky |
|
|
63
|
+
| Misc | dayjs, react-select, nouislider, classnames, uuid, react-lottie, @agilant/toga-blox, FontAwesome Pro |
|
|
64
|
+
|
|
65
|
+
E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
|
|
66
|
+
|
|
67
|
+
## Bootstrap & provider stack
|
|
68
|
+
|
|
69
|
+
- `src/main.tsx` — initializes **Sentry** (browser tracing + replay) and mounts `<App/>` in React
|
|
70
|
+
`StrictMode`; imports global CSS.
|
|
71
|
+
- `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a
|
|
72
|
+
`createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
PersistQueryClientProvider (client=queryClient, persister → localStorage "commerce")
|
|
76
|
+
└─ AuthProvider (contexts/AuthContext.tsx — auth state, tenant/login resolution)
|
|
77
|
+
└─ ThemeProvider (themeConfig/ThemeContext.tsx — CSS-variable theming)
|
|
78
|
+
└─ ToasterProvider (contexts/ToasterContext.tsx — toast notifications)
|
|
79
|
+
└─ RouterProvider (routes.tsx)
|
|
80
|
+
+ ReactQueryDevtools (initialIsOpen=false)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`App` also runs a mount effect managing **edit-order mode**: it reads a `localStorage.synced`
|
|
84
|
+
flag and calls `exitEditOrderModeGlobalSyncReset()` when an edit-order session was abandoned
|
|
85
|
+
outside `/cart`, and subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
|
|
86
|
+
|
|
87
|
+
> The cache is **persisted**: query results survive refreshes via `PersistQueryClientProvider`.
|
|
88
|
+
> Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate
|
|
89
|
+
> deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
|
|
90
|
+
|
|
91
|
+
## Routing (`src/routes.tsx`, createBrowserRouter)
|
|
92
|
+
|
|
93
|
+
Public:
|
|
94
|
+
- `/` → `AuthRedirect` (→ `/home` if authenticated, else renders nothing while the auth flow runs)
|
|
95
|
+
- `/login` → `LoginRoute` (→ `/home` if already authenticated, else `<Login/>`)
|
|
96
|
+
- `/reset-password` → `ResetPasswordPage`
|
|
97
|
+
|
|
98
|
+
Authenticated (wrapped by `PrivateRoute` → `AuthLayout`):
|
|
99
|
+
- `/home`, `/bundle-view`, `/filter`, `/cart`, `/get-support`, `/account`, `/item-view`,
|
|
100
|
+
`/order-details`, `/privacy-policy`, `/terms-conditions`; unmatched → `Navigate to="/home"`.
|
|
101
|
+
|
|
102
|
+
Mechanics:
|
|
103
|
+
- **`PrivateRoute`** gates on `useAuth().isAuthenticated` + `useAuthenticationFlow().isLoading`;
|
|
104
|
+
shows an `AuthLoading` spinner while loading, redirects to `/login` if unauthenticated.
|
|
105
|
+
- **`AuthLayout`** wraps every authenticated route and **stays mounted across route changes**
|
|
106
|
+
(header, footer, nav, notifications live here). Its `errorElement` renders `<ErrorMessage errorType={500}/>`.
|
|
107
|
+
⚠️ Because it never remounts, React Query `refetchOnMount` only fires **once per session** for
|
|
108
|
+
queries owned by AuthLayout — route-change refreshes must be wired explicitly (e.g. the
|
|
109
|
+
notifications query invalidates on route change keyed on `user?.uuid`).
|
|
110
|
+
- Routes are **statically imported** (no `React.lazy`); Vite handles chunking at build time.
|
|
111
|
+
|
|
112
|
+
## Page architecture — MVVM convention
|
|
113
|
+
|
|
114
|
+
Every page under `src/pages/<Page>/` follows the same shape (View ← ViewModel ← API ← FIELDS):
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
src/pages/<Page>/
|
|
118
|
+
├── <Page>Page.tsx / index.ts # View — layout & rendering only
|
|
119
|
+
├── types.ts # page-local TS types
|
|
120
|
+
├── view/ # sub-components, modals, loading skeletons
|
|
121
|
+
├── viewModel/
|
|
122
|
+
│ ├── use<Page>ViewModel.ts # state + React Query hooks + business logic
|
|
123
|
+
│ ├── index.ts
|
|
124
|
+
│ └── FIELDS/<CLIENT>/[<LANGUAGE>/][<ROLE>/]*.json|*.ts # per-tenant content/config
|
|
125
|
+
├── api/<Page>Api.ts # axios calls for this page
|
|
126
|
+
├── hooks/ # page-scoped hooks
|
|
127
|
+
└── helpers/ # pure compute/format helpers
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Pages: `Account, BundleView, Cart, Filter, GetSupport, Home, ItemsView, Login, OrderDetails,
|
|
131
|
+
PrivacyPolicy, ResetPassword, TermsConditions`. The **ViewModel hook is the seam**: it calls
|
|
132
|
+
`useAssignClientFields(fieldKey, language, user)` to load the right tenant/role/language content,
|
|
133
|
+
runs the page's queries, and returns ready-to-render data to the View. (See client-fields doc.)
|
|
134
|
+
|
|
135
|
+
## State management
|
|
136
|
+
|
|
137
|
+
### Zustand stores (`src/stores/`)
|
|
138
|
+
Most use the `persist` middleware (localStorage). Verified files:
|
|
139
|
+
|
|
140
|
+
| Store | Responsibility |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `useUserStore` | Logged-in user (persona ids, role flags, currency) |
|
|
143
|
+
| `useCartStoreZu` | Cart items + bundles, per-item quantity-change loading |
|
|
144
|
+
| `useCartSalesQuoteZu` | Active sales order (ship-to, contact, cost center, notes, emails) |
|
|
145
|
+
| `useCartOverlayZu` | Cart preview overlay state |
|
|
146
|
+
| `useEditOrderZu` | "Edit existing order" mode flag + original snapshot |
|
|
147
|
+
| `useEditOrderUuidStore` | UUID of the order being edited |
|
|
148
|
+
| `useSelectedUserZu` | "Order for" user selected in cart |
|
|
149
|
+
| `useReturnOriginalUserFromViewAsStore` | "View as another user" return-to-self tracking |
|
|
150
|
+
| `useEmailOptionsStore` | Notification CC email options (see cart-notification-emails) |
|
|
151
|
+
| `useBundleBuilderZu` | Bundle-builder wizard selections |
|
|
152
|
+
| `useFieldsStore` | **`fieldKey` (tenant) + `language`** — drives field/theme resolution (persist key `fields-key`) |
|
|
153
|
+
| `useSettingsModalStore` | Login/settings modal open flag |
|
|
154
|
+
| `useSettingsErrorStore` | Settings validation errors (suppresses data queries when set) |
|
|
155
|
+
|
|
156
|
+
### React Query (`src/queries/`, `src/hooks/useApiQuery.ts`, `useApiMutation.ts`)
|
|
157
|
+
- Global default `staleTime` 24h; per-query overrides (user/persona data fetched fresh on login).
|
|
158
|
+
- Cache persisted to localStorage (`"commerce"`); rehydrated on load.
|
|
159
|
+
- Page queries are typically `enabled` on `isAuthenticated && user?.uuid && !settingsModalEnabled`.
|
|
160
|
+
|
|
161
|
+
**Two-layer state model:** React Query owns *server* state (catalog, orders, user); Zustand owns
|
|
162
|
+
*local* state (cart, selected user, edit mode, tenant/language). The cart is synced to the backend
|
|
163
|
+
sales order via `src/api/syncSalesOrder*.ts` helpers.
|
|
164
|
+
|
|
165
|
+
## API layer (`src/api/`)
|
|
166
|
+
|
|
167
|
+
`src/api/axiosInstance.ts` is the shared client. Key behaviors (all verified):
|
|
168
|
+
- **Base URL by tenant:** `host = "VITE_API_" + window.location.hostname.toUpperCase().split(".")[0]`;
|
|
169
|
+
`baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So `compass.togacommerce` →
|
|
170
|
+
`VITE_API_COMPASS`, falling back to `VITE_API`. `timeout: 180000`.
|
|
171
|
+
- **Transaction id:** a request interceptor stamps every call with `params.transactionId = uuidv4()`.
|
|
172
|
+
- **Auth token:** a second request interceptor attaches `Authorization: Bearer <token>` — using the
|
|
173
|
+
stored `accessToken` if a `user` exists in localStorage, otherwise fetching a **public token** via
|
|
174
|
+
`POST {baseURL}/auth/public`.
|
|
175
|
+
- **1 ms delay interceptor:** a third request interceptor `await delay(1)` before sending.
|
|
176
|
+
- **401 handling:** response interceptor attempts `POST {baseURL}/auth/refresh` (Bearer refresh
|
|
177
|
+
token) once (`_retry`), retries the original request on success, else calls `performLogout()`
|
|
178
|
+
(clears all auth + cart + `commerce` cache keys and redirects to `/`).
|
|
179
|
+
- Failures are reported to **Sentry** with `errorType: "API"` tags.
|
|
180
|
+
|
|
181
|
+
The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors`; see api2 arch);
|
|
182
|
+
generic CRUD helpers live in `src/api/genericApi.ts` (paginated `getData`, `saveData`, `updateData`,
|
|
183
|
+
`deleteData`, optional `?depth=-1` for nested responses).
|
|
184
|
+
|
|
185
|
+
## Authentication & roles
|
|
186
|
+
|
|
187
|
+
- `src/contexts/AuthContext.tsx` derives `host` from the hostname (`hostname.toUpperCase().split(".")[0]`),
|
|
188
|
+
loads `getClientLoginFields(host)` (tenant config), decides persona-switcher visibility
|
|
189
|
+
(`determineShouldShowPersonaSwitcher` — QUAD special-cased), then runs
|
|
190
|
+
`getLoginSettings(...)` which writes `useFieldsStore.fieldKey = host` and the language, and
|
|
191
|
+
hydrates `useUserStore`.
|
|
192
|
+
- `src/hooks/useAuthenticationFlow.ts` orchestrates the post-login fetch sequence
|
|
193
|
+
(user data → persona → contact → settings).
|
|
194
|
+
- **Role flags** on the user object are tenant-specific:
|
|
195
|
+
- COMPASS / COMPASSCANADA → `ADMIN` (`_isAdmin`), `SUPERUSER` (`_isSuperAdmin`),
|
|
196
|
+
`MANAGER` (`_isSupervisor`), else `USER`.
|
|
197
|
+
- QUAD → `GLOBALADMIN` (`_isGlobalAdmin`), `BUYER` (`_isBuyer`), `ITSHOPPER` (`_isItShopper`), else `USER`.
|
|
198
|
+
- **Personas:** `user._personaIds` is a colon-delimited string; `getLoginSettings` has special
|
|
199
|
+
handling collapsing persona-24 cases for non-QUAD tenants.
|
|
200
|
+
|
|
201
|
+
## Build & deploy
|
|
202
|
+
|
|
203
|
+
- **Per-tenant dev/build scripts** (`package.json`): `compass`, `compasscanada`, `quad`,
|
|
204
|
+
`togacommerce` run `vite --mode development --host <tenant>.togacommerce`; production builds are
|
|
205
|
+
`build` / `buildAlpha` / `buildBeta` / `buildGamma` / `buildQcSecurity` (each installs the matching
|
|
206
|
+
`@agilant/toga-blox` npm channel, then `tsc` + `vite build --mode <env>`).
|
|
207
|
+
- `vite.config.ts`: React plugin + Sentry Vite plugin (sourcemap upload); dev `server.allowedHosts`
|
|
208
|
+
= `compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`; PostCSS via
|
|
209
|
+
`postcss.config.cjs`; `optimizeDeps.include: ["react-router-dom"]`.
|
|
210
|
+
- Local dev requires `*.togacommerce` hostnames to resolve to localhost (hosts file) so tenant
|
|
211
|
+
resolution works.
|
|
212
|
+
- Deployment runs on **AWS Amplify** — see [amplify-build-and-deploy](workflows/amplify-build-and-deploy.md).
|
|
213
|
+
A `Dockerfile`/`docker-compose.yml` also exist for containerized dev.
|
|
214
|
+
|
|
215
|
+
## Directory map (`src/`)
|
|
216
|
+
|
|
217
|
+
| Path | Purpose |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `main.tsx` / `App.tsx` | bootstrap, Sentry, provider stack |
|
|
220
|
+
| `routes.tsx` | router + route guards |
|
|
221
|
+
| `api/` | axios instance, generic CRUD, sales-order sync helpers |
|
|
222
|
+
| `contexts/` | `AuthContext`, `ToasterContext` (+ `helpers/getLoginSettings.ts`) |
|
|
223
|
+
| `stores/` | Zustand stores (see table) |
|
|
224
|
+
| `queries/` | login/prefetch React Query definitions |
|
|
225
|
+
| `hooks/` | shared hooks (`useAssignClientFields`, `useAuthenticationFlow`, `useApiQuery/Mutation`, `useBreakpoint`, `useDynamicConditionalFieldOptions`, `useCancelEditOrder`) |
|
|
226
|
+
| `pages/` | feature pages (MVVM) |
|
|
227
|
+
| `components/` | shared UI (Base* atoms, `AuthLayout`, `Header`/`Footer`, `ThemeSwitcher`, `BaseDetailField`, guardrails, error messages, address forms, cards) |
|
|
228
|
+
| `themeConfig/` | `themes.json`, `ThemeContext.tsx`, `types.ts` (theming) |
|
|
229
|
+
| `fieldsConfig/` | global per-tenant client config + the master `FIELDS` registry |
|
|
230
|
+
| `styles/`, `fonts/`, `assets/` | global CSS, web fonts, images/SVGs (some per-tenant) |
|
|
231
|
+
| `utils/` | formatting, cart/address/persona helpers, query helpers |
|
|
232
|
+
| `globalTypes.ts` | shared TS types (API envelope, User, Address, Item/Bundle, SalesOrder, …) |
|
|
233
|
+
|
|
234
|
+
## Gotchas
|
|
235
|
+
|
|
236
|
+
- **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit
|
|
237
|
+
invalidation for data that must refresh on navigation.
|
|
238
|
+
- **24h `staleTime` + persisted cache** → users can see stale catalog/pricing; invalidate on the
|
|
239
|
+
events that should bust it.
|
|
240
|
+
- **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the
|
|
241
|
+
first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS,
|
|
242
|
+
theme → DEFAULT). Use the per-tenant dev scripts.
|
|
243
|
+
- **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase
|
|
244
|
+
(`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as
|
|
245
|
+
lowercase `en`/`fr`. See the client-fields doc.
|
|
246
|
+
</content>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Client Fields — per-tenant / language / role content & config
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- src/fieldsConfig/index.ts
|
|
13
|
+
- src/fieldsConfig/getClientLoginFields.ts
|
|
14
|
+
- src/fieldsConfig/clientFields/COMPASS.json
|
|
15
|
+
- src/fieldsConfig/clientFields/COMPASSCANADA.json
|
|
16
|
+
- src/fieldsConfig/clientFields/QUAD.json
|
|
17
|
+
- src/hooks/useAssignClientFields.ts
|
|
18
|
+
- src/hooks/useDynamicConditionalFieldOptions.ts
|
|
19
|
+
- src/stores/useFieldsStore.ts
|
|
20
|
+
- src/components/BaseDetailField/BaseDetailField.tsx
|
|
21
|
+
related:
|
|
22
|
+
- 2.0/apps/toga2-commerce/architecture.md
|
|
23
|
+
- 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
|
|
24
|
+
- 2.0/apps/toga2-commerce/features/cart-notification-emails.md
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. Instead
|
|
30
|
+
each page's labels, field lists, validation, visibility, and static content come from **JSON config
|
|
31
|
+
resolved per tenant, per language, and per role**. There are **two layers**:
|
|
32
|
+
|
|
33
|
+
- **Layer A — global client config** (`src/fieldsConfig/clientFields/<TENANT>.json`): a small file
|
|
34
|
+
per tenant controlling the language switcher and which user API fields to fetch at login.
|
|
35
|
+
- **Layer B — the per-page `FIELDS` registry** (`src/fieldsConfig/index.ts` → `FIELDS`): the big
|
|
36
|
+
`tenant → language → role → page-section` object that every page ViewModel reads.
|
|
37
|
+
|
|
38
|
+
## Layer A — global client config
|
|
39
|
+
|
|
40
|
+
`src/fieldsConfig/clientFields/{COMPASS,COMPASSCANADA,QUAD}.json`, each:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"languageSwitcher": { "uuid": "1", "isEnabled": true|false },
|
|
45
|
+
"userApiFields": ["firstName", "lastName", "email", "uuid", "_isAdmin", ...]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `languageSwitcher.isEnabled` — **only COMPASSCANADA is `true`** (English/French). COMPASS and
|
|
50
|
+
QUAD are `false`.
|
|
51
|
+
- `userApiFields` — the allow-list of user columns fetched at login; importantly it includes the
|
|
52
|
+
tenant's **role flags**: COMPASS/COMPASSCANADA carry `_isAdmin`, `_isSupervisor`, `_isSuperAdmin`;
|
|
53
|
+
QUAD carries `_isGlobalAdmin`, `_isItShopper`, `_isBuyer`. All include `_personaIds`.
|
|
54
|
+
|
|
55
|
+
Resolved by **`src/fieldsConfig/getClientLoginFields.ts`** — a `switch(client)` returning the JSON;
|
|
56
|
+
**default is `COMPASS`** (not DEFAULT). Called from `AuthContext` as `getClientLoginFields(host)`.
|
|
57
|
+
The `languageSwitcher.isEnabled` value feeds the login-settings branch in `getLoginSettings.ts`.
|
|
58
|
+
|
|
59
|
+
## Layer B — the per-page FIELDS registry
|
|
60
|
+
|
|
61
|
+
`src/fieldsConfig/index.ts` (~1100 lines) imports every page's per-tenant field constants (which
|
|
62
|
+
themselves wrap the on-disk JSON under `pages/<Page>/viewModel/FIELDS/...`) and assembles one object:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export const FIELDS: Record<string, any> = {
|
|
66
|
+
COMPASS: {
|
|
67
|
+
en: {
|
|
68
|
+
ADMIN: { HEADERFIELDS, HOMEPAGEFIELDS, ORDERDETAILS, CARTPAGEFIELDS, /* ~20 sections */ },
|
|
69
|
+
SUPERUSER: { ... },
|
|
70
|
+
MANAGER: { ... },
|
|
71
|
+
USER: { ... },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
COMPASSCANADA: { en: { ADMIN, SUPERUSER, MANAGER, USER }, fr: { ... } },
|
|
75
|
+
QUAD: { en: { GLOBALADMIN, BUYER, ITSHOPPER /*, USER */ } },
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Resolution dimensions & order
|
|
80
|
+
|
|
81
|
+
`FIELDS[ tenant ][ language ][ role ]` → an object of **page-section** constants
|
|
82
|
+
(`HEADERFIELDS`, `HOMEPAGEFIELDS`, `FOOTERFIELDS`, `ITEMSPAGE`, `ORDERDETAILS`, `GETSUPPORTPAGE`,
|
|
83
|
+
`BUNDLEDETAILSPAGE`, `FILTERPAGEFIELDS`, `CARTPAGEFIELDS`, `NEWUSERFORMFIELDS`, `CANTFINDUSERFIELDS`,
|
|
84
|
+
`PRIVACYPOLICYPAGEFIELDS`, `TERMSCONDITIONSPAGEFIELDS`, `ACCOUNTTABFIELDS`, `MYACCOUNTFIELDS`,
|
|
85
|
+
`ORDERHISTORYFIELDS`, `ADD/EDIT/SELECTNEWPRIMARY/SHIPPINGADDRESSESFIELDS`, `CARTOVERLAY`,
|
|
86
|
+
`ERRORFIELDS`, `LOGINSETTINGSFIELDS`, …).
|
|
87
|
+
|
|
88
|
+
- **Tenant** keys: `COMPASS`, `COMPASSCANADA`, `QUAD`.
|
|
89
|
+
- **Language** keys are **lowercase `en` / `fr`** in the registry. ⚠️ This differs from the on-disk
|
|
90
|
+
**folder** names, which are uppercase `ENGLISH` / `FRENCH`. Only COMPASSCANADA has `fr`.
|
|
91
|
+
- **Role** keys: COMPASS/COMPASSCANADA → `ADMIN`, `SUPERUSER`, `MANAGER`, `USER`;
|
|
92
|
+
QUAD → `GLOBALADMIN`, `BUYER`, `ITSHOPPER` (+ `USER`).
|
|
93
|
+
|
|
94
|
+
On disk the JSON lives at `pages/<Page>/viewModel/FIELDS/<CLIENT>/[<LANGUAGE>/][<ROLE>/]<NAME>FIELDS.json`,
|
|
95
|
+
and **not every page uses every dimension**:
|
|
96
|
+
- Pages with full role split: Home, OrderDetails, BundleView, Filter, Cart, Account.
|
|
97
|
+
- Pages with **no role layer** (role-neutral content): PrivacyPolicy, GetSupport, ItemsView,
|
|
98
|
+
TermsConditions, ResetPassword (resolved at tenant/language level, shared across roles).
|
|
99
|
+
- Language layer (`ENGLISH`/`FRENCH` folders) only exists for COMPASSCANADA; COMPASS and QUAD put
|
|
100
|
+
files directly under the tenant (their registry language is `en`).
|
|
101
|
+
|
|
102
|
+
## The resolver — `useAssignClientFields`
|
|
103
|
+
|
|
104
|
+
`src/hooks/useAssignClientFields.ts` is called by **every page ViewModel** (and several form
|
|
105
|
+
ViewModels) with `(fieldKey, language, user)`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
let role: string;
|
|
109
|
+
if (clientName === "COMPASS" || clientName === "COMPASSCANADA") {
|
|
110
|
+
if (user?._isAdmin === "1") role = "ADMIN";
|
|
111
|
+
else if (user?._isSuperAdmin === "1") role = "SUPERUSER";
|
|
112
|
+
else if (user?._isSupervisor === "1") role = "MANAGER";
|
|
113
|
+
else role = "USER";
|
|
114
|
+
} else { // QUAD (and any non-Compass tenant)
|
|
115
|
+
if (user?._isGlobalAdmin === "1") role = "GLOBALADMIN";
|
|
116
|
+
else if (user?._isBuyer === "1") role = "BUYER";
|
|
117
|
+
else if (user?._isItShopper === "1") role = "ITSHOPPER";
|
|
118
|
+
else role = "USER";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// wrapped in React Query, keyed for caching:
|
|
122
|
+
const queryKey = ["clientFields", clientName, language, role];
|
|
123
|
+
const data = FIELDS?.[clientName]?.[language]?.[role] || null;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Inputs:
|
|
127
|
+
- **`clientName`** = `useFieldsStore.fieldKey` (the tenant `host`, set at login — see architecture doc).
|
|
128
|
+
- **`language`** = `useFieldsStore.language` (default `"en"`; set to `"fr"` etc. via `setLanguage`
|
|
129
|
+
from the login settings switcher or `MySettingsView`).
|
|
130
|
+
- **`user`** = the logged-in user (role flags).
|
|
131
|
+
|
|
132
|
+
It returns `{ fields, loading, error, resetFieldsMapCache }`; `resetFieldsMapCache` invalidates the
|
|
133
|
+
`["clientFields"]` query (used when role/language/tenant changes mid-session). Page ViewModels then
|
|
134
|
+
pluck the section they need, e.g. `fieldsCall["MYACCOUNTFIELDS"]`.
|
|
135
|
+
|
|
136
|
+
## What's inside a FIELDS JSON
|
|
137
|
+
|
|
138
|
+
Shape varies by page, but common forms:
|
|
139
|
+
- **Static content pages** (PrivacyPolicy, TermsConditions, GetSupport): headings + text/HTML blocks
|
|
140
|
+
(`label` may contain HTML).
|
|
141
|
+
- **Form pages** (ResetPassword, address forms, NewUser): `formFields[]` with
|
|
142
|
+
`inputType`, `isRequired`, `validationRequirements[]`, labels, placeholders.
|
|
143
|
+
- **Data/detail pages** (Account, OrderDetails): field descriptors mapping data to display, e.g.
|
|
144
|
+
`{ uuid, label, valueKey, valueType: "currency"|"address"|"status", icon, colSpan, classes }`,
|
|
145
|
+
plus `pageSettings` (e.g. which API fields to fetch: `fetchUserDataFields`, `fetchUserOrdersDetailsApiFields`).
|
|
146
|
+
|
|
147
|
+
Common field attributes: `uuid`, `label`, `valueKey` (path into the data object), `inputType`,
|
|
148
|
+
`valueType` (render hint), `colSpan`/`classes` (Tailwind), `isRequired`, `icon`, `options[]`.
|
|
149
|
+
|
|
150
|
+
## Consumers
|
|
151
|
+
|
|
152
|
+
- **`src/components/BaseDetailField/`** (`BaseDetailField.tsx`, `renderBaseDetailFieldLabel`,
|
|
153
|
+
`renderBaseDetailFieldValue`) — generic renderer: takes a field descriptor + data object, applies
|
|
154
|
+
`valueType` formatting (currency/address/status) and the descriptor's Tailwind classes.
|
|
155
|
+
- **`src/pages/BundleView/hooks/useBundleFields.ts`** — wraps `useAssignClientFields` and reads
|
|
156
|
+
bundle quantity rules from `BUNDLEDETAILSPAGE.pageSettings`.
|
|
157
|
+
- **`src/hooks/useDynamicConditionalFieldOptions.ts`** — swaps dependent select options at runtime
|
|
158
|
+
(e.g. state/province options change with the selected country).
|
|
159
|
+
|
|
160
|
+
## Onboarding / editing fields — practical notes
|
|
161
|
+
|
|
162
|
+
- **New tenant:** add `clientFields/<TENANT>.json`, a `getClientLoginFields` case, a `<TENANT>`
|
|
163
|
+
branch in `FIELDS` (with the tenant's role keys), and the per-page `FIELDS/<TENANT>/...` JSON +
|
|
164
|
+
the corresponding imports in `index.ts`.
|
|
165
|
+
- **New role:** extend the role-derivation in `useAssignClientFields.ts` **and** add the role key
|
|
166
|
+
under each `FIELDS[tenant][lang]` branch + per-page role folders.
|
|
167
|
+
- **New language:** set `languageSwitcher.isEnabled` in the tenant's `clientFields` JSON, add the
|
|
168
|
+
`fr`/`xx` branch to `FIELDS[tenant]`, and the `<LANGUAGE>` on-disk folders.
|
|
169
|
+
- **Editing copy:** change the page's `FIELDS/<CLIENT>/[<LANG>/][<ROLE>/]<NAME>FIELDS.json`. Remember
|
|
170
|
+
to update **all** affected role/language variants — there is no inheritance/fallback between roles.
|
|
171
|
+
|
|
172
|
+
## Gotchas
|
|
173
|
+
|
|
174
|
+
- **`en`/`fr` (registry) vs `ENGLISH`/`FRENCH` (folders)** — mismatching these is the classic bug;
|
|
175
|
+
the runtime lookup uses lowercase.
|
|
176
|
+
- **No fallback** in `FIELDS[client][language][role]` — a missing tenant/role/language combo returns
|
|
177
|
+
`null` and the page renders empty. Keep all role variants in sync.
|
|
178
|
+
- Role is derived from string `"1"` flags on the user (`user?._isAdmin === "1"`), not booleans.
|
|
179
|
+
- `getClientLoginFields` defaults to **COMPASS** for an unknown host, which can mask a
|
|
180
|
+
misconfigured tenant in local dev.
|
|
181
|
+
|
|
182
|
+
## Change history
|
|
183
|
+
- 2026-06-23 — Initial: documented the two-layer field system (global `clientFields/<TENANT>.json`
|
|
184
|
+
+ the per-page `FIELDS` registry), the `tenant→language→role` resolution in `useAssignClientFields`,
|
|
185
|
+
the `en`/`fr` vs `ENGLISH`/`FRENCH` distinction, role derivation per tenant, and field JSON shapes
|
|
186
|
+
(apeterson)
|
|
187
|
+
</content>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Multi-Tenant Resolution & Theming
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: toga2-commerce
|
|
5
|
+
project: TOGa Commerce
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["apeterson"]
|
|
11
|
+
files:
|
|
12
|
+
- src/themeConfig/themes.json
|
|
13
|
+
- src/themeConfig/ThemeContext.tsx
|
|
14
|
+
- src/themeConfig/types.ts
|
|
15
|
+
- src/components/ThemeSwitcher/ThemeSwitcher.tsx
|
|
16
|
+
- src/components/AuthLayout/AuthLayout.tsx
|
|
17
|
+
- src/api/axiosInstance.ts
|
|
18
|
+
- src/contexts/AuthContext.tsx
|
|
19
|
+
- tailwind.config.js
|
|
20
|
+
related:
|
|
21
|
+
- 2.0/apps/toga2-commerce/architecture.md
|
|
22
|
+
- 2.0/apps/toga2-commerce/features/client-fields.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
|
|
27
|
+
`toga2-commerce` serves multiple clients from one codebase. A request's **tenant** is derived
|
|
28
|
+
from the **hostname**, and that single tenant string fans out into three independent systems:
|
|
29
|
+
|
|
30
|
+
1. **API base URL** — `axiosInstance.ts` picks `VITE_API_<TENANT>`.
|
|
31
|
+
2. **Theme** — `ThemeContext` applies the tenant's color set as CSS variables.
|
|
32
|
+
3. **Fields/content** — the per-tenant field config (separate doc: [client-fields](client-fields.md)).
|
|
33
|
+
|
|
34
|
+
Current tenants: **COMPASS**, **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback theme.
|
|
35
|
+
|
|
36
|
+
## Tenant resolution — the hostname is the source of truth
|
|
37
|
+
|
|
38
|
+
The same derivation appears in three places (intentionally — there is no single shared tenant util):
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const fullHostName = window.location.hostname.toUpperCase(); // "COMPASS.TOGACOMMERCE"
|
|
42
|
+
const host = fullHostName.split(".")[0]; // "COMPASS"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **`src/api/axiosInstance.ts`** (lines ~8-11): `const host = "VITE_API_" + hostName[0];`
|
|
46
|
+
→ `baseURL = import.meta.env[host] || import.meta.env.VITE_API`. So the API endpoint is
|
|
47
|
+
tenant-specific (`VITE_API_COMPASS`, `VITE_API_QUAD`, …) with `VITE_API` as fallback.
|
|
48
|
+
- **`src/contexts/AuthContext.tsx`** (lines ~68-70): derives `host` and uses it for
|
|
49
|
+
`getClientLoginFields(host)`, persona-switcher logic, and to set `fieldKey` (see below).
|
|
50
|
+
- **`src/themeConfig/ThemeContext.tsx`** (lines ~13-15): derives `host` as the **default** theme key.
|
|
51
|
+
|
|
52
|
+
This is why the per-tenant **dev scripts** matter: `npm run compass` runs
|
|
53
|
+
`vite --host compass.togacommerce`, and `vite.config.ts` `server.allowedHosts` permits
|
|
54
|
+
`compass.togacommerce`, `compasscanada.togacommerce`, `quad.togacommerce`. Plain `localhost`
|
|
55
|
+
won't match a tenant, so it falls back (theme → DEFAULT, fields → COMPASS).
|
|
56
|
+
|
|
57
|
+
### From hostname to active theme — `fieldKey` is the real trigger
|
|
58
|
+
|
|
59
|
+
The theme is **not** applied directly from the raw hostname at mount. The chain is:
|
|
60
|
+
|
|
61
|
+
1. On login, `AuthContext` → `getLoginSettings(...)` writes `useFieldsStore.fieldKey = host`.
|
|
62
|
+
2. **`src/components/AuthLayout/AuthLayout.tsx`** runs:
|
|
63
|
+
```ts
|
|
64
|
+
const { theme, loadThemeForClient } = useThemeContext();
|
|
65
|
+
useEffect(() => { loadThemeForClient(fieldKey); }, [fieldKey, loadThemeForClient]);
|
|
66
|
+
```
|
|
67
|
+
So whenever `fieldKey` resolves, the matching theme loads.
|
|
68
|
+
3. **On first mount**, `ThemeProvider` loads `localStorage["current-client-id"]` (set by a prior
|
|
69
|
+
`loadThemeForClient`) or **`"DEFAULT"`** — not the hostname — so the very first paint before
|
|
70
|
+
login uses the last-saved or default theme.
|
|
71
|
+
|
|
72
|
+
## Theme config & data shape
|
|
73
|
+
|
|
74
|
+
- **`src/themeConfig/themes.json`** — top-level keys `DEFAULT`, `COMPASS`, `COMPASSCANADA`, `QUAD`.
|
|
75
|
+
Each entry: `{ id, name, colors: {...} }`.
|
|
76
|
+
- **`src/themeConfig/types.ts`** (`ClientTheme`) — the `colors` object is the only themed data.
|
|
77
|
+
Token groups (each typically with `default/hover/disabled/active` states, some with `bg-`,
|
|
78
|
+
`text-`, `border-`, `icon-` prefixes):
|
|
79
|
+
- `primary` (a numeric color scale, e.g. `50`/`500`/`900`)
|
|
80
|
+
- `btn` (`primary`, `tertiary`), `btn-nav`, `btn-nav-badge`, `btn-tabView-alt1`
|
|
81
|
+
- `input`, `checkBox`, `tile`, `text` (`body-primary`/`secondary`/`tertiary`)
|
|
82
|
+
|
|
83
|
+
> The theme controls **colors only**. Fonts, spacing, etc. are global Tailwind config / `@font-face`
|
|
84
|
+
> in `src/fonts/`, not per-tenant theme data.
|
|
85
|
+
|
|
86
|
+
## How the theme is applied — CSS variables on `<html>`
|
|
87
|
+
|
|
88
|
+
`ThemeContext.applyThemeToDOM` recursively flattens `themeData.colors` into CSS custom properties
|
|
89
|
+
on `document.documentElement`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// leaf string → root.style.setProperty(`--${prefix}-${key}`, value)
|
|
93
|
+
// nested object → recurse with prefix `${prefix}-${key}`
|
|
94
|
+
applyNestedColors(themeData.colors, "color");
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
So `colors.btn.primary["bg-default"]` becomes `--color-btn-primary-bg-default`. **`tailwind.config.js`**
|
|
98
|
+
maps semantic color tokens to these variables (`var(--color-...)`), so components style with normal
|
|
99
|
+
Tailwind classes and automatically pick up the active tenant's palette. `loadThemeForClient` also
|
|
100
|
+
persists the chosen key to `localStorage["current-client-id"]`.
|
|
101
|
+
|
|
102
|
+
## ThemeSwitcher
|
|
103
|
+
|
|
104
|
+
`src/components/ThemeSwitcher/ThemeSwitcher.tsx` is a manual `<select>` over `availableThemes`
|
|
105
|
+
(from `Object.values(themes.json)`) calling `loadThemeForClient(e.target.value)`. It's an override
|
|
106
|
+
/ dev-testing affordance for previewing tenants — normal users get their theme from `fieldKey`.
|
|
107
|
+
|
|
108
|
+
## Per-tenant assets
|
|
109
|
+
|
|
110
|
+
Logos and some imagery are **hard-coded imports with conditional rendering** keyed on `theme?.id`
|
|
111
|
+
(e.g. AuthLayout selects the QUAD logo vs. the Compass logo). They are not part of `themes.json`.
|
|
112
|
+
|
|
113
|
+
## Onboarding a new tenant — checklist
|
|
114
|
+
|
|
115
|
+
1. **Theme** — add a new top-level key to `src/themeConfig/themes.json` with the full `colors` set
|
|
116
|
+
(copy DEFAULT/COMPASS and recolor); confirm `tailwind.config.js` tokens cover any new groups.
|
|
117
|
+
2. **API env** — add `VITE_API_<TENANT>` to the `.env.<mode>` files (else it falls back to `VITE_API`).
|
|
118
|
+
3. **Dev script + allowedHosts** — add `"<tenant>": "vite --mode development --host <tenant>.togacommerce"`
|
|
119
|
+
to `package.json` and `<tenant>.togacommerce` to `vite.config.ts` `server.allowedHosts`; add the
|
|
120
|
+
host to the local hosts file.
|
|
121
|
+
4. **Client login config** — add `src/fieldsConfig/clientFields/<TENANT>.json` and a `case` in
|
|
122
|
+
`src/fieldsConfig/getClientLoginFields.ts` (see client-fields doc).
|
|
123
|
+
5. **Fields** — add the `<TENANT>` branch to the master `FIELDS` registry and the per-page
|
|
124
|
+
`FIELDS/<TENANT>/...` JSON (see client-fields doc).
|
|
125
|
+
6. **Roles** — if the tenant's role flags differ from COMPASS/QUAD, extend the role-derivation
|
|
126
|
+
in `src/hooks/useAssignClientFields.ts` and `AuthContext`'s persona logic.
|
|
127
|
+
7. **Assets** — add tenant logos and wire the conditional `theme?.id` rendering.
|
|
128
|
+
|
|
129
|
+
## Gotchas
|
|
130
|
+
|
|
131
|
+
- Tenant detection is **hostname-only** — there is no env-var or build-flag override of the tenant
|
|
132
|
+
at runtime. Wrong host = wrong tenant.
|
|
133
|
+
- The theme's first paint uses `localStorage["current-client-id"]` or DEFAULT, *then* corrects to
|
|
134
|
+
`fieldKey` once `AuthLayout` mounts — expect a brief default-themed flash pre-login.
|
|
135
|
+
- Adding a color token group to `themes.json` does nothing visually unless `tailwind.config.js`
|
|
136
|
+
references the corresponding `--color-*` variable.
|
|
137
|
+
|
|
138
|
+
## Change history
|
|
139
|
+
- 2026-06-23 — Initial: documented hostname→tenant resolution (axios/AuthContext/ThemeContext),
|
|
140
|
+
`fieldKey`-driven theme application via AuthLayout, CSS-variable flattening of `themes.json` colors,
|
|
141
|
+
ThemeSwitcher, and the new-tenant onboarding checklist (apeterson)
|
|
142
|
+
</content>
|
package/knowledge/INDEX.md
CHANGED
|
@@ -26,7 +26,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
26
26
|
- **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
|
|
27
27
|
- **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
|
|
28
28
|
- **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
|
|
29
|
-
- **toga2-commerce** (TOGa Commerce) —
|
|
29
|
+
- **toga2-commerce** (TOGa Commerce) — 5 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
|
|
30
30
|
- **toga25-supply** (TOGa 2.5 Supply) — 5 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
|
|
31
31
|
- **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
|
|
32
32
|
|
package/package.json
CHANGED