@solutiondesk/changelog 0.0.0 → 1.0.0-9fc05ac
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/LICENSE +21 -0
- package/README.md +74 -28
- package/dist/index.cjs +255 -0
- package/dist/index.d.cts +229 -0
- package/dist/index.d.ts +229 -0
- package/dist/index.js +245 -0
- package/dist/react.cjs +508 -0
- package/dist/react.d.cts +288 -0
- package/dist/react.d.ts +288 -0
- package/dist/react.js +501 -0
- package/package.json +75 -7
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP part shared by the public SolutionDesk SDKs. Runs on any runtime with
|
|
3
|
+
* a WHATWG `fetch` (Node ≥ 18, Deno, Bun, edge runtimes, browsers) — and on
|
|
4
|
+
* purpose names no DOM or Node type, so the published `.d.ts` files need neither
|
|
5
|
+
* `lib: ["dom"]` nor `@types/node`.
|
|
6
|
+
*/
|
|
7
|
+
/** The production API. */
|
|
8
|
+
declare const DEFAULT_BASE_URL = "https://api.solutiondesk.io";
|
|
9
|
+
/** The response parts the SDK reads — what every `fetch` implementation returns. */
|
|
10
|
+
interface FetchResponseLike {
|
|
11
|
+
readonly status: number;
|
|
12
|
+
readonly ok: boolean;
|
|
13
|
+
readonly headers: {
|
|
14
|
+
get(name: string): string | null;
|
|
15
|
+
};
|
|
16
|
+
text(): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
/** What the SDK passes to `fetch` (plus whatever you set in `fetchOptions`). */
|
|
19
|
+
interface FetchInitLike {
|
|
20
|
+
method: string;
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
body?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A `fetch` implementation. The global `fetch` fits, so do `undici`, `node-fetch`
|
|
26
|
+
* and wrappers (e.g. one that adds tracing). Declared bivariantly so a fetch with
|
|
27
|
+
* a richer `init` type is accepted.
|
|
28
|
+
*/
|
|
29
|
+
type FetchLike = {
|
|
30
|
+
bivarianceHack(input: string, init?: FetchInitLike): Promise<FetchResponseLike>;
|
|
31
|
+
}["bivarianceHack"];
|
|
32
|
+
/** A response kept for `If-None-Match` revalidation. */
|
|
33
|
+
interface CachedResponse {
|
|
34
|
+
etag: string;
|
|
35
|
+
body: unknown;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Where the client keeps answers for ETag revalidation. Any `Map` fits; bring
|
|
39
|
+
* your own to share it between clients or to bound it differently.
|
|
40
|
+
*/
|
|
41
|
+
interface EtagCache {
|
|
42
|
+
get(key: string): CachedResponse | undefined;
|
|
43
|
+
set(key: string, value: CachedResponse): unknown;
|
|
44
|
+
}
|
|
45
|
+
/** Options every client factory takes. */
|
|
46
|
+
interface BaseClientOptions {
|
|
47
|
+
/** The API origin, without `/v1`. Defaults to `https://api.solutiondesk.io`. */
|
|
48
|
+
baseUrl?: string;
|
|
49
|
+
/** A `fetch` implementation; defaults to the global one. */
|
|
50
|
+
fetch?: FetchLike;
|
|
51
|
+
/** Headers sent with every request (e.g. `accept-language` forwarded from your visitor). */
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Revalidate with `If-None-Match`: the client remembers each answer's `ETag`
|
|
55
|
+
* and, when the API answers `304 Not Modified`, returns the remembered body.
|
|
56
|
+
* `true` keeps up to 500 answers in memory; or pass your own {@link EtagCache}.
|
|
57
|
+
* Off by default (your framework's fetch cache may already do this).
|
|
58
|
+
*/
|
|
59
|
+
etagCache?: boolean | EtagCache;
|
|
60
|
+
}
|
|
61
|
+
/** Per-call options. */
|
|
62
|
+
interface RequestOptions {
|
|
63
|
+
/** Cancels the request (an `AbortSignal`). */
|
|
64
|
+
signal?: unknown;
|
|
65
|
+
/** Extra headers for this request. */
|
|
66
|
+
headers?: Record<string, string>;
|
|
67
|
+
/**
|
|
68
|
+
* Extra `fetch` options for this request, passed through unchanged — e.g.
|
|
69
|
+
* Next.js `{ next: { revalidate: 60, tags: ["legal"] } }` or `{ cache: "no-store" }`.
|
|
70
|
+
*/
|
|
71
|
+
fetchOptions?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/** Any non-2xx answer of the API (and a failed network request, with `status` 0). */
|
|
74
|
+
declare class SolutionDeskError extends Error {
|
|
75
|
+
/** The HTTP status; `0` when no answer arrived (network error, aborted). */
|
|
76
|
+
readonly status: number;
|
|
77
|
+
/** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
|
|
78
|
+
readonly body: unknown;
|
|
79
|
+
/** The requested URL. */
|
|
80
|
+
readonly url: string;
|
|
81
|
+
/** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
|
|
82
|
+
readonly fields: Record<string, string>;
|
|
83
|
+
constructor(message: string, init: {
|
|
84
|
+
status: number;
|
|
85
|
+
body?: unknown;
|
|
86
|
+
url: string;
|
|
87
|
+
cause?: unknown;
|
|
88
|
+
});
|
|
89
|
+
/** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
|
|
90
|
+
get isNotFound(): boolean;
|
|
91
|
+
/** `429`: over the rate limit; try again later. */
|
|
92
|
+
get isRateLimited(): boolean;
|
|
93
|
+
}
|
|
94
|
+
/** A bounded in-memory {@link EtagCache} (oldest entry dropped first). */
|
|
95
|
+
declare function createMemoryEtagCache(maxEntries?: number): EtagCache;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The answers of the public changelog API (`GET /v1/changelog/{workspace}/…`).
|
|
99
|
+
* Written by hand so the package has no dependencies; a drift test in the
|
|
100
|
+
* SolutionDesk repository keeps them identical to the API's own contracts.
|
|
101
|
+
*/
|
|
102
|
+
type ChangelogCategory = "new" | "improved" | "fixed" | "announcement";
|
|
103
|
+
type ChangelogWidgetPosition = "bottom-right" | "bottom-left";
|
|
104
|
+
/** A published entry. */
|
|
105
|
+
interface PublicChangelogEntry {
|
|
106
|
+
slug: string;
|
|
107
|
+
title: string;
|
|
108
|
+
/** One or two sentences — what a "What's new" panel shows. May be empty. */
|
|
109
|
+
summary: string;
|
|
110
|
+
category: ChangelogCategory;
|
|
111
|
+
/** ISO 8601. The list is sorted by it, newest first. */
|
|
112
|
+
publishedAt: string;
|
|
113
|
+
/** The details as the team wrote them (Markdown). */
|
|
114
|
+
markdown: string;
|
|
115
|
+
/**
|
|
116
|
+
* The details as safe HTML: raw HTML in the text is escaped, links are only
|
|
117
|
+
* http(s)/mailto/relative, images only from https addresses. Ready for
|
|
118
|
+
* `innerHTML` / `dangerouslySetInnerHTML`. Empty when there are no details.
|
|
119
|
+
*/
|
|
120
|
+
html: string;
|
|
121
|
+
/** The entry on the workspace's hosted changelog page. */
|
|
122
|
+
url: string;
|
|
123
|
+
}
|
|
124
|
+
/** `GET /v1/changelog/{workspace}` — settings and one page of entries. */
|
|
125
|
+
interface PublicChangelogPayload {
|
|
126
|
+
/** The workspace as the hosted page shows it; `logoUrl` is relative to the API. */
|
|
127
|
+
workspace: {
|
|
128
|
+
name: string;
|
|
129
|
+
slug: string;
|
|
130
|
+
logoUrl: string | null;
|
|
131
|
+
};
|
|
132
|
+
/** The heading the team chose; empty ⇒ use your own ("What's new"). */
|
|
133
|
+
title: string;
|
|
134
|
+
/** The team's settings for the "What's new" button (empty label ⇒ your own). */
|
|
135
|
+
widget: {
|
|
136
|
+
label: string;
|
|
137
|
+
position: ChangelogWidgetPosition;
|
|
138
|
+
};
|
|
139
|
+
/** The hosted changelog page. */
|
|
140
|
+
pageUrl: string;
|
|
141
|
+
entries: PublicChangelogEntry[];
|
|
142
|
+
/** Pass as `cursor` for the next page; null on the last one. */
|
|
143
|
+
nextCursor: string | null;
|
|
144
|
+
}
|
|
145
|
+
/** `GET /v1/changelog/{workspace}/entries/{slug}`. */
|
|
146
|
+
interface PublicChangelogEntryPayload {
|
|
147
|
+
entry: PublicChangelogEntry;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ChangelogClientOptions extends BaseClientOptions {
|
|
151
|
+
/** Your workspace's slug — the `acme` in `https://api.solutiondesk.io/v1/changelog/acme`. */
|
|
152
|
+
workspace: string;
|
|
153
|
+
}
|
|
154
|
+
interface ListEntriesParams {
|
|
155
|
+
/** Only entries of this category. */
|
|
156
|
+
category?: ChangelogCategory;
|
|
157
|
+
/** Page size, 1–50 (default 20). */
|
|
158
|
+
limit?: number;
|
|
159
|
+
/** `nextCursor` of the previous page. */
|
|
160
|
+
cursor?: string | null;
|
|
161
|
+
}
|
|
162
|
+
interface ChangelogClient {
|
|
163
|
+
/** The workspace this client reads. */
|
|
164
|
+
readonly workspace: string;
|
|
165
|
+
/**
|
|
166
|
+
* The team's settings and one page of published entries, newest first
|
|
167
|
+
* (`GET /v1/changelog/{workspace}`). Throws a `SolutionDeskError` with status
|
|
168
|
+
* 404 when the workspace doesn't exist or its changelog is turned off.
|
|
169
|
+
*/
|
|
170
|
+
list(params?: ListEntriesParams, options?: RequestOptions): Promise<PublicChangelogPayload>;
|
|
171
|
+
/** Every published entry, page by page — e.g. for a sitemap, an RSS feed or a static build. */
|
|
172
|
+
allEntries(params?: Omit<ListEntriesParams, "cursor">, options?: RequestOptions): AsyncGenerator<PublicChangelogEntry, void, undefined>;
|
|
173
|
+
/**
|
|
174
|
+
* One published entry (`GET /v1/changelog/{workspace}/entries/{slug}`).
|
|
175
|
+
* Throws a `SolutionDeskError` with status 404 for an unknown or unpublished entry.
|
|
176
|
+
*/
|
|
177
|
+
getEntry(slug: string, options?: RequestOptions): Promise<PublicChangelogEntryPayload>;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A client for the public changelog API — no key, no login; only published
|
|
181
|
+
* entries. Safe in the browser and on the server.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* const changelog = createChangelogClient({ workspace: "acme" });
|
|
185
|
+
* const { entries, nextCursor } = await changelog.list({ limit: 10 });
|
|
186
|
+
*/
|
|
187
|
+
declare function createChangelogClient(options: ChangelogClientOptions): ChangelogClient;
|
|
188
|
+
|
|
189
|
+
/** Where "last seen" is kept — `window.localStorage`, `sessionStorage` or your own. */
|
|
190
|
+
interface SeenStorage {
|
|
191
|
+
getItem(key: string): string | null;
|
|
192
|
+
setItem(key: string, value: string): void;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Remembers when a visitor last looked at the changelog, so you can show a
|
|
196
|
+
* "new" badge. It lives where you keep it (by default the browser's
|
|
197
|
+
* `localStorage`); nothing about the visitor is sent to SolutionDesk.
|
|
198
|
+
*/
|
|
199
|
+
interface SeenTracker {
|
|
200
|
+
/** ISO time of the newest entry the visitor has seen, or null before the first look. */
|
|
201
|
+
lastSeen(): string | null;
|
|
202
|
+
/** Remember that the visitor has seen everything up to `publishedAt`. */
|
|
203
|
+
markSeen(publishedAt: string): void;
|
|
204
|
+
/**
|
|
205
|
+
* How many of `entries` are new to the visitor. On the very first visit
|
|
206
|
+
* nothing counts as new — call {@link SeenTracker.markSeen} with the newest
|
|
207
|
+
* entry then, so the next entry is the first one that counts.
|
|
208
|
+
*/
|
|
209
|
+
unseen(entries: readonly {
|
|
210
|
+
publishedAt: string;
|
|
211
|
+
}[]): number;
|
|
212
|
+
}
|
|
213
|
+
/** The browser's `localStorage`, or null (server rendering, privacy mode). */
|
|
214
|
+
declare function browserStorage(): SeenStorage | null;
|
|
215
|
+
/**
|
|
216
|
+
* A {@link SeenTracker} for one workspace. Uses the key
|
|
217
|
+
* `sd-changelog-seen:<workspace>` — the same one the SolutionDesk widget uses,
|
|
218
|
+
* so a visitor who opened either sees no badge in the other.
|
|
219
|
+
*/
|
|
220
|
+
declare function createSeenTracker(workspace: string, storage?: SeenStorage | null): SeenTracker;
|
|
221
|
+
type ChangelogLocale = "de" | "en";
|
|
222
|
+
/** The category names the hosted page uses, for your own UI. */
|
|
223
|
+
declare const CATEGORY_LABELS: Record<ChangelogLocale, Record<ChangelogCategory, string>>;
|
|
224
|
+
/** The category colors of the hosted page and the dashboard (hex). */
|
|
225
|
+
declare const CATEGORY_COLORS: Record<ChangelogCategory, string>;
|
|
226
|
+
/** A category's name in German (`de*`) or English (everything else). */
|
|
227
|
+
declare function categoryLabel(category: ChangelogCategory, locale?: string): string;
|
|
228
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// ../sdk-core/src/http.ts
|
|
6
|
+
var DEFAULT_BASE_URL = "https://api.solutiondesk.io";
|
|
7
|
+
var SolutionDeskError = class extends Error {
|
|
8
|
+
constructor(message, init) {
|
|
9
|
+
super(message);
|
|
10
|
+
/** The HTTP status; `0` when no answer arrived (network error, aborted). */
|
|
11
|
+
__publicField(this, "status");
|
|
12
|
+
/** The parsed JSON body of the error answer (usually `{ error: string }`), else its text. */
|
|
13
|
+
__publicField(this, "body");
|
|
14
|
+
/** The requested URL. */
|
|
15
|
+
__publicField(this, "url");
|
|
16
|
+
/** Messages per input field on a `400` (e.g. `{ email: "Enter a valid email address." }`). */
|
|
17
|
+
__publicField(this, "fields");
|
|
18
|
+
this.name = "SolutionDeskError";
|
|
19
|
+
this.status = init.status;
|
|
20
|
+
this.body = init.body ?? null;
|
|
21
|
+
this.url = init.url;
|
|
22
|
+
this.fields = fieldsOf(init.body);
|
|
23
|
+
if (init.cause !== void 0) {
|
|
24
|
+
Object.defineProperty(this, "cause", { value: init.cause, enumerable: false });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** `404`: unknown workspace, module turned off or unknown item — on purpose indistinguishable. */
|
|
28
|
+
get isNotFound() {
|
|
29
|
+
return this.status === 404;
|
|
30
|
+
}
|
|
31
|
+
/** `429`: over the rate limit; try again later. */
|
|
32
|
+
get isRateLimited() {
|
|
33
|
+
return this.status === 429;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function fieldsOf(body) {
|
|
37
|
+
if (!body || typeof body !== "object") return {};
|
|
38
|
+
const fields = body.fields;
|
|
39
|
+
if (!fields || typeof fields !== "object") return {};
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
42
|
+
if (typeof value === "string") out[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function messageOf(body, status) {
|
|
47
|
+
if (body && typeof body === "object") {
|
|
48
|
+
const { error, message, statusCode } = body;
|
|
49
|
+
if (typeof statusCode === "number" && typeof message === "string" && message) return message;
|
|
50
|
+
if (typeof error === "string" && error) return error;
|
|
51
|
+
if (typeof message === "string" && message) return message;
|
|
52
|
+
if (Array.isArray(message) && typeof message[0] === "string") return message[0];
|
|
53
|
+
}
|
|
54
|
+
if (typeof body === "string" && body.trim() && body.length <= 300) return body.trim();
|
|
55
|
+
return `SolutionDesk answered ${status}.`;
|
|
56
|
+
}
|
|
57
|
+
function createMemoryEtagCache(maxEntries = 500) {
|
|
58
|
+
const entries = /* @__PURE__ */ new Map();
|
|
59
|
+
return {
|
|
60
|
+
get: (key) => entries.get(key),
|
|
61
|
+
set(key, value) {
|
|
62
|
+
entries.delete(key);
|
|
63
|
+
entries.set(key, value);
|
|
64
|
+
if (entries.size > maxEntries) {
|
|
65
|
+
const oldest = entries.keys().next().value;
|
|
66
|
+
if (oldest !== void 0) entries.delete(oldest);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function queryString(query) {
|
|
72
|
+
if (!query) return "";
|
|
73
|
+
const parts = [];
|
|
74
|
+
for (const [key, value] of Object.entries(query)) {
|
|
75
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
76
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
77
|
+
}
|
|
78
|
+
return parts.length ? `?${parts.join("&")}` : "";
|
|
79
|
+
}
|
|
80
|
+
var seg = (value) => encodeURIComponent(String(value));
|
|
81
|
+
function parseBody(text) {
|
|
82
|
+
if (!text) return null;
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(text);
|
|
85
|
+
} catch {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function createHttp(options = {}) {
|
|
90
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
91
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
92
|
+
const cache = options.etagCache === true ? createMemoryEtagCache() : options.etagCache ? options.etagCache : void 0;
|
|
93
|
+
const url = (path, query) => `${baseUrl}/v1${path}${queryString(query)}`;
|
|
94
|
+
async function requestWithMeta(req) {
|
|
95
|
+
if (!fetchImpl) {
|
|
96
|
+
throw new Error("No global fetch available: pass `fetch` to the client.");
|
|
97
|
+
}
|
|
98
|
+
const target = url(req.path, req.query);
|
|
99
|
+
const headers = {
|
|
100
|
+
accept: req.accept ?? "application/json",
|
|
101
|
+
...options.headers,
|
|
102
|
+
...req.options?.headers
|
|
103
|
+
};
|
|
104
|
+
if (req.body !== void 0) headers["content-type"] = "application/json";
|
|
105
|
+
if (req.apiKey) headers.authorization = `Bearer ${req.apiKey}`;
|
|
106
|
+
const cacheKey = req.method === "GET" && !req.apiKey ? target : void 0;
|
|
107
|
+
const cached = cacheKey && cache ? cache.get(cacheKey) : void 0;
|
|
108
|
+
if (cached && !Object.keys(headers).some((h) => h.toLowerCase() === "if-none-match")) {
|
|
109
|
+
headers["if-none-match"] = cached.etag;
|
|
110
|
+
}
|
|
111
|
+
const init = {
|
|
112
|
+
...req.options?.fetchOptions,
|
|
113
|
+
method: req.method,
|
|
114
|
+
headers
|
|
115
|
+
};
|
|
116
|
+
if (req.body !== void 0) init.body = JSON.stringify(req.body);
|
|
117
|
+
if (req.options?.signal !== void 0) init.signal = req.options.signal;
|
|
118
|
+
let response;
|
|
119
|
+
try {
|
|
120
|
+
response = await fetchImpl(target, init);
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
123
|
+
throw new SolutionDeskError(`The request to SolutionDesk failed: ${reason}`, {
|
|
124
|
+
status: 0,
|
|
125
|
+
url: target,
|
|
126
|
+
cause
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
const finalUrl = response.url;
|
|
130
|
+
const meta = {
|
|
131
|
+
status: response.status,
|
|
132
|
+
finalUrl: typeof finalUrl === "string" && finalUrl ? finalUrl : null
|
|
133
|
+
};
|
|
134
|
+
if (response.status === 304 && cached) {
|
|
135
|
+
return { data: cached.body, ...meta };
|
|
136
|
+
}
|
|
137
|
+
const text = await response.text();
|
|
138
|
+
const body = response.ok && req.responseType === "text" ? text : parseBody(text);
|
|
139
|
+
if (!response.ok) {
|
|
140
|
+
throw new SolutionDeskError(messageOf(body, response.status), {
|
|
141
|
+
status: response.status,
|
|
142
|
+
body,
|
|
143
|
+
url: target
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const etag = response.headers.get("etag");
|
|
147
|
+
if (cacheKey && cache && etag) cache.set(cacheKey, { etag, body });
|
|
148
|
+
return { data: body, ...meta };
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
baseUrl,
|
|
152
|
+
url,
|
|
153
|
+
requestWithMeta,
|
|
154
|
+
request: async (req) => (await requestWithMeta(req)).data
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function required(value, name) {
|
|
158
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
159
|
+
throw new TypeError(`SolutionDesk: \`${name}\` is required.`);
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/client.ts
|
|
165
|
+
function createChangelogClient(options) {
|
|
166
|
+
const workspace = required(options.workspace, "workspace");
|
|
167
|
+
const http = createHttp(options);
|
|
168
|
+
const base = `/changelog/${seg(workspace)}`;
|
|
169
|
+
const list = (params = {}, requestOptions) => http.request({
|
|
170
|
+
method: "GET",
|
|
171
|
+
path: base,
|
|
172
|
+
query: { category: params.category, limit: params.limit, cursor: params.cursor },
|
|
173
|
+
options: requestOptions
|
|
174
|
+
});
|
|
175
|
+
async function* allEntries(params = {}, requestOptions) {
|
|
176
|
+
let cursor = null;
|
|
177
|
+
do {
|
|
178
|
+
const page = await list(
|
|
179
|
+
{ ...params, limit: params.limit ?? 50, cursor },
|
|
180
|
+
requestOptions
|
|
181
|
+
);
|
|
182
|
+
yield* page.entries;
|
|
183
|
+
cursor = page.nextCursor;
|
|
184
|
+
} while (cursor);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
workspace,
|
|
188
|
+
list,
|
|
189
|
+
allEntries,
|
|
190
|
+
getEntry: (slug, requestOptions) => http.request({
|
|
191
|
+
method: "GET",
|
|
192
|
+
path: `${base}/entries/${seg(required(slug, "slug"))}`,
|
|
193
|
+
options: requestOptions
|
|
194
|
+
})
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/seen.ts
|
|
199
|
+
function browserStorage() {
|
|
200
|
+
try {
|
|
201
|
+
const storage = globalThis.localStorage;
|
|
202
|
+
return storage ?? null;
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function createSeenTracker(workspace, storage = browserStorage()) {
|
|
208
|
+
const key = `sd-changelog-seen:${workspace.toLowerCase()}`;
|
|
209
|
+
const lastSeen = () => {
|
|
210
|
+
try {
|
|
211
|
+
return storage?.getItem(key) ?? null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
lastSeen,
|
|
218
|
+
markSeen(publishedAt) {
|
|
219
|
+
try {
|
|
220
|
+
storage?.setItem(key, publishedAt);
|
|
221
|
+
} catch {
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
unseen(entries) {
|
|
225
|
+
const seen = lastSeen();
|
|
226
|
+
if (!seen) return 0;
|
|
227
|
+
return entries.filter((entry) => entry.publishedAt > seen).length;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
var CATEGORY_LABELS = {
|
|
232
|
+
en: { new: "New", improved: "Improved", fixed: "Fixed", announcement: "Announcement" },
|
|
233
|
+
de: { new: "Neu", improved: "Verbessert", fixed: "Behoben", announcement: "Ank\xFCndigung" }
|
|
234
|
+
};
|
|
235
|
+
var CATEGORY_COLORS = {
|
|
236
|
+
new: "#2563eb",
|
|
237
|
+
improved: "#7c3aed",
|
|
238
|
+
fixed: "#16a34a",
|
|
239
|
+
announcement: "#d97706"
|
|
240
|
+
};
|
|
241
|
+
function categoryLabel(category, locale = "en") {
|
|
242
|
+
return CATEGORY_LABELS[locale.toLowerCase().startsWith("de") ? "de" : "en"][category];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { CATEGORY_COLORS, CATEGORY_LABELS, DEFAULT_BASE_URL, SolutionDeskError, browserStorage, categoryLabel, createChangelogClient, createMemoryEtagCache, createSeenTracker };
|