@hartl-services/medusa-base 1.0.0 → 1.1.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.
Files changed (71) hide show
  1. package/.medusa/server/src/admin/index.js +117 -1
  2. package/.medusa/server/src/admin/index.mjs +118 -2
  3. package/.medusa/server/src/documents/plugin-features.js +15 -1
  4. package/.medusa/server/src/documents/templates/financial-document.js +69 -51
  5. package/.medusa/server/src/documents/templates/format.js +44 -2
  6. package/.medusa/server/src/documents/templates/html-factory.js +7 -5
  7. package/.medusa/server/src/documents/templates/html-utils.js +12 -2
  8. package/.medusa/server/src/documents/templates/layout.js +115 -70
  9. package/.medusa/server/src/documents/templates/packing-list.js +10 -8
  10. package/.medusa/server/src/documents/utils/document-render-settings.js +59 -0
  11. package/.medusa/server/src/email/application/handle-invite-event.js +8 -2
  12. package/.medusa/server/src/email/application/handle-password-reset-event.js +16 -4
  13. package/.medusa/server/src/email/domain/order-summary.js +103 -0
  14. package/.medusa/server/src/email/domain/refund-summary.js +43 -0
  15. package/.medusa/server/src/email/domain/return-summary.js +118 -0
  16. package/.medusa/server/src/email/domain/shipment-summary.js +74 -0
  17. package/.medusa/server/src/email/plugin-features.js +9 -1
  18. package/.medusa/server/src/email/rendering/default-templates.js +693 -65
  19. package/.medusa/server/src/email/rendering/load-email-shop-details.js +36 -0
  20. package/.medusa/server/src/email/rendering/render-email.js +16 -3
  21. package/.medusa/server/src/email/rendering/resolve-shop-configuration.js +20 -1
  22. package/.medusa/server/src/email/types/template-data.js +155 -2
  23. package/.medusa/server/src/modules/plugin-features/domain/plugin-settings.js +4 -3
  24. package/.medusa/server/src/public/descriptor.js +3 -1
  25. package/.medusa/server/src/public/shop-master-data.js +2 -2
  26. package/.medusa/server/src/shop-config/build-food-shop-config.js +5 -1
  27. package/.medusa/server/src/shop-config/options.js +5 -1
  28. package/.medusa/server/src/workflows/documents/issue-document.js +4 -2
  29. package/.medusa/server/src/workflows/documents/issue-packing-list.js +8 -2
  30. package/.medusa/server/src/workflows/email/dispatch-email-message.js +4 -2
  31. package/.medusa/server/src/workflows/email/prepare-order-canceled-email.js +42 -4
  32. package/.medusa/server/src/workflows/email/prepare-order-received-email.js +31 -5
  33. package/.medusa/server/src/workflows/email/prepare-refund-email.js +25 -1
  34. package/.medusa/server/src/workflows/email/prepare-return-email.js +64 -3
  35. package/.medusa/server/src/workflows/email/prepare-shipment-email.js +36 -3
  36. package/README.md +166 -9
  37. package/dist/admin-components/features/plugin-features/components/ImageSettingInput.d.ts +15 -0
  38. package/dist/admin-components/features/plugin-features/components/ImageSettingInput.js +42 -0
  39. package/dist/admin-components/features/plugin-features/components/PluginSettingsForm.js +4 -2
  40. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatureEnabled.d.ts +1 -1
  41. package/dist/admin-components/features/plugin-features/hooks/usePluginFeatures.d.ts +4 -4
  42. package/dist/admin-components/features/plugin-features/hooks/usePluginSettingsForm.d.ts +1 -1
  43. package/dist/admin-components/features/plugin-features/lib/image-upload.d.ts +6 -0
  44. package/dist/admin-components/features/plugin-features/lib/image-upload.js +17 -0
  45. package/dist/admin-components/features/plugin-features/lib/plugin-features-admin-client.d.ts +4 -4
  46. package/dist/admin-components/i18n/texts.d.ts +6 -0
  47. package/dist/admin-components/i18n/texts.js +6 -0
  48. package/dist/documents/templates/financial-document.d.ts +3 -3
  49. package/dist/documents/templates/financial-document.js +68 -50
  50. package/dist/documents/templates/format.d.ts +14 -0
  51. package/dist/documents/templates/format.js +43 -1
  52. package/dist/documents/templates/html-factory.d.ts +6 -2
  53. package/dist/documents/templates/html-factory.js +6 -4
  54. package/dist/documents/templates/html-utils.d.ts +6 -0
  55. package/dist/documents/templates/html-utils.js +11 -1
  56. package/dist/documents/templates/layout.d.ts +21 -3
  57. package/dist/documents/templates/layout.js +114 -69
  58. package/dist/documents/templates/packing-list.js +9 -7
  59. package/dist/documents/types/documents.d.ts +17 -0
  60. package/dist/documents/types/html-factory.d.ts +1 -0
  61. package/dist/email/types/shop-configuration.d.ts +25 -0
  62. package/dist/email/types/template-data.d.ts +509 -0
  63. package/dist/email/types/template-data.js +154 -1
  64. package/dist/public/descriptor.js +2 -0
  65. package/dist/public/shop-master-data.d.ts +1 -1
  66. package/dist/public/shop-master-data.js +1 -1
  67. package/dist/public/types.d.ts +2 -1
  68. package/dist/shop-config/build-food-shop-config.js +4 -0
  69. package/dist/shop-config/options.d.ts +10 -3
  70. package/dist/shop-config/options.js +4 -0
  71. package/package.json +2 -2
@@ -4,8 +4,61 @@ exports.prepareReturnEmailWorkflow = exports.prepareReturnEmail = void 0;
4
4
  const utils_1 = require("@medusajs/framework/utils");
5
5
  const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
6
6
  const business_keys_1 = require("../../email/domain/business-keys");
7
+ const return_summary_1 = require("../../email/domain/return-summary");
8
+ const load_email_shop_details_1 = require("../../email/rendering/load-email-shop-details");
7
9
  const email_1 = require("../../modules/email");
8
10
  const dispatch_email_message_1 = require("./dispatch-email-message");
