@solutiondesk/changelog 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -10
- package/dist/index.cjs +11 -3
- package/dist/index.d.cts +60 -16
- package/dist/index.d.ts +60 -16
- package/dist/index.js +11 -3
- package/dist/react.cjs +40 -9
- package/dist/react.d.cts +63 -11
- package/dist/react.d.ts +63 -11
- package/dist/react.js +40 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @solutiondesk/changelog
|
|
2
2
|
|
|
3
|
-
Typed client for the public [SolutionDesk](https://solutiondesk.io)
|
|
3
|
+
Typed client for the public [SolutionDesk](https://solutiondesk.io) changelogs, plus React hooks and unstyled components. Your team writes the updates for each of its products in SolutionDesk; your app or website shows them with this package.
|
|
4
4
|
|
|
5
5
|
- The client has no dependencies, ships ESM and CommonJS with full TypeScript types, and runs wherever `fetch` does: Node.js 18+, Deno, Bun, edge runtimes and browsers.
|
|
6
6
|
- No key and no login: the API only ever returns published entries.
|
|
@@ -10,39 +10,40 @@ Typed client for the public [SolutionDesk](https://solutiondesk.io) changelog, p
|
|
|
10
10
|
npm install @solutiondesk/changelog
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
No code at all? The [
|
|
13
|
+
No code at all? The [Products guide](https://help.solutiondesk.io/docs/team/changelog) shows the hosted page and the ready-made "What's new" button (two lines of HTML).
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
import { createChangelogClient } from "@solutiondesk/changelog";
|
|
19
19
|
|
|
20
|
-
const changelog = createChangelogClient({ workspace: "acme" });
|
|
20
|
+
const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
|
|
21
21
|
|
|
22
22
|
const { title, entries, nextCursor } = await changelog.list({ limit: 10 });
|
|
23
23
|
const { entry } = await changelog.getEntry("export-to-csv");
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`workspace`
|
|
26
|
+
`workspace` and `product` are the two parts of the address shown under **Products → your product → Page & settings → For developers** (`https://api.solutiondesk.io/v1/changelog/acme/acme-mobile`). Leave `product` out to read the workspace's default product.
|
|
27
27
|
|
|
28
28
|
## What you can read
|
|
29
29
|
|
|
30
30
|
| Method | API | Returns |
|
|
31
31
|
| --- | --- | --- |
|
|
32
|
-
| `list({ category, limit, cursor })` | `GET /v1/changelog/{workspace}` | The
|
|
32
|
+
| `list({ category, limit, cursor })` | `GET /v1/changelog/{workspace}/{product}` | The `product`, its title and button settings, the hosted `pageUrl`, one page of entries (newest first) and `nextCursor` (`limit` 1–50, default 20). |
|
|
33
33
|
| `allEntries({ category })` | the same, page by page | An async iterator over every published entry, e.g. for a sitemap or an RSS feed. |
|
|
34
|
-
| `getEntry(slug)` | `GET /v1/changelog/{workspace}/entries/{slug}` | One entry. |
|
|
34
|
+
| `getEntry(slug)` | `GET /v1/changelog/{workspace}/{product}/entries/{slug}` | One entry and its product. |
|
|
35
|
+
| `listProducts()` | `GET /v1/changelog/{workspace}/products` | Every product (name, slug, description, `pageUrl`), the default first. |
|
|
35
36
|
|
|
36
37
|
Each entry has `title`, `summary` (one or two sentences), `category` (`new`, `improved`, `fixed`, `announcement`), `publishedAt`, the details as `markdown` and as `html`, and `url` (the entry on the hosted page). `html` is safe to insert as it is: raw HTML in the team's text is escaped, links only go to web addresses and paths, images only come from https addresses.
|
|
37
38
|
|
|
38
|
-
A workspace that doesn't exist, a changelog that is turned off and an unpublished entry all answer `404` (`SolutionDeskError` with `isNotFound`).
|
|
39
|
+
A workspace or product that doesn't exist, a changelog that is turned off and an unpublished entry all answer `404` (`SolutionDeskError` with `isNotFound`).
|
|
39
40
|
|
|
40
41
|
## "New" badge
|
|
41
42
|
|
|
42
43
|
```ts
|
|
43
44
|
import { createSeenTracker } from "@solutiondesk/changelog";
|
|
44
45
|
|
|
45
|
-
const seen = createSeenTracker("acme"); // the browser's localStorage by default
|
|
46
|
+
const seen = createSeenTracker("acme", undefined, "acme-mobile"); // the browser's localStorage by default
|
|
46
47
|
const unseen = seen.unseen(entries); // entries newer than the visitor's last look
|
|
47
48
|
seen.markSeen(entries[0].publishedAt); // when they open your panel
|
|
48
49
|
```
|
|
@@ -57,10 +58,10 @@ On the very first visit nothing counts as new — remember the newest entry then
|
|
|
57
58
|
import { ChangelogFeed, WhatsNew } from "@solutiondesk/changelog/react";
|
|
58
59
|
|
|
59
60
|
// In your app's header: a button with the unseen count that opens the latest entries.
|
|
60
|
-
<WhatsNew workspace="acme" classNames={{ button: "btn", badge: "badge", panel: "popover" }} />
|
|
61
|
+
<WhatsNew workspace="acme" product="acme-mobile" classNames={{ button: "btn", badge: "badge", panel: "popover" }} />
|
|
61
62
|
|
|
62
63
|
// A whole changelog page: entries with their details and "Older updates".
|
|
63
|
-
<ChangelogFeed workspace="acme" locale="de" entryClassNames={{ title: "text-xl font-semibold" }} />
|
|
64
|
+
<ChangelogFeed workspace="acme" product="acme-mobile" locale="de" entryClassNames={{ title: "text-xl font-semibold" }} />
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
The components render plain elements with `className` props and `data-sd-*` attributes (`data-sd-changelog-entry`, `data-category`, `data-sd-whats-new-badge` …) and ship no CSS. Both render nothing (or your `fallback`) when the changelog can't be loaded, so they never break your page.
|
|
@@ -73,6 +74,7 @@ import { useChangelog } from "@solutiondesk/changelog/react";
|
|
|
73
74
|
function Updates() {
|
|
74
75
|
const { status, entries, unseen, markAllSeen, hasMore, loadMore } = useChangelog({
|
|
75
76
|
workspace: "acme",
|
|
77
|
+
product: "acme-mobile",
|
|
76
78
|
limit: 5,
|
|
77
79
|
});
|
|
78
80
|
if (status !== "ready") return null;
|
package/dist/index.cjs
CHANGED
|
@@ -166,8 +166,10 @@ function required(value, name) {
|
|
|
166
166
|
// src/client.ts
|
|
167
167
|
function createChangelogClient(options) {
|
|
168
168
|
const workspace = required(options.workspace, "workspace");
|
|
169
|
+
const product = options.product?.trim() || void 0;
|
|
169
170
|
const http = createHttp(options);
|
|
170
|
-
const
|
|
171
|
+
const root = `/changelog/${seg(workspace)}`;
|
|
172
|
+
const base = product ? `${root}/${seg(product)}` : root;
|
|
171
173
|
const list = (params = {}, requestOptions) => http.request({
|
|
172
174
|
method: "GET",
|
|
173
175
|
path: base,
|
|
@@ -187,12 +189,18 @@ function createChangelogClient(options) {
|
|
|
187
189
|
}
|
|
188
190
|
return {
|
|
189
191
|
workspace,
|
|
192
|
+
product,
|
|
190
193
|
list,
|
|
191
194
|
allEntries,
|
|
192
195
|
getEntry: (slug, requestOptions) => http.request({
|
|
193
196
|
method: "GET",
|
|
194
197
|
path: `${base}/entries/${seg(required(slug, "slug"))}`,
|
|
195
198
|
options: requestOptions
|
|
199
|
+
}),
|
|
200
|
+
listProducts: (requestOptions) => http.request({
|
|
201
|
+
method: "GET",
|
|
202
|
+
path: `${root}/products`,
|
|
203
|
+
options: requestOptions
|
|
196
204
|
})
|
|
197
205
|
};
|
|
198
206
|
}
|
|
@@ -206,8 +214,8 @@ function browserStorage() {
|
|
|
206
214
|
return null;
|
|
207
215
|
}
|
|
208
216
|
}
|
|
209
|
-
function createSeenTracker(workspace, storage = browserStorage()) {
|
|
210
|
-
const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
|
|
217
|
+
function createSeenTracker(workspace, storage = browserStorage(), product) {
|
|
218
|
+
const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
|
|
211
219
|
const lastSeen = () => {
|
|
212
220
|
try {
|
|
213
221
|
return storage?.getItem(key) ?? null;
|
package/dist/index.d.cts
CHANGED
|
@@ -108,20 +108,48 @@ interface PublicChangelogEntry {
|
|
|
108
108
|
/** One or two sentences — what a "What's new" panel shows. May be empty. */
|
|
109
109
|
summary: string;
|
|
110
110
|
category: ChangelogCategory;
|
|
111
|
+
/** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
|
|
112
|
+
releaseVersion: string;
|
|
113
|
+
/** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
|
|
114
|
+
downloadUrl: string | null;
|
|
111
115
|
/** ISO 8601. The list is sorted by it, newest first. */
|
|
112
116
|
publishedAt: string;
|
|
113
117
|
/** The details as the team wrote them (Markdown). */
|
|
114
118
|
markdown: string;
|
|
115
119
|
/**
|
|
116
120
|
* The details as safe HTML: raw HTML in the text is escaped, links are only
|
|
117
|
-
* http(s)/mailto/relative, images only
|
|
118
|
-
* `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
121
|
+
* http(s)/mailto/relative, images only the ones uploaded to the entry (with
|
|
122
|
+
* `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
123
|
+
* there are no details.
|
|
119
124
|
*/
|
|
120
125
|
html: string;
|
|
121
126
|
/** The entry on the workspace's hosted changelog page. */
|
|
122
127
|
url: string;
|
|
123
128
|
}
|
|
124
|
-
/**
|
|
129
|
+
/** A product with its own changelog. */
|
|
130
|
+
interface PublicChangelogProduct {
|
|
131
|
+
name: string;
|
|
132
|
+
slug: string;
|
|
133
|
+
/** One sentence about the product. May be empty. */
|
|
134
|
+
description: string;
|
|
135
|
+
/** The product's hosted changelog page. */
|
|
136
|
+
pageUrl: string;
|
|
137
|
+
/** The default product: addresses without a product show this one. */
|
|
138
|
+
isDefault: boolean;
|
|
139
|
+
}
|
|
140
|
+
/** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
|
|
141
|
+
interface PublicChangelogProductsPayload {
|
|
142
|
+
workspace: {
|
|
143
|
+
name: string;
|
|
144
|
+
slug: string;
|
|
145
|
+
logoUrl: string | null;
|
|
146
|
+
};
|
|
147
|
+
products: PublicChangelogProduct[];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
|
|
151
|
+
* page of its entries (`GET /v1/changelog/{workspace}`: the default product).
|
|
152
|
+
*/
|
|
125
153
|
interface PublicChangelogPayload {
|
|
126
154
|
/** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
|
|
127
155
|
workspace: {
|
|
@@ -129,6 +157,8 @@ interface PublicChangelogPayload {
|
|
|
129
157
|
slug: string;
|
|
130
158
|
logoUrl: string | null;
|
|
131
159
|
};
|
|
160
|
+
/** The product these entries belong to. */
|
|
161
|
+
product: PublicChangelogProduct;
|
|
132
162
|
/** The heading the team chose; empty ⇒ use your own ("What's new"). */
|
|
133
163
|
title: string;
|
|
134
164
|
/** The team's settings for the "What's new" button (empty label ⇒ your own). */
|
|
@@ -136,20 +166,27 @@ interface PublicChangelogPayload {
|
|
|
136
166
|
label: string;
|
|
137
167
|
position: ChangelogWidgetPosition;
|
|
138
168
|
};
|
|
139
|
-
/** The hosted changelog page. */
|
|
169
|
+
/** The product's hosted changelog page. */
|
|
140
170
|
pageUrl: string;
|
|
141
171
|
entries: PublicChangelogEntry[];
|
|
142
172
|
/** Pass as `cursor` for the next page; null on the last one. */
|
|
143
173
|
nextCursor: string | null;
|
|
144
174
|
}
|
|
145
|
-
/** `GET /v1/changelog/{workspace}/entries/{slug}`. */
|
|
175
|
+
/** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
|
|
146
176
|
interface PublicChangelogEntryPayload {
|
|
177
|
+
/** The product whose changelog shows the entry. */
|
|
178
|
+
product: PublicChangelogProduct;
|
|
147
179
|
entry: PublicChangelogEntry;
|
|
148
180
|
}
|
|
149
181
|
|
|
150
182
|
interface ChangelogClientOptions extends BaseClientOptions {
|
|
151
183
|
/** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
|
|
152
184
|
workspace: string;
|
|
185
|
+
/**
|
|
186
|
+
* The product whose changelog to read (its web address, e.g. `acme-mobile`).
|
|
187
|
+
* Without it, the workspace's default product.
|
|
188
|
+
*/
|
|
189
|
+
product?: string;
|
|
153
190
|
}
|
|
154
191
|
interface ListEntriesParams {
|
|
155
192
|
/** Only entries of this category. */
|
|
@@ -162,26 +199,32 @@ interface ListEntriesParams {
|
|
|
162
199
|
interface ChangelogClient {
|
|
163
200
|
/** The workspace this client reads. */
|
|
164
201
|
readonly workspace: string;
|
|
202
|
+
/** The product this client reads, or undefined for the default product. */
|
|
203
|
+
readonly product: string | undefined;
|
|
165
204
|
/**
|
|
166
|
-
* The
|
|
167
|
-
* (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError`
|
|
168
|
-
* 404 when the workspace doesn't exist or
|
|
205
|
+
* The product's settings and one page of its published entries, newest first
|
|
206
|
+
* (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
|
|
207
|
+
* with status 404 when the workspace or product doesn't exist or the
|
|
208
|
+
* changelog is turned off.
|
|
169
209
|
*/
|
|
170
210
|
list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
|
|
171
211
|
/** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
|
|
172
212
|
allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
|
|
173
213
|
/**
|
|
174
|
-
* One published entry (
|
|
175
|
-
* Throws a `SolutionDeskError` with status 404
|
|
214
|
+
* One published entry of the product (`…/{product}/entries/{slug}`; without
|
|
215
|
+
* a product, of any product). Throws a `SolutionDeskError` with status 404
|
|
216
|
+
* for an unknown or unpublished entry.
|
|
176
217
|
*/
|
|
177
218
|
getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
|
|
219
|
+
/** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
|
|
220
|
+
listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
|
|
178
221
|
}
|
|
179
222
|
/**
|
|
180
223
|
* A client for the public changelog API — no key, no login; only published
|
|
181
224
|
* entries. Safe in the browser and on the server.
|
|
182
225
|
*
|
|
183
226
|
* @example
|
|
184
|
-
* const changelog = createChangelogClient({ workspace: "acme" });
|
|
227
|
+
* const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
|
|
185
228
|
* const { entries, nextCursor } = await changelog.list({ limit: 10 });
|
|
186
229
|
*/
|
|
187
230
|
declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
|
|
@@ -213,11 +256,12 @@ interface SeenTracker {
|
|
|
213
256
|
/** The browser's `localStorage`, or null (server rendering, privacy mode). */
|
|
214
257
|
declare function browserStorage(): SeenStorage | null;
|
|
215
258
|
/**
|
|
216
|
-
* A {@link SeenTracker} for one workspace. Uses the key
|
|
217
|
-
* `sd-changelog-seen:<workspace>`
|
|
218
|
-
*
|
|
259
|
+
* A {@link SeenTracker} for one workspace's product. Uses the key
|
|
260
|
+
* `sd-changelog-seen:<workspace>` (default product) or
|
|
261
|
+
* `sd-changelog-seen:<workspace>:<product>` — the same ones the SolutionDesk
|
|
262
|
+
* widget uses, so a visitor who opened either sees no badge in the other.
|
|
219
263
|
*/
|
|
220
|
-
declare function createSeenTracker(workspace: string, storage?: SeenStorage | null): SeenTracker;
|
|
264
|
+
declare function createSeenTracker(workspace: string, storage?: SeenStorage | null, product?: string): SeenTracker;
|
|
221
265
|
type ChangelogLocale = "de" | "en";
|
|
222
266
|
/** The category names the hosted page uses, for your own UI. */
|
|
223
267
|
declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
|
|
@@ -226,4 +270,4 @@ declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
|
|
|
226
270
|
/** A category's name in German (`de*`) or English (everything else). */
|
|
227
271
|
declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
|
|
228
272
|
|
|
229
|
-
export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
|
|
273
|
+
export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type PublicChangelogProduct, type PublicChangelogProductsPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
|
package/dist/index.d.ts
CHANGED
|
@@ -108,20 +108,48 @@ interface PublicChangelogEntry {
|
|
|
108
108
|
/** One or two sentences — what a "What's new" panel shows. May be empty. */
|
|
109
109
|
summary: string;
|
|
110
110
|
category: ChangelogCategory;
|
|
111
|
+
/** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
|
|
112
|
+
releaseVersion: string;
|
|
113
|
+
/** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
|
|
114
|
+
downloadUrl: string | null;
|
|
111
115
|
/** ISO 8601. The list is sorted by it, newest first. */
|
|
112
116
|
publishedAt: string;
|
|
113
117
|
/** The details as the team wrote them (Markdown). */
|
|
114
118
|
markdown: string;
|
|
115
119
|
/**
|
|
116
120
|
* The details as safe HTML: raw HTML in the text is escaped, links are only
|
|
117
|
-
* http(s)/mailto/relative, images only
|
|
118
|
-
* `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
121
|
+
* http(s)/mailto/relative, images only the ones uploaded to the entry (with
|
|
122
|
+
* `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
123
|
+
* there are no details.
|
|
119
124
|
*/
|
|
120
125
|
html: string;
|
|
121
126
|
/** The entry on the workspace's hosted changelog page. */
|
|
122
127
|
url: string;
|
|
123
128
|
}
|
|
124
|
-
/**
|
|
129
|
+
/** A product with its own changelog. */
|
|
130
|
+
interface PublicChangelogProduct {
|
|
131
|
+
name: string;
|
|
132
|
+
slug: string;
|
|
133
|
+
/** One sentence about the product. May be empty. */
|
|
134
|
+
description: string;
|
|
135
|
+
/** The product's hosted changelog page. */
|
|
136
|
+
pageUrl: string;
|
|
137
|
+
/** The default product: addresses without a product show this one. */
|
|
138
|
+
isDefault: boolean;
|
|
139
|
+
}
|
|
140
|
+
/** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
|
|
141
|
+
interface PublicChangelogProductsPayload {
|
|
142
|
+
workspace: {
|
|
143
|
+
name: string;
|
|
144
|
+
slug: string;
|
|
145
|
+
logoUrl: string | null;
|
|
146
|
+
};
|
|
147
|
+
products: PublicChangelogProduct[];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
|
|
151
|
+
* page of its entries (`GET /v1/changelog/{workspace}`: the default product).
|
|
152
|
+
*/
|
|
125
153
|
interface PublicChangelogPayload {
|
|
126
154
|
/** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
|
|
127
155
|
workspace: {
|
|
@@ -129,6 +157,8 @@ interface PublicChangelogPayload {
|
|
|
129
157
|
slug: string;
|
|
130
158
|
logoUrl: string | null;
|
|
131
159
|
};
|
|
160
|
+
/** The product these entries belong to. */
|
|
161
|
+
product: PublicChangelogProduct;
|
|
132
162
|
/** The heading the team chose; empty ⇒ use your own ("What's new"). */
|
|
133
163
|
title: string;
|
|
134
164
|
/** The team's settings for the "What's new" button (empty label ⇒ your own). */
|
|
@@ -136,20 +166,27 @@ interface PublicChangelogPayload {
|
|
|
136
166
|
label: string;
|
|
137
167
|
position: ChangelogWidgetPosition;
|
|
138
168
|
};
|
|
139
|
-
/** The hosted changelog page. */
|
|
169
|
+
/** The product's hosted changelog page. */
|
|
140
170
|
pageUrl: string;
|
|
141
171
|
entries: PublicChangelogEntry[];
|
|
142
172
|
/** Pass as `cursor` for the next page; null on the last one. */
|
|
143
173
|
nextCursor: string | null;
|
|
144
174
|
}
|
|
145
|
-
/** `GET /v1/changelog/{workspace}/entries/{slug}`. */
|
|
175
|
+
/** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
|
|
146
176
|
interface PublicChangelogEntryPayload {
|
|
177
|
+
/** The product whose changelog shows the entry. */
|
|
178
|
+
product: PublicChangelogProduct;
|
|
147
179
|
entry: PublicChangelogEntry;
|
|
148
180
|
}
|
|
149
181
|
|
|
150
182
|
interface ChangelogClientOptions extends BaseClientOptions {
|
|
151
183
|
/** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
|
|
152
184
|
workspace: string;
|
|
185
|
+
/**
|
|
186
|
+
* The product whose changelog to read (its web address, e.g. `acme-mobile`).
|
|
187
|
+
* Without it, the workspace's default product.
|
|
188
|
+
*/
|
|
189
|
+
product?: string;
|
|
153
190
|
}
|
|
154
191
|
interface ListEntriesParams {
|
|
155
192
|
/** Only entries of this category. */
|
|
@@ -162,26 +199,32 @@ interface ListEntriesParams {
|
|
|
162
199
|
interface ChangelogClient {
|
|
163
200
|
/** The workspace this client reads. */
|
|
164
201
|
readonly workspace: string;
|
|
202
|
+
/** The product this client reads, or undefined for the default product. */
|
|
203
|
+
readonly product: string | undefined;
|
|
165
204
|
/**
|
|
166
|
-
* The
|
|
167
|
-
* (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError`
|
|
168
|
-
* 404 when the workspace doesn't exist or
|
|
205
|
+
* The product's settings and one page of its published entries, newest first
|
|
206
|
+
* (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
|
|
207
|
+
* with status 404 when the workspace or product doesn't exist or the
|
|
208
|
+
* changelog is turned off.
|
|
169
209
|
*/
|
|
170
210
|
list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
|
|
171
211
|
/** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
|
|
172
212
|
allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
|
|
173
213
|
/**
|
|
174
|
-
* One published entry (
|
|
175
|
-
* Throws a `SolutionDeskError` with status 404
|
|
214
|
+
* One published entry of the product (`…/{product}/entries/{slug}`; without
|
|
215
|
+
* a product, of any product). Throws a `SolutionDeskError` with status 404
|
|
216
|
+
* for an unknown or unpublished entry.
|
|
176
217
|
*/
|
|
177
218
|
getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
|
|
219
|
+
/** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
|
|
220
|
+
listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
|
|
178
221
|
}
|
|
179
222
|
/**
|
|
180
223
|
* A client for the public changelog API — no key, no login; only published
|
|
181
224
|
* entries. Safe in the browser and on the server.
|
|
182
225
|
*
|
|
183
226
|
* @example
|
|
184
|
-
* const changelog = createChangelogClient({ workspace: "acme" });
|
|
227
|
+
* const changelog = createChangelogClient({ workspace: "acme", product: "acme-mobile" });
|
|
185
228
|
* const { entries, nextCursor } = await changelog.list({ limit: 10 });
|
|
186
229
|
*/
|
|
187
230
|
declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
|
|
@@ -213,11 +256,12 @@ interface SeenTracker {
|
|
|
213
256
|
/** The browser's `localStorage`, or null (server rendering, privacy mode). */
|
|
214
257
|
declare function browserStorage(): SeenStorage | null;
|
|
215
258
|
/**
|
|
216
|
-
* A {@link SeenTracker} for one workspace. Uses the key
|
|
217
|
-
* `sd-changelog-seen:<workspace>`
|
|
218
|
-
*
|
|
259
|
+
* A {@link SeenTracker} for one workspace's product. Uses the key
|
|
260
|
+
* `sd-changelog-seen:<workspace>` (default product) or
|
|
261
|
+
* `sd-changelog-seen:<workspace>:<product>` — the same ones the SolutionDesk
|
|
262
|
+
* widget uses, so a visitor who opened either sees no badge in the other.
|
|
219
263
|
*/
|
|
220
|
-
declare function createSeenTracker(workspace: string, storage?: SeenStorage | null): SeenTracker;
|
|
264
|
+
declare function createSeenTracker(workspace: string, storage?: SeenStorage | null, product?: string): SeenTracker;
|
|
221
265
|
type ChangelogLocale = "de" | "en";
|
|
222
266
|
/** The category names the hosted page uses, for your own UI. */
|
|
223
267
|
declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
|
|
@@ -226,4 +270,4 @@ declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
|
|
|
226
270
|
/** A category's name in German (`de*`) or English (everything else). */
|
|
227
271
|
declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
|
|
228
272
|
|
|
229
|
-
export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
|
|
273
|
+
export { type BaseClientOptions, CATEGORY_COLORS, CATEGORY_LABELS, type CachedResponse, type ChangelogCategory, type ChangelogClient, type ChangelogClientOptions, type ChangelogLocale, type ChangelogWidgetPosition, DEFAULT_BASE_URL, type EtagCache, type FetchInitLike, type FetchLike, type FetchResponseLike, type ListEntriesParams, type PublicChangelogEntry, type PublicChangelogEntryPayload, type PublicChangelogPayload, type PublicChangelogProduct, type PublicChangelogProductsPayload, type RequestOptions, type SeenStorage, type SeenTracker, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
|
package/dist/index.js
CHANGED
|
@@ -164,8 +164,10 @@ function required(value, name) {
|
|
|
164
164
|
// src/client.ts
|
|
165
165
|
function createChangelogClient(options) {
|
|
166
166
|
const workspace = required(options.workspace, "workspace");
|
|
167
|
+
const product = options.product?.trim() || void 0;
|
|
167
168
|
const http = createHttp(options);
|
|
168
|
-
const
|
|
169
|
+
const root = `/changelog/${seg(workspace)}`;
|
|
170
|
+
const base = product ? `${root}/${seg(product)}` : root;
|
|
169
171
|
const list = (params = {}, requestOptions) => http.request({
|
|
170
172
|
method: "GET",
|
|
171
173
|
path: base,
|
|
@@ -185,12 +187,18 @@ function createChangelogClient(options) {
|
|
|
185
187
|
}
|
|
186
188
|
return {
|
|
187
189
|
workspace,
|
|
190
|
+
product,
|
|
188
191
|
list,
|
|
189
192
|
allEntries,
|
|
190
193
|
getEntry: (slug, requestOptions) => http.request({
|
|
191
194
|
method: "GET",
|
|
192
195
|
path: `${base}/entries/${seg(required(slug, "slug"))}`,
|
|
193
196
|
options: requestOptions
|
|
197
|
+
}),
|
|
198
|
+
listProducts: (requestOptions) => http.request({
|
|
199
|
+
method: "GET",
|
|
200
|
+
path: `${root}/products`,
|
|
201
|
+
options: requestOptions
|
|
194
202
|
})
|
|
195
203
|
};
|
|
196
204
|
}
|
|
@@ -204,8 +212,8 @@ function browserStorage() {
|
|
|
204
212
|
return null;
|
|
205
213
|
}
|
|
206
214
|
}
|
|
207
|
-
function createSeenTracker(workspace, storage = browserStorage()) {
|
|
208
|
-
const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
|
|
215
|
+
function createSeenTracker(workspace, storage = browserStorage(), product) {
|
|
216
|
+
const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
|
|
209
217
|
const lastSeen = () => {
|
|
210
218
|
try {
|
|
211
219
|
return storage?.getItem(key) ?? null;
|
package/dist/react.cjs
CHANGED
|
@@ -16,8 +16,8 @@ function browserStorage() {
|
|
|
16
16
|
return null;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
function createSeenTracker(workspace, storage = browserStorage()) {
|
|
20
|
-
const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
|
|
19
|
+
function createSeenTracker(workspace, storage = browserStorage(), product) {
|
|
20
|
+
const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
|
|
21
21
|
const lastSeen = () => {
|
|
22
22
|
try {
|
|
23
23
|
return storage?.getItem(key) ?? null;
|
|
@@ -210,8 +210,10 @@ function required(value, name) {
|
|
|
210
210
|
// src/client.ts
|
|
211
211
|
function createChangelogClient(options) {
|
|
212
212
|
const workspace = required(options.workspace, "workspace");
|
|
213
|
+
const product = options.product?.trim() || void 0;
|
|
213
214
|
const http = createHttp(options);
|
|
214
|
-
const
|
|
215
|
+
const root = `/changelog/${seg(workspace)}`;
|
|
216
|
+
const base = product ? `${root}/${seg(product)}` : root;
|
|
215
217
|
const list = (params = {}, requestOptions) => http.request({
|
|
216
218
|
method: "GET",
|
|
217
219
|
path: base,
|
|
@@ -231,34 +233,49 @@ function createChangelogClient(options) {
|
|
|
231
233
|
}
|
|
232
234
|
return {
|
|
233
235
|
workspace,
|
|
236
|
+
product,
|
|
234
237
|
list,
|
|
235
238
|
allEntries,
|
|
236
239
|
getEntry: (slug, requestOptions) => http.request({
|
|
237
240
|
method: "GET",
|
|
238
241
|
path: `${base}/entries/${seg(required(slug, "slug"))}`,
|
|
239
242
|
options: requestOptions
|
|
243
|
+
}),
|
|
244
|
+
listProducts: (requestOptions) => http.request({
|
|
245
|
+
method: "GET",
|
|
246
|
+
path: `${root}/products`,
|
|
247
|
+
options: requestOptions
|
|
240
248
|
})
|
|
241
249
|
};
|
|
242
250
|
}
|
|
243
251
|
|
|
244
252
|
// src/react/use-changelog.ts
|
|
245
253
|
function useChangelog(options) {
|
|
246
|
-
const {
|
|
254
|
+
const {
|
|
255
|
+
workspace,
|
|
256
|
+
product,
|
|
257
|
+
client: given,
|
|
258
|
+
limit = 10,
|
|
259
|
+
category,
|
|
260
|
+
trackSeen = true,
|
|
261
|
+
initialData
|
|
262
|
+
} = options;
|
|
247
263
|
const { baseUrl, fetch: fetchImpl, headers, etagCache } = options;
|
|
248
264
|
const headersKey = JSON.stringify(headers ?? {});
|
|
249
265
|
const client = react.useMemo(
|
|
250
266
|
() => given ?? createChangelogClient({
|
|
251
267
|
workspace,
|
|
268
|
+
product,
|
|
252
269
|
baseUrl,
|
|
253
270
|
fetch: fetchImpl,
|
|
254
271
|
headers: JSON.parse(headersKey),
|
|
255
272
|
etagCache
|
|
256
273
|
}),
|
|
257
|
-
[given, workspace, baseUrl, fetchImpl, headersKey, etagCache]
|
|
274
|
+
[given, workspace, product, baseUrl, fetchImpl, headersKey, etagCache]
|
|
258
275
|
);
|
|
259
276
|
const tracker = react.useMemo(
|
|
260
|
-
() => trackSeen ? createSeenTracker(workspace, options.storage) : null,
|
|
261
|
-
[trackSeen, workspace, options.storage]
|
|
277
|
+
() => trackSeen ? createSeenTracker(workspace, options.storage, given?.product ?? product) : null,
|
|
278
|
+
[trackSeen, workspace, options.storage, given, product]
|
|
262
279
|
);
|
|
263
280
|
const [data, setData] = react.useState(initialData ?? null);
|
|
264
281
|
const [entries, setEntries] = react.useState(initialData?.entries ?? []);
|
|
@@ -364,11 +381,25 @@ function ChangelogEntryView({
|
|
|
364
381
|
return /* @__PURE__ */ jsxRuntime.jsxs("article", { "data-sd-changelog-entry": "", "data-category": entry.category, className: classNames.entry, children: [
|
|
365
382
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { "data-sd-changelog-meta": "", className: classNames.meta, children: [
|
|
366
383
|
/* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: entry.publishedAt, className: classNames.date, children: formatChangelogDate(entry.publishedAt, locale) }),
|
|
367
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, 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
|
|
368
389
|
] }),
|
|
369
390
|
/* @__PURE__ */ jsxRuntime.jsx(Title, { className: classNames.title, children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: entry.url, children: entry.title }) }),
|
|
370
391
|
entry.summary ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: classNames.summary, children: entry.summary }) : null,
|
|
371
|
-
showBody ? /* @__PURE__ */ jsxRuntime.jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : 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
|
|
372
403
|
] });
|
|
373
404
|
}
|
|
374
405
|
function ChangelogFeed(props) {
|
package/dist/react.d.cts
CHANGED
|
@@ -15,20 +15,48 @@ interface PublicChangelogEntry {
|
|
|
15
15
|
/** One or two sentences — what a "What's new" panel shows. May be empty. */
|
|
16
16
|
summary: string;
|
|
17
17
|
category: ChangelogCategory;
|
|
18
|
+
/** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
|
|
19
|
+
releaseVersion: string;
|
|
20
|
+
/** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
|
|
21
|
+
downloadUrl: string | null;
|
|
18
22
|
/** ISO 8601. The list is sorted by it, newest first. */
|
|
19
23
|
publishedAt: string;
|
|
20
24
|
/** The details as the team wrote them (Markdown). */
|
|
21
25
|
markdown: string;
|
|
22
26
|
/**
|
|
23
27
|
* The details as safe HTML: raw HTML in the text is escaped, links are only
|
|
24
|
-
* http(s)/mailto/relative, images only
|
|
25
|
-
* `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
28
|
+
* http(s)/mailto/relative, images only the ones uploaded to the entry (with
|
|
29
|
+
* `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
30
|
+
* there are no details.
|
|
26
31
|
*/
|
|
27
32
|
html: string;
|
|
28
33
|
/** The entry on the workspace's hosted changelog page. */
|
|
29
34
|
url: string;
|
|
30
35
|
}
|
|
31
|
-
/**
|
|
36
|
+
/** A product with its own changelog. */
|
|
37
|
+
interface PublicChangelogProduct {
|
|
38
|
+
name: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
/** One sentence about the product. May be empty. */
|
|
41
|
+
description: string;
|
|
42
|
+
/** The product's hosted changelog page. */
|
|
43
|
+
pageUrl: string;
|
|
44
|
+
/** The default product: addresses without a product show this one. */
|
|
45
|
+
isDefault: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
|
|
48
|
+
interface PublicChangelogProductsPayload {
|
|
49
|
+
workspace: {
|
|
50
|
+
name: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
logoUrl: string | null;
|
|
53
|
+
};
|
|
54
|
+
products: PublicChangelogProduct[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
|
|
58
|
+
* page of its entries (`GET /v1/changelog/{workspace}`: the default product).
|
|
59
|
+
*/
|
|
32
60
|
interface PublicChangelogPayload {
|
|
33
61
|
/** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
|
|
34
62
|
workspace: {
|
|
@@ -36,6 +64,8 @@ interface PublicChangelogPayload {
|
|
|
36
64
|
slug: string;
|
|
37
65
|
logoUrl: string | null;
|
|
38
66
|
};
|
|
67
|
+
/** The product these entries belong to. */
|
|
68
|
+
product: PublicChangelogProduct;
|
|
39
69
|
/** The heading the team chose; empty ⇒ use your own ("What's new"). */
|
|
40
70
|
title: string;
|
|
41
71
|
/** The team's settings for the "What's new" button (empty label ⇒ your own). */
|
|
@@ -43,14 +73,16 @@ interface PublicChangelogPayload {
|
|
|
43
73
|
label: string;
|
|
44
74
|
position: ChangelogWidgetPosition;
|
|
45
75
|
};
|
|
46
|
-
/** The hosted changelog page. */
|
|
76
|
+
/** The product's hosted changelog page. */
|
|
47
77
|
pageUrl: string;
|
|
48
78
|
entries: PublicChangelogEntry[];
|
|
49
79
|
/** Pass as `cursor` for the next page; null on the last one. */
|
|
50
80
|
nextCursor: string | null;
|
|
51
81
|
}
|
|
52
|
-
/** `GET /v1/changelog/{workspace}/entries/{slug}`. */
|
|
82
|
+
/** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
|
|
53
83
|
interface PublicChangelogEntryPayload {
|
|
84
|
+
/** The product whose changelog shows the entry. */
|
|
85
|
+
product: PublicChangelogProduct;
|
|
54
86
|
entry: PublicChangelogEntry;
|
|
55
87
|
}
|
|
56
88
|
|
|
@@ -122,6 +154,11 @@ interface RequestOptions {
|
|
|
122
154
|
interface ChangelogClientOptions extends BaseClientOptions {
|
|
123
155
|
/** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
|
|
124
156
|
workspace: string;
|
|
157
|
+
/**
|
|
158
|
+
* The product whose changelog to read (its web address, e.g. `acme-mobile`).
|
|
159
|
+
* Without it, the workspace's default product.
|
|
160
|
+
*/
|
|
161
|
+
product?: string;
|
|
125
162
|
}
|
|
126
163
|
interface ListEntriesParams {
|
|
127
164
|
/** Only entries of this category. */
|
|
@@ -134,19 +171,25 @@ interface ListEntriesParams {
|
|
|
134
171
|
interface ChangelogClient {
|
|
135
172
|
/** The workspace this client reads. */
|
|
136
173
|
readonly workspace: string;
|
|
174
|
+
/** The product this client reads, or undefined for the default product. */
|
|
175
|
+
readonly product: string | undefined;
|
|
137
176
|
/**
|
|
138
|
-
* The
|
|
139
|
-
* (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError`
|
|
140
|
-
* 404 when the workspace doesn't exist or
|
|
177
|
+
* The product's settings and one page of its published entries, newest first
|
|
178
|
+
* (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
|
|
179
|
+
* with status 404 when the workspace or product doesn't exist or the
|
|
180
|
+
* changelog is turned off.
|
|
141
181
|
*/
|
|
142
182
|
list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
|
|
143
183
|
/** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
|
|
144
184
|
allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
|
|
145
185
|
/**
|
|
146
|
-
* One published entry (
|
|
147
|
-
* Throws a `SolutionDeskError` with status 404
|
|
186
|
+
* One published entry of the product (`…/{product}/entries/{slug}`; without
|
|
187
|
+
* a product, of any product). Throws a `SolutionDeskError` with status 404
|
|
188
|
+
* for an unknown or unpublished entry.
|
|
148
189
|
*/
|
|
149
190
|
getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
|
|
191
|
+
/** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
|
|
192
|
+
listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
|
|
150
193
|
}
|
|
151
194
|
|
|
152
195
|
/** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
|
|
@@ -158,6 +201,8 @@ interface SeenStorage {
|
|
|
158
201
|
interface UseChangelogOptions extends Partial<Omit<ChangelogClientOptions, "workspace">> {
|
|
159
202
|
/** Your workspace's slug. */
|
|
160
203
|
workspace: string;
|
|
204
|
+
/** The product whose changelog to show (its web address); else the default product. */
|
|
205
|
+
product?: string;
|
|
161
206
|
/** Use this client instead of making one (e.g. one shared by your whole app). */
|
|
162
207
|
client?: ChangelogClient;
|
|
163
208
|
/** Entries per page (1–50, default 10). */
|
|
@@ -222,11 +267,18 @@ interface ChangelogEntryClassNames {
|
|
|
222
267
|
meta?: string;
|
|
223
268
|
date?: string;
|
|
224
269
|
category?: string;
|
|
270
|
+
/** The release (`Version 2.3.0`). */
|
|
271
|
+
version?: string;
|
|
272
|
+
/** The download link. */
|
|
273
|
+
download?: string;
|
|
225
274
|
title?: string;
|
|
226
275
|
summary?: string;
|
|
227
276
|
body?: string;
|
|
228
277
|
}
|
|
229
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* One entry: date, category, release, title (linking to the hosted page),
|
|
280
|
+
* summary, details and the download link.
|
|
281
|
+
*/
|
|
230
282
|
declare function ChangelogEntryView({ entry, locale, showBody, classNames, titleAs: Title, }: {
|
|
231
283
|
entry: PublicChangelogEntry;
|
|
232
284
|
/** Language of the date and the category name (`de*` → German, else English). */
|
package/dist/react.d.ts
CHANGED
|
@@ -15,20 +15,48 @@ interface PublicChangelogEntry {
|
|
|
15
15
|
/** One or two sentences — what a "What's new" panel shows. May be empty. */
|
|
16
16
|
summary: string;
|
|
17
17
|
category: ChangelogCategory;
|
|
18
|
+
/** The release this change ships in (`2.3.0`); empty when it isn't tied to one. */
|
|
19
|
+
releaseVersion: string;
|
|
20
|
+
/** Where the release can be downloaded (an app store page, GitHub Releases …), or null. */
|
|
21
|
+
downloadUrl: string | null;
|
|
18
22
|
/** ISO 8601. The list is sorted by it, newest first. */
|
|
19
23
|
publishedAt: string;
|
|
20
24
|
/** The details as the team wrote them (Markdown). */
|
|
21
25
|
markdown: string;
|
|
22
26
|
/**
|
|
23
27
|
* The details as safe HTML: raw HTML in the text is escaped, links are only
|
|
24
|
-
* http(s)/mailto/relative, images only
|
|
25
|
-
* `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
28
|
+
* http(s)/mailto/relative, images only the ones uploaded to the entry (with
|
|
29
|
+
* `srcset`). Ready for `innerHTML` / `dangerouslySetInnerHTML`. Empty when
|
|
30
|
+
* there are no details.
|
|
26
31
|
*/
|
|
27
32
|
html: string;
|
|
28
33
|
/** The entry on the workspace's hosted changelog page. */
|
|
29
34
|
url: string;
|
|
30
35
|
}
|
|
31
|
-
/**
|
|
36
|
+
/** A product with its own changelog. */
|
|
37
|
+
interface PublicChangelogProduct {
|
|
38
|
+
name: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
/** One sentence about the product. May be empty. */
|
|
41
|
+
description: string;
|
|
42
|
+
/** The product's hosted changelog page. */
|
|
43
|
+
pageUrl: string;
|
|
44
|
+
/** The default product: addresses without a product show this one. */
|
|
45
|
+
isDefault: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** `GET /v1/changelog/{workspace}/products` — every product, the default first. */
|
|
48
|
+
interface PublicChangelogProductsPayload {
|
|
49
|
+
workspace: {
|
|
50
|
+
name: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
logoUrl: string | null;
|
|
53
|
+
};
|
|
54
|
+
products: PublicChangelogProduct[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `GET /v1/changelog/{workspace}/{product}` — a product's settings and one
|
|
58
|
+
* page of its entries (`GET /v1/changelog/{workspace}`: the default product).
|
|
59
|
+
*/
|
|
32
60
|
interface PublicChangelogPayload {
|
|
33
61
|
/** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
|
|
34
62
|
workspace: {
|
|
@@ -36,6 +64,8 @@ interface PublicChangelogPayload {
|
|
|
36
64
|
slug: string;
|
|
37
65
|
logoUrl: string | null;
|
|
38
66
|
};
|
|
67
|
+
/** The product these entries belong to. */
|
|
68
|
+
product: PublicChangelogProduct;
|
|
39
69
|
/** The heading the team chose; empty ⇒ use your own ("What's new"). */
|
|
40
70
|
title: string;
|
|
41
71
|
/** The team's settings for the "What's new" button (empty label ⇒ your own). */
|
|
@@ -43,14 +73,16 @@ interface PublicChangelogPayload {
|
|
|
43
73
|
label: string;
|
|
44
74
|
position: ChangelogWidgetPosition;
|
|
45
75
|
};
|
|
46
|
-
/** The hosted changelog page. */
|
|
76
|
+
/** The product's hosted changelog page. */
|
|
47
77
|
pageUrl: string;
|
|
48
78
|
entries: PublicChangelogEntry[];
|
|
49
79
|
/** Pass as `cursor` for the next page; null on the last one. */
|
|
50
80
|
nextCursor: string | null;
|
|
51
81
|
}
|
|
52
|
-
/** `GET /v1/changelog/{workspace}/entries/{slug}`. */
|
|
82
|
+
/** `GET /v1/changelog/{workspace}/{product}/entries/{slug}`. */
|
|
53
83
|
interface PublicChangelogEntryPayload {
|
|
84
|
+
/** The product whose changelog shows the entry. */
|
|
85
|
+
product: PublicChangelogProduct;
|
|
54
86
|
entry: PublicChangelogEntry;
|
|
55
87
|
}
|
|
56
88
|
|
|
@@ -122,6 +154,11 @@ interface RequestOptions {
|
|
|
122
154
|
interface ChangelogClientOptions extends BaseClientOptions {
|
|
123
155
|
/** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
|
|
124
156
|
workspace: string;
|
|
157
|
+
/**
|
|
158
|
+
* The product whose changelog to read (its web address, e.g. `acme-mobile`).
|
|
159
|
+
* Without it, the workspace's default product.
|
|
160
|
+
*/
|
|
161
|
+
product?: string;
|
|
125
162
|
}
|
|
126
163
|
interface ListEntriesParams {
|
|
127
164
|
/** Only entries of this category. */
|
|
@@ -134,19 +171,25 @@ interface ListEntriesParams {
|
|
|
134
171
|
interface ChangelogClient {
|
|
135
172
|
/** The workspace this client reads. */
|
|
136
173
|
readonly workspace: string;
|
|
174
|
+
/** The product this client reads, or undefined for the default product. */
|
|
175
|
+
readonly product: string | undefined;
|
|
137
176
|
/**
|
|
138
|
-
* The
|
|
139
|
-
* (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError`
|
|
140
|
-
* 404 when the workspace doesn't exist or
|
|
177
|
+
* The product's settings and one page of its published entries, newest first
|
|
178
|
+
* (`GET /v1/changelog/{workspace}/{product}`). Throws a `SolutionDeskError`
|
|
179
|
+
* with status 404 when the workspace or product doesn't exist or the
|
|
180
|
+
* changelog is turned off.
|
|
141
181
|
*/
|
|
142
182
|
list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
|
|
143
183
|
/** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
|
|
144
184
|
allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
|
|
145
185
|
/**
|
|
146
|
-
* One published entry (
|
|
147
|
-
* Throws a `SolutionDeskError` with status 404
|
|
186
|
+
* One published entry of the product (`…/{product}/entries/{slug}`; without
|
|
187
|
+
* a product, of any product). Throws a `SolutionDeskError` with status 404
|
|
188
|
+
* for an unknown or unpublished entry.
|
|
148
189
|
*/
|
|
149
190
|
getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
|
|
191
|
+
/** Every product of the workspace, the default first (`GET /v1/changelog/{workspace}/products`). */
|
|
192
|
+
listProducts(options?: RequestOptions): Promise<PublicChangelogProductsPayload>;
|
|
150
193
|
}
|
|
151
194
|
|
|
152
195
|
/** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
|
|
@@ -158,6 +201,8 @@ interface SeenStorage {
|
|
|
158
201
|
interface UseChangelogOptions extends Partial<Omit<ChangelogClientOptions, "workspace">> {
|
|
159
202
|
/** Your workspace's slug. */
|
|
160
203
|
workspace: string;
|
|
204
|
+
/** The product whose changelog to show (its web address); else the default product. */
|
|
205
|
+
product?: string;
|
|
161
206
|
/** Use this client instead of making one (e.g. one shared by your whole app). */
|
|
162
207
|
client?: ChangelogClient;
|
|
163
208
|
/** Entries per page (1–50, default 10). */
|
|
@@ -222,11 +267,18 @@ interface ChangelogEntryClassNames {
|
|
|
222
267
|
meta?: string;
|
|
223
268
|
date?: string;
|
|
224
269
|
category?: string;
|
|
270
|
+
/** The release (`Version 2.3.0`). */
|
|
271
|
+
version?: string;
|
|
272
|
+
/** The download link. */
|
|
273
|
+
download?: string;
|
|
225
274
|
title?: string;
|
|
226
275
|
summary?: string;
|
|
227
276
|
body?: string;
|
|
228
277
|
}
|
|
229
|
-
/**
|
|
278
|
+
/**
|
|
279
|
+
* One entry: date, category, release, title (linking to the hosted page),
|
|
280
|
+
* summary, details and the download link.
|
|
281
|
+
*/
|
|
230
282
|
declare function ChangelogEntryView({ entry, locale, showBody, classNames, titleAs: Title, }: {
|
|
231
283
|
entry: PublicChangelogEntry;
|
|
232
284
|
/** Language of the date and the category name (`de*` → German, else English). */
|
package/dist/react.js
CHANGED
|
@@ -14,8 +14,8 @@ function browserStorage() {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
function createSeenTracker(workspace, storage = browserStorage()) {
|
|
18
|
-
const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
|
|
17
|
+
function createSeenTracker(workspace, storage = browserStorage(), product) {
|
|
18
|
+
const key = `sd-changelog-seen:${workspace.toLowerCase()}${product ? `:${product.toLowerCase()}` : ""}`;
|
|
19
19
|
const lastSeen = () => {
|
|
20
20
|
try {
|
|
21
21
|
return storage?.getItem(key) ?? null;
|
|
@@ -208,8 +208,10 @@ function required(value, name) {
|
|
|
208
208
|
// src/client.ts
|
|
209
209
|
function createChangelogClient(options) {
|
|
210
210
|
const workspace = required(options.workspace, "workspace");
|
|
211
|
+
const product = options.product?.trim() || void 0;
|
|
211
212
|
const http = createHttp(options);
|
|
212
|
-
const
|
|
213
|
+
const root = `/changelog/${seg(workspace)}`;
|
|
214
|
+
const base = product ? `${root}/${seg(product)}` : root;
|
|
213
215
|
const list = (params = {}, requestOptions) => http.request({
|
|
214
216
|
method: "GET",
|
|
215
217
|
path: base,
|
|
@@ -229,34 +231,49 @@ function createChangelogClient(options) {
|
|
|
229
231
|
}
|
|
230
232
|
return {
|
|
231
233
|
workspace,
|
|
234
|
+
product,
|
|
232
235
|
list,
|
|
233
236
|
allEntries,
|
|
234
237
|
getEntry: (slug, requestOptions) => http.request({
|
|
235
238
|
method: "GET",
|
|
236
239
|
path: `${base}/entries/${seg(required(slug, "slug"))}`,
|
|
237
240
|
options: requestOptions
|
|
241
|
+
}),
|
|
242
|
+
listProducts: (requestOptions) => http.request({
|
|
243
|
+
method: "GET",
|
|
244
|
+
path: `${root}/products`,
|
|
245
|
+
options: requestOptions
|
|
238
246
|
})
|
|
239
247
|
};
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
// src/react/use-changelog.ts
|
|
243
251
|
function useChangelog(options) {
|
|
244
|
-
const {
|
|
252
|
+
const {
|
|
253
|
+
workspace,
|
|
254
|
+
product,
|
|
255
|
+
client: given,
|
|
256
|
+
limit = 10,
|
|
257
|
+
category,
|
|
258
|
+
trackSeen = true,
|
|
259
|
+
initialData
|
|
260
|
+
} = options;
|
|
245
261
|
const { baseUrl, fetch: fetchImpl, headers, etagCache } = options;
|
|
246
262
|
const headersKey = JSON.stringify(headers ?? {});
|
|
247
263
|
const client = useMemo(
|
|
248
264
|
() => given ?? createChangelogClient({
|
|
249
265
|
workspace,
|
|
266
|
+
product,
|
|
250
267
|
baseUrl,
|
|
251
268
|
fetch: fetchImpl,
|
|
252
269
|
headers: JSON.parse(headersKey),
|
|
253
270
|
etagCache
|
|
254
271
|
}),
|
|
255
|
-
[given, workspace, baseUrl, fetchImpl, headersKey, etagCache]
|
|
272
|
+
[given, workspace, product, baseUrl, fetchImpl, headersKey, etagCache]
|
|
256
273
|
);
|
|
257
274
|
const tracker = useMemo(
|
|
258
|
-
() => trackSeen ? createSeenTracker(workspace, options.storage) : null,
|
|
259
|
-
[trackSeen, workspace, options.storage]
|
|
275
|
+
() => trackSeen ? createSeenTracker(workspace, options.storage, given?.product ?? product) : null,
|
|
276
|
+
[trackSeen, workspace, options.storage, given, product]
|
|
260
277
|
);
|
|
261
278
|
const [data, setData] = useState(initialData ?? null);
|
|
262
279
|
const [entries, setEntries] = useState(initialData?.entries ?? []);
|
|
@@ -362,11 +379,25 @@ function ChangelogEntryView({
|
|
|
362
379
|
return /* @__PURE__ */ jsxs("article", { "data-sd-changelog-entry": "", "data-category": entry.category, className: classNames.entry, children: [
|
|
363
380
|
/* @__PURE__ */ jsxs("div", { "data-sd-changelog-meta": "", className: classNames.meta, children: [
|
|
364
381
|
/* @__PURE__ */ jsx("time", { dateTime: entry.publishedAt, className: classNames.date, children: formatChangelogDate(entry.publishedAt, locale) }),
|
|
365
|
-
/* @__PURE__ */ jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) })
|
|
382
|
+
/* @__PURE__ */ jsx("span", { "data-sd-changelog-category": entry.category, className: classNames.category, children: categoryLabel(entry.category, locale) }),
|
|
383
|
+
entry.releaseVersion ? /* @__PURE__ */ jsxs("span", { "data-sd-changelog-version": "", className: classNames.version, children: [
|
|
384
|
+
"Version ",
|
|
385
|
+
entry.releaseVersion
|
|
386
|
+
] }) : null
|
|
366
387
|
] }),
|
|
367
388
|
/* @__PURE__ */ jsx(Title, { className: classNames.title, children: /* @__PURE__ */ jsx("a", { href: entry.url, children: entry.title }) }),
|
|
368
389
|
entry.summary ? /* @__PURE__ */ jsx("p", { className: classNames.summary, children: entry.summary }) : null,
|
|
369
|
-
showBody ? /* @__PURE__ */ jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null
|
|
390
|
+
showBody ? /* @__PURE__ */ jsx(ChangelogEntryBody, { html: entry.html, className: classNames.body }) : null,
|
|
391
|
+
entry.downloadUrl ? /* @__PURE__ */ jsx(
|
|
392
|
+
"a",
|
|
393
|
+
{
|
|
394
|
+
href: entry.downloadUrl,
|
|
395
|
+
rel: "noopener",
|
|
396
|
+
"data-sd-changelog-download": "",
|
|
397
|
+
className: classNames.download,
|
|
398
|
+
children: locale.toLowerCase().startsWith("de") ? "Herunterladen" : "Download"
|
|
399
|
+
}
|
|
400
|
+
) : null
|
|
370
401
|
] });
|
|
371
402
|
}
|
|
372
403
|
function ChangelogFeed(props) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solutiondesk/changelog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "Typed, zero-dependency client for the SolutionDesk changelog, plus React hooks and unstyled components for a \"What's new\" feed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"solutiondesk",
|