@dev.smartpricing/message-composer-layer 4.3.2-fix.1 → 4.4.1-next.0
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/README.md +184 -48
- package/app/api/client.ts +12 -0
- package/app/api/compilation.ts +13 -3
- package/app/api/dynamicValues.ts +5 -2
- package/app/api/messageGroups.ts +23 -12
- package/app/components/Common/AppContextErrors.vue +30 -0
- package/app/components/Common/HtmlPreviewIframe.vue +17 -3
- package/app/components/Common/MessageGroupSelect.vue +153 -26
- package/app/components/Common/MessagePreviewSlideover.vue +3 -0
- package/app/components/Composer/MessageLibrary.vue +17 -7
- package/app/components/MessageSendFlow/EmailPreview.vue +78 -0
- package/app/components/MessageSendFlow/LanguageSelect.vue +39 -0
- package/app/components/MessageSendFlow/MessageSendFlow.vue +252 -0
- package/app/components/MessageSendFlow/ParametersForm.vue +51 -0
- package/app/components/TemplateList/BaseTable.vue +156 -108
- package/app/components/Whatsapp/Preview.vue +12 -4
- package/app/components/Whatsapp/PreviewPanel.vue +7 -1
- package/app/composables/composerContext.ts +4 -2
- package/app/composables/useMessageGroupList.ts +186 -0
- package/app/composables/useMessageSendFlow.ts +246 -0
- package/app/composables/usePagedMessageGroups.ts +66 -0
- package/app/composables/useTemplateOperations.ts +13 -3
- package/app/layouts/default.vue +1 -0
- package/app/queries/compilation.ts +1 -1
- package/app/queries/dynamicValues.ts +8 -2
- package/app/queries/messageGroups.ts +29 -16
- package/app/stores/appContext.ts +12 -6
- package/app/types/tracking.ts +17 -0
- package/app/utils/testIds.const.ts +29 -0
- package/i18n/locales/de.ts +56 -2
- package/i18n/locales/en.ts +53 -2
- package/i18n/locales/es.ts +54 -2
- package/i18n/locales/fr.ts +53 -2
- package/i18n/locales/it.ts +55 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,75 +1,211 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dev.smartpricing/message-composer-layer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Message Composer as a [Nuxt layer](https://nuxt.com/docs/getting-started/layers). A host app extends it and gets the composer pages (message library, editor, preview, brands, images) plus embeddable components such as the **Message Send Flow**.
|
|
4
|
+
|
|
5
|
+
The layer is published on the private Smartpricing npm registry together with `@dev.smartpricing/message-composer-utils` (shared types and helpers, subpath exports `/types` and `/utils`). Both packages share one version number.
|
|
6
|
+
|
|
7
|
+
## Host setup
|
|
8
|
+
|
|
9
|
+
1. Install the layer and its peer:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @dev.smartpricing/message-composer-layer
|
|
13
|
+
pnpm add -D nuxt-ui-layer@github:smartpricing/smartness-nuxt-ui#v1.14.4 # peer, >= 1.3.0
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`nuxt-ui-layer` is extended by `@dev.smartpricing/platform-layer`, which this layer extends, so the host does not list it in `extends`.
|
|
17
|
+
|
|
18
|
+
2. Extend it and set the API keys. `BACKEND_BASE_URL`, `PLATFORM_URL` and `CSRF_COOKIE_NAME` are declared by `@dev.smartpricing/platform-layer`, which this layer extends; set them once for both.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// nuxt.config.ts
|
|
22
|
+
export default defineNuxtConfig({
|
|
23
|
+
extends: ['@dev.smartpricing/message-composer-layer'],
|
|
24
|
+
runtimeConfig: {
|
|
25
|
+
public: {
|
|
26
|
+
COMPOSER_API_URL: '', // NUXT_PUBLIC_COMPOSER_API_URL
|
|
27
|
+
BACKEND_BASE_URL: '', // NUXT_PUBLIC_BACKEND_BASE_URL
|
|
28
|
+
PLATFORM_URL: '', // NUXT_PUBLIC_PLATFORM_URL
|
|
29
|
+
CSRF_COOKIE_NAME: '', // NUXT_PUBLIC_CSRF_COOKIE_NAME: smt_csrf | smt_csrf_stage | smt_csrf_dev
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
3. Provide the composer context before any layer component renders (`app.vue` or a plugin). `product` is required; everything else has a default.
|
|
36
|
+
|
|
37
|
+
```vue
|
|
38
|
+
<script setup lang="ts">
|
|
39
|
+
defineComposerContext({
|
|
40
|
+
product: ref('chat'), // 'connect' | 'chat' | 'pms' | 'pricing'
|
|
41
|
+
organization_id: useAuth().organizationId,
|
|
42
|
+
channels: ref(['whatsapp']),
|
|
43
|
+
mainLanguage: ref('it'),
|
|
44
|
+
metaWabaId: useAuth().wabaId, // required for the whatsapp channel
|
|
45
|
+
metaAccessToken: useAuth().metaToken, // required for the whatsapp channel
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
useNuxtApp().hook('composer:auth-error', () => {
|
|
49
|
+
// 401 from the composer API: the session is gone, decide what to do
|
|
50
|
+
})
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Auth is cookie-based through platform-layer. The full `ComposerContext` interface lives in `app/composables/composerContext.ts`; the design and the parent/layer contract are in [`docs/nuxt-layer-design.md`](../../docs/nuxt-layer-design.md).
|
|
55
|
+
|
|
56
|
+
4. Tracking is opt-in: set `composer.tracking: true` in the host's `app.config.ts` to have the layer post its events to `POST /tracking/event`.
|
|
57
|
+
|
|
58
|
+
The layer's pages are mounted at root (`/`, `/template/...`, `/preview/...`, `/brands`, `/images`). Remap them with `pages:extend` if the host already owns those routes.
|
|
59
|
+
|
|
60
|
+
## Message Send Flow
|
|
61
|
+
|
|
62
|
+
`MessageSendFlow` walks an operator from "which Message" to "here is what to send", in two steps inside a container the host owns (Smartchat: a modal):
|
|
63
|
+
|
|
64
|
+
1. **Message**: pick a **Sendable** Message Group (at least one approved Message; groups with none are greyed out with a reason) and one of its approved languages.
|
|
65
|
+
2. **Parameters**: one required input per operator parameter of the chosen Message, prefilled from the host, editable. `brand_*` keys are hidden and filled server-side. **Confirm** calls `POST /message-groups/:id/compile` once (`strict: true`, `strict_language: true`) and emits the **Sendable Message** the backend built.
|
|
66
|
+
|
|
67
|
+
The preview follows every step in a right panel: the picked Message as the guest receives it (the WhatsApp bubble for `whatsapp_v1`, the compiled email for `email_v1`), with the typed values substituted live. Plan a container at least ~1100 px wide (Nuxt UI `max-w-6xl`); the panel is 400 px, the stepper 200 px.
|
|
68
|
+
|
|
69
|
+
Back and Next move between steps, Cancel is always available, Confirm only on Parameters. The action bar renders under the steps, or wherever `actionsTo` points (a modal footer). The component has no outer padding: the host's container pads it. The component owns no overlay and no send, and it keeps its state after `compiled`: closing the container is the host's decision. There is no reset API; remount it (`:key`) to start over.
|
|
70
|
+
|
|
71
|
+
The `/compile` contract (request, response with `message`, error codes) is documented in [`docs/compile-and-send.md`](../../docs/compile-and-send.md).
|
|
72
|
+
|
|
73
|
+
### Mount
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<script setup lang="ts">
|
|
77
|
+
import type { SendableMessage } from '@dev.smartpricing/message-composer-utils/types'
|
|
78
|
+
|
|
79
|
+
const open = ref(true)
|
|
80
|
+
const groupId = ref<string>() // optional preselect, read back after a pick
|
|
81
|
+
|
|
82
|
+
function onCompiled(message: SendableMessage) {
|
|
83
|
+
open.value = false
|
|
84
|
+
// hand `message` to your send pipeline, see "Smartchat glue" below
|
|
85
|
+
}
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<UModal v-model:open="open" title="New WhatsApp conversation">
|
|
90
|
+
<template #body>
|
|
91
|
+
<MessageSendFlow
|
|
92
|
+
v-model:message-group-id="groupId"
|
|
93
|
+
language="it"
|
|
94
|
+
:parameters="{ guest_name: 'Mario', guest_fullname: 'Mario Rossi' }"
|
|
95
|
+
actions-to="#send-flow-actions"
|
|
96
|
+
@compiled="onCompiled"
|
|
97
|
+
@cancel="open = false"
|
|
98
|
+
@error="(e) => console.warn(e.code, e.details)"
|
|
99
|
+
/>
|
|
100
|
+
</template>
|
|
101
|
+
<template #footer>
|
|
102
|
+
<!-- Cancel, Back, Next and Confirm render here -->
|
|
103
|
+
<div id="send-flow-actions" class="w-full" />
|
|
104
|
+
</template>
|
|
105
|
+
</UModal>
|
|
106
|
+
</template>
|
|
107
|
+
```
|
|
4
108
|
|
|
5
|
-
|
|
109
|
+
### Props
|
|
6
110
|
|
|
7
|
-
|
|
111
|
+
| Prop | Type | Default | Description |
|
|
112
|
+
| ---------------- | ------------------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
113
|
+
| `messageGroupId` | `string` (`v-model`) | `undefined` | Two-way: preselect a group, read back what the operator picked. A preselected group that is not Sendable raises `NOT_SENDABLE` and leaves the picker usable. |
|
|
114
|
+
| `renderType` | `RenderType` | `'whatsapp_v1'` | Channel of the Messages offered: `whatsapp_v1` or `email_v1`. Picker, languages, parameters, preview and the Sendable Message follow it. |
|
|
115
|
+
| `language` | `string` | `undefined` | Preferred language (see the language rule). |
|
|
116
|
+
| `parameters` | `Record<string, string>` | `undefined` | Editable prefill of the parameter inputs. Keys the chosen Message does not need are ignored. |
|
|
117
|
+
| `brandId` | `string` | `undefined` | Brand override forwarded to `/compile` as `brand_id`; defaults to the group's brand. |
|
|
118
|
+
| `actionsTo` | `string \| HTMLElement` | `undefined` | Where the Cancel/Back/Next/Confirm bar renders, e.g. a `UModal` footer element. Unset, it renders under the steps. |
|
|
8
119
|
|
|
9
|
-
|
|
10
|
-
# npm
|
|
11
|
-
npm install
|
|
120
|
+
### Emits
|
|
12
121
|
|
|
13
|
-
|
|
14
|
-
|
|
122
|
+
| Event | Payload | When |
|
|
123
|
+
| ---------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `compiled` | `SendableMessage` | Confirm succeeded. The Sendable Message is the `message` field of the `/compile` response, untouched: `WhatsappSendableMessage` for `whatsapp_v1`, `EmailSendableMessage` for `email_v1`. |
|
|
125
|
+
| `cancel` | none | The operator pressed Cancel (on any step). |
|
|
126
|
+
| `error` | `{ code: MessageSendFlowErrorCode, message: string, details?: unknown }` | Every failure the inline alert shows, so the host can log or surface it. `message` is the layer's translated text, the same string the alert shows. |
|
|
15
127
|
|
|
16
|
-
|
|
17
|
-
yarn install
|
|
128
|
+
Types: `SendableMessage` from `@dev.smartpricing/message-composer-utils/types`; `MessageSendFlowError`, `MessageSendFlowErrorCode` and `MessageSendFlowEmits` from the layer's `app/composables/useMessageSendFlow.ts`.
|
|
18
129
|
|
|
19
|
-
|
|
20
|
-
bun install
|
|
21
|
-
```
|
|
130
|
+
### Error codes
|
|
22
131
|
|
|
23
|
-
|
|
132
|
+
| Code | Raised by | Meaning | `details` |
|
|
133
|
+
| ------------------------ | --------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
|
|
134
|
+
| `NOT_SENDABLE` | layer | The preselected or picked group has no approved Message. The picker stays usable; the alert clears once a Sendable group is picked. | none |
|
|
135
|
+
| `MISSING_PARAMETERS` | layer | Confirm pressed with an empty required parameter (checked client-side before calling `/compile`). | `{ missing: string[] }` |
|
|
136
|
+
| `MISSING_PARAMETERS` | backend | `/compile` reported a required key as absent. | the backend error body |
|
|
137
|
+
| `TEMPLATE_NOT_SUBMITTED` | backend | The WhatsApp template was never submitted to Meta. | the backend error body |
|
|
138
|
+
| `TEMPLATE_NOT_APPROVED` | backend | The Message is not approved by Meta. | the backend error body |
|
|
139
|
+
| `COMPILE_FAILED` | layer | Any other `/compile` failure, or a response without `message`. | the backend body or the error |
|
|
24
140
|
|
|
25
|
-
|
|
141
|
+
A 401 is not an `error` emit: it reaches the host through the `composer:auth-error` Nuxt hook like everywhere else in the layer.
|
|
26
142
|
|
|
27
|
-
|
|
28
|
-
# npm
|
|
29
|
-
npm run dev
|
|
143
|
+
### Language rule
|
|
30
144
|
|
|
31
|
-
|
|
32
|
-
pnpm dev
|
|
145
|
+
The language selected in step 1 is the `language` prop when that Message is approved, otherwise the first approved Message in the group's `messages[]` order. It is recomputed whenever the group or the `language` prop changes. Non-approved languages stay visible as disabled radio cards showing their status; only approved ones can be picked.
|
|
33
146
|
|
|
34
|
-
|
|
35
|
-
yarn dev
|
|
147
|
+
### Reused blocks and their new props
|
|
36
148
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```
|
|
149
|
+
- `MessageGroupSelect` (auto-import `CommonMessageGroupSelect`) gained `eligibility?: 'active' | 'sendable'`. `active` (default, the composer's own picker) selects groups whose every Message is approved; `sendable` selects groups with at least one approved Message. The disabled tooltip and the "hide inactive" filter follow the mode. The send flow uses `sendable`. Details in [`docs/message-group-select-migration.md`](../../docs/message-group-select-migration.md).
|
|
150
|
+
- `WhatsappPreview` gained `parameters?: Record<string, string>`: non-empty values override the organization-level resolved values, keys not in the map keep the resolved mock, an empty string shows the raw `{{key}}` placeholder.
|
|
40
151
|
|
|
41
|
-
|
|
152
|
+
### Exported blocks
|
|
42
153
|
|
|
43
|
-
|
|
154
|
+
All of these are auto-imported in a host that extends the layer, so a different layout can be composed without forking:
|
|
44
155
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
156
|
+
| Auto-import name | File | Role |
|
|
157
|
+
| ------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
158
|
+
| `MessageSendFlow` | `app/components/MessageSendFlow/MessageSendFlow.vue` | The composed two-step flow. |
|
|
159
|
+
| `MessageSendFlowLanguageSelect` | `app/components/MessageSendFlow/LanguageSelect.vue` | Radio cards of a group's languages; props `languages`, `v-model` code. |
|
|
160
|
+
| `MessageSendFlowParametersForm` | `app/components/MessageSendFlow/ParametersForm.vue` | One input per key; props `parameterKeys`, `placeholders`, `loading`, `v-model` values. |
|
|
161
|
+
| `CommonMessageGroupSelect` | `app/components/Common/MessageGroupSelect.vue` | The picker, with `eligibility="sendable"`. |
|
|
162
|
+
| `WhatsappPreview` | `app/components/Whatsapp/Preview.vue` | The bubble, with the `parameters` override. |
|
|
163
|
+
| `MessageSendFlowEmailPreview` | `app/components/MessageSendFlow/EmailPreview.vue` | The compiled email (subject, HTML) for the typed values; props `messageGroupId`, `language`, `parameters`, `brandId`. |
|
|
164
|
+
|
|
165
|
+
The data flow (group, language rule, parameters, one compile on Confirm, error mapping) is `useMessageSendFlow()` in `app/composables/useMessageSendFlow.ts`. Test ids for every control are in `messageSendFlowTestIds` (`app/utils/testIds.const.ts`).
|
|
166
|
+
|
|
167
|
+
### Smartchat glue
|
|
48
168
|
|
|
49
|
-
|
|
50
|
-
pnpm build
|
|
169
|
+
The emitted Sendable Message is the content object Smartchat already sends, so the glue is a two-field pick:
|
|
51
170
|
|
|
52
|
-
|
|
53
|
-
|
|
171
|
+
```ts
|
|
172
|
+
import type { SendableMessage } from '@dev.smartpricing/message-composer-utils/types'
|
|
54
173
|
|
|
55
|
-
|
|
56
|
-
|
|
174
|
+
function onCompiled(message: SendableMessage) {
|
|
175
|
+
sendMessage({
|
|
176
|
+
contentType: message.type, // 'WhatsappTemplateMessage' | 'HtmlMessage'
|
|
177
|
+
content: message,
|
|
178
|
+
language:
|
|
179
|
+
message.type === 'WhatsappTemplateMessage' ? message.meta.data.language.code : language,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
57
182
|
```
|
|
58
183
|
|
|
59
|
-
|
|
184
|
+
For WhatsApp, `message.meta.data` is the Meta template object (`name`, `language`, `components` with the filled parameters) and `message.meta.template` is the raw translated template stored at submission, the one Smartchat renders in the bubble. For email, `message` is the `HtmlMessage` content compile-and-send publishes (`subject`, `text` and `rawHtml` both the compiled HTML, `skipTranslation: true`); it carries no language, so the host keeps the one the operator picked from its own state.
|
|
60
185
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
186
|
+
### Tracking
|
|
187
|
+
|
|
188
|
+
With tracking enabled the flow reports `send_flow.confirm` (`message_name`, `message_category`, `language`, `parameters_count`), `send_flow.cancel` (`step`: `message` | `parameters`) and `send_flow.error` (`code`).
|
|
189
|
+
|
|
190
|
+
### Dev harness
|
|
191
|
+
|
|
192
|
+
`apps/frontend` (this monorepo's standalone host) has a permanent `/debug/send-flow` page that plays the host: preselect, preferred language, prefill JSON, brand id, inline or modal container, remount, and a log of every emit.
|
|
64
193
|
|
|
65
|
-
|
|
66
|
-
pnpm preview
|
|
194
|
+
## Development
|
|
67
195
|
|
|
68
|
-
|
|
69
|
-
yarn preview
|
|
196
|
+
Run from `apps/layer`:
|
|
70
197
|
|
|
71
|
-
|
|
72
|
-
|
|
198
|
+
```bash
|
|
199
|
+
pnpm typecheck # nuxt typecheck
|
|
200
|
+
pnpm lint # eslint app/ --fix
|
|
201
|
+
pnpm test:e2e # playwright
|
|
202
|
+
pnpm translations:check # every locale has every key
|
|
73
203
|
```
|
|
74
204
|
|
|
75
|
-
|
|
205
|
+
There is no dev server in the layer itself: use `apps/frontend`, which extends `../layer`.
|
|
206
|
+
|
|
207
|
+
## Release
|
|
208
|
+
|
|
209
|
+
1. Bump `version` in `apps/layer/package.json` and `packages/message-composer-utils/package.json` together (semver: minor for a new component or prop, major for a change to the host contract).
|
|
210
|
+
2. Build the tarball from `apps/layer` with `pnpm pack`; `build.sh` builds the Docker image of the standalone deployment, not the npm package.
|
|
211
|
+
3. Publish both packages to the private registry.
|
package/app/api/client.ts
CHANGED
|
@@ -24,6 +24,11 @@ export const apiClient = createApiClient({
|
|
|
24
24
|
(useRuntimeConfig().public.BACKEND_BASE_URL as string) ||
|
|
25
25
|
(useRuntimeConfig().public.PLATFORM_URL as string),
|
|
26
26
|
platformURL: useRuntimeConfig().public.PLATFORM_URL as string,
|
|
27
|
+
// The composer backend answers a dead access token with @fastify/jwt's flat
|
|
28
|
+
// body (`code: FST_JWT_*` at top level), never the platform's nested
|
|
29
|
+
// `error.code: session.*` that the default predicate looks for. Refresh on
|
|
30
|
+
// any 401, as Connect's client does; the retry decides.
|
|
31
|
+
shouldRefresh: (status) => status === 401,
|
|
27
32
|
onRefreshFailed: () => {
|
|
28
33
|
useNuxtApp().callHook('composer:auth-error', { statusCode: 401 })
|
|
29
34
|
},
|
|
@@ -45,6 +50,13 @@ export const apiClient = createApiClient({
|
|
|
45
50
|
}
|
|
46
51
|
},
|
|
47
52
|
onResponseError({ response, options }) {
|
|
53
|
+
// A 401 that reaches here survived the refresh attempt (or its retry): the
|
|
54
|
+
// session is dead. platform-layer only calls onRefreshFailed for its own
|
|
55
|
+
// `session.*` codes, so tell the host here for the composer's `FST_JWT_*`.
|
|
56
|
+
if (response.status === 401) {
|
|
57
|
+
useNuxtApp().callHook('composer:auth-error', { statusCode: 401 })
|
|
58
|
+
return
|
|
59
|
+
}
|
|
48
60
|
if ((options as { _skipErrorToast?: boolean })._skipErrorToast) return
|
|
49
61
|
|
|
50
62
|
const t = useNuxtApp().$i18n.t
|
package/app/api/compilation.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
GetResolvedParametersResponse,
|
|
6
6
|
CompileMessageRequest,
|
|
7
7
|
CompileEmailResponse,
|
|
8
|
+
CompileMessageResponse,
|
|
8
9
|
PreviewMessageRequest,
|
|
9
10
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
10
11
|
import { apiClient } from './client'
|
|
@@ -26,11 +27,20 @@ export const compilationApi = {
|
|
|
26
27
|
`/message-groups/${messageGroupId}/parameters/resolved?language=${language}`,
|
|
27
28
|
),
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* WhatsApp responses carry `message`, the Sendable Message (docs/compile-and-send.md).
|
|
32
|
+
* `skipErrorToast` lets a caller with its own inline error surface silence the global toast.
|
|
33
|
+
*/
|
|
34
|
+
compile: (
|
|
35
|
+
messageGroupId: string,
|
|
36
|
+
data: CompileMessageRequest,
|
|
37
|
+
options: { skipErrorToast?: boolean } = {},
|
|
38
|
+
) =>
|
|
39
|
+
apiClient<CompileMessageResponse>(`/message-groups/${messageGroupId}/compile`, {
|
|
31
40
|
method: 'POST',
|
|
32
41
|
body: data,
|
|
33
|
-
|
|
42
|
+
_skipErrorToast: options.skipErrorToast,
|
|
43
|
+
} as Record<string, unknown>),
|
|
34
44
|
|
|
35
45
|
previewEmail: (data: PreviewMessageRequest) =>
|
|
36
46
|
apiClient<CompileEmailResponse>('/message-groups/preview', {
|
package/app/api/dynamicValues.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DynamicValuesResponse,
|
|
3
|
+
MessageGroupProduct,
|
|
4
|
+
} from '@dev.smartpricing/message-composer-utils/types'
|
|
2
5
|
import { apiClient } from './client'
|
|
3
6
|
|
|
4
7
|
export const dynamicValuesApi = {
|
|
5
|
-
get: (product:
|
|
8
|
+
get: (product: MessageGroupProduct, language: string) => {
|
|
6
9
|
const params = new URLSearchParams()
|
|
7
10
|
params.set('product', product)
|
|
8
11
|
params.set('language', language)
|
package/app/api/messageGroups.ts
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
MessageGroupFromDb,
|
|
3
|
+
PaginatedResponse,
|
|
3
4
|
MessageGroupWithMessages,
|
|
4
5
|
MessageGroupWithMessageSummary,
|
|
5
6
|
MessageGroupValidationResponse,
|
|
6
7
|
PostEntity,
|
|
7
8
|
MessageGroup,
|
|
8
|
-
RenderType,
|
|
9
9
|
DuplicateMessageGroupBody,
|
|
10
|
+
ListMessageGroupsQuery,
|
|
10
11
|
} from '@dev.smartpricing/message-composer-utils/types'
|
|
11
12
|
import { apiClient } from './client'
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Query params of `GET /message-groups`, straight from the shared schema
|
|
16
|
+
* (ADR 0007): snake_case, as on the wire. `visibility` other than `public`
|
|
17
|
+
* is admin-only (403 otherwise); every field is optional here, the server
|
|
18
|
+
* applies the defaults.
|
|
19
|
+
*/
|
|
20
|
+
export type ListMessageGroupsOptions = Partial<ListMessageGroupsQuery>
|
|
21
|
+
|
|
22
|
+
function listQueryString(options: ListMessageGroupsOptions = {}): string {
|
|
23
|
+
const params = new URLSearchParams()
|
|
24
|
+
for (const [name, value] of Object.entries(options)) {
|
|
25
|
+
if (value !== undefined && value !== '') params.set(name, String(value))
|
|
26
|
+
}
|
|
27
|
+
return params.size ? `?${params.toString()}` : ''
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
export const messageGroupsApi = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
if (options?.tagsMode) params.set('tags_mode', options.tagsMode)
|
|
21
|
-
|
|
22
|
-
const query = params.size ? `?${params.toString()}` : ''
|
|
23
|
-
return apiClient<MessageGroupWithMessageSummary[]>(`/message-groups${query}`)
|
|
24
|
-
},
|
|
31
|
+
/** One page of the server-side filtered list, in the Laravel envelope (ADR 0012). */
|
|
32
|
+
list: (options?: ListMessageGroupsOptions) =>
|
|
33
|
+
apiClient<PaginatedResponse<MessageGroupWithMessageSummary>>(
|
|
34
|
+
`/message-groups${listQueryString(options)}`,
|
|
35
|
+
),
|
|
25
36
|
|
|
26
37
|
getById: (id: string) => apiClient<MessageGroupWithMessages>(`/message-groups/${id}`),
|
|
27
38
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useAppContextStore } from '~/stores/appContext'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Host-integration problems reported by the app-context store (missing
|
|
6
|
+
* Product, missing Meta config, ...). Shown persistently: the host has to fix
|
|
7
|
+
* its context, the user cannot.
|
|
8
|
+
*/
|
|
9
|
+
const { validationErrors } = storeToRefs(useAppContextStore())
|
|
10
|
+
const messages = computed(() => Object.values(validationErrors.value))
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<UAlert
|
|
15
|
+
v-if="messages.length"
|
|
16
|
+
color="error"
|
|
17
|
+
variant="subtle"
|
|
18
|
+
icon="i-lucide-alert-triangle"
|
|
19
|
+
:title="$t('common.context_error_title')"
|
|
20
|
+
class="rounded-none"
|
|
21
|
+
>
|
|
22
|
+
<template #description>
|
|
23
|
+
<ul class="list-disc pl-4 space-y-1">
|
|
24
|
+
<li v-for="message in messages" :key="message">
|
|
25
|
+
{{ message }}
|
|
26
|
+
</li>
|
|
27
|
+
</ul>
|
|
28
|
+
</template>
|
|
29
|
+
</UAlert>
|
|
30
|
+
</template>
|
|
@@ -17,6 +17,8 @@ const props = withDefaults(
|
|
|
17
17
|
|
|
18
18
|
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframe')
|
|
19
19
|
const measuredHeight = ref(0)
|
|
20
|
+
// The first document comes from `srcdoc`; later ones are written in place (see the watch).
|
|
21
|
+
const initialHtml = props.html
|
|
20
22
|
|
|
21
23
|
const heightStyle = computed(() => {
|
|
22
24
|
if (!measuredHeight.value) return `${props.minHeight}px`
|
|
@@ -44,10 +46,22 @@ function onLoad() {
|
|
|
44
46
|
observeContent()
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Replace the document in place instead of re-binding `srcdoc`: a `srcdoc`
|
|
51
|
+
* change is a navigation, which blanks the frame and drops the height for a
|
|
52
|
+
* few frames. Writing keeps the frame painted and the last measured height
|
|
53
|
+
* until the new content is measured.
|
|
54
|
+
*/
|
|
47
55
|
watch(
|
|
48
56
|
() => props.html,
|
|
49
|
-
() => {
|
|
50
|
-
|
|
57
|
+
(html) => {
|
|
58
|
+
const doc = iframeRef.value?.contentDocument
|
|
59
|
+
if (!doc) return
|
|
60
|
+
doc.open()
|
|
61
|
+
doc.write(html)
|
|
62
|
+
doc.close()
|
|
63
|
+
fitToContent()
|
|
64
|
+
observeContent()
|
|
51
65
|
},
|
|
52
66
|
)
|
|
53
67
|
|
|
@@ -59,7 +73,7 @@ onBeforeUnmount(() => {
|
|
|
59
73
|
<template>
|
|
60
74
|
<iframe
|
|
61
75
|
ref="iframe"
|
|
62
|
-
:srcdoc="
|
|
76
|
+
:srcdoc="initialHtml"
|
|
63
77
|
:title="title"
|
|
64
78
|
sandbox="allow-same-origin"
|
|
65
79
|
:style="{ height: heightStyle }"
|