@carlonicora/nextjs-jsonapi 3.11.0 → 3.13.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/dist/{AssistantInterface-DwdBzS9f.d.ts → AssistantInterface-RvZzlcA_.d.ts} +2 -0
- package/dist/{AssistantInterface-DMh-zApr.d.mts → AssistantInterface-jL56dyNc.d.mts} +2 -0
- package/dist/{AssistantMessageInterface-0zyVingf.d.ts → AssistantMessageInterface-CuaMDs2i.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-ddyG114R.d.mts → AssistantMessageInterface-Du3ShThp.d.mts} +1 -1
- package/dist/{BlockNoteEditor-XOVNTXDY.js → BlockNoteEditor-CFVSHS46.js} +19 -19
- package/dist/{BlockNoteEditor-XOVNTXDY.js.map → BlockNoteEditor-CFVSHS46.js.map} +1 -1
- package/dist/{BlockNoteEditor-NBHDEZ7W.mjs → BlockNoteEditor-SERY337S.mjs} +4 -4
- package/dist/{BlockNoteViewer-C2UUOBAF.js → BlockNoteViewer-CWFPYYIU.js} +3 -3
- package/dist/{BlockNoteViewer-C2UUOBAF.js.map → BlockNoteViewer-CWFPYYIU.js.map} +1 -1
- package/dist/{BlockNoteViewer-TYLVTIUD.mjs → BlockNoteViewer-P2VKEFRN.mjs} +2 -2
- package/dist/{ai-connection.module-28f-hux0.d.mts → HandbookSectionInterface-BOxIdBdd.d.mts} +54 -1
- package/dist/{ai-connection.module-Bqwh_DnR.d.ts → HandbookSectionInterface-CAXxbDw0.d.ts} +54 -1
- package/dist/{auth.interface-B6ZQLGjS.d.ts → auth.interface-BC1jp_ni.d.ts} +1 -1
- package/dist/{auth.interface-jYD8Gc4P.d.mts → auth.interface-dR2-6gPM.d.mts} +1 -1
- package/dist/billing/index.d.mts +27 -0
- package/dist/billing/index.d.ts +27 -0
- package/dist/billing/index.js +697 -450
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +282 -35
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-6J2SQZ3G.js → chunk-IB7DTXWG.js} +847 -129
- package/dist/chunk-IB7DTXWG.js.map +1 -0
- package/dist/{chunk-YN4LOVL4.js → chunk-LVE3BJ6K.js} +2106 -1109
- package/dist/chunk-LVE3BJ6K.js.map +1 -0
- package/dist/{chunk-M5QAHSCY.mjs → chunk-OSFYCSTW.mjs} +725 -7
- package/dist/chunk-OSFYCSTW.mjs.map +1 -0
- package/dist/{chunk-XNGI4HDZ.mjs → chunk-UQXE3SME.mjs} +2100 -1103
- package/dist/chunk-UQXE3SME.mjs.map +1 -0
- package/dist/{chunk-IMSIVIIC.mjs → chunk-VZKJIAND.mjs} +2 -2
- package/dist/{chunk-V6CGCARD.js → chunk-XH4IURUD.js} +7 -7
- package/dist/{chunk-V6CGCARD.js.map → chunk-XH4IURUD.js.map} +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +258 -7
- package/dist/components/index.d.ts +258 -7
- package/dist/components/index.js +30 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +29 -3
- package/dist/contexts/index.d.mts +17 -5
- package/dist/contexts/index.d.ts +17 -5
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +493 -12
- package/dist/core/index.d.ts +493 -12
- package/dist/core/index.js +38 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +37 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +101 -101
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +39 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38 -2
- package/dist/{notification.interface-DYF56CtE.d.ts → notification.interface-CjzV_KII.d.ts} +1 -1
- package/dist/{notification.interface-BWOCvKRP.d.mts → notification.interface-MnRt1Tjm.d.mts} +1 -1
- package/dist/{s3.service-D_dswjgz.d.mts → s3.service-C0Yq2Sx3.d.mts} +2 -2
- package/dist/{s3.service-DserU9VK.d.ts → s3.service-CUGarZs3.d.ts} +2 -2
- package/dist/server/index.d.mts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{useSocket-BZG9oX-L.d.mts → useSocket-BcH7l7B8.d.mts} +1 -1
- package/dist/{useSocket-CiyTtYFj.d.ts → useSocket-DCZd_eJk.d.ts} +1 -1
- package/package.json +1 -1
- package/src/components/containers/ReactMarkdownContainer.tsx +61 -11
- package/src/components/index.ts +1 -0
- package/src/core/index.ts +5 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/assistant/components/parts/AssistantComposer.tsx +22 -3
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +13 -3
- package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +10 -3
- package/src/features/assistant/contexts/AssistantContext.tsx +30 -3
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +32 -0
- package/src/features/assistant/data/Assistant.ts +2 -0
- package/src/features/assistant/data/AssistantInterface.ts +2 -0
- package/src/features/assistant/data/AssistantService.ts +4 -0
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +5 -1
- package/src/features/assistant-message/data/AssistantMessage.ts +27 -2
- package/src/features/billing/i18n-keys.ts +7 -0
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +108 -2
- package/src/features/billing/stripe-price/components/forms/__tests__/PriceEditor.spec.tsx +182 -0
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +152 -25
- package/src/features/billing/stripe-price/components/lists/__tests__/PricesList.spec.tsx +268 -0
- package/src/features/billing/stripe-price/data/__tests__/stripe-price.transfer.spec.ts +265 -0
- package/src/features/billing/stripe-price/data/stripe-price.fields.ts +1 -0
- package/src/features/billing/stripe-price/data/stripe-price.transfer.ts +194 -0
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +66 -2
- package/src/features/content/data/content.interface.ts +1 -1
- package/src/features/handbook/HandbookPageModule.ts +41 -0
- package/src/features/handbook/HandbookSectionModule.ts +35 -0
- package/src/features/handbook/HandbookThreadMessageModule.ts +21 -0
- package/src/features/handbook/HandbookThreadModule.ts +29 -0
- package/src/features/handbook/components/containers/HandbookAdminContainer.tsx +17 -0
- package/src/features/handbook/components/containers/HandbookAskContainer.tsx +142 -0
- package/src/features/handbook/components/containers/HandbookAskSheet.tsx +149 -0
- package/src/features/handbook/components/containers/HandbookPageContainer.tsx +143 -0
- package/src/features/handbook/components/containers/__tests__/HandbookAskContainer.spec.tsx +268 -0
- package/src/features/handbook/components/containers/__tests__/HandbookAskSheet.spec.tsx +143 -0
- package/src/features/handbook/components/containers/__tests__/HandbookPageContainer.spec.tsx +288 -0
- package/src/features/handbook/components/details/HandbookPageDetails.tsx +27 -0
- package/src/features/handbook/components/details/HandbookPageNeighbours.tsx +72 -0
- package/src/features/handbook/components/details/HandbookPageToc.tsx +165 -0
- package/src/features/handbook/components/forms/HandbookSyncButton.tsx +43 -0
- package/src/features/handbook/components/forms/__tests__/HandbookSyncButton.spec.tsx +42 -0
- package/src/features/handbook/components/index.ts +12 -0
- package/src/features/handbook/components/lists/HandbookContents.tsx +196 -0
- package/src/features/handbook/components/lists/HandbookSectionGroup.tsx +153 -0
- package/src/features/handbook/components/lists/__tests__/HandbookContents.spec.tsx +247 -0
- package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +62 -0
- package/src/features/handbook/components/parts/HandbookPageNavigator.tsx +143 -0
- package/src/features/handbook/contexts/HandbookContext.tsx +109 -0
- package/src/features/handbook/contexts/__tests__/HandbookContext.spec.tsx +131 -0
- package/src/features/handbook/data/HandbookPage.spec.ts +136 -0
- package/src/features/handbook/data/HandbookPage.ts +99 -0
- package/src/features/handbook/data/HandbookPageFields.ts +31 -0
- package/src/features/handbook/data/HandbookPageInput.ts +8 -0
- package/src/features/handbook/data/HandbookPageInterface.ts +29 -0
- package/src/features/handbook/data/HandbookPageService.ts +60 -0
- package/src/features/handbook/data/HandbookSection.spec.ts +58 -0
- package/src/features/handbook/data/HandbookSection.ts +73 -0
- package/src/features/handbook/data/HandbookSectionInput.ts +12 -0
- package/src/features/handbook/data/HandbookSectionInterface.ts +26 -0
- package/src/features/handbook/data/HandbookSectionService.ts +26 -0
- package/src/features/handbook/data/HandbookThread.ts +114 -0
- package/src/features/handbook/data/HandbookThreadInterface.ts +31 -0
- package/src/features/handbook/data/HandbookThreadMessage.ts +144 -0
- package/src/features/handbook/data/HandbookThreadMessageInterface.ts +35 -0
- package/src/features/handbook/data/HandbookThreadMessageService.ts +30 -0
- package/src/features/handbook/data/HandbookThreadService.ts +86 -0
- package/src/features/handbook/data/__tests__/HandbookThreadService.spec.ts +112 -0
- package/src/features/handbook/data/handbookDisplay.spec.ts +92 -0
- package/src/features/handbook/data/handbookDisplay.ts +55 -0
- package/src/features/handbook/data/index.ts +16 -0
- package/src/features/handbook/hooks/__tests__/useHandbookAsk.spec.ts +198 -0
- package/src/features/handbook/hooks/useHandbookAsk.ts +213 -0
- package/src/features/handbook/i18n-keys.ts +46 -0
- package/src/features/handbook/index.ts +7 -0
- package/src/features/how-to/components/forms/HowToEditor.tsx +0 -1
- package/src/features/how-to/components/forms/HowToReindexButton.spec.tsx +84 -0
- package/src/features/how-to/components/forms/HowToReindexButton.tsx +46 -0
- package/src/features/how-to/components/index.ts +1 -0
- package/src/features/how-to/components/lists/HowToList.tsx +5 -1
- package/src/features/how-to/data/HowTo.spec.ts +61 -1
- package/src/features/how-to/data/HowToService.ts +13 -0
- package/dist/chunk-6J2SQZ3G.js.map +0 -1
- package/dist/chunk-M5QAHSCY.mjs.map +0 -1
- package/dist/chunk-XNGI4HDZ.mjs.map +0 -1
- package/dist/chunk-YN4LOVL4.js.map +0 -1
- /package/dist/{BlockNoteEditor-NBHDEZ7W.mjs.map → BlockNoteEditor-SERY337S.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-TYLVTIUD.mjs.map → BlockNoteViewer-P2VKEFRN.mjs.map} +0 -0
- /package/dist/{chunk-IMSIVIIC.mjs.map → chunk-VZKJIAND.mjs.map} +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { StripePriceInterface } from "../stripe-price.interface";
|
|
3
|
+
import {
|
|
4
|
+
STRIPE_PRICE_TRANSFER_KIND,
|
|
5
|
+
STRIPE_PRICE_TRANSFER_VERSION,
|
|
6
|
+
buildStripePriceTransferDocument,
|
|
7
|
+
parseStripePriceTransferDocument,
|
|
8
|
+
priceToSeed,
|
|
9
|
+
stripePriceTransferFilename,
|
|
10
|
+
} from "../stripe-price.transfer";
|
|
11
|
+
|
|
12
|
+
// A plain object, never the real model: the model's `currency` / `priceType`
|
|
13
|
+
// getters throw when the attribute is absent, and the point of these tests is
|
|
14
|
+
// the transformation, not the hydration.
|
|
15
|
+
const makePrice = (overrides: Partial<Record<keyof StripePriceInterface, unknown>> = {}): StripePriceInterface =>
|
|
16
|
+
({
|
|
17
|
+
id: "price-node-1",
|
|
18
|
+
stripePriceId: "price_1SourceEnv",
|
|
19
|
+
productId: "product-node-1",
|
|
20
|
+
active: true,
|
|
21
|
+
currency: "eur",
|
|
22
|
+
unitAmount: 5900,
|
|
23
|
+
priceType: "recurring",
|
|
24
|
+
recurring: { interval: "month", intervalCount: 1, usageType: "licensed" },
|
|
25
|
+
nickname: "Standard",
|
|
26
|
+
lookupKey: "standard_monthly",
|
|
27
|
+
metadata: { env: "production" },
|
|
28
|
+
description: "The standard plan",
|
|
29
|
+
features: ["Unlimited projects", "Priority support"],
|
|
30
|
+
token: 1000,
|
|
31
|
+
isTrial: false,
|
|
32
|
+
priceFeatures: [{ id: "feature-node-9", name: "AI assistant" }],
|
|
33
|
+
...overrides,
|
|
34
|
+
}) as unknown as StripePriceInterface;
|
|
35
|
+
|
|
36
|
+
describe("stripe-price.transfer", () => {
|
|
37
|
+
describe("priceToSeed", () => {
|
|
38
|
+
it("carries no identity across environments", () => {
|
|
39
|
+
const seed = priceToSeed(makePrice());
|
|
40
|
+
|
|
41
|
+
expect(seed).not.toHaveProperty("id");
|
|
42
|
+
expect(seed).not.toHaveProperty("stripePriceId");
|
|
43
|
+
expect(seed).not.toHaveProperty("productId");
|
|
44
|
+
expect(seed).not.toHaveProperty("active");
|
|
45
|
+
expect(seed).not.toHaveProperty("lookupKey");
|
|
46
|
+
expect(seed).not.toHaveProperty("metadata");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps unitAmount in minor units", () => {
|
|
50
|
+
expect(priceToSeed(makePrice({ unitAmount: 5900 })).unitAmount).toBe(5900);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("defaults a missing unitAmount to 0", () => {
|
|
54
|
+
expect(priceToSeed(makePrice({ unitAmount: undefined })).unitAmount).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("exports platform features by NAME, never by id", () => {
|
|
58
|
+
const seed = priceToSeed(
|
|
59
|
+
makePrice({
|
|
60
|
+
priceFeatures: [
|
|
61
|
+
{ id: "feature-node-9", name: "AI assistant" },
|
|
62
|
+
{ id: "feature-node-3", name: "Custom domains" },
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(seed.platformFeatures).toEqual(["AI assistant", "Custom domains"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("omits platformFeatures entirely when the price has none", () => {
|
|
71
|
+
const seed = priceToSeed(makePrice({ priceFeatures: [] }));
|
|
72
|
+
|
|
73
|
+
expect(seed).not.toHaveProperty("platformFeatures");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("exports interval 'one_time' for a one-time price", () => {
|
|
77
|
+
const seed = priceToSeed(makePrice({ priceType: "one_time", recurring: undefined }));
|
|
78
|
+
|
|
79
|
+
expect(seed.interval).toBe("one_time");
|
|
80
|
+
expect(seed).not.toHaveProperty("intervalCount");
|
|
81
|
+
expect(seed).not.toHaveProperty("usageType");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("falls back to a monthly interval when a recurring price has no recurring block", () => {
|
|
85
|
+
const seed = priceToSeed(makePrice({ priceType: "recurring", recurring: undefined }));
|
|
86
|
+
|
|
87
|
+
expect(seed.interval).toBe("month");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("keeps a token of 0, which means 'no AI' rather than 'unset'", () => {
|
|
91
|
+
expect(priceToSeed(makePrice({ token: 0 })).token).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("omits optional keys instead of writing undefined", () => {
|
|
95
|
+
const seed = priceToSeed(
|
|
96
|
+
makePrice({
|
|
97
|
+
nickname: undefined,
|
|
98
|
+
description: "",
|
|
99
|
+
features: [],
|
|
100
|
+
token: undefined,
|
|
101
|
+
isTrial: undefined,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(Object.keys(seed).sort()).toEqual(
|
|
106
|
+
["currency", "interval", "intervalCount", "platformFeatures", "unitAmount", "usageType"].sort(),
|
|
107
|
+
);
|
|
108
|
+
expect(JSON.stringify(seed)).not.toContain("undefined");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("survives a model whose currency and priceType getters throw", () => {
|
|
112
|
+
const throwing = {
|
|
113
|
+
get currency(): string {
|
|
114
|
+
throw new Error("currency is not defined");
|
|
115
|
+
},
|
|
116
|
+
get priceType(): "one_time" | "recurring" {
|
|
117
|
+
throw new Error("priceType is not defined");
|
|
118
|
+
},
|
|
119
|
+
unitAmount: 1500,
|
|
120
|
+
recurring: { interval: "year", intervalCount: 1 },
|
|
121
|
+
priceFeatures: [],
|
|
122
|
+
} as unknown as StripePriceInterface;
|
|
123
|
+
|
|
124
|
+
expect(priceToSeed(throwing)).toEqual({ unitAmount: 1500, currency: "usd", interval: "year", intervalCount: 1 });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("buildStripePriceTransferDocument", () => {
|
|
129
|
+
it("round-trips through JSON unchanged", () => {
|
|
130
|
+
const document = buildStripePriceTransferDocument(makePrice());
|
|
131
|
+
|
|
132
|
+
const parsed = parseStripePriceTransferDocument(JSON.stringify(document));
|
|
133
|
+
|
|
134
|
+
expect(parsed).toEqual(document);
|
|
135
|
+
expect(parsed.price).toEqual(document.price);
|
|
136
|
+
expect(parsed.kind).toBe(STRIPE_PRICE_TRANSFER_KIND);
|
|
137
|
+
expect(parsed.version).toBe(STRIPE_PRICE_TRANSFER_VERSION);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("serialises no identity at all", () => {
|
|
141
|
+
const serialised = JSON.stringify(buildStripePriceTransferDocument(makePrice()));
|
|
142
|
+
|
|
143
|
+
expect(serialised).not.toContain("stripePriceId");
|
|
144
|
+
expect(serialised).not.toContain("price_1SourceEnv");
|
|
145
|
+
expect(serialised).not.toContain("productId");
|
|
146
|
+
expect(serialised).not.toContain("product-node-1");
|
|
147
|
+
expect(serialised).not.toContain("price-node-1");
|
|
148
|
+
expect(serialised).not.toContain("feature-node-9");
|
|
149
|
+
expect(serialised).not.toContain("lookupKey");
|
|
150
|
+
expect(serialised).not.toContain("metadata");
|
|
151
|
+
expect(serialised).not.toMatch(/"id"/);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("stripePriceTransferFilename", () => {
|
|
156
|
+
it("slugs the nickname", () => {
|
|
157
|
+
expect(stripePriceTransferFilename(makePrice({ nickname: "Standard" }))).toBe("price-standard.json");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("collapses non-alphanumerics and trims the edges", () => {
|
|
161
|
+
expect(stripePriceTransferFilename(makePrice({ nickname: " Pro // Yearly (EU)! " }))).toBe(
|
|
162
|
+
"price-pro-yearly-eu.json",
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("falls back to amount and currency without a nickname", () => {
|
|
167
|
+
expect(stripePriceTransferFilename(makePrice({ nickname: undefined, unitAmount: 5900, currency: "eur" }))).toBe(
|
|
168
|
+
"price-5900-eur.json",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("never emits a path separator", () => {
|
|
173
|
+
const filename = stripePriceTransferFilename(makePrice({ nickname: "../../etc/passwd" }));
|
|
174
|
+
|
|
175
|
+
expect(filename).not.toContain("/");
|
|
176
|
+
expect(filename).not.toContain("\\");
|
|
177
|
+
expect(filename).toBe("price-etc-passwd.json");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("parseStripePriceTransferDocument", () => {
|
|
182
|
+
const validDocument = () => ({
|
|
183
|
+
kind: STRIPE_PRICE_TRANSFER_KIND,
|
|
184
|
+
version: STRIPE_PRICE_TRANSFER_VERSION,
|
|
185
|
+
price: { unitAmount: 5900, currency: "eur", interval: "month" },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("rejects text that is not JSON", () => {
|
|
189
|
+
expect(() => parseStripePriceTransferDocument("not json at all")).toThrow(/not valid JSON/i);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("rejects a wrong kind", () => {
|
|
193
|
+
const raw = JSON.stringify({ ...validDocument(), kind: "stripe-product" });
|
|
194
|
+
|
|
195
|
+
expect(() => parseStripePriceTransferDocument(raw)).toThrow(/kind/i);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("rejects a future version", () => {
|
|
199
|
+
const raw = JSON.stringify({ ...validDocument(), version: 99 });
|
|
200
|
+
|
|
201
|
+
expect(() => parseStripePriceTransferDocument(raw)).toThrow(/version/i);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("rejects a missing unitAmount", () => {
|
|
205
|
+
const raw = JSON.stringify({ ...validDocument(), price: { currency: "eur", interval: "month" } });
|
|
206
|
+
|
|
207
|
+
expect(() => parseStripePriceTransferDocument(raw)).toThrow(/unitAmount/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("rejects a negative unitAmount and an unknown interval", () => {
|
|
211
|
+
expect(() =>
|
|
212
|
+
parseStripePriceTransferDocument(
|
|
213
|
+
JSON.stringify({ ...validDocument(), price: { unitAmount: -1, currency: "eur", interval: "month" } }),
|
|
214
|
+
),
|
|
215
|
+
).toThrow(/unitAmount/);
|
|
216
|
+
|
|
217
|
+
expect(() =>
|
|
218
|
+
parseStripePriceTransferDocument(
|
|
219
|
+
JSON.stringify({ ...validDocument(), price: { unitAmount: 100, currency: "eur", interval: "fortnight" } }),
|
|
220
|
+
),
|
|
221
|
+
).toThrow(/interval/);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("strips unknown keys instead of failing on them", () => {
|
|
225
|
+
const raw = JSON.stringify({
|
|
226
|
+
...validDocument(),
|
|
227
|
+
price: {
|
|
228
|
+
unitAmount: 5900,
|
|
229
|
+
currency: "eur",
|
|
230
|
+
interval: "month",
|
|
231
|
+
stripePriceId: "price_1OtherEnv",
|
|
232
|
+
productId: "product-node-2",
|
|
233
|
+
id: "price-node-2",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const parsed = parseStripePriceTransferDocument(raw);
|
|
238
|
+
|
|
239
|
+
expect(parsed.price).toEqual({ unitAmount: 5900, currency: "eur", interval: "month" });
|
|
240
|
+
expect(parsed.price).not.toHaveProperty("stripePriceId");
|
|
241
|
+
expect(parsed.price).not.toHaveProperty("productId");
|
|
242
|
+
expect(parsed.price).not.toHaveProperty("id");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("keeps every optional key it recognises", () => {
|
|
246
|
+
const price = {
|
|
247
|
+
unitAmount: 5900,
|
|
248
|
+
currency: "eur",
|
|
249
|
+
interval: "year",
|
|
250
|
+
intervalCount: 2,
|
|
251
|
+
usageType: "metered",
|
|
252
|
+
nickname: "Standard",
|
|
253
|
+
description: "The standard plan",
|
|
254
|
+
features: ["Unlimited projects"],
|
|
255
|
+
token: 0,
|
|
256
|
+
isTrial: true,
|
|
257
|
+
platformFeatures: ["AI assistant"],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const parsed = parseStripePriceTransferDocument(JSON.stringify({ ...validDocument(), price }));
|
|
261
|
+
|
|
262
|
+
expect(parsed.price).toEqual(price);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { StripePriceInterface } from "./stripe-price.interface";
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Stripe Price Transfer Document
|
|
6
|
+
// ----------------------------------------------------------------------------
|
|
7
|
+
// Pure, DOM-free: no React, no window, no Blob, no fetch. Everything here is a
|
|
8
|
+
// plain data transformation so it can be unit-tested without a renderer, and so
|
|
9
|
+
// both the export (download) and the clone (prefill an editor) paths can share
|
|
10
|
+
// one definition of "the editable shape of a price".
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const STRIPE_PRICE_TRANSFER_KIND = "stripe-price";
|
|
14
|
+
export const STRIPE_PRICE_TRANSFER_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
/** Versions this build can still read. Older documents stay parseable here. */
|
|
17
|
+
const SUPPORTED_VERSIONS: number[] = [STRIPE_PRICE_TRANSFER_VERSION];
|
|
18
|
+
|
|
19
|
+
/** The editable shape of a price, portable between environments. */
|
|
20
|
+
export type StripePriceSeed = {
|
|
21
|
+
unitAmount: number; // MINOR units (cents), exactly as Stripe stores it
|
|
22
|
+
currency: string;
|
|
23
|
+
interval: "one_time" | "day" | "week" | "month" | "year";
|
|
24
|
+
intervalCount?: number;
|
|
25
|
+
usageType?: "licensed" | "metered";
|
|
26
|
+
nickname?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
features?: string[]; // free-text marketing bullets
|
|
29
|
+
token?: number;
|
|
30
|
+
isTrial?: boolean;
|
|
31
|
+
platformFeatures?: string[]; // Feature entity NAMES (never ids — ids differ per environment)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type StripePriceTransferDocument = {
|
|
35
|
+
kind: typeof STRIPE_PRICE_TRANSFER_KIND;
|
|
36
|
+
version: typeof STRIPE_PRICE_TRANSFER_VERSION;
|
|
37
|
+
price: StripePriceSeed;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const seedSchema = z.object({
|
|
41
|
+
unitAmount: z.number().min(0),
|
|
42
|
+
currency: z.string().min(1),
|
|
43
|
+
interval: z.enum(["one_time", "day", "week", "month", "year"]),
|
|
44
|
+
intervalCount: z.number().min(1).optional(),
|
|
45
|
+
usageType: z.enum(["licensed", "metered"]).optional(),
|
|
46
|
+
nickname: z.string().optional(),
|
|
47
|
+
description: z.string().optional(),
|
|
48
|
+
features: z.array(z.string()).optional(),
|
|
49
|
+
token: z.number().optional(),
|
|
50
|
+
isTrial: z.boolean().optional(),
|
|
51
|
+
platformFeatures: z.array(z.string()).optional(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `currency` and `priceType` are getters that THROW when the attribute is
|
|
56
|
+
* absent (stripe-price.ts:42-58), and `productId` demonstrably is absent on a
|
|
57
|
+
* hydrated price (see the note in PriceEditor). Read anything model-backed
|
|
58
|
+
* through here so a partially-hydrated price degrades to a default instead of
|
|
59
|
+
* exploding mid-export.
|
|
60
|
+
*/
|
|
61
|
+
function readOptional<T>(read: () => T): T | undefined {
|
|
62
|
+
try {
|
|
63
|
+
return read();
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nonEmptyStrings(values: string[] | undefined): string[] {
|
|
70
|
+
if (!Array.isArray(values)) return [];
|
|
71
|
+
return values.map((value) => value.trim()).filter((value) => value.length > 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clone path: a live price -> the editable seed.
|
|
76
|
+
*
|
|
77
|
+
* Carries NO identity: no id, no stripePriceId, no productId, no active, no
|
|
78
|
+
* lookupKey, no metadata. A clone is a brand-new Stripe price and Stripe mints
|
|
79
|
+
* its own id on create; copying any of those across would either collide or
|
|
80
|
+
* silently re-point the new price at the source environment's objects.
|
|
81
|
+
*/
|
|
82
|
+
export function priceToSeed(price: StripePriceInterface): StripePriceSeed {
|
|
83
|
+
const priceType = readOptional(() => price.priceType);
|
|
84
|
+
const currency = readOptional(() => price.currency);
|
|
85
|
+
const recurring = price.recurring;
|
|
86
|
+
|
|
87
|
+
// Same derivation as PriceEditor's getDefaultValues, so a cloned seed and the
|
|
88
|
+
// form it prefills never disagree about what interval a price has.
|
|
89
|
+
const interval: StripePriceSeed["interval"] =
|
|
90
|
+
priceType === "one_time" ? "one_time" : (recurring?.interval ?? "month");
|
|
91
|
+
|
|
92
|
+
// MINOR units on purpose. PriceEditor divides by 100 for display; the
|
|
93
|
+
// document stays in the unit Stripe itself stores.
|
|
94
|
+
const seed: StripePriceSeed = {
|
|
95
|
+
unitAmount: price.unitAmount ?? 0,
|
|
96
|
+
currency: currency ?? "usd",
|
|
97
|
+
interval,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (interval !== "one_time") {
|
|
101
|
+
if (typeof recurring?.intervalCount === "number") seed.intervalCount = recurring.intervalCount;
|
|
102
|
+
if (recurring?.usageType) seed.usageType = recurring.usageType;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nickname = price.nickname?.trim();
|
|
106
|
+
if (nickname) seed.nickname = nickname;
|
|
107
|
+
|
|
108
|
+
const description = price.description?.trim();
|
|
109
|
+
if (description) seed.description = description;
|
|
110
|
+
|
|
111
|
+
const features = nonEmptyStrings(price.features);
|
|
112
|
+
if (features.length > 0) seed.features = features;
|
|
113
|
+
|
|
114
|
+
// `0` is a meaningful token value ("no AI"), so test for the type, never for
|
|
115
|
+
// truthiness.
|
|
116
|
+
if (typeof price.token === "number" && !Number.isNaN(price.token)) seed.token = price.token;
|
|
117
|
+
|
|
118
|
+
if (typeof price.isTrial === "boolean") seed.isTrial = price.isTrial;
|
|
119
|
+
|
|
120
|
+
// NAMES, never ids: Feature ids differ per environment, names are the stable
|
|
121
|
+
// handle an importing environment can resolve against its own Feature rows.
|
|
122
|
+
const platformFeatures = nonEmptyStrings(price.priceFeatures?.map((feature) => feature.name));
|
|
123
|
+
if (platformFeatures.length > 0) seed.platformFeatures = platformFeatures;
|
|
124
|
+
|
|
125
|
+
return seed;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Export path. */
|
|
129
|
+
export function buildStripePriceTransferDocument(price: StripePriceInterface): StripePriceTransferDocument {
|
|
130
|
+
return {
|
|
131
|
+
kind: STRIPE_PRICE_TRANSFER_KIND,
|
|
132
|
+
version: STRIPE_PRICE_TRANSFER_VERSION,
|
|
133
|
+
price: priceToSeed(price),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function slugify(value: string): string {
|
|
138
|
+
return value
|
|
139
|
+
.toLowerCase()
|
|
140
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
141
|
+
.replace(/^-+|-+$/g, "");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Export path: a safe download filename, e.g. "price-standard.json". */
|
|
145
|
+
export function stripePriceTransferFilename(price: StripePriceInterface): string {
|
|
146
|
+
const seed = priceToSeed(price);
|
|
147
|
+
const fromNickname = seed.nickname ? slugify(seed.nickname) : "";
|
|
148
|
+
// Fallback keeps two exports of different prices from colliding when neither
|
|
149
|
+
// is nicknamed. Both halves are slugified, so no separator can survive.
|
|
150
|
+
const slug = fromNickname || slugify(`${seed.unitAmount}-${seed.currency}`);
|
|
151
|
+
|
|
152
|
+
return `price-${slug || "export"}.json`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Import path. Throws an Error when the text is not a valid document. */
|
|
156
|
+
export function parseStripePriceTransferDocument(raw: string): StripePriceTransferDocument {
|
|
157
|
+
let candidate: unknown;
|
|
158
|
+
try {
|
|
159
|
+
candidate = JSON.parse(raw);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new Error("The file is not valid JSON.");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate))
|
|
165
|
+
throw new Error("The file is not a price transfer document.");
|
|
166
|
+
|
|
167
|
+
const document = candidate as Record<string, unknown>;
|
|
168
|
+
|
|
169
|
+
if (document.kind !== STRIPE_PRICE_TRANSFER_KIND)
|
|
170
|
+
throw new Error(
|
|
171
|
+
`The file is not a price transfer document: expected kind "${STRIPE_PRICE_TRANSFER_KIND}", found ${JSON.stringify(document.kind)}.`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (typeof document.version !== "number" || !SUPPORTED_VERSIONS.includes(document.version))
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Unsupported price transfer version ${JSON.stringify(document.version)}. Supported: ${SUPPORTED_VERSIONS.join(", ")}.`,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Unknown keys are stripped rather than rejected, so a document written by a
|
|
180
|
+
// newer build of the same version still imports.
|
|
181
|
+
const parsed = seedSchema.safeParse(document.price);
|
|
182
|
+
if (!parsed.success) {
|
|
183
|
+
const details = parsed.error.issues
|
|
184
|
+
.map((issue) => `${issue.path.join(".") || "price"}: ${issue.message}`)
|
|
185
|
+
.join("; ");
|
|
186
|
+
throw new Error(`The price in the file is not valid. ${details}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
kind: STRIPE_PRICE_TRANSFER_KIND,
|
|
191
|
+
version: STRIPE_PRICE_TRANSFER_VERSION,
|
|
192
|
+
price: parsed.data,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ColumnDef } from "@tanstack/react-table";
|
|
4
|
+
import { CopyIcon, DownloadIcon } from "lucide-react";
|
|
4
5
|
import { useTranslations } from "next-intl";
|
|
5
6
|
import { useMemo } from "react";
|
|
6
7
|
import { Modules } from "../../../../core";
|
|
7
8
|
import { registerTableGenerator, TableContent, usePageUrlGenerator, UseTableStructureHook } from "../../../../hooks";
|
|
8
|
-
import { Badge, Link } from "../../../../shadcnui";
|
|
9
|
+
import { Badge, Button, Link } from "../../../../shadcnui";
|
|
9
10
|
import { formatCurrency, formatInterval } from "../../components/utils/currency";
|
|
10
11
|
import { StripePriceFields } from "../data/stripe-price.fields";
|
|
11
12
|
import { StripePriceInterface } from "../data/stripe-price.interface";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Row-level handlers the ACTIONS column calls. They reach this hook through
|
|
16
|
+
* `ContentListTable`'s `context` prop, which the table forwards verbatim as
|
|
17
|
+
* `params.context` (ContentListTable.tsx:88-95, hooks/types.ts:27) — the table
|
|
18
|
+
* generator registry hands the hook nothing else from the call site.
|
|
19
|
+
*
|
|
20
|
+
* Both are optional on purpose: the same generator serves lists that render the
|
|
21
|
+
* actions column without wiring anything (or that omit the column entirely), so
|
|
22
|
+
* each button is rendered only when its handler is actually present rather than
|
|
23
|
+
* rendering a dead control.
|
|
24
|
+
*/
|
|
25
|
+
type StripePriceRowActions = {
|
|
26
|
+
onClone?: (price: StripePriceInterface) => void;
|
|
27
|
+
onExport?: (price: StripePriceInterface) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
13
30
|
export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInterface, StripePriceFields> = (
|
|
14
31
|
params,
|
|
15
32
|
) => {
|
|
16
33
|
const t = useTranslations();
|
|
17
34
|
const generateUrl = usePageUrlGenerator();
|
|
18
35
|
|
|
36
|
+
const onClone = params.context?.onClone as StripePriceRowActions["onClone"];
|
|
37
|
+
const onExport = params.context?.onExport as StripePriceRowActions["onExport"];
|
|
38
|
+
|
|
19
39
|
const tableData = useMemo(() => {
|
|
20
40
|
return params.data.map((price: StripePriceInterface) => {
|
|
21
41
|
const entry: TableContent<StripePriceInterface> = { jsonApiData: price };
|
|
@@ -105,13 +125,57 @@ export const useStripePriceTableStructure: UseTableStructureHook<StripePriceInte
|
|
|
105
125
|
enableSorting: false,
|
|
106
126
|
enableHiding: false,
|
|
107
127
|
}),
|
|
128
|
+
[StripePriceFields.actions]: () => ({
|
|
129
|
+
id: "actions",
|
|
130
|
+
accessorKey: "actions",
|
|
131
|
+
header: t("billing.admin.prices.fields.actions"),
|
|
132
|
+
cell: ({ row }: { row: TableContent<StripePriceInterface> }) => {
|
|
133
|
+
const price: StripePriceInterface = row.original.jsonApiData;
|
|
134
|
+
return (
|
|
135
|
+
<span className="flex items-center justify-end gap-x-1">
|
|
136
|
+
{onClone && (
|
|
137
|
+
<Button
|
|
138
|
+
type="button"
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="icon"
|
|
141
|
+
aria-label={t("billing.admin.prices.actions.clone")}
|
|
142
|
+
onClick={(event) => {
|
|
143
|
+
// The row itself can be clickable (ContentListTable's
|
|
144
|
+
// `onRowClick`), so a row action must never bubble into it.
|
|
145
|
+
event.stopPropagation();
|
|
146
|
+
onClone(price);
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
<CopyIcon />
|
|
150
|
+
</Button>
|
|
151
|
+
)}
|
|
152
|
+
{onExport && (
|
|
153
|
+
<Button
|
|
154
|
+
type="button"
|
|
155
|
+
variant="ghost"
|
|
156
|
+
size="icon"
|
|
157
|
+
aria-label={t("billing.admin.prices.actions.export")}
|
|
158
|
+
onClick={(event) => {
|
|
159
|
+
event.stopPropagation();
|
|
160
|
+
onExport(price);
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<DownloadIcon />
|
|
164
|
+
</Button>
|
|
165
|
+
)}
|
|
166
|
+
</span>
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
enableSorting: false,
|
|
170
|
+
enableHiding: false,
|
|
171
|
+
}),
|
|
108
172
|
};
|
|
109
173
|
|
|
110
174
|
const columns = useMemo(() => {
|
|
111
175
|
return params.fields.map((field) => fieldColumnMap[field]?.()).filter((col) => col !== undefined) as ColumnDef<
|
|
112
176
|
TableContent<StripePriceInterface>
|
|
113
177
|
>[];
|
|
114
|
-
}, [params.fields, fieldColumnMap, t, generateUrl]);
|
|
178
|
+
}, [params.fields, fieldColumnMap, t, generateUrl, onClone, onExport]);
|
|
115
179
|
|
|
116
180
|
return useMemo(() => ({ data: tableData, columns: columns }), [tableData, columns]);
|
|
117
181
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BookOpenIcon } from "lucide-react";
|
|
2
|
+
import { createJsonApiInclusion } from "../../core";
|
|
3
|
+
import { ModuleFactory } from "../../permissions";
|
|
4
|
+
import { HandbookPage } from "./data/HandbookPage";
|
|
5
|
+
|
|
6
|
+
export const HandbookPageModule = (factory: ModuleFactory) =>
|
|
7
|
+
factory({
|
|
8
|
+
moduleId: "b1f1c2d6-6a0e-4e93-9b6d-3f5f4a2c81e7",
|
|
9
|
+
pageUrl: "/administration/handbook",
|
|
10
|
+
name: "handbookpages",
|
|
11
|
+
model: HandbookPage,
|
|
12
|
+
icon: BookOpenIcon,
|
|
13
|
+
inclusions: {
|
|
14
|
+
lists: {
|
|
15
|
+
// `content` is deliberately omitted: the list renders titles and
|
|
16
|
+
// status, and a thousand pages of markdown do not belong in a list
|
|
17
|
+
// payload. `section`, `order` and `summary` are the index fields the
|
|
18
|
+
// contents page groups, orders and describes pages by, so they are the
|
|
19
|
+
// one part of the payload the list surface cannot do without.
|
|
20
|
+
//
|
|
21
|
+
// `displayTitle` and `displaySummary` are the translated halves of the
|
|
22
|
+
// two strings the contents page prints, and a sparse fieldset that
|
|
23
|
+
// omitted them would leave every row in English. `displayContent` is
|
|
24
|
+
// NOT here for the same reason `content` is not: it is the whole
|
|
25
|
+
// markdown body, and it belongs to the detail read alone.
|
|
26
|
+
fields: [
|
|
27
|
+
createJsonApiInclusion("handbookpages", [
|
|
28
|
+
"path",
|
|
29
|
+
"title",
|
|
30
|
+
"displayTitle",
|
|
31
|
+
"section",
|
|
32
|
+
"order",
|
|
33
|
+
"summary",
|
|
34
|
+
"displaySummary",
|
|
35
|
+
"wordCount",
|
|
36
|
+
"aiStatus",
|
|
37
|
+
]),
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LibraryIcon } from "lucide-react";
|
|
2
|
+
import { createJsonApiInclusion } from "../../core";
|
|
3
|
+
import { ModuleFactory } from "../../permissions";
|
|
4
|
+
import { HandbookSection } from "./data/HandbookSection";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One top-level directory of the documentation tree. Like the thread message,
|
|
8
|
+
* it has no `pageUrl` — a section has no page of its own; it is a heading on
|
|
9
|
+
* the contents surface and nothing more.
|
|
10
|
+
*/
|
|
11
|
+
export const HandbookSectionModule = (factory: ModuleFactory) =>
|
|
12
|
+
factory({
|
|
13
|
+
moduleId: "6b0a2f74-1c53-4a0e-9c1d-2e7b4a9d6f30",
|
|
14
|
+
name: "handbooksections",
|
|
15
|
+
model: HandbookSection,
|
|
16
|
+
icon: LibraryIcon,
|
|
17
|
+
inclusions: {
|
|
18
|
+
lists: {
|
|
19
|
+
// `HandbookSectionService.findMany` is the ONLY read of this resource and
|
|
20
|
+
// it limits to these fields, so the translated halves have to be here:
|
|
21
|
+
// without them every section heading on the contents page and in the
|
|
22
|
+
// reader's navigator would stay English however good the translation is.
|
|
23
|
+
fields: [
|
|
24
|
+
createJsonApiInclusion("handbooksections", [
|
|
25
|
+
"key",
|
|
26
|
+
"title",
|
|
27
|
+
"displayTitle",
|
|
28
|
+
"summary",
|
|
29
|
+
"displaySummary",
|
|
30
|
+
"order",
|
|
31
|
+
]),
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MessageCircleQuestionIcon } from "lucide-react";
|
|
2
|
+
import { createJsonApiInclusion } from "../../core";
|
|
3
|
+
import { ModuleFactory } from "../../permissions";
|
|
4
|
+
import { HandbookThreadMessage } from "./data/HandbookThreadMessage";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One turn of a handbook conversation. Like the thread, it has no `pageUrl` —
|
|
8
|
+
* a message is only ever read as part of its thread.
|
|
9
|
+
*/
|
|
10
|
+
export const HandbookThreadMessageModule = (factory: ModuleFactory) =>
|
|
11
|
+
factory({
|
|
12
|
+
moduleId: "bef354f2-70e1-4f4f-9953-10d3d74cad0c",
|
|
13
|
+
name: "handbookthreadmessages",
|
|
14
|
+
model: HandbookThreadMessage,
|
|
15
|
+
icon: MessageCircleQuestionIcon,
|
|
16
|
+
inclusions: {
|
|
17
|
+
lists: {
|
|
18
|
+
fields: [createJsonApiInclusion("handbookthreadmessages", ["role", "content", "position", "sources"])],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|