11
+ /**
12
+ * The snapshot must never block the return confirmation: a return that
13
+ * cannot be read or mapped is confirmed without it.
14
+ */
15
+ const returnSummaryData = async (container, orderReturn) => {
16
+ try {
17
+ const query = container.resolve(utils_1.ContainerRegistrationKeys.QUERY);
18
+ const { data: returns } = await query.graph({
19
+ entity: "return",
20
+ fields: [...return_summary_1.RETURN_SUMMARY_FIELDS],
21
+ filters: { id: orderReturn.id },
22
+ });
23
+ const { data: locations } = orderReturn.location_id
24
+ ? await query.graph({
25
+ entity: "stock_location",
26
+ fields: [...return_summary_1.RETURN_LOCATION_FIELDS],
27
+ filters: { id: orderReturn.location_id },
28
+ })
29
+ : { data: [] };
30
+ return {
31
+ summary: (0, return_summary_1.toReturnSummary)(returns[0], locations[0] ?? null, await (0, load_email_shop_details_1.loadEmailShopDetails)(container)),
32
+ };
33
+ }
34
+ catch {
35
+ container
36
+ .resolve(utils_1.ContainerRegistrationKeys.LOGGER)
37
+ .warn("Email: return summary unavailable; confirming without it.");
38
+ return {};
39
+ }
40
+ };
41
+ /**
42
+ * The snapshot must never block the return receipt: a return that cannot be
43
+ * read or mapped is confirmed without it.
44
+ */
45
+ const returnReceivedSummaryData = async (container, returnId) => {
46
+ try {
47
+ const query = container.resolve(utils_1.ContainerRegistrationKeys.QUERY);
48
+ const { data } = await query.graph({
49
+ entity: "return",
50
+ fields: [...return_summary_1.RETURN_RECEIVED_SUMMARY_FIELDS],
51
+ filters: { id: returnId },
52
+ });
53
+ return { summary: (0, return_summary_1.toReturnReceivedSummary)(data[0]) };
54
+ }
55
+ catch {
56
+ container
57
+ .resolve(utils_1.ContainerRegistrationKeys.LOGGER)
58
+ .warn("Email: received return summary unavailable; confirming without it.");
59
+ return {};
60
+ }
61
+ };
9
62
  const prepareReturnEmail = async (container, input) => {
10
63
  const orders = container.resolve(utils_1.Modules.ORDER);
11
64
  const orderReturn = (await orders.retrieveReturn(input.return_id));
@@ -32,7 +85,11 @@ const prepareReturnEmail = async (container, input) => {
32
85
  ...requestedIdentity,
33
86
  recipient: order.email ?? null,
34
87
  fact: "requested",
35
- template_data: { order_id: order.id, return_id: orderReturn.id },
88
+ template_data: {
89
+ order_id: order.id,
90
+ return_id: orderReturn.id,
91
+ ...(await returnSummaryData(container, orderReturn)),
92
+ },
36
93
  });
37
94
  }
38
95
  const receivedIdentity = {
@@ -53,7 +110,11 @@ const prepareReturnEmail = async (container, input) => {
53
110
  ...receivedIdentity,
54
111
  recipient: order.email ?? null,
55
112
  fact: "received",
56
- template_data: { order_id: order.id, return_id: orderReturn.id },
113
+ template_data: {
114
+ order_id: order.id,
115
+ return_id: orderReturn.id,
116
+ ...(await returnReceivedSummaryData(container, orderReturn.id)),
117
+ },
57
118
  });
58
119
  }
