payload-plugin-newsletter 0.9.2 → 0.11.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/CHANGELOG.md +75 -0
- package/README.md +11 -5
- package/dist/components.cjs +107 -33
- package/dist/components.cjs.map +1 -1
- package/dist/components.d.cts +0 -2
- package/dist/components.d.ts +0 -2
- package/dist/components.js +107 -33
- package/dist/components.js.map +1 -1
- package/dist/fields.cjs +109 -9
- package/dist/fields.cjs.map +1 -1
- package/dist/fields.js +113 -9
- package/dist/fields.js.map +1 -1
- package/dist/index.cjs +559 -932
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +562 -931
- package/dist/index.js.map +1 -1
- package/dist/types.cjs +0 -2
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +3 -83
- package/dist/types.d.ts +3 -83
- package/dist/types.js +0 -2
- package/dist/types.js.map +1 -1
- package/dist/utils.cjs +104 -26
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.d.cts +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +104 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
+
## [0.11.0] - 2025-07-20
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Enhanced rich text editor for broadcast emails with:
|
|
5
|
+
- Fixed toolbar showing available formatting options
|
|
6
|
+
- Inline toolbar for text selection
|
|
7
|
+
- Image upload support with Media collection integration
|
|
8
|
+
- Custom email blocks (Button and Divider)
|
|
9
|
+
- Enhanced link feature with "open in new tab" option
|
|
10
|
+
- Comprehensive image handling in email HTML conversion:
|
|
11
|
+
- Responsive images with proper email-safe HTML
|
|
12
|
+
- Support for captions and alt text
|
|
13
|
+
- Automatic media URL handling for different storage backends
|
|
14
|
+
- Media collection setup documentation (`docs/guides/media-collection-setup.md`)
|
|
15
|
+
- Prerequisites section in README mentioning Media collection requirement
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Removed 'name' field from Broadcasts collection (now uses 'subject' as title)
|
|
19
|
+
- Updated Broadcast collection admin UI:
|
|
20
|
+
- Uses subject as the display title
|
|
21
|
+
- Shows recipientCount in default columns instead of name
|
|
22
|
+
- Enhanced `convertToEmailSafeHtml` utility to handle:
|
|
23
|
+
- Upload nodes for images
|
|
24
|
+
- Custom block nodes (button and divider)
|
|
25
|
+
- Media URL configuration
|
|
26
|
+
- Improved link handling with target attribute support
|
|
27
|
+
- Broadcast API sync now uses subject as the name field
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Broadcast hooks now properly handle the absence of name field
|
|
31
|
+
- Email preview components updated to work without channel references
|
|
32
|
+
|
|
33
|
+
## [0.10.0] - 2025-07-20
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
- **BREAKING**: Simplified plugin architecture to single-channel design
|
|
37
|
+
- Removed Channels collection entirely
|
|
38
|
+
- Each Payload instance now connects to a single Broadcast channel
|
|
39
|
+
- Channel is determined by the API token (Broadcast tokens are channel-specific)
|
|
40
|
+
- Removed `channelId` field from Broadcast type
|
|
41
|
+
- Removed channel management methods from providers
|
|
42
|
+
- Updated newsletter management configuration
|
|
43
|
+
- Removed `collections.channels` from plugin config
|
|
44
|
+
- Broadcasts no longer have channel relationships
|
|
45
|
+
- All broadcasts use the global provider configuration
|
|
46
|
+
- Improved provider capabilities
|
|
47
|
+
- Set `supportsMultipleChannels` to `false` for all providers
|
|
48
|
+
- Removed channel-specific error codes (`CHANNEL_NOT_FOUND`, `INVALID_CHANNEL`)
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
- Comprehensive documentation for single-channel architecture (`docs/guides/single-channel-broadcast.md`)
|
|
52
|
+
- Clear migration path from multi-channel to single-channel design
|
|
53
|
+
|
|
54
|
+
### Removed
|
|
55
|
+
- Channels collection (`src/collections/Channels.ts`)
|
|
56
|
+
- Channel utility functions (`src/providers/utils/getChannelProvider.ts`)
|
|
57
|
+
- Channel type definitions (`src/types/channel.ts`)
|
|
58
|
+
- Channel imports and references throughout the codebase
|
|
59
|
+
|
|
60
|
+
## [0.9.3] - 2025-07-20
|
|
61
|
+
|
|
62
|
+
### Changed
|
|
63
|
+
- Simplified Broadcast provider configuration to use a single token instead of separate development/production tokens
|
|
64
|
+
- Changed `tokens: { production, development }` to `token` field
|
|
65
|
+
- Users should now manage different tokens via environment variables
|
|
66
|
+
- Improved settings configuration hierarchy
|
|
67
|
+
- Settings from Payload admin UI now properly override config defaults
|
|
68
|
+
- Added support for configuring `fromAddress`, `fromName`, and `replyTo` in admin UI
|
|
69
|
+
- Enhanced reply-to email handling
|
|
70
|
+
- Broadcast provider now supports fallback chain: request → settings → from address
|
|
71
|
+
- Added `replyTo` field support to match Broadcast API capabilities
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- Fixed Broadcast provider tests to match new single token configuration
|
|
75
|
+
|
|
1
76
|
## [0.9.2] - 2025-07-20
|
|
2
77
|
|
|
3
78
|
### Fixed
|
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@ A complete newsletter management plugin for [Payload CMS](https://github.com/pay
|
|
|
22
22
|
- ✅ **Email Validation** - Built-in validation for email client compatibility (v0.9.0+)
|
|
23
23
|
- 📝 **Email-Safe Editor** - Rich text editor limited to email-compatible features (v0.9.0+)
|
|
24
24
|
|
|
25
|
+
## Prerequisites
|
|
26
|
+
|
|
27
|
+
- Payload CMS v3.0.0 or higher
|
|
28
|
+
- A Media collection configured in your Payload project (required for image support in broadcasts)
|
|
29
|
+
|
|
25
30
|
## Quick Start
|
|
26
31
|
|
|
27
32
|
### 1. Install the plugin
|
|
@@ -587,17 +592,18 @@ providers: {
|
|
|
587
592
|
providers: {
|
|
588
593
|
default: 'broadcast',
|
|
589
594
|
broadcast: {
|
|
590
|
-
apiUrl:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
development: process.env.BROADCAST_DEV_TOKEN,
|
|
594
|
-
},
|
|
595
|
+
apiUrl: process.env.BROADCAST_API_URL,
|
|
596
|
+
token: process.env.BROADCAST_TOKEN,
|
|
597
|
+
// Optional: These can be set here as defaults or configured in the admin UI
|
|
595
598
|
fromAddress: 'hello@yoursite.com',
|
|
596
599
|
fromName: 'Your Newsletter',
|
|
600
|
+
replyTo: 'replies@yoursite.com',
|
|
597
601
|
},
|
|
598
602
|
}
|
|
599
603
|
```
|
|
600
604
|
|
|
605
|
+
**Note**: Settings configured in the Payload admin UI take precedence over these config values. The config values serve as defaults when settings haven't been configured yet.
|
|
606
|
+
|
|
601
607
|
## TypeScript
|
|
602
608
|
|
|
603
609
|
The plugin is fully typed. Import types as needed:
|
package/dist/components.cjs
CHANGED
|
@@ -917,9 +917,17 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
917
917
|
"ol",
|
|
918
918
|
"li",
|
|
919
919
|
"blockquote",
|
|
920
|
-
"hr"
|
|
920
|
+
"hr",
|
|
921
|
+
"img",
|
|
922
|
+
"div",
|
|
923
|
+
"table",
|
|
924
|
+
"tr",
|
|
925
|
+
"td",
|
|
926
|
+
"th",
|
|
927
|
+
"tbody",
|
|
928
|
+
"thead"
|
|
921
929
|
],
|
|
922
|
-
ALLOWED_ATTR: ["href", "style", "target", "rel", "align"],
|
|
930
|
+
ALLOWED_ATTR: ["href", "style", "target", "rel", "align", "src", "alt", "width", "height", "border", "cellpadding", "cellspacing"],
|
|
923
931
|
ALLOWED_STYLES: {
|
|
924
932
|
"*": [
|
|
925
933
|
"color",
|
|
@@ -950,58 +958,62 @@ var EMAIL_SAFE_CONFIG = {
|
|
|
950
958
|
FORBID_ATTR: ["class", "id", "onclick", "onload", "onerror"]
|
|
951
959
|
};
|
|
952
960
|
async function convertToEmailSafeHtml(editorState, options) {
|
|
953
|
-
const rawHtml = await lexicalToEmailHtml(editorState);
|
|
961
|
+
const rawHtml = await lexicalToEmailHtml(editorState, options?.mediaUrl);
|
|
954
962
|
const sanitizedHtml = import_isomorphic_dompurify.default.sanitize(rawHtml, EMAIL_SAFE_CONFIG);
|
|
955
963
|
if (options?.wrapInTemplate) {
|
|
956
964
|
return wrapInEmailTemplate(sanitizedHtml, options.preheader);
|
|
957
965
|
}
|
|
958
966
|
return sanitizedHtml;
|
|
959
967
|
}
|
|
960
|
-
async function lexicalToEmailHtml(editorState) {
|
|
968
|
+
async function lexicalToEmailHtml(editorState, mediaUrl) {
|
|
961
969
|
const { root } = editorState;
|
|
962
970
|
if (!root || !root.children) {
|
|
963
971
|
return "";
|
|
964
972
|
}
|
|
965
|
-
const html = root.children.map((node) => convertNode(node)).join("");
|
|
973
|
+
const html = root.children.map((node) => convertNode(node, mediaUrl)).join("");
|
|
966
974
|
return html;
|
|
967
975
|
}
|
|
968
|
-
function convertNode(node) {
|
|
976
|
+
function convertNode(node, mediaUrl) {
|
|
969
977
|
switch (node.type) {
|
|
970
978
|
case "paragraph":
|
|
971
|
-
return convertParagraph(node);
|
|
979
|
+
return convertParagraph(node, mediaUrl);
|
|
972
980
|
case "heading":
|
|
973
|
-
return convertHeading(node);
|
|
981
|
+
return convertHeading(node, mediaUrl);
|
|
974
982
|
case "list":
|
|
975
|
-
return convertList(node);
|
|
983
|
+
return convertList(node, mediaUrl);
|
|
976
984
|
case "listitem":
|
|
977
|
-
return convertListItem(node);
|
|
985
|
+
return convertListItem(node, mediaUrl);
|
|
978
986
|
case "blockquote":
|
|
979
|
-
return convertBlockquote(node);
|
|
987
|
+
return convertBlockquote(node, mediaUrl);
|
|
980
988
|
case "text":
|
|
981
989
|
return convertText(node);
|
|
982
990
|
case "link":
|
|
983
|
-
return convertLink(node);
|
|
991
|
+
return convertLink(node, mediaUrl);
|
|
984
992
|
case "linebreak":
|
|
985
993
|
return "<br>";
|
|
994
|
+
case "upload":
|
|
995
|
+
return convertUpload(node, mediaUrl);
|
|
996
|
+
case "block":
|
|
997
|
+
return convertBlock(node, mediaUrl);
|
|
986
998
|
default:
|
|
987
999
|
if (node.children) {
|
|
988
|
-
return node.children.map(convertNode).join("");
|
|
1000
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
989
1001
|
}
|
|
990
1002
|
return "";
|
|
991
1003
|
}
|
|
992
1004
|
}
|
|
993
|
-
function convertParagraph(node) {
|
|
1005
|
+
function convertParagraph(node, mediaUrl) {
|
|
994
1006
|
const align = getAlignment(node.format);
|
|
995
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1007
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
996
1008
|
if (!children.trim()) {
|
|
997
1009
|
return '<p style="margin: 0 0 16px 0; min-height: 1em;"> </p>';
|
|
998
1010
|
}
|
|
999
1011
|
return `<p style="margin: 0 0 16px 0; text-align: ${align};">${children}</p>`;
|
|
1000
1012
|
}
|
|
1001
|
-
function convertHeading(node) {
|
|
1013
|
+
function convertHeading(node, mediaUrl) {
|
|
1002
1014
|
const tag = node.tag || "h1";
|
|
1003
1015
|
const align = getAlignment(node.format);
|
|
1004
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1016
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1005
1017
|
const styles = {
|
|
1006
1018
|
h1: "font-size: 32px; font-weight: 700; margin: 0 0 24px 0; line-height: 1.2;",
|
|
1007
1019
|
h2: "font-size: 24px; font-weight: 600; margin: 0 0 16px 0; line-height: 1.3;",
|
|
@@ -1010,18 +1022,18 @@ function convertHeading(node) {
|
|
|
1010
1022
|
const style = `${styles[tag] || styles.h3} text-align: ${align};`;
|
|
1011
1023
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1012
1024
|
}
|
|
1013
|
-
function convertList(node) {
|
|
1025
|
+
function convertList(node, mediaUrl) {
|
|
1014
1026
|
const tag = node.listType === "number" ? "ol" : "ul";
|
|
1015
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1027
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1016
1028
|
const style = tag === "ul" ? "margin: 0 0 16px 0; padding-left: 24px; list-style-type: disc;" : "margin: 0 0 16px 0; padding-left: 24px; list-style-type: decimal;";
|
|
1017
1029
|
return `<${tag} style="${style}">${children}</${tag}>`;
|
|
1018
1030
|
}
|
|
1019
|
-
function convertListItem(node) {
|
|
1020
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1031
|
+
function convertListItem(node, mediaUrl) {
|
|
1032
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1021
1033
|
return `<li style="margin: 0 0 8px 0;">${children}</li>`;
|
|
1022
1034
|
}
|
|
1023
|
-
function convertBlockquote(node) {
|
|
1024
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1035
|
+
function convertBlockquote(node, mediaUrl) {
|
|
1036
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1025
1037
|
const style = "margin: 0 0 16px 0; padding-left: 16px; border-left: 4px solid #e5e7eb; color: #6b7280;";
|
|
1026
1038
|
return `<blockquote style="${style}">${children}</blockquote>`;
|
|
1027
1039
|
}
|
|
@@ -1041,10 +1053,76 @@ function convertText(node) {
|
|
|
1041
1053
|
}
|
|
1042
1054
|
return text;
|
|
1043
1055
|
}
|
|
1044
|
-
function convertLink(node) {
|
|
1045
|
-
const children = node.children?.map(convertNode).join("") || "";
|
|
1056
|
+
function convertLink(node, mediaUrl) {
|
|
1057
|
+
const children = node.children?.map((child) => convertNode(child, mediaUrl)).join("") || "";
|
|
1046
1058
|
const url = node.fields?.url || "#";
|
|
1047
|
-
|
|
1059
|
+
const newTab = node.fields?.newTab ?? false;
|
|
1060
|
+
const targetAttr = newTab ? ' target="_blank"' : "";
|
|
1061
|
+
const relAttr = newTab ? ' rel="noopener noreferrer"' : "";
|
|
1062
|
+
return `<a href="${escapeHtml(url)}"${targetAttr}${relAttr} style="color: #2563eb; text-decoration: underline;">${children}</a>`;
|
|
1063
|
+
}
|
|
1064
|
+
function convertUpload(node, mediaUrl) {
|
|
1065
|
+
const upload = node.value;
|
|
1066
|
+
if (!upload) return "";
|
|
1067
|
+
let src = "";
|
|
1068
|
+
if (typeof upload === "string") {
|
|
1069
|
+
src = upload;
|
|
1070
|
+
} else if (upload.url) {
|
|
1071
|
+
src = upload.url;
|
|
1072
|
+
} else if (upload.filename && mediaUrl) {
|
|
1073
|
+
src = `${mediaUrl}/${upload.filename}`;
|
|
1074
|
+
}
|
|
1075
|
+
const alt = node.fields?.altText || upload.alt || "";
|
|
1076
|
+
const caption = node.fields?.caption || "";
|
|
1077
|
+
const imgHtml = `<img src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" style="max-width: 100%; height: auto; display: block; margin: 0 auto;" />`;
|
|
1078
|
+
if (caption) {
|
|
1079
|
+
return `
|
|
1080
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1081
|
+
${imgHtml}
|
|
1082
|
+
<p style="margin: 8px 0 0 0; font-size: 14px; color: #6b7280; font-style: italic;">${escapeHtml(caption)}</p>
|
|
1083
|
+
</div>
|
|
1084
|
+
`;
|
|
1085
|
+
}
|
|
1086
|
+
return `<div style="margin: 0 0 16px 0; text-align: center;">${imgHtml}</div>`;
|
|
1087
|
+
}
|
|
1088
|
+
function convertBlock(node, mediaUrl) {
|
|
1089
|
+
const blockType = node.fields?.blockName;
|
|
1090
|
+
switch (blockType) {
|
|
1091
|
+
case "button":
|
|
1092
|
+
return convertButtonBlock(node.fields);
|
|
1093
|
+
case "divider":
|
|
1094
|
+
return convertDividerBlock(node.fields);
|
|
1095
|
+
default:
|
|
1096
|
+
if (node.children) {
|
|
1097
|
+
return node.children.map((child) => convertNode(child, mediaUrl)).join("");
|
|
1098
|
+
}
|
|
1099
|
+
return "";
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function convertButtonBlock(fields) {
|
|
1103
|
+
const text = fields?.text || "Click here";
|
|
1104
|
+
const url = fields?.url || "#";
|
|
1105
|
+
const style = fields?.style || "primary";
|
|
1106
|
+
const styles = {
|
|
1107
|
+
primary: "background-color: #2563eb; color: #ffffff; border: 2px solid #2563eb;",
|
|
1108
|
+
secondary: "background-color: #6b7280; color: #ffffff; border: 2px solid #6b7280;",
|
|
1109
|
+
outline: "background-color: transparent; color: #2563eb; border: 2px solid #2563eb;"
|
|
1110
|
+
};
|
|
1111
|
+
const buttonStyle = `${styles[style] || styles.primary} display: inline-block; padding: 12px 24px; font-size: 16px; font-weight: 600; text-decoration: none; border-radius: 6px; text-align: center;`;
|
|
1112
|
+
return `
|
|
1113
|
+
<div style="margin: 0 0 16px 0; text-align: center;">
|
|
1114
|
+
<a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer" style="${buttonStyle}">${escapeHtml(text)}</a>
|
|
1115
|
+
</div>
|
|
1116
|
+
`;
|
|
1117
|
+
}
|
|
1118
|
+
function convertDividerBlock(fields) {
|
|
1119
|
+
const style = fields?.style || "solid";
|
|
1120
|
+
const styles = {
|
|
1121
|
+
solid: "border-top: 1px solid #e5e7eb;",
|
|
1122
|
+
dashed: "border-top: 1px dashed #e5e7eb;",
|
|
1123
|
+
dotted: "border-top: 1px dotted #e5e7eb;"
|
|
1124
|
+
};
|
|
1125
|
+
return `<hr style="${styles[style] || styles.solid} margin: 24px 0; border-bottom: none; border-left: none; border-right: none;" />`;
|
|
1048
1126
|
}
|
|
1049
1127
|
function getAlignment(format) {
|
|
1050
1128
|
if (!format) return "left";
|
|
@@ -1210,7 +1288,6 @@ var EmailPreview = ({
|
|
|
1210
1288
|
content,
|
|
1211
1289
|
subject,
|
|
1212
1290
|
preheader,
|
|
1213
|
-
channel,
|
|
1214
1291
|
mode = "desktop",
|
|
1215
1292
|
onValidation
|
|
1216
1293
|
}) => {
|
|
@@ -1233,7 +1310,7 @@ var EmailPreview = ({
|
|
|
1233
1310
|
const personalizedHtml = replacePersonalizationTags(emailHtml, SAMPLE_DATA);
|
|
1234
1311
|
const previewHtml = addEmailHeader(personalizedHtml, {
|
|
1235
1312
|
subject,
|
|
1236
|
-
from:
|
|
1313
|
+
from: "Newsletter <noreply@example.com>",
|
|
1237
1314
|
to: SAMPLE_DATA["subscriber.email"]
|
|
1238
1315
|
});
|
|
1239
1316
|
setHtml(previewHtml);
|
|
@@ -1248,7 +1325,7 @@ var EmailPreview = ({
|
|
|
1248
1325
|
}
|
|
1249
1326
|
};
|
|
1250
1327
|
convertContent();
|
|
1251
|
-
}, [content, subject, preheader,
|
|
1328
|
+
}, [content, subject, preheader, onValidation]);
|
|
1252
1329
|
(0, import_react5.useEffect)(() => {
|
|
1253
1330
|
if (iframeRef.current && html) {
|
|
1254
1331
|
const doc = iframeRef.current.contentDocument;
|
|
@@ -1485,7 +1562,6 @@ var EmailPreviewField = () => {
|
|
|
1485
1562
|
content: fields.content?.value || null,
|
|
1486
1563
|
subject: fields.subject?.value || "Email Subject",
|
|
1487
1564
|
preheader: fields.preheader?.value,
|
|
1488
|
-
channel: fields.channel?.value,
|
|
1489
1565
|
mode: previewMode,
|
|
1490
1566
|
onValidation: handleValidation
|
|
1491
1567
|
}
|
|
@@ -1505,8 +1581,7 @@ var BroadcastEditor = (props) => {
|
|
|
1505
1581
|
const [validationSummary, setValidationSummary] = (0, import_react7.useState)("");
|
|
1506
1582
|
const fields = (0, import_ui2.useFormFields)(([fields2]) => ({
|
|
1507
1583
|
subject: fields2.subject,
|
|
1508
|
-
preheader: fields2.preheader
|
|
1509
|
-
channel: fields2.channel
|
|
1584
|
+
preheader: fields2.preheader
|
|
1510
1585
|
}));
|
|
1511
1586
|
const handleValidation = (0, import_react7.useCallback)((result) => {
|
|
1512
1587
|
setIsValid(result.valid);
|
|
@@ -1644,7 +1719,6 @@ var BroadcastEditor = (props) => {
|
|
|
1644
1719
|
content: value,
|
|
1645
1720
|
subject: fields.subject?.value || "Email Subject",
|
|
1646
1721
|
preheader: fields.preheader?.value,
|
|
1647
|
-
channel: fields.channel?.value,
|
|
1648
1722
|
mode: previewMode,
|
|
1649
1723
|
onValidation: handleValidation
|
|
1650
1724
|
}
|