@solutiondesk/changelog 0.0.0 → 1.0.0-2cf6d3e

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/react.cjs ADDED
@@ -0,0 +1,539 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
9
+
10
+ // src/seen.ts
11
+ function browserStorage() {
12
+ try {
13
+ const storage = globalThis.localStorage;
14
+ return storage ?? null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function createSeenTracker(workspace, storage = browserStorage(), product) {
20
+ const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
21
+ const lastSeen = () => {
22
+ try {
23
+ return storage?.getItem(key) ?? null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ };
28
+ return {
29
+ lastSeen,
30
+ markSeen(publishedAt) {
31
+ try {
32
+ storage?.setItem(key, publishedAt);
33
+ } catch {
34
+ }
35
+ },
36
+ unseen(entries) {
37
+ const seen = lastSeen();
38
+ if (!seen) return 0;
39
+ return entries.filter((entry) => entry.publishedAt > seen).length;
40
+ }
41
+ };
42
+ }
43
+ var CATEGORY_LABELS = {
44
+ en: { new: "New", improved: "Improved", fixed: "Fixed", announcement: "Announcement" },
45
+ de: { new: "Neu", improved: "Verbessert", fixed: "Behoben", announcement: "Ank\xFCndigung" }
46
+ };
47
+ function categoryLabel(category, locale = "en") {
48
+ return CATEGORY_LABELS[locale.toLowerCase().startsWith("de") ? "de" : "en"][category];
49
+ }
50
+
51
+ // ../sdk-core/src/http.ts
52
+ var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
53
+ var SolutionDeskError = class extends Error {
54
+ constructor(message, init) {
55
+ super(message);
56
+ /** The HTTP status; `0` when no answer arrived (network error, aborted). */
57
+ __publicField(this, "status");
58
+ /** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
59
+ __publicField(this, "body");
60
+ /** The requested URL. */
61
+ __publicField(this, "url");
62
+ /** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
63
+ __publicField(this, "fields");
64
+ this.name = "SolutionDeskError";
65
+ this.status = init.status;
66
+ this.body = init.body ?? null;
67
+ this.url = init.url;
68
+ this.fields = fieldsOf(init.body);
69
+ if (init.cause !== void 0) {
70
+ Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
71
+ }
72
+ }
73
+ /** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
74
+ get isNotFound() {
75
+ return this.status === 404;
76
+ }
77
+ /** `429`: over the rate limit; try again later. */
78
+ get isRateLimited() {
79
+ return this.status === 429;
80
+ }
81
+ };
82
+ function fieldsOf(body) {
83
+ if (!body || typeof body !== "object") return {};
84
+ const fields = body.fields;
85
+ if (!fields || typeof fields !== "object") return {};
86
+ const out = {};
87
+ for (const [key, value] of Object.entries(fields)) {
88
+ if (typeof value === "string") out[key] = value;
89
+ }
90
+ return out;
91
+ }
92
+ function messageOf(body, status) {
93
+ if (body && typeof body === "object") {
94
+ const { error, message, statusCode } = body;
95
+ if (typeof statusCode === "number" && typeof message === "string" && message) return message;
96
+ if (typeof error === "string" && error) return error;
97
+ if (typeof message === "string" && message) return message;
98
+ if (Array.isArray(message) && typeof message[0] === "string") return message[0];
99
+ }
100
+ if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
101
+ return `SolutionDesk answered ${status}.`;
102
+ }
103
+ function createMemoryEtagCache(maxEntries = 500) {
104
+ const entries = /* @__PURE__ */ new Map();
105
+ return {
106
+ get: (key) => entries.get(key),
107
+ set(key, value) {
108
+ entries.delete(key);
109
+ entries.set(key, value);
110
+ if (entries.size > maxEntries) {
111
+ const oldest = entries.keys().next().value;
112
+ if (oldest !== void 0) entries.delete(oldest);
113
+ }
114
+ }
115
+ };
116
+ }
117
+ function queryString(query) {
118
+ if (!query) return "";
119
+ const parts = [];
120
+ for (const [key, value] of Object.entries(query)) {
121
+ if (value === void 0 || value === null || value === "") continue;
122
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
123
+ }
124
+ return parts.length ? `?${parts.join("&")}` : "";
125
+ }
126
+ var seg = (value) => encodeURIComponent(String(value));
127
+ function parseBody(text) {
128
+ if (!text) return null;
129
+ try {
130
+ return JSON.parse(text);
131
+ } catch {
132
+ return text;
133
+ }
134
+ }
135
+ function createHttp(options = {}) {
136
+ const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
137
+ const fetchImpl = options.fetch ?? globalThis.fetch;
138
+ const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
139
+ const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
140
+ async function requestWithMeta(req) {
141
+ if (!fetchImpl) {
142
+ throw new Error("No global fetch available: pass `fetch` to the client.");
143
+ }
144
+ const target = url(req.path, req.query);
145
+ const headers = {
146
+ accept: req.accept ?? "application/json",
147
+ ...options.headers,
148
+ ...req.options?.headers
149
+ };
150
+ if (req.body !== void 0) headers["content-type"] = "application/json";
151
+ if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
152
+ const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
153
+ const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
154
+ if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
155
+ headers["if-none-match"] = cached.etag;
156
+ }
157
+ const init = {
158
+ ...req.options?.fetchOptions,
159
+ method: req.method,
160
+ headers
161
+ };
162
+ if (req.body !== void 0) init.body = JSON.stringify(req.body);
163
+ if (req.options?.signal !== void 0) init.signal = req.options.signal;
164
+ let response;
165
+ try {
166
+ response = await fetchImpl(target, init);
167
+ } catch (cause) {
168
+ const reason = cause instanceof Error ? cause.message : String(cause);
169
+ throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
170
+ status: 0,
171
+ url: target,
172
+ cause
173
+ });
174
+ }
175
+ const finalUrl = response.url;
176
+ const meta = {
177
+ status: response.status,
178
+ finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
179
+ };
180
+ if (response.status === 304 && cached) {
181
+ return { data: cached.body, ...meta };
182
+ }
183
+ const text = await response.text();
184
+ const body = response.ok && req.responseType === "text" ? text : parseBody(text);
185
+ if (!response.ok) {
186
+ throw new SolutionDeskError(messageOf(body, response.status), {
187
+ status: response.status,
188
+ body,
189
+ url: target
190
+ });
191
+ }
192
+ const etag = response.headers.get("etag");
193
+ if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
194
+ return { data: body, ...meta };
195
+ }
196
+ return {
197
+ baseUrl,
198
+ url,
199
+ requestWithMeta,
200
+ request: async (req) => (await requestWithMeta(req)).data
201
+ };
202
+ }
203
+ function required(value, name) {
204
+ if (typeof value !== "string" || !value.trim()) {
205
+ throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
206
+ }
207
+ return value;
208
+ }
209
+
210
+ // src/client.ts
211
+ function createChangelogClient(options) {
212
+ const workspace = required(options.workspace, "workspace");
213
+ const product = options.product?.trim() || void 0;
214
+ const http = createHttp(options);
215
+ const root = `/changelog/${seg(workspace)}`;
216
+ const base = product ? `${root}/${seg(product)}` : root;
217
+ const list = (params = {}, requestOptions) => http.request({
218
+ method: "GET",
219
+ path: base,
220
+ query: { category: params.category, limit: params.limit, cursor: params.cursor },
221
+ options: requestOptions
222
+ });
223
+ async function* allEntries(params = {}, requestOptions) {
224
+ let cursor = null;
225
+ do {
226
+ const page = await list(
227
+ { ...params, limit: params.limit ?? 50, cursor },
228
+ requestOptions
229
+ );
230
+ yield* page.entries;
231
+ cursor = page.nextCursor;
232
+ } while (cursor);
233
+ }
234
+ return {
235
+ workspace,
236
+ product,
237
+ list,
238
+ allEntries,
239
+ getEntry: (slug, requestOptions) => http.request({
240
+ method: "GET",
241
+ path: `${base}/entries/${seg(required(slug, "slug"))}`,
242
+ options: requestOptions
243
+ }),
244
+ listProducts: (requestOptions) => http.request({
245
+ method: "GET",
246
+ path: `${root}/products`,
247
+ options: requestOptions
248
+ })
249
+ };
250
+ }
251
+
252
+ // src/react/use-changelog.ts
253
+ function useChangelog(options) {
254
+ const {
255
+ workspace,
256
+ product,
257
+ client: given,
258
+ limit = 10,
259
+ category,
260
+ trackSeen = true,
261
+ initialData
262
+ } = options;
263
+ const { baseUrl, fetch: fetchImpl, headers, etagCache } = options;
264
+ const headersKey = JSON.stringify(headers ?? {});
265
+ const client = react.useMemo(
266
+ () => given ?? createChangelogClient({
267
+ workspace,
268
+ product,
269
+ baseUrl,
270
+ fetch: fetchImpl,
271
+ headers: JSON.parse(headersKey),
272
+ etagCache
273
+ }),
274
+ [given, workspace, product, baseUrl, fetchImpl, headersKey, etagCache]
275
+ );
276
+ const tracker = react.useMemo(
277
+ () => trackSeen ? createSeenTracker(workspace, options.storage, given?.product ?? product) : null,
278
+ [trackSeen, workspace, options.storage, given, product]
279
+ );
280
+ const [data, setData] = react.useState(initialData ?? null);
281
+ const [entries, setEntries] = react.useState(initialData?.entries ?? []);
282
+ const [status, setStatus] = react.useState(
283
+ initialData ? "ready" : "loading"
284
+ );
285
+ const [error, setError] = react.useState(null);
286
+ const [unseen, setUnseen] = react.useState(0);
287
+ const [loadingMore, setLoadingMore] = react.useState(false);
288
+ const generation = react.useRef(0);
289
+ const moreInFlight = react.useRef(false);
290
+ const settle = react.useCallback(
291
+ (page) => {
292
+ if (!tracker) return;
293
+ const newest = page.entries[0]?.publishedAt;
294
+ const seen = tracker.lastSeen();
295
+ if (!seen && newest) tracker.markSeen(newest);
296
+ setUnseen(tracker.unseen(page.entries));
297
+ },
298
+ [tracker]
299
+ );
300
+ const reload = react.useCallback(async () => {
301
+ const run = ++generation.current;
302
+ try {
303
+ const page = await client.list({ limit, category });
304
+ if (run !== generation.current) return;
305
+ setData(page);
306
+ setEntries(page.entries);
307
+ setError(null);
308
+ setStatus("ready");
309
+ settle(page);
310
+ } catch (caught) {
311
+ if (run !== generation.current) return;
312
+ setError(caught);
313
+ setStatus("error");
314
+ }
315
+ }, [client, limit, category, settle]);
316
+ react.useEffect(() => {
317
+ void reload();
318
+ return () => {
319
+ generation.current++;
320
+ };
321
+ }, [reload]);
322
+ const loadMore = react.useCallback(async () => {
323
+ if (!data?.nextCursor || moreInFlight.current) return;
324
+ const run = generation.current;
325
+ moreInFlight.current = true;
326
+ setLoadingMore(true);
327
+ try {
328
+ const page = await client.list({ limit, category, cursor: data.nextCursor });
329
+ if (run !== generation.current) return;
330
+ setData(page);
331
+ setEntries((current) => [...current, ...page.entries]);
332
+ setError(null);
333
+ } catch (caught) {
334
+ if (run === generation.current) setError(caught);
335
+ } finally {
336
+ moreInFlight.current = false;
337
+ setLoadingMore(false);
338
+ }
339
+ }, [client, limit, category, data]);
340
+ const markAllSeen = react.useCallback(() => {
341
+ const newest = entries[0]?.publishedAt;
342
+ if (tracker && newest) tracker.markSeen(newest);
343
+ setUnseen(0);
344
+ }, [tracker, entries]);
345
+ return {
346
+ status,
347
+ entries,
348
+ data,
349
+ error,
350
+ unseen,
351
+ markAllSeen,
352
+ hasMore: !!data?.nextCursor,
353
+ loadingMore,
354
+ loadMore,
355
+ reload
356
+ };
357
+ }
358
+ function formatChangelogDate(iso, locale = "en") {
359
+ return new Intl.DateTimeFormat(locale, { day: "numeric", month: "long", year: "numeric" }).format(
360
+ new Date(iso)
361
+ );
362
+ }
363
+ function ChangelogEntryBody({ html, className }) {
364
+ if (!html) return null;
365
+ return /* @__PURE__ */ jsxRuntime.jsx(
366
+ "div",
367
+ {
368
+ "data-sd-changelog-body": "",
369
+ className,
370
+ dangerouslySetInnerHTML: { __html: html }
371
+ }
372
+ );
373
+ }
374
+ function ChangelogEntryView({
375
+ entry,
376
+ locale = "en",
377
+ showBody = true,
378
+ classNames = {},
379
+ titleAs: Title = "h3"
380
+ }) {
381
+ return /* @__PURE__ */ jsxRuntime.jsxs("article", { "data-sd-changelog-entry": "", "data-category": entry.category, className: classNames.entry, children: [
382
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-sd-changelog-meta": "", className: classNames.meta, children: [
383
+ /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: entry.publishedAt, className: classNames.date, children: formatChangelogDate(entry.publishedAt, locale) }),
384
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) }),
385
+ entry.releaseVersion ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-sd-changelog-version": "", className: classNames.version, children: [
386
+ "Version ",
387
+ entry.releaseVersion
388
+ ] }) : null
389
+ ] }),
390
+ /* @__PURE__ */ jsxRuntime.jsx(Title, { className: classNames.title, children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: entry.url, children: entry.title }) }),
391
+ entry.summary ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: classNames.summary, children: entry.summary }) : null,
392
+ showBody ? /* @__PURE__ */ jsxRuntime.jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null,
393
+ entry.downloadUrl ? /* @__PURE__ */ jsxRuntime.jsx(
394
+ "a",
395
+ {
396
+ href: entry.downloadUrl,
397
+ rel: "noopener",
398
+ "data-sd-changelog-download": "",
399
+ className: classNames.download,
400
+ children: locale.toLowerCase().startsWith("de") ? "Herunterladen" : "Download"
401
+ }
402
+ ) : null
403
+ ] });
404
+ }
405
+ function ChangelogFeed(props) {
406
+ const {
407
+ locale = "en",
408
+ showBody = true,
409
+ className,
410
+ entryClassNames,
411
+ renderEntry,
412
+ loading = null,
413
+ empty = null,
414
+ fallback = null,
415
+ moreLabel,
416
+ moreClassName,
417
+ ...options
418
+ } = props;
419
+ const changelog = useChangelog({ trackSeen: false, ...options });
420
+ const de = locale.toLowerCase().startsWith("de");
421
+ if (changelog.status === "loading") return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: loading });
422
+ if (changelog.status === "error") return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: fallback });
423
+ if (changelog.entries.length === 0) return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: empty });
424
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-sd-changelog": "", className, children: [
425
+ /* @__PURE__ */ jsxRuntime.jsx("ol", { "data-sd-changelog-list": "", children: changelog.entries.map((entry) => /* @__PURE__ */ jsxRuntime.jsx("li", { id: entry.slug, children: renderEntry ? renderEntry(entry) : /* @__PURE__ */ jsxRuntime.jsx(
426
+ ChangelogEntryView,
427
+ {
428
+ entry,
429
+ locale,
430
+ showBody,
431
+ classNames: entryClassNames
432
+ }
433
+ ) }, entry.slug)) }),
434
+ changelog.hasMore ? /* @__PURE__ */ jsxRuntime.jsx(
435
+ "button",
436
+ {
437
+ type: "button",
438
+ "data-sd-changelog-more": "",
439
+ className: moreClassName,
440
+ disabled: changelog.loadingMore,
441
+ "aria-busy": changelog.loadingMore || void 0,
442
+ onClick: () => void changelog.loadMore(),
443
+ children: moreLabel ?? (de ? "\xC4ltere Neuigkeiten" : "Older updates")
444
+ }
445
+ ) : null
446
+ ] });
447
+ }
448
+ function WhatsNew(props) {
449
+ const {
450
+ locale = "en",
451
+ label,
452
+ title,
453
+ classNames = {},
454
+ entryClassNames,
455
+ renderBadge,
456
+ ...options
457
+ } = props;
458
+ const changelog = useChangelog(options);
459
+ const [open, setOpen] = react.useState(false);
460
+ const root = react.useRef(null);
461
+ const panelId = react.useId();
462
+ const de = locale.toLowerCase().startsWith("de");
463
+ const fallback = de ? "Was ist neu?" : "What's new";
464
+ react.useEffect(() => {
465
+ if (!open) return;
466
+ const onKey = (event) => event.key === "Escape" && setOpen(false);
467
+ const onClick = (event) => {
468
+ if (root.current && !root.current.contains(event.target)) setOpen(false);
469
+ };
470
+ document.addEventListener("keydown", onKey);
471
+ document.addEventListener("mousedown", onClick);
472
+ return () => {
473
+ document.removeEventListener("keydown", onKey);
474
+ document.removeEventListener("mousedown", onClick);
475
+ };
476
+ }, [open]);
477
+ if (changelog.status !== "ready" || !changelog.data) return null;
478
+ const { data } = changelog;
479
+ return /* @__PURE__ */ jsxRuntime.jsxs(
480
+ "div",
481
+ {
482
+ ref: root,
483
+ "data-sd-whats-new": "",
484
+ "data-open": open ? "" : void 0,
485
+ className: classNames.root,
486
+ style: { position: "relative" },
487
+ children: [
488
+ /* @__PURE__ */ jsxRuntime.jsxs(
489
+ "button",
490
+ {
491
+ type: "button",
492
+ "aria-expanded": open,
493
+ "aria-controls": panelId,
494
+ className: classNames.button,
495
+ onClick: () => {
496
+ if (!open) changelog.markAllSeen();
497
+ setOpen(!open);
498
+ },
499
+ children: [
500
+ label ?? (data.widget.label || fallback),
501
+ changelog.unseen > 0 ? renderBadge ? renderBadge(changelog.unseen) : /* @__PURE__ */ jsxRuntime.jsx("span", { "data-sd-whats-new-badge": "", className: classNames.badge, children: changelog.unseen }) : null
502
+ ]
503
+ }
504
+ ),
505
+ open ? /* @__PURE__ */ jsxRuntime.jsxs(
506
+ "div",
507
+ {
508
+ id: panelId,
509
+ role: "dialog",
510
+ "aria-label": typeof title === "string" ? title : data.title || fallback,
511
+ "data-sd-whats-new-panel": "",
512
+ className: classNames.panel,
513
+ style: classNames.panel ? void 0 : { position: "absolute", zIndex: 50 },
514
+ children: [
515
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "data-sd-whats-new-header": "", className: classNames.header, children: title ?? (data.title || fallback) }),
516
+ /* @__PURE__ */ jsxRuntime.jsx("ol", { "data-sd-whats-new-list": "", className: classNames.list, children: changelog.entries.map((entry) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsx(
517
+ ChangelogEntryView,
518
+ {
519
+ entry,
520
+ locale,
521
+ showBody: false,
522
+ classNames: entryClassNames
523
+ }
524
+ ) }, entry.slug)) }),
525
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: data.pageUrl, "data-sd-whats-new-all": "", className: classNames.footer, children: de ? "Alle Neuigkeiten" : "All updates" })
526
+ ]
527
+ }
528
+ ) : null
529
+ ]
530
+ }
531
+ );
532
+ }
533
+
534
+ exports.ChangelogEntryBody = ChangelogEntryBody;
535
+ exports.ChangelogEntryView = ChangelogEntryView;
536
+ exports.ChangelogFeed = ChangelogFeed;
537
+ exports.WhatsNew = WhatsNew;
538
+ exports.formatChangelogDate = formatChangelogDate;
539
+ exports.useChangelog = useChangelog;