59
120
  await (0, dispatch_email_message_1.dispatchEmailMessageWorkflow)(container).run({
@@ -72,4 +133,4 @@ const prepareReturnEmail = async (container, input) => {
72
133
  exports.prepareReturnEmail = prepareReturnEmail;
73
134
  const prepareReturnEmailStep = (0, workflows_sdk_1.createStep)("email-prepare-return", async (input, { container }) => new workflows_sdk_1.StepResponse(await (0, exports.prepareReturnEmail)(container, input)));
74
135
  exports.prepareReturnEmailWorkflow = (0, workflows_sdk_1.createWorkflow)("email-prepare-return", (input) => new workflows_sdk_1.WorkflowResponse(prepareReturnEmailStep(input)));
75
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlcGFyZS1yZXR1cm4tZW1haWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL2VtYWlsL3ByZXBhcmUtcmV0dXJuLWVtYWlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUlBLHFEQUFpRTtBQUNqRSxxRUFLMkM7QUFFM0Msb0VBRzBDO0FBQzFDLCtDQUFtRDtBQUVuRCxxRUFBd0U7QUFxQmpFLE1BQU0sa0JBQWtCLEdBQUcsS0FBSyxFQUNyQyxTQUEwQixFQUMxQixLQUE4QixFQUNLLEVBQUU7SUFDckMsTUFBTSxNQUFNLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FBc0IsZUFBTyxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3JFLE1BQU0sV0FBVyxHQUFHLENBQUMsTUFBTSxNQUFNLENBQUMsY0FBYyxDQUM5QyxLQUFLLENBQUMsU0FBUyxDQUNoQixDQUFpQixDQUFDO0lBQ25CLElBQUksV0FBVyxDQUFDLFFBQVEsS0FBSyxLQUFLLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDNUMsTUFBTSxJQUFJLG1CQUFXLENBQ25CLG1CQUFXLENBQUMsS0FBSyxDQUFDLFlBQVksRUFDOUIsZ0RBQWdELENBQ2pELENBQUM7SUFDSixDQUFDO0lBQ0QsTUFBTSxLQUFLLEdBQUcsTUFBTSxNQUFNLENBQUMsYUFBYSxDQUFDLEtBQUssQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUN6RCxNQUFNLEtBQUssR0FBRyxTQUFTLENBQUMsT0FBTyxDQUFxQixvQkFBWSxDQUFDLENBQUM7SUFDbEUsTUFBTSxlQUFlLEdBQ25CLFdBQVcsQ0FBQyxlQUFlLEtBQUssSUFBSSxDQUFDLENBQUMsQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO0lBQ3pFLE1BQU0saUJBQWlCLEdBQUc7UUFDeEIsSUFBSSxFQUFFLHdCQUFpQztRQUN2QyxZQUFZLEVBQUUsSUFBQSwwQ0FBMEIsRUFBQyxXQUFXLENBQUMsRUFBRSxD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
136
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlcGFyZS1yZXR1cm4tZW1haWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL2VtYWlsL3ByZXBhcmUtcmV0dXJuLWVtYWlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUtBLHFEQUltQztBQUNuQyxxRUFLMkM7QUFFM0Msb0VBRzBDO0FBQzFDLHNFQU0yQztBQUMzQywyRkFBcUY7QUFFckYsK0NBQW1EO0FBRW5ELHFFQUF3RTtBQXNCeEU7OztHQUdHO0FBQ0gsTUFBTSxpQkFBaUIsR0FBRyxLQUFLLEVBQzdCLFNBQTBCLEVBQzFCLFdBQXlCLEVBQ2EsRUFBRTtJQUN4QyxJQUFJLENBQUM7UUFDSCxNQUFNLEtBQUssR0FBRyxTQUFTLENBQUMsT0FBTyxDQUFDLGlDQUF5QixDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ2pFLE1BQU0sRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLEdBQUcsTUFBTSxLQUFLLENBQUMsS0FBSyxDQUFDO1lBQzFDLE1BQU0sRUFBRSxRQUFRO1lBQ2hCLE1BQU0sRUFBRSxDQUFDLEdBQUcsc0NBQXFCLENBQUM7WUFDbEMsT0FBTyxFQUFFLEVBQUUsRUFBRSxFQUFFLFdBQVcsQ0FBQyxFQUFFLEVBQUU7U0FDaEMsQ0FBQyxDQUFDO1FBQ0gsTUFBTSxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsR0FBRyxXQUFXLENBQUMsV0FBVztZQUNqRCxDQUFDLENBQUMsTUFBTSxLQUFLLENBQUMsS0FBSyxDQUFDO2dCQUNoQixNQUFNLEVBQUUsZ0JBQWdCO2dCQUN4QixNQUFNLEVBQUUsQ0FBQyxHQUFHLHVDQUFzQixDQUFDO2dCQUNuQyxPQUFPLEVBQUUsRUFBRSxFQUFFLEVBQUUsV0FBVyxDQUFDLFdBQVcsRUFBRTthQUN6QyxDQUFDO1lBQ0osQ0FBQyxDQUFDLEVBQUUsSUFBSSxFQUFFLEVBQUUsRUFBRSxDQUFDO1FBQ2pCLE9BQU87WUFDTCxPQUFPLEVBQUUsSUFBQSxn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
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.prepareShipmentEmailWorkflow = exports.prepareShipmentEmail = void 0;
4
4
  const utils_1 = require("@medusajs/framework/utils");
5
5
  const workflows_sdk_1 = require("@medusajs/framework/workflows-sdk");
6
+ const core_flows_1 = require("@medusajs/medusa/core-flows");
6
7
  const business_keys_1 = require("../../email/domain/business-keys");
7
8
  const documents_fact_fingerprint_1 = require("../../email/domain/documents-fact-fingerprint");
9
+ const shipment_summary_1 = require("../../email/domain/shipment-summary");
8
10
  const email_1 = require("../../modules/email");
9
11
  const dispatch_email_message_1 = require("./dispatch-email-message");
10
12
  const documents_required_facts_1 = require("./documents-required-facts");
@@ -17,6 +19,27 @@ const findOrderIdForFulfillment = async (container, fulfillmentId) => {
17
19
  }
18
20
  return records[0].order_id;
19
21
  };
22
+ /**
23
+ * The shipment snapshot must never block the shipping confirmation: a
24
+ * fulfillment that cannot be read or mapped is confirmed without it.
25
+ */
26
+ const shipmentSummaryData = async (container, order, fulfillmentId) => {
27
+ try {
28
+ const query = container.resolve(utils_1.ContainerRegistrationKeys.QUERY);
29
+ const { data } = await query.graph({
30
+ entity: "fulfillment",
31
+ fields: [...shipment_summary_1.SHIPMENT_FULFILLMENT_FIELDS],
32
+ filters: { id: fulfillmentId },
33
+ });
34
+ return { shipment: (0, shipment_summary_1.toShipmentSummary)(order, data[0]) };
35
+ }
36
+ catch {
37
+ container
38
+ .resolve(utils_1.ContainerRegistrationKeys.LOGGER)
39
+ .warn("Email: shipment summary unavailable; confirming without it.");
40
+ return {};
41
+ }
42
+ };
20
43
  const prepareShipmentEmail = async (container, input) => {
21
44
  const email = container.resolve(email_1.EMAIL_MODULE);
22
45
  const businessKey = (0, business_keys_1.orderShippedBusinessKey)(input.fulfillment_id);
@@ -39,6 +62,7 @@ const prepareShipmentEmail = async (container, input) => {
39
62
  template_data: {
40
63
  order_id: input.order_id,
41
64
  fulfillment_id: input.fulfillment_id,
65
+ invoice_attached: input.decision === "carrier",
42
66
  documents_fact_fingerprint: (0, documents_fact_fingerprint_1.fingerprintDocumentsFact)({
43
67
  schema_version: input.schema_version,
44
68
  order_id: input.order_id,
@@ -54,8 +78,16 @@ const prepareShipmentEmail = async (container, input) => {
54
78
  })
55
79
  : await (async () => {
56
80
  const orderId = await findOrderIdForFulfillment(container, input.fulfillment_id);
57
- const orders = container.resolve(utils_1.Modules.ORDER);
58
- const order = await orders.retrieveOrder(orderId);
81
+ const { result } = await (0, core_flows_1.getOrderDetailWorkflow)(container).run({
82
+ input: {
83
+ order_id: orderId,
84
+ fields: ["id", "email", ...shipment_summary_1.SHIPMENT_ORDER_FIELDS],
85
+ },
86
+ });
87
+ if (!result) {
88
+ throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, "The shipped fulfillment's order was not found.");
89
+ }
90
+ const order = result;
59
91
  return await email.upsertMessageFact({
60
92
  ...identity,
61
93
  fact: "shipment",
@@ -63,6 +95,7 @@ const prepareShipmentEmail = async (container, input) => {
63
95
  template_data: {
64
96
  order_id: order.id,
65
97
  fulfillment_id: input.fulfillment_id,
98
+ ...(await shipmentSummaryData(container, order, input.fulfillment_id)),
66
99
  },
67
100
  suppression_code: input.no_notification === true ? "notifications_disabled" : null,
68
101
  });
@@ -79,4 +112,4 @@ const prepareShipmentEmail = async (container, input) => {
79
112
  exports.prepareShipmentEmail = prepareShipmentEmail;
80
113
  const prepareShipmentEmailStep = (0, workflows_sdk_1.createStep)("email-prepare-shipment", async (input, { container }) => new workflows_sdk_1.StepResponse(await (0, exports.prepareShipmentEmail)(container, input)));
81
114
  exports.prepareShipmentEmailWorkflow = (0, workflows_sdk_1.createWorkflow)("email-prepare-shipment", (input) => new workflows_sdk_1.WorkflowResponse(prepareShipmentEmailStep(input)));
82
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlcGFyZS1zaGlwbWVudC1lbWFpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy93b3JrZmxvd3MvZW1haWwvcHJlcGFyZS1zaGlwbWVudC1lbWFpbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFJQSxxREFJbUM7QUFDbkMscUVBSzJDO0FBRzNDLG9FQUEyRTtBQUMzRSw4RkFBeUY7QUFDekYsK0NBQW1EO0FBRW5ELHFFQUF3RTtBQUN4RSx5RUFBMkU7QUF3QzNFLE1BQU0seUJBQXlCLEdBQUcsS0FBSyxFQUNyQyxTQUEwQixFQUMxQixhQUFxQixFQUNKLEVBQUU7SUFDbkIsTUFBTSxLQUFLLEdBQUcsU0FBUyxDQUFDLE9BQU8sQ0FDN0IsaUNBQXlCLENBQUMsSUFBSSxDQUNMLENBQUM7SUFDNUIsTUFBTSxvQkFBb0IsR0FBRyxLQUFLLENBQUMsYUFBYSxDQUM5QyxlQUFPLENBQUMsS0FBSyxFQUNiLFVBQVUsRUFDVixlQUFPLENBQUMsV0FBVyxFQUNuQixnQkFBZ0IsQ0FDakIsQ0FBQztJQUNGLE1BQU0sT0FBTyxHQUFHLE1BQU0sb0JBQW9CLENBQUMsSUFBSSxDQUM3QyxFQUFFLGNBQWMsRUFBRSxhQUFhLEVBQUUsRUFDakMsRUFBRSxNQUFNLEVBQUUsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUN6QixDQUFDO0lBQ0YsSUFBSSxPQUFPLENBQUMsTUFBTSxLQUFLLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxRQUFRLEVBQUUsQ0FBQztRQUNsRCxNQUFNLElBQUksbUJBQVcsQ0FDbkIsbUJBQVcsQ0FBQyxLQUFLLENBQUMsU0FBUyxFQUMzQixtREFBbUQsQ0FDcEQsQ0FBQztJQUNKLENBQUM7SUFDRCxPQUFPLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUM7QUFDN0IsQ0FBQyxDQUFDO0FBRUssTUFBTSxvQkFBb0IsR0FBRyxLQUFLLEVBQ3ZDLFNBQTBCLEVB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
115
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlcGFyZS1zaGlwbWVudC1lbWFpbC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy93b3JrZmxvd3MvZW1haWwvcHJlcGFyZS1zaGlwbWVudC1lbWFpbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFDQSxxREFJbUM7QUFDbkMscUVBSzJDO0FBQzNDLDREQUFxRTtBQUdyRSxvRUFBMkU7QUFDM0UsOEZBQXlGO0FBQ3pGLDBFQUk2QztBQUU3QywrQ0FBbUQ7QUFFbkQscUVBQXdFO0FBQ3hFLHlFQUEyRTtBQXdDM0UsTUFBTSx5QkFBeUIsR0FBRyxLQUFLLEVBQ3JDLFNBQTBCLEVBQzFCLGFBQXFCLEVBQ0osRUFBRTtJQUNuQixNQUFNLEtBQUssR0FBRyxTQUFTLENBQUMsT0FBTyxDQUM3QixpQ0FBeUIsQ0FBQyxJQUFJLENBQ0wsQ0FBQztJQUM1QixNQUFNLG9CQUFvQixHQUFHLEtBQUssQ0FBQyxhQUFhLENBQzlDLGVBQU8sQ0FBQyxLQUFLLEVBQ2IsVUFBVSxFQUNWLGVBQU8sQ0FBQyxXQUFXLEVBQ25CLGdCQUFnQixDQUNqQixDQUFDO0lBQ0YsTUFBTSxPQUFPLEdBQUcsTUFBTSxvQkFBb0IsQ0FBQyxJQUFJLENBQzdDLEVBQUUsY0FBYyxFQUFFLGFBQWEsRUFBRSxFQUNqQyxFQUFFLE1BQU0sRUFBRSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQ3pCLENBQUM7SUFDRixJQUFJLE9BQU8sQ0FBQyxNQUFNLEtBQUssQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxFQUFFLFFBQVEsRUFBRSxDQUFDO1FBQ2xELE1BQU0sSUFBSSxtQkFBVyxDQUNuQixtQkFBVyxDQUFDLEtBQUssQ0FBQyxTQUFTLEVBQzNCLG1EQUFtRCxDQUNwRCxDQUFDO0lBQ0osQ0FBQztJQUNELE9BQU8sT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQztBQUM3QixDQUFDLENBQUM7QUFFRjs7O0dBR0c7QUFD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
package/README.md CHANGED
@@ -318,7 +318,10 @@ class DemoModuleService extends MedusaService({ /* models */ }) {
318
318
 
319
319
  - Types: `string`, `url` (http/https), `number` (finite, optional inclusive
320
320
  `min`/`max`), `boolean`, `secret`, `select` (`options: [{ value, label }]`,
321
- non-empty, unique values; the stored value must be one of them). Keys
321
+ non-empty, unique values; the stored value must be one of them), `image`
322
+ (the http(s) URL of a PNG/JPEG of at most 2 MiB that the Admin uploads
323
+ through Medusa's `/admin/uploads` into the public File Module storage and
324
+ previews; replacing or removing it leaves the old file in storage). Keys
322
325
  follow the feature key format and are unique. A `secret` has no default;
323
326
  any other default must match its type, options and bounds. `options` exist
324
327
  only on `select`, `min`/`max` only on `number`. `required` defaults to
@@ -891,6 +894,10 @@ meantime, such as a password-reset link, is not sent). A toggle read
891
894
  failure fails the subscriber with its fixed sentinel (the Event Bus retries
892
895
  it) or fails the job run.
893
896
 
897
+ The plugin declares one setting, edited in the Einstellungen section of its
898
+ Admin page: `logo` (type `image`, optional), the PNG/JPG shown on top of every
899
+ mail. It is not a Shop-Stammdaten field; Documents has its own `logo`.
900
+
894
901
  Admin surfaces of a disabled feature are hidden. The E-Mail settings page
895
902
  keeps `PluginFeatureSections` visible (so the plugin can be switched back on)
896
903
  and gates the Terms PDF versions and mail log sections below it on the
@@ -1060,20 +1067,130 @@ has at most 35 characters. Email runtime-validates every contract before use.
1060
1067
  - Password-reset messages cannot be retried by the generic retry job or resent
1061
1068
  by an Admin.
1062
1069
  - Unknown transport outcomes are never automatically resent.
1063
- - Shop branding is only the configured `displayName`; a shop overrides the
1064
- default layout or single renderers in its own configuration, not in the
1065
- plugin.
1070
+ - Shop branding is the configured `displayName` plus the `logo` setting of
1071
+ the E-Mail plugin and the company details of the Shop-Stammdaten; a shop overrides the default layout or
1072
+ single renderers in its own configuration, not in the plugin.
1066
1073
 
1067
1074
  Read this section and the central design before changing Email domain behavior.
1068
1075
 
1069
1076
  ### Shop configuration and default templates
1070
1077
 
1071
- The E-Mail module ships a neutral German layout and one renderer per accepted kind.
1072
- Both show the shop only as the configured `displayName` (escaped in HTML; the
1073
- layout header and footer, the password-reset and invite subjects). A shop
1078
+ The E-Mail module ships a neutral German layout in grey tones (table-based,
1079
+ inline styles) and one renderer per accepted kind. The layout shows, from top
1080
+ to bottom: the `logo` centered (an `image` setting of the E-Mail plugin,
1081
+ edited on its Admin page; without one, the configured `displayName`), a white card with the subject as heading and the
1082
+ renderer's body, a contact line ("Fragen? Dann schreib uns an … oder ruf uns
1083
+ an unter …", only with `email`/`phone`), and a footer with the company details
1084
+ (`legal_name` or `displayName`, address, UID, Firmenbuchnummer and
1085
+ Firmenbuchgericht, website) plus "Diese E-Mail wurde automatisch versendet.".
1086
+ Missing master data fields are left out. Email reads the logo and the
1087
+ Shop-Stammdaten when it sends a mail, so a change applies to the next mail; if
1088
+ a read fails the mail is sent without that part (logo or company details)
1089
+ instead of failing. Besides
1090
+ the layout, `displayName` appears in the password-reset and invite subjects.
1091
+ A custom `layout` receives the same data as `shop` (`EmailShopDetails`, every
1092
+ field optional) next to `bodyHtml`, `preheader` and `subject`. A shop
1074
1093
  therefore needs only `displayName`, `affiliatePortalUrl`, `inviteUrl`,
1075
1094
  `passwordResetUrl` and `orderNotificationEmail`.
1076
1095
 
1096
+ The order confirmation (`order.received`) receives an `OrderSummary` as
1097
+ `summary`: the display ID, the items with quantity and gross amount, the
1098
+ shipping method names, subtotal, shipping, discount, included tax and total,
1099
+ plus the billing and shipping address. Email freezes it when the order is
1100
+ placed, so a resend shows the order as it was placed. `summary` is absent for
1101
+ messages stored before the summary existed; a custom renderer must handle
1102
+ that case.
1103
+
1104
+ The shop's order notification (`order.received.admin`) receives the same
1105
+ `summary` (with the order's `email` and `placed_at`). Its subject names the
1106
+ order number and the customer ("Neue Bestellung #1042 von Ada Beispiel"),
1107
+ an info box lists number, customer, e-mail and order time, followed by an
1108
+ "Im Admin öffnen" button and the items, totals and addresses. Two optional
1109
+ `ShopEmailConfiguration` fields drive it: `adminOrderUrl({ orderId })` (the
1110
+ button is left out without it) and `timeZone` (IANA; the order time is left
1111
+ out without it). `buildFoodShopConfig` sets both (`<MEDUSA_BACKEND_URL>/app/
1112
+ orders/<id>` and its `timeZone` option). The layout receives `audience`
1113
+ (`"shop"` for this kind and `user.invite`, `"customer"` for all others) and
1114
+ shows the contact line only to customers.
1115
+
1116
+ The shipping confirmation (`order.shipped`, one per fulfillment) receives a
1117
+ `ShipmentSummary` as `shipment`, frozen when the shipment fact arrives: the
1118
+ display ID, the shipping option name, the tracking numbers and URLs entered
1119
+ at shipping, the parcel's items (title and variant of the order line,
1120
+ shipped quantity), the delivery address and `more_to_follow` (some ordered
1121
+ quantity is not shipped yet). `invoice_attached` is `true` when Documents chose
1122
+ this shipment for the invoice PDF. Its subject is "Deine Bestellung bei
1123
+ <displayName> ist unterwegs"; a single http(s) tracking URL becomes a
1124
+ "Sendung verfolgen" button, several become one link each. A fulfillment that
1125
+ cannot be read is confirmed without `shipment`, like a message stored before
1126
+ the snapshot existed.
1127
+
1128
+ The cancellation confirmation (`order.canceled`) receives the same
1129
+ `OrderSummary` as `summary` plus `canceled_at`, frozen when the cancellation
1130
+ fact arrives, and `cancellation_attached` (`true` when Documents attached at
1131
+ least one cancellation document). Its subject is "Deine Bestellung #1042 bei
1132
+ <displayName> wurde storniert"; an info box lists the order number and the
1133
+ cancellation date (left out without `timeZone`), followed by a fixed refund
1134
+ note and the canceled items and totals without addresses. An order that
1135
+ cannot be summarized is confirmed without `summary` and without its internal
1136
+ ID.
1137
+
1138
+ The return confirmation (`order.return_requested`) receives a `ReturnSummary`
1139
+ as `summary`, frozen when the return is requested: return and order numbers,
1140
+ the return shipping method, the items to send back (title and variant of the
1141
+ order line, quantity, return reason label) and the return address. It is
1142
+ the stock location's address, addressed to the location's company or else
1143
+ the master data's `legal_name`; without a location address it falls back to
1144
+ the master data's company address. Its subject is "Deine Rücksendung zur
1145
+ Bestellung #1042 bei <displayName>"; the mail asks the customer to enclose
1146
+ the return number (`#R5`). A return that cannot be read is confirmed without
1147
+ `summary` and without internal IDs.
1148
+
1149
+ The return receipt (`order.return_received`, sent once the return is
1150
+ completely received) receives a `ReturnReceivedSummary` as `summary`: return
1151
+ and order numbers, `received_at` and the received items (title and variant
1152
+ of the order line, received quantity and the damaged part of it). Its subject
1153
+ is "Deine Rücksendung zur Bestellung #1042 bei <displayName> ist
1154
+ eingetroffen"; the arrival date needs `timeZone`, damaged units are noted per
1155
+ item, and a fixed sentence says that an eventual refund follows on the
1156
+ original payment method, without promising a separate mail (`order.refunded`
1157
+ only exists with Documents).
1158
+
1159
+ The refund confirmation (`order.refunded`, one per non-cancellation refund,
1160
+ Documents only) receives a `RefundSummary` as `summary`, frozen when the
1161
+ Documents refund fact arrives and read through the refund's payment: order
1162
+ number, refunded gross amount and currency, `refunded_at` and the Admin's
1163
+ refund reason label. `cancellation_attached` is `true` when Documents attached
1164
+ a cancellation document. Its subject is "Deine Erstattung zur Bestellung #1042
1165
+ bei <displayName>"; the info box shows number, amount, date (needs
1166
+ `timeZone`) and reason, followed by a fixed note on bank processing times.
1167
+ Without `summary` the mail keeps the intro and the attachment note.
1168
+
1169
+ The password reset (`auth.password_reset`) has the subject "Neues Passwort
1170
+ für dein Kundenkonto bei <displayName>", a "Neues Passwort festlegen" button,
1171
+ the link as copyable text below it, the link's lifetime ("Der Link ist 15
1172
+ Minuten gültig.", computed from the token's `exp` − `iat` and left out
1173
+ without `iat`) and a note that the mail can be ignored. The lifetime is
1174
+ passed as the ephemeral `valid_minutes` next to `reset_url` and is never
1175
+ persisted.
1176
+
1177
+ The affiliate reminder (`affiliate.consignment_report_due`) greets the
1178
+ affiliate by `affiliate_display_name`, names the report month ("September
1179
+ 2026", Austrian month names) and the due date ("10.10.2026") as calendar
1180
+ values formatted in UTC so no time zone shifts them, explains that the counted
1181
+ stock determines the billed sales, links `affiliatePortalUrl` as a "Zum
1182
+ Partnerbereich" button and says the mail can be ignored once the report is
1183
+ submitted. The button leads to a 404 while the affiliate plugin's
1184
+ `partner_portal` toggle is off.
1185
+
1186
+ The Admin invite (`user.invite`) has the subject "Einladung ins Admin-Team von
1187
+ <displayName>", an "Einladung annehmen" button with the link as copyable text,
1188
+ the invite's current expiry ("Die Einladung ist gültig bis 02.10.2026, 01:40
1189
+ Uhr.", needs `timeZone`; read from the invite at send time, so a resend shows
1190
+ its new expiry) and a note that the mail can be ignored. The expiry is passed
1191
+ as the ephemeral `expires_at` next to `invite_url`. The invite goes to the
1192
+ shop's team, so its layout audience is `"shop"` (no contact line).
1193
+
1077
1194
  - `layout` (optional) replaces the default layout completely.
1078
1195
  - `renderers` (optional, `Partial<EmailRendererMap>`) replaces only the listed
1079
1196
  kinds; every other kind keeps its default renderer.
@@ -1384,8 +1501,9 @@ below. `medusa db:migrate` creates its tables.
1384
1501
  `@hartl-services/medusa-base/config` exports, next to the environment helpers,
1385
1502
  build-time values for `medusa-config.ts` and tests:
1386
1503
 
1387
- - `createDefaultDocumentHtmlFactory({ timeZone })` — the built-in factory, to
1388
- use as it is or to wrap (for example to add a header to the default HTML).
1504
+ - `createDefaultDocumentHtmlFactory({ timeZone, theme? })` — the built-in
1505
+ factory, to use as it is, with a shop theme (see "Layout, logo, payment
1506
+ terms and shop theme" below) or to wrap.
1389
1507
  - `DOCUMENTS_BUILD_PLACEHOLDERS` — `{ DOCUMENT_PDF_EXECUTABLE: "google-chrome" }`.
1390
1508
  `medusa build` evaluates `medusa-config.ts` without the runtime-only
1391
1509
  environment, so spread it under the real environment while building.
@@ -1393,6 +1511,44 @@ build-time values for `medusa-config.ts` and tests:
1393
1511
  The document types (`DocumentMerchantOption`, `DocumentHtmlFactory`,
1394
1512
  `DocumentRenderInput`, ...) come from `@hartl-services/medusa-base/types`.
1395
1513
 
1514
+ ### Layout, logo, payment terms and shop theme
1515
+
1516
+ The default markup prints an A4 letterhead: logo top left, document title and
1517
+ key data (number, order, date, customer UID for exempt B2B supplies) on the
1518
+ right, the billing address on the left, the item table with batch and
1519
+ best-before date per line, the totals below the price columns and a
1520
+ three-column footer (sender with UID and company register, contact, bank) on
1521
+ every page. Packing lists show only the sender in the footer. Every page
1522
+ carries "Seite x von y" bottom right in an 8 mm bottom page margin
1523
+ (`@page { @bottom-right }`; the page count exists only in page margin boxes).
1524
+
1525
+ Settings under Settings → "Hartl Services Plugins" → **Dokumente**, read on
1526
+ every render and passed as `input.settings` (`DocumentRenderSettings`). Like
1527
+ the merchant data they are not part of the snapshot: a regenerated document
1528
+ shows the current values.
1529
+
1530
+ - **Logo** (`logo`, PNG or JPG): each render downloads it (10 s timeout, at
1531
+ most 2 MiB) and passes it inline as `settings.logo_src` (`data:` URL), so
1532
+ Chromium never waits on the network. Without a logo, or when the download
1533
+ fails (logged as a warning), the document shows the merchant's legal name;
1534
+ a logo never blocks issuance.
1535
+ - **Zahlungskonditionen** (`payment_terms`, text): printed below the totals
1536
+ of invoices only (not on cancellations or credit notes); blank means none.
1537
+ Line breaks in the text are kept.
1538
+
1539
+ **Theme:** `createDefaultDocumentHtmlFactory({ timeZone, theme })` keeps the
1540
+ content and markup and changes only the look. `theme.stylesheet` is CSS
1541
+ appended after the default stylesheet (later rules win; the defaults use the
1542
+ custom properties `--accent`, `--text`, `--muted`, `--line`, `--table-head`,
1543
+ `--page-x`, `--page-top`, `--footer-height`). Put a watermark or other
1544
+ full-page image into `@page { background: ... / 210mm 297mm }`: it covers the
1545
+ whole sheet on every page, whereas Chromium clips fixed elements at the page
1546
+ margin and sizes an inline `<svg>` in them wrongly from the second page on.
1547
+ Restyle the page number with `@page { @bottom-right { ... } }`. Embed web
1548
+ fonts as `data:` URLs; the container's Chromium has only its system fonts. A
1549
+ shop passes its factory through the preset option `documentHtmlFactory` (see
1550
+ "Shop-Config-Preset").
1551
+
1396
1552
  ### Merchant data
1397
1553
 
1398
1554
  The legal sender printed on every document comes from the base plugin's shop
@@ -2207,6 +2363,7 @@ validate the environment; a missing variable fails with the variable's name.
2207
2363
  | `orderNotificationEmail` | Receives order notification e-mails; must look like an e-mail address. |
2208
2364
  | `timeZone` | IANA time zone (validated with `Intl`). Chooses the annual document-number period, the printed dates on documents and the affiliate consignment reminder time zone. |
2209
2365
  | `storefrontCountryCode` | 2-letter lowercase country code of the storefront route. Links: `<STOREFRONT_URL>/<cc>/partnerportal` (affiliate portal), `<STOREFRONT_URL>/<cc>/reset-password` (the e-mail plugin appends `#token=`), referral base `<STOREFRONT_URL>/<cc>`. The Admin invite link is `<MEDUSA_BACKEND_URL>/app/invite?token=`. |
2366
+ | `documentHtmlFactory` | Optional `DocumentHtmlFactory` that replaces the default document markup, typically `createDefaultDocumentHtmlFactory({ timeZone, theme })` from `/config` with the shop's CSS and watermark (see "Layout, logo, payment terms and shop theme"). Must be a function. |
2210
2367
  | `loadEnvFile` | Default `true`: calls `loadEnv(NODE_ENV \|\| "development", process.cwd())`. Pass `false` when the environment is already complete (tests, isolated child processes). |
2211
2368
 
2212
2369
  ### Environment variables
@@ -0,0 +1,15 @@
1
+ type ImageSettingInputProps = {
2
+ id: string;
3
+ hintId: string;
4
+ label: string;
5
+ /** Public URL of the current image; `""` = none. */
6
+ value: string;
7
+ disabled: boolean;
8
+ onChange: (url: string) => void;
9
+ };
10
+ /**
11
+ * Uploads an image to the public File Module storage and hands its URL to
12
+ * the settings draft; the form stores it on save like any other value.
13
+ */
14
+ export declare const ImageSettingInput: ({ id, hintId, label, value, disabled, onChange, }: ImageSettingInputProps) => import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Hint, Text } from "@medusajs/ui";
3
+ import { useMutation } from "@tanstack/react-query";
4
+ import { useRef, useState } from "react";
5
+ import { texts } from "../../../i18n/texts";
6
+ import { sdk } from "../../../lib/sdk";
7
+ import { validateSettingImage } from "../lib/image-upload";
8
+ /**
9
+ * Uploads an image to the public File Module storage and hands its URL to
10
+ * the settings draft; the form stores it on save like any other value.
11
+ */
12
+ export const ImageSettingInput = ({ id, hintId, label, value, disabled, onChange, }) => {
13
+ const fileInput = useRef(null);
14
+ const [error, setError] = useState(null);
15
+ const upload = useMutation({
16
+ mutationFn: async (file) => {
17
+ const { files } = await sdk.admin.upload.create({ files: [file] });
18
+ const url = files[0]?.url;
19
+ if (!url) {
20
+ throw new Error("upload returned no file");
21
+ }
22
+ return url;
23
+ },
24
+ onSuccess: (url) => onChange(url),
25
+ onError: () => setError(texts.settings.imageUploadFailed),
26
+ });
27
+ const busy = disabled || upload.isPending;
28
+ const selectFile = (file) => {
29
+ if (!file) {
30
+ return;
31
+ }
32
+ const invalid = validateSettingImage(file);
33
+ setError(invalid);
34
+ if (!invalid) {
35
+ upload.mutate(file);
36
+ }
37
+ };
38
+ return (_jsxs("div", { className: "grid gap-2", children: [value && (_jsx("div", { className: "bg-ui-bg-subtle flex items-center justify-center rounded-md border p-4", children: _jsx("img", { src: value, alt: texts.settings.imagePreviewAlt(label), className: "max-h-24 max-w-full object-contain" }) })), _jsx("input", { ref: fileInput, id: id, "aria-describedby": hintId, type: "file", accept: "image/png,image/jpeg", className: "hidden", onChange: (event) => {
39
+ selectFile(event.target.files?.[0]);
40
+ event.target.value = "";
41
+ } }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { type: "button", size: "small", variant: "secondary", isLoading: upload.isPending, disabled: busy, onClick: () => fileInput.current?.click(), children: value ? texts.settings.imageReplace : texts.settings.imageUpload }), value && (_jsx(Button, { type: "button", size: "small", variant: "transparent", disabled: busy, onClick: () => onChange(""), children: texts.settings.imageRemove })), !value && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.imageNone }))] }), error && _jsx(Hint, { variant: "error", children: error })] }));
42
+ };
@@ -2,6 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Alert, Button, Container, Heading, Hint, Input, Label, Select, Switch, Text, } from "@medusajs/ui";
3
3
  import { texts } from "../../../i18n/texts";
4
4
  import { usePluginSettingsForm } from "../hooks/usePluginSettingsForm";
5
+ import { ImageSettingInput } from "./ImageSettingInput";
5
6
  const fieldId = (pluginKey, key) => `plugin-setting-${pluginKey}-${key}`;
6
7
  export const missingLabels = (plugin) => plugin.missing
7
8
  .map((key) => plugin.settings.find((setting) => setting.key === key)?.label ?? key)
@@ -27,13 +28,14 @@ const SettingField = ({ pluginKey, setting, invalidatesLabels, value, invalid, d
27
28
  ? texts.settings.secretClearUndoLabel(setting.label)
28
29
  : texts.settings.secretClearLabel(setting.label), onClick: () => onChange(clearPending ? "" : null), children: clearPending
29
30
  ? texts.settings.secretClearUndo
30
- : texts.settings.secretClear }))] })) : setting.type === "select" ? (_jsxs(Select, { value: typeof value === "string" ? value : "", disabled: disabled, onValueChange: (next) => onChange(next), children: [_jsx(Select.Trigger, { id: id, "aria-describedby": hintId, children: _jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }), _jsx(Select.Content, { children: setting.options.map((option) => (_jsx(Select.Item, { value: option.value, children: option.label }, option.value))) })] })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: "number", step: "any", min: setting.min ?? undefined, max: setting.max ?? undefined, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: setting.type === "url" ? "url" : "text", value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), _jsxs("div", { id: hintId, className: "grid gap-1", children: [setting.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatesLabels && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels) })), bounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: bounds ?? texts.settings.invalidNumber }))] })] }));
31
+ : texts.settings.secretClear }))] })) : setting.type === "select" ? (_jsxs(Select, { value: typeof value === "string" ? value : "", disabled: disabled, onValueChange: (next) => onChange(next), children: [_jsx(Select.Trigger, { id: id, "aria-describedby": hintId, children: _jsx(Select.Value, { placeholder: texts.settings.selectPlaceholder }) }), _jsx(Select.Content, { children: setting.options.map((option) => (_jsx(Select.Item, { value: option.value, children: option.label }, option.value))) })] })) : setting.type === "image" ? (_jsx(ImageSettingInput, { id: id, hintId: hintId, label: setting.label, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (url) => onChange(url) })) : setting.type === "number" ? (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: "number", step: "any", min: setting.min ?? undefined, max: setting.max ?? undefined, value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value, event.target.validity.badInput) })) : (_jsx(Input, { id: id, "aria-describedby": hintId, "aria-invalid": invalid, type: setting.type === "url" ? "url" : "text", value: typeof value === "string" ? value : "", disabled: disabled, onChange: (event) => onChange(event.target.value) })), _jsxs("div", { id: hintId, className: "grid gap-1", children: [setting.description && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: setting.description })), clearPending && (_jsx(Hint, { variant: "info", children: texts.settings.secretClearPending })), invalidatesLabels && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: texts.settings.invalidates(invalidatesLabels) })), bounds && !invalid && (_jsx(Text, { size: "small", className: "text-ui-fg-muted", children: bounds })), invalid && (_jsx(Hint, { variant: "error", children: bounds ?? texts.settings.invalidNumber }))] })] }));
31
32
  };
32
33
  export const PluginSettingsForm = ({ plugin }) => {
33
34
  const form = usePluginSettingsForm(plugin);
34
35
  const pending = form.mutation.isPending;
35
36
  const hasTextFields = plugin.settings.some((setting) => setting.type !== "boolean" &&
36
37
  setting.type !== "secret" &&
37
- setting.type !== "select");
38
+ setting.type !== "select" &&
39
+ setting.type !== "image");
38
40
  return (_jsxs(Container, { className: "divide-y p-0", "aria-busy": pending, children: [_jsxs("div", { className: "px-6 py-4", children: [_jsx(Heading, { level: "h2", children: texts.sections.settings }), hasTextFields && (_jsx(Text, { size: "small", className: "text-ui-fg-subtle", children: texts.settings.clearHint }))] }), _jsxs("form", { onSubmit: form.submit, className: "grid gap-5 px-6 py-5", children: [!plugin.configured && (_jsx(Alert, { variant: "warning", children: texts.settings.missing(missingLabels(plugin)) })), plugin.settings.map((setting) => (_jsx(SettingField, { pluginKey: plugin.plugin_key, setting: setting, invalidatesLabels: invalidatesLabels(plugin, setting), value: form.draft[setting.key], invalid: form.invalid.includes(setting.key), disabled: pending, onChange: (value, badInput) => form.setValue(setting.key, value, badInput) }, setting.key))), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { type: "submit", size: "small", isLoading: pending, disabled: !form.dirty || form.invalid.length > 0 || pending, children: texts.settings.save }) })] })] }));
39
41
  };
@@ -74,7 +74,7 @@ export declare const pluginFeatureEnabledQueryOptions: (pluginKey: string, featu
74
74
  source: "default" | "stored" | "config" | "none";
75
75
  invalidates: string[];
76
76
  } | {
77
- type: "string" | "boolean" | "url";
77
+ type: "string" | "boolean" | "url" | "image";
78
78
  default: string | number | boolean | null;
79
79
  value: string | number | boolean | null;
80
80
  key: string;
@@ -54,7 +54,7 @@ export declare function usePluginFeatures(): import("@tanstack/react-query").Use
54
54
  source: "default" | "stored" | "config" | "none";
55
55
  invalidates: string[];
56
56
  } | {
57
- type: "string" | "boolean" | "url";
57
+ type: "string" | "boolean" | "url" | "image";
58
58
  default: string | number | boolean | null;
59
59
  value: string | number | boolean | null;
60
60
  key: string;
@@ -141,7 +141,7 @@ export declare function usePluginFeature(pluginKey: string | undefined): import(
141
141
  source: "default" | "stored" | "config" | "none";
142
142
  invalidates: string[];
143
143
  } | {
144
- type: "string" | "boolean" | "url";
144
+ type: "string" | "boolean" | "url" | "image";
145
145
  default: string | number | boolean | null;
146
146
  value: string | number | boolean | null;
147
147
  key: string;
@@ -222,7 +222,7 @@ export declare function useUpdatePluginFeatures(): import("@tanstack/react-query
222
222
  source: "default" | "stored" | "config" | "none";
223
223
  invalidates: string[];
224
224
  } | {
225
- type: "string" | "boolean" | "url";
225
+ type: "string" | "boolean" | "url" | "image";
226
226
  default: string | number | boolean | null;
227
227
  value: string | number | boolean | null;
228
228
  key: string;
@@ -307,7 +307,7 @@ export declare function useUpdatePluginSettings(pluginKey: string): import("@tan
307
307
  source: "default" | "stored" | "config" | "none";
308
308
  invalidates: string[];
309
309
  } | {
310
- type: "string" | "boolean" | "url";
310
+ type: "string" | "boolean" | "url" | "image";
311
311
  default: string | number | boolean | null;
312
312
  value: string | number | boolean | null;
313
313
  key: string;
@@ -63,7 +63,7 @@ export declare function usePluginSettingsForm(plugin: AdminPluginFeatures): {
63
63
  source: "default" | "stored" | "config" | "none";
64
64
  invalidates: string[];
65
65
  } | {
66
- type: "string" | "boolean" | "url";
66
+ type: "string" | "boolean" | "url" | "image";
67
67
  default: string | number | boolean | null;
68
68
  value: string | number | boolean | null;
69
69
  key: string;
@@ -0,0 +1,6 @@
1
+ /** Largest image an `image` setting accepts in the Admin. */
2
+ export declare const SETTING_IMAGE_MAX_BYTES: number;
3
+ type ImageSelection = Pick<File, "size" | "type">;
4
+ /** Why `file` cannot be uploaded for an `image` setting; `null` = valid. */
5
+ export declare const validateSettingImage: (file: ImageSelection) => string | null;
6
+ export {};
@@ -0,0 +1,17 @@
1
+ /** Largest image an `image` setting accepts in the Admin. */
2
+ export const SETTING_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
3
+ // Gmail and Outlook do not display SVG or WebP in e-mails.
4
+ const IMAGE_TYPES = new Set(["image/png", "image/jpeg"]);
5
+ /** Why `file` cannot be uploaded for an `image` setting; `null` = valid. */
6
+ export const validateSettingImage = (file) => {
7
+ if (!IMAGE_TYPES.has(file.type)) {
8
+ return "Bitte eine PNG- oder JPG-Datei auswählen.";
9
+ }
10
+ if (file.size === 0) {
11
+ return "Die Bilddatei ist leer.";
12
+ }
13
+ if (file.size > SETTING_IMAGE_MAX_BYTES) {
14
+ return "Die Bilddatei darf maximal 2 MiB groß sein.";
15
+ }
16
+ return null;
17
+ };