@rwdocs/backstage-plugin-rw 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/api/RwClient.esm.js +65 -0
  2. package/dist/api/RwClient.esm.js.map +1 -1
  3. package/dist/components/CommentInboxList.esm.js +262 -0
  4. package/dist/components/CommentInboxList.esm.js.map +1 -0
  5. package/dist/components/CommentInboxPage.esm.js +10 -0
  6. package/dist/components/CommentInboxPage.esm.js.map +1 -0
  7. package/dist/components/RwDocsViewer.esm.js +9 -3
  8. package/dist/components/RwDocsViewer.esm.js.map +1 -1
  9. package/dist/components/RwEntityDocsViewer.esm.js +30 -9
  10. package/dist/components/RwEntityDocsViewer.esm.js.map +1 -1
  11. package/dist/components/constants.esm.js +5 -1
  12. package/dist/components/constants.esm.js.map +1 -1
  13. package/dist/components/inboxBuckets.esm.js +38 -0
  14. package/dist/components/inboxBuckets.esm.js.map +1 -0
  15. package/dist/components/useInboxData.esm.js +70 -0
  16. package/dist/components/useInboxData.esm.js.map +1 -0
  17. package/dist/components/useInboxFilters.esm.js +40 -0
  18. package/dist/components/useInboxFilters.esm.js.map +1 -0
  19. package/dist/components/useSectionRefResolver.esm.js +3 -4
  20. package/dist/components/useSectionRefResolver.esm.js.map +1 -1
  21. package/dist/hooks/useRwDocsIconLinkProps.esm.js +2 -1
  22. package/dist/hooks/useRwDocsIconLinkProps.esm.js.map +1 -1
  23. package/dist/index.d.ts +10 -81
  24. package/dist/plugin.esm.js +41 -3
  25. package/dist/plugin.esm.js.map +1 -1
  26. package/package.json +12 -9
  27. package/config.d.ts +0 -1
  28. package/dist/components/entityPath.esm.js +0 -10
  29. package/dist/components/entityPath.esm.js.map +0 -1
  30. package/dist/components/parseAnnotation.esm.js +0 -35
  31. package/dist/components/parseAnnotation.esm.js.map +0 -1
@@ -18,6 +18,71 @@ class RwClient {
18
18
  getFetch() {
19
19
  return this.fetchApi.fetch;
20
20
  }
21
+ async getCommentsEnabled() {
22
+ const base = await this.discoveryApi.getBaseUrl("rw");
23
+ const res = await this.fetchApi.fetch(`${base}/comments/config`);
24
+ if (!res.ok) return false;
25
+ const body = await res.json();
26
+ return Boolean(body.enabled);
27
+ }
28
+ async getCommentInbox(query = {}) {
29
+ const base = await this.discoveryApi.getBaseUrl("rw");
30
+ const params = new URLSearchParams();
31
+ if (query.cursor) params.set("cursor", query.cursor);
32
+ else {
33
+ if (query.filter) params.set("filter", query.filter);
34
+ if (query.sort) params.set("sort", query.sort);
35
+ }
36
+ if (query.limit) params.set("limit", String(query.limit));
37
+ const qs = params.toString();
38
+ const res = await this.fetchApi.fetch(`${base}/comments/inbox${qs ? `?${qs}` : ""}`);
39
+ if (!res.ok) throw new Error(`Inbox request failed: ${res.status}`);
40
+ return res.json();
41
+ }
42
+ createCommentClient(siteRef) {
43
+ const json = async (res) => {
44
+ if (!res.ok) throw new Error(`Comment request failed: ${res.status}`);
45
+ return res.json();
46
+ };
47
+ const baseUrl = () => this.discoveryApi.getBaseUrl("rw");
48
+ return {
49
+ list: async (documentId, opts) => {
50
+ const base = await baseUrl();
51
+ const q = new URLSearchParams({ siteRef, documentId });
52
+ return json(
53
+ await this.fetchApi.fetch(`${base}/comments?${q.toString()}`, { signal: opts?.signal })
54
+ );
55
+ },
56
+ create: async (input) => {
57
+ const base = await baseUrl();
58
+ return json(
59
+ await this.fetchApi.fetch(`${base}/comments`, {
60
+ method: "POST",
61
+ headers: { "Content-Type": "application/json" },
62
+ body: JSON.stringify({ siteRef, ...input })
63
+ })
64
+ );
65
+ },
66
+ update: async (id, input) => {
67
+ const base = await baseUrl();
68
+ return json(
69
+ await this.fetchApi.fetch(`${base}/comments/${encodeURIComponent(id)}`, {
70
+ method: "PATCH",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify(input)
73
+ })
74
+ );
75
+ },
76
+ delete: async (id) => {
77
+ const base = await baseUrl();
78
+ return json(
79
+ await this.fetchApi.fetch(`${base}/comments/${encodeURIComponent(id)}`, {
80
+ method: "DELETE"
81
+ })
82
+ );
83
+ }
84
+ };
85
+ }
21
86
  }
22
87
 
23
88
  export { RwClient, rwApiRef };
@@ -1 +1 @@
1
- {"version":3,"file":"RwClient.esm.js","sources":["../../src/api/RwClient.ts"],"sourcesContent":["import { createApiRef } from \"@backstage/core-plugin-api\";\nimport type { DiscoveryApi, FetchApi } from \"@backstage/core-plugin-api\";\n\nexport interface RwApi {\n getBaseUrl(): Promise<string>;\n getSiteBaseUrl(entityRef: string): Promise<string>;\n getFetch(): typeof fetch;\n}\n\nexport const rwApiRef = createApiRef<RwApi>({ id: \"plugin.rw.api\" });\n\nexport class RwClient implements RwApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi;\n }\n\n async getBaseUrl(): Promise<string> {\n return this.discoveryApi.getBaseUrl(\"rw\");\n }\n\n async getSiteBaseUrl(entityRef: string): Promise<string> {\n const base = await this.discoveryApi.getBaseUrl(\"rw\");\n return `${base}/site/${entityRef}`;\n }\n\n getFetch(): typeof fetch {\n return this.fetchApi.fetch;\n }\n}\n"],"names":[],"mappings":";;AASO,MAAM,QAAA,GAAW,YAAA,CAAoB,EAAE,EAAA,EAAI,iBAAiB;AAE5D,MAAM,QAAA,CAA0B;AAAA,EACpB,YAAA;AAAA,EACA,QAAA;AAAA,EAEjB,YAAY,OAAA,EAA6D;AACvE,IAAA,IAAA,CAAK,eAAe,OAAA,CAAQ,YAAA;AAC5B,IAAA,IAAA,CAAK,WAAW,OAAA,CAAQ,QAAA;AAAA,EAC1B;AAAA,EAEA,MAAM,UAAA,GAA8B;AAClC,IAAA,OAAO,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW,IAAI,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAM,eAAe,SAAA,EAAoC;AACvD,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,YAAA,CAAa,WAAW,IAAI,CAAA;AACpD,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,MAAA,EAAS,SAAS,CAAA,CAAA;AAAA,EAClC;AAAA,EAEA,QAAA,GAAyB;AACvB,IAAA,OAAO,KAAK,QAAA,CAAS,KAAA;AAAA,EACvB;AACF;;;;"}
1
+ {"version":3,"file":"RwClient.esm.js","sources":["../../src/api/RwClient.ts"],"sourcesContent":["import { createApiRef } from \"@backstage/core-plugin-api\";\nimport type { DiscoveryApi, FetchApi } from \"@backstage/core-plugin-api\";\nimport type { InboxQuery, InboxResponse } from \"@rwdocs/backstage-plugin-rw-common\";\nimport type {\n CommentApiClient,\n Comment,\n CreateCommentRequest,\n UpdateCommentRequest,\n} from \"@rwdocs/viewer\";\n\nexport type { InboxItem, InboxQuery, InboxResponse } from \"@rwdocs/backstage-plugin-rw-common\";\n\nexport interface RwApi {\n getBaseUrl(): Promise<string>;\n getSiteBaseUrl(entityRef: string): Promise<string>;\n getFetch(): typeof fetch;\n getCommentsEnabled(): Promise<boolean>;\n getCommentInbox(query?: InboxQuery): Promise<InboxResponse>;\n createCommentClient(siteRef: string): CommentApiClient;\n}\n\nexport const rwApiRef = createApiRef<RwApi>({ id: \"plugin.rw.api\" });\n\nexport class RwClient implements RwApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi;\n }\n\n async getBaseUrl(): Promise<string> {\n return this.discoveryApi.getBaseUrl(\"rw\");\n }\n\n async getSiteBaseUrl(entityRef: string): Promise<string> {\n const base = await this.discoveryApi.getBaseUrl(\"rw\");\n return `${base}/site/${entityRef}`;\n }\n\n getFetch(): typeof fetch {\n return this.fetchApi.fetch;\n }\n\n async getCommentsEnabled(): Promise<boolean> {\n const base = await this.discoveryApi.getBaseUrl(\"rw\");\n const res = await this.fetchApi.fetch(`${base}/comments/config`);\n if (!res.ok) return false;\n const body = await res.json();\n return Boolean(body.enabled);\n }\n\n async getCommentInbox(query: InboxQuery = {}): Promise<InboxResponse> {\n const base = await this.discoveryApi.getBaseUrl(\"rw\");\n const params = new URLSearchParams();\n if (query.cursor) params.set(\"cursor\", query.cursor);\n else {\n if (query.filter) params.set(\"filter\", query.filter);\n if (query.sort) params.set(\"sort\", query.sort);\n }\n if (query.limit) params.set(\"limit\", String(query.limit));\n const qs = params.toString();\n const res = await this.fetchApi.fetch(`${base}/comments/inbox${qs ? `?${qs}` : \"\"}`);\n if (!res.ok) throw new Error(`Inbox request failed: ${res.status}`);\n return res.json();\n }\n\n createCommentClient(siteRef: string): CommentApiClient {\n const json = async (res: Response) => {\n if (!res.ok) throw new Error(`Comment request failed: ${res.status}`);\n return res.json();\n };\n const baseUrl = () => this.discoveryApi.getBaseUrl(\"rw\");\n return {\n list: async (documentId: string, opts?: { signal?: AbortSignal }): Promise<Comment[]> => {\n const base = await baseUrl();\n const q = new URLSearchParams({ siteRef, documentId });\n return json(\n await this.fetchApi.fetch(`${base}/comments?${q.toString()}`, { signal: opts?.signal }),\n );\n },\n create: async (input: CreateCommentRequest): Promise<Comment> => {\n const base = await baseUrl();\n return json(\n await this.fetchApi.fetch(`${base}/comments`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ siteRef, ...input }),\n }),\n );\n },\n update: async (id: string, input: UpdateCommentRequest): Promise<Comment> => {\n const base = await baseUrl();\n return json(\n await this.fetchApi.fetch(`${base}/comments/${encodeURIComponent(id)}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(input),\n }),\n );\n },\n delete: async (id: string): Promise<Comment> => {\n const base = await baseUrl();\n return json(\n await this.fetchApi.fetch(`${base}/comments/${encodeURIComponent(id)}`, {\n method: \"DELETE\",\n }),\n );\n },\n };\n }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,262 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { memo, useRef, useEffect } from 'react';
3
+ import { useRouteRef } from '@backstage/core-plugin-api';
4
+ import { entityRouteRef, useEntityPresentation } from '@backstage/plugin-catalog-react';
5
+ import { parseEntityRef } from '@backstage/catalog-model';
6
+ import { buildCommentDeepLinkSuffix } from '@rwdocs/backstage-plugin-rw-common';
7
+ import { Text, Flex, Card, CardHeader, CardBody, ToggleButtonGroup, ToggleButton, Button } from '@backstage/ui';
8
+ import { Progress, ErrorPanel, EmptyState, Link } from '@backstage/core-components';
9
+ import { useInboxFilters } from './useInboxFilters.esm.js';
10
+ import { useInboxData } from './useInboxData.esm.js';
11
+ import { bucketByActivity } from './inboxBuckets.esm.js';
12
+
13
+ function relativeTime(iso) {
14
+ const diff = Date.now() - new Date(iso).getTime();
15
+ const minutes = Math.floor(diff / 6e4);
16
+ if (minutes < 1) return "just now";
17
+ if (minutes < 60) return `${minutes}m ago`;
18
+ const hours = Math.floor(minutes / 60);
19
+ if (hours < 24) return `${hours}h ago`;
20
+ const days = Math.floor(hours / 24);
21
+ if (days < 7) return `${days}d ago`;
22
+ if (days < 30) return `${Math.floor(days / 7)}w ago`;
23
+ if (days < 365) return `${Math.floor(days / 30)}mo ago`;
24
+ return `${Math.floor(days / 365)}y ago`;
25
+ }
26
+ function absoluteTime(iso) {
27
+ return new Date(iso).toLocaleString();
28
+ }
29
+ function replyState(replyCount) {
30
+ if (replyCount <= 0) return "No replies yet";
31
+ return `${replyCount} ${replyCount === 1 ? "reply" : "replies"}`;
32
+ }
33
+ function docTitlePlaceholder(viewerPath) {
34
+ const last = viewerPath.split("/").filter(Boolean).pop() ?? "";
35
+ const words = last.replace(/[-_]/g, " ").trim();
36
+ return words ? words.charAt(0).toUpperCase() + words.slice(1) : "Overview";
37
+ }
38
+ function Dot() {
39
+ return /* @__PURE__ */ jsx(Text, { as: "span", variant: "body-small", color: "secondary", children: "\xB7" });
40
+ }
41
+ function MetaLink({ to, label }) {
42
+ return /* @__PURE__ */ jsx(Link, { to, style: { color: "var(--bui-fg-secondary)", textDecorationColor: "currentColor" }, children: /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", children: label }) });
43
+ }
44
+ function InboxToolbar({
45
+ show,
46
+ sort,
47
+ allCount,
48
+ unansweredCount,
49
+ onShowChange,
50
+ onSortChange
51
+ }) {
52
+ const sortLabel = sort === "newest" ? "Sort by activity, newest first. Activates oldest first." : "Sort by activity, oldest first. Activates newest first.";
53
+ return /* @__PURE__ */ jsxs(Flex, { direction: "row", align: "center", justify: "between", pt: "1", pb: "3", children: [
54
+ /* @__PURE__ */ jsxs(
55
+ ToggleButtonGroup,
56
+ {
57
+ "aria-label": "Filter comments",
58
+ selectionMode: "single",
59
+ disallowEmptySelection: true,
60
+ selectedKeys: [show],
61
+ onSelectionChange: (keys) => {
62
+ const next = [...keys][0];
63
+ if (next === "all" || next === "unanswered") onShowChange(next);
64
+ },
65
+ children: [
66
+ /* @__PURE__ */ jsx(ToggleButton, { id: "all", children: `Open (${allCount})` }),
67
+ /* @__PURE__ */ jsx(ToggleButton, { id: "unanswered", children: `Unanswered (${unansweredCount})` })
68
+ ]
69
+ }
70
+ ),
71
+ /* @__PURE__ */ jsx(
72
+ Button,
73
+ {
74
+ variant: "tertiary",
75
+ size: "small",
76
+ "aria-label": sortLabel,
77
+ onPress: () => onSortChange(sort === "newest" ? "oldest" : "newest"),
78
+ children: `Activity ${sort === "newest" ? "\u2193" : "\u2191"}`
79
+ }
80
+ )
81
+ ] });
82
+ }
83
+ const CommentInboxRow = memo(function CommentInboxRow2({
84
+ item,
85
+ entityRoute,
86
+ isLast
87
+ }) {
88
+ const authorHref = entityRoute(parseEntityRef(item.author.id));
89
+ const authorName = useEntityPresentation(item.author.id).primaryTitle;
90
+ const entityTitle = useEntityPresentation(item.entityRef).primaryTitle;
91
+ const href = `${entityRoute(parseEntityRef(item.entityRef))}${buildCommentDeepLinkSuffix({ viewerPath: item.viewerPath, commentId: item.commentId })}`;
92
+ const needsReply = item.replyCount <= 0;
93
+ return /* @__PURE__ */ jsxs(
94
+ Flex,
95
+ {
96
+ direction: "row",
97
+ align: "start",
98
+ gap: "2",
99
+ py: "2",
100
+ style: {
101
+ // Only vertical padding here: horizontal insets come from CardBody, so
102
+ // the comment text and this divider sit on the card's content edge —
103
+ // flush with the bucket heading above, instead of 12px deeper.
104
+ // The divider separates rows, so the last row in a card omits it — its
105
+ // border would otherwise double up against the card's own bottom edge.
106
+ borderBottom: isLast ? void 0 : "1px solid var(--bui-border, rgba(0,0,0,0.08))"
107
+ },
108
+ children: [
109
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: "1", style: { flex: 1, minWidth: 0 }, children: [
110
+ /* @__PURE__ */ jsx(
111
+ Link,
112
+ {
113
+ to: href,
114
+ title: item.bodySnippet,
115
+ style: {
116
+ display: "block",
117
+ minWidth: 0,
118
+ color: "var(--bui-fg-primary)",
119
+ textDecorationColor: "currentColor"
120
+ },
121
+ children: /* @__PURE__ */ jsx(Text, { truncate: true, weight: "bold", style: { display: "block" }, children: item.bodySnippet })
122
+ }
123
+ ),
124
+ /* @__PURE__ */ jsxs(Flex, { direction: "row", align: "center", gap: "1", style: { flexWrap: "wrap" }, children: [
125
+ /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", children: item.pageTitle || docTitlePlaceholder(item.viewerPath) }),
126
+ /* @__PURE__ */ jsx(Dot, {}),
127
+ /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", children: entityTitle }),
128
+ /* @__PURE__ */ jsx(Dot, {}),
129
+ /* @__PURE__ */ jsx(MetaLink, { to: authorHref, label: authorName })
130
+ ] })
131
+ ] }),
132
+ /* @__PURE__ */ jsxs(
133
+ Flex,
134
+ {
135
+ direction: "column",
136
+ align: "end",
137
+ gap: "1",
138
+ style: { flexShrink: 0, minWidth: 88, textAlign: "right", whiteSpace: "nowrap" },
139
+ children: [
140
+ /* @__PURE__ */ jsx(Text, { variant: "body-small", color: needsReply ? "warning" : "secondary", children: replyState(item.replyCount) }),
141
+ /* @__PURE__ */ jsx(Text, { as: "span", variant: "body-small", color: "secondary", title: absoluteTime(item.updatedAt), children: relativeTime(item.updatedAt) })
142
+ ]
143
+ }
144
+ )
145
+ ]
146
+ }
147
+ );
148
+ });
149
+ const cardInset = { paddingInline: "var(--bui-space-5)" };
150
+ function InboxFooter({
151
+ shown,
152
+ total,
153
+ hasMore,
154
+ loadingMore,
155
+ onLoadMore
156
+ }) {
157
+ const sentinel = useRef(null);
158
+ useEffect(() => {
159
+ if (!hasMore) return void 0;
160
+ const el = sentinel.current;
161
+ if (!el) return void 0;
162
+ const io = new IntersectionObserver((entries) => {
163
+ if (entries.some((e) => e.isIntersecting)) onLoadMore();
164
+ });
165
+ io.observe(el);
166
+ return () => io.disconnect();
167
+ }, [hasMore, onLoadMore]);
168
+ return /* @__PURE__ */ jsxs(Flex, { direction: "column", align: "center", gap: "2", py: "4", children: [
169
+ /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", children: hasMore ? `Showing ${shown} of ${total}` : `All ${total} shown` }),
170
+ hasMore && /* @__PURE__ */ jsxs(Fragment, { children: [
171
+ /* @__PURE__ */ jsx("div", { ref: sentinel, "aria-hidden": true, style: { height: 1, width: "100%" } }),
172
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", onPress: onLoadMore, isDisabled: loadingMore, children: loadingMore ? "Loading\u2026" : "Load more" })
173
+ ] })
174
+ ] });
175
+ }
176
+ function CommentInboxList() {
177
+ const entityRoute = useRouteRef(entityRouteRef);
178
+ const { show, sort, setShow, setSort } = useInboxFilters();
179
+ const {
180
+ built,
181
+ items,
182
+ openCount,
183
+ unansweredCount,
184
+ hasMore,
185
+ loading,
186
+ hasLoaded,
187
+ loadingMore,
188
+ error,
189
+ loadMore
190
+ } = useInboxData({ show, sort });
191
+ if (loading && !hasLoaded) return /* @__PURE__ */ jsx(Progress, {});
192
+ if (error) return /* @__PURE__ */ jsx(ErrorPanel, { error });
193
+ if (!built) {
194
+ return /* @__PURE__ */ jsx(
195
+ EmptyState,
196
+ {
197
+ missing: "info",
198
+ title: "Attribution still building\u2026",
199
+ description: "Comment ownership is being computed. This page will populate once the first build completes."
200
+ }
201
+ );
202
+ }
203
+ if (openCount === 0) {
204
+ return /* @__PURE__ */ jsx(
205
+ EmptyState,
206
+ {
207
+ missing: "content",
208
+ title: "No open comments",
209
+ description: "There are no open comments on docs your teams own."
210
+ }
211
+ );
212
+ }
213
+ return (
214
+ // Centre the column to a reading measure. The filter is the page's lead
215
+ // element; each date bucket is its own card below it.
216
+ /* @__PURE__ */ jsxs("div", { style: { maxWidth: 900, marginInline: "auto" }, children: [
217
+ /* @__PURE__ */ jsx(
218
+ InboxToolbar,
219
+ {
220
+ show,
221
+ sort,
222
+ allCount: openCount,
223
+ unansweredCount,
224
+ onShowChange: setShow,
225
+ onSortChange: setSort
226
+ }
227
+ ),
228
+ items.length === 0 ? /* @__PURE__ */ jsx(Text, { variant: "body-small", color: "secondary", style: { padding: "8px 12px" }, children: "No unanswered threads \u2014 every open thread has at least one reply." }) : (
229
+ // Date.now() is read at render (not memoised): bucket boundaries are
230
+ // relative to "now", so a frozen value would mis-bucket threads as the
231
+ // page stays open. items is already filtered + sorted by the backend, and
232
+ // bucket order is derived from that item order (not the `sort` flag) so the
233
+ // headers can't flip ahead of the rows during a sort-change refetch.
234
+ /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "3", children: bucketByActivity(items, Date.now()).map((bucket) => /* @__PURE__ */ jsxs(Card, { children: [
235
+ /* @__PURE__ */ jsx(CardHeader, { style: cardInset, children: /* @__PURE__ */ jsx(Text, { as: "h2", variant: "title-small", weight: "bold", children: bucket.label }) }),
236
+ /* @__PURE__ */ jsx(CardBody, { style: cardInset, children: /* @__PURE__ */ jsx(Flex, { direction: "column", gap: "0", children: bucket.items.map((item, index) => /* @__PURE__ */ jsx(
237
+ CommentInboxRow,
238
+ {
239
+ item,
240
+ entityRoute,
241
+ isLast: index === bucket.items.length - 1
242
+ },
243
+ item.commentId
244
+ )) }) })
245
+ ] }, bucket.key)) })
246
+ ),
247
+ /* @__PURE__ */ jsx(
248
+ InboxFooter,
249
+ {
250
+ shown: items.length,
251
+ total: show === "unanswered" ? unansweredCount : openCount,
252
+ hasMore,
253
+ loadingMore,
254
+ onLoadMore: loadMore
255
+ }
256
+ )
257
+ ] })
258
+ );
259
+ }
260
+
261
+ export { CommentInboxList };
262
+ //# sourceMappingURL=CommentInboxList.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentInboxList.esm.js","sources":["../../src/components/CommentInboxList.tsx"],"sourcesContent":["import { memo, useEffect, useRef } from \"react\";\nimport { useRouteRef } from \"@backstage/core-plugin-api\";\nimport { entityRouteRef, useEntityPresentation } from \"@backstage/plugin-catalog-react\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { buildCommentDeepLinkSuffix } from \"@rwdocs/backstage-plugin-rw-common\";\nimport {\n Button,\n Card,\n CardBody,\n CardHeader,\n Flex,\n Text,\n ToggleButton,\n ToggleButtonGroup,\n} from \"@backstage/ui\";\nimport { EmptyState, ErrorPanel, Link, Progress } from \"@backstage/core-components\";\nimport type { InboxItem } from \"../api/RwClient\";\nimport type { ShowFilter, SortOrder } from \"./useInboxFilters\";\nimport { useInboxFilters } from \"./useInboxFilters\";\nimport { useInboxData } from \"./useInboxData\";\nimport { bucketByActivity } from \"./inboxBuckets\";\n\nfunction relativeTime(iso: string): string {\n const diff = Date.now() - new Date(iso).getTime();\n const minutes = Math.floor(diff / 60000);\n if (minutes < 1) return \"just now\";\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n if (days < 30) return `${Math.floor(days / 7)}w ago`;\n if (days < 365) return `${Math.floor(days / 30)}mo ago`;\n return `${Math.floor(days / 365)}y ago`;\n}\n\n/** Absolute local timestamp for the hover tooltip behind the relative time. */\nfunction absoluteTime(iso: string): string {\n return new Date(iso).toLocaleString();\n}\n\nfunction replyState(replyCount: number): string {\n if (replyCount <= 0) return \"No replies yet\";\n return `${replyCount} ${replyCount === 1 ? \"reply\" : \"replies\"}`;\n}\n\n/**\n * Fallback page title derived from the viewer path: humanize the last path\n * segment. Defensive fallback; the backend always populates pageTitle, so\n * this triggers only for malformed/pre-migration payloads.\n */\nfunction docTitlePlaceholder(viewerPath: string): string {\n const last = viewerPath.split(\"/\").filter(Boolean).pop() ?? \"\";\n const words = last.replace(/[-_]/g, \" \").trim();\n return words ? words.charAt(0).toUpperCase() + words.slice(1) : \"Overview\";\n}\n\nfunction Dot() {\n return (\n <Text as=\"span\" variant=\"body-small\" color=\"secondary\">\n ·\n </Text>\n );\n}\n\n/**\n * A muted link for the context line. The link colour matches the surrounding\n * secondary text so the hover underline isn't drawn in link-blue under grey.\n * Used for the author — the one navigable facet here (handy when you don't\n * recognise who left the comment); the doc title and entity are plain labels.\n */\nfunction MetaLink({ to, label }: { to: string; label: string }) {\n return (\n <Link to={to} style={{ color: \"var(--bui-fg-secondary)\", textDecorationColor: \"currentColor\" }}>\n <Text variant=\"body-small\" color=\"secondary\">\n {label}\n </Text>\n </Link>\n );\n}\n\ntype EntityRoute = (params: { kind: string; namespace: string; name: string }) => string;\n\n/**\n * Filter + sort controls for the inbox list. Two controls, each placed over the\n * column it acts on: a segmented filter on the left (over the prose) and a\n * click-to-flip sort on the right (over the recency rail). React-Aria-based, so\n * selection/press come through `onSelectionChange`/`onPress`, not `onClick`.\n */\nfunction InboxToolbar({\n show,\n sort,\n allCount,\n unansweredCount,\n onShowChange,\n onSortChange,\n}: {\n show: ShowFilter;\n sort: SortOrder;\n allCount: number;\n unansweredCount: number;\n onShowChange: (next: ShowFilter) => void;\n onSortChange: (next: SortOrder) => void;\n}) {\n const sortLabel =\n sort === \"newest\"\n ? \"Sort by activity, newest first. Activates oldest first.\"\n : \"Sort by activity, oldest first. Activates newest first.\";\n\n return (\n <Flex direction=\"row\" align=\"center\" justify=\"between\" pt=\"1\" pb=\"3\">\n <ToggleButtonGroup\n aria-label=\"Filter comments\"\n selectionMode=\"single\"\n disallowEmptySelection\n selectedKeys={[show]}\n onSelectionChange={(keys) => {\n // React-Aria's Key is untyped (string | number), so the ToggleButton\n // `id`s below are coupled to ShowFilter only by this guard: a new\n // segment must add its id here and to ShowFilter, or its clicks no-op.\n const next = [...keys][0];\n if (next === \"all\" || next === \"unanswered\") onShowChange(next);\n }}\n >\n {/* \"Open\" not \"All\": every thread here is open, so this segment is the\n full open set and its count doubles as the page's headline number.\n Counts are parenthesised so the number reads as a count of the label,\n and kept monochrome — the warning token stays on the per-row rail\n (\"No replies yet\"), where it's contextual and actionable, rather than\n shouting an aggregate magnitude from a nav control. */}\n <ToggleButton id=\"all\">{`Open (${allCount})`}</ToggleButton>\n <ToggleButton id=\"unanswered\">{`Unanswered (${unansweredCount})`}</ToggleButton>\n </ToggleButtonGroup>\n <Button\n variant=\"tertiary\"\n size=\"small\"\n aria-label={sortLabel}\n onPress={() => onSortChange(sort === \"newest\" ? \"oldest\" : \"newest\")}\n >\n {`Activity ${sort === \"newest\" ? \"↓\" : \"↑\"}`}\n </Button>\n </Flex>\n );\n}\n\n/**\n * One inbox row. Extracted from the list so the per-row `useEntityPresentation`\n * hooks can run (hooks can't be called inside a `.map` callback). The presentation\n * API resolves the author ref and the owning-entity ref to display names client-side\n * (live from the catalog: an entity's title, or \"namespace/name\" for a non-default\n * namespace; humanized for refs with no entity, e.g. a guest) — no backend call.\n *\n * Memoised: the list re-renders on every loadMore append, but each row's props\n * (item identity is stable per page, entityRoute and isLast unchanged) don't, so\n * prior rows skip re-rendering and re-running their useEntityPresentation hooks.\n */\nconst CommentInboxRow = memo(function CommentInboxRow({\n item,\n entityRoute,\n isLast,\n}: {\n item: InboxItem;\n entityRoute: EntityRoute;\n isLast: boolean;\n}) {\n const authorHref = entityRoute(parseEntityRef(item.author.id));\n const authorName = useEntityPresentation(item.author.id).primaryTitle;\n\n const entityTitle = useEntityPresentation(item.entityRef).primaryTitle;\n\n const href = `${entityRoute(parseEntityRef(item.entityRef))}${buildCommentDeepLinkSuffix({ viewerPath: item.viewerPath, commentId: item.commentId })}`;\n const needsReply = item.replyCount <= 0;\n\n return (\n <Flex\n direction=\"row\"\n align=\"start\"\n gap=\"2\"\n py=\"2\"\n style={{\n // Only vertical padding here: horizontal insets come from CardBody, so\n // the comment text and this divider sit on the card's content edge —\n // flush with the bucket heading above, instead of 12px deeper.\n // The divider separates rows, so the last row in a card omits it — its\n // border would otherwise double up against the card's own bottom edge.\n borderBottom: isLast ? undefined : \"1px solid var(--bui-border, rgba(0,0,0,0.08))\",\n }}\n >\n {/* Left block — grows. Line 1 is the comment (the hero you read); line 2\n is doc/entity/author context. */}\n <Flex direction=\"column\" gap=\"1\" style={{ flex: 1, minWidth: 0 }}>\n {/* Text `truncate` only clips when its own box has a width, so it needs\n display:block — inside the Link it would otherwise be inline (width 0)\n and overflow past the row into the right rail. */}\n <Link\n to={href}\n title={item.bodySnippet}\n // Match the link colour to the bold snippet text (primary fg) and draw\n // the hover underline in currentColor, so it's not the default link-blue\n // under black text — same colour-matching fix as MetaLink below.\n style={{\n display: \"block\",\n minWidth: 0,\n color: \"var(--bui-fg-primary)\",\n textDecorationColor: \"currentColor\",\n }}\n >\n <Text truncate weight=\"bold\" style={{ display: \"block\" }}>\n {item.bodySnippet}\n </Text>\n </Link>\n {/* One uniform run of muted metadata: doc · entity · author. Deliberately\n doc-first (not the broad→narrow breadcrumb order): the doc title is the\n unique, descriptive field, so leading with it makes each row's context\n line immediately distinct while scanning a date-sorted list. The entity\n (the broader container) follows as a category; the author, least\n decisive for triage, comes last.\n Only the author is a link: the snippet above already links into the\n doc, and the entity's catalog page isn't a triage destination, so doc\n and entity are plain labels. entityTitle/authorName come from\n useEntityPresentation, so the entity already shows its title (or\n \"namespace/name\" for a non-default namespace) and the author its\n display name. */}\n <Flex direction=\"row\" align=\"center\" gap=\"1\" style={{ flexWrap: \"wrap\" }}>\n <Text variant=\"body-small\" color=\"secondary\">\n {item.pageTitle || docTitlePlaceholder(item.viewerPath)}\n </Text>\n <Dot />\n <Text variant=\"body-small\" color=\"secondary\">\n {entityTitle}\n </Text>\n <Dot />\n <MetaLink to={authorHref} label={authorName} />\n </Flex>\n </Flex>\n {/* Right rail — fixed width, right-aligned: reply state over recency, so\n the eye scans recency straight down the right edge. */}\n <Flex\n direction=\"column\"\n align=\"end\"\n gap=\"1\"\n style={{ flexShrink: 0, minWidth: 88, textAlign: \"right\", whiteSpace: \"nowrap\" }}\n >\n <Text variant=\"body-small\" color={needsReply ? \"warning\" : \"secondary\"}>\n {replyState(item.replyCount)}\n </Text>\n <Text as=\"span\" variant=\"body-small\" color=\"secondary\" title={absoluteTime(item.updatedAt)}>\n {relativeTime(item.updatedAt)}\n </Text>\n </Flex>\n </Flex>\n );\n});\n\n/**\n * Widen the card's horizontal padding from BUI's default 12px (--bui-space-3),\n * which sits too tight against the card border at this reading width, to 20px.\n * Applied identically to the header and the body so the bucket heading and the\n * rows share one content edge. (Card/CardHeader/CardBody expose no padding prop,\n * so this overrides their class via inline style; vertical padding is untouched.)\n */\nconst cardInset = { paddingInline: \"var(--bui-space-5)\" } as const;\n\n/**\n * Pagination footer with an IntersectionObserver sentinel for auto-loading on\n * scroll, a \"Showing N of M\" / \"All N shown\" status line, and a \"Load more\"\n * button as the keyboard / no-JS affordance.\n */\nfunction InboxFooter({\n shown,\n total,\n hasMore,\n loadingMore,\n onLoadMore,\n}: {\n shown: number;\n total: number;\n hasMore: boolean;\n loadingMore: boolean;\n onLoadMore: () => void;\n}) {\n const sentinel = useRef<HTMLDivElement>(null);\n\n // Re-created whenever hasMore or onLoadMore changes so the observer always\n // holds the current callback (avoids a stale closure firing the wrong page's\n // loadMore). The 1px sentinel div stays in the DOM when !hasMore; the early\n // return just skips attaching the observer.\n useEffect(() => {\n if (!hasMore) return undefined;\n const el = sentinel.current;\n if (!el) return undefined;\n const io = new IntersectionObserver((entries) => {\n if (entries.some((e) => e.isIntersecting)) onLoadMore();\n });\n io.observe(el);\n return () => io.disconnect();\n }, [hasMore, onLoadMore]);\n\n return (\n <Flex direction=\"column\" align=\"center\" gap=\"2\" py=\"4\">\n <Text variant=\"body-small\" color=\"secondary\">\n {hasMore ? `Showing ${shown} of ${total}` : `All ${total} shown`}\n </Text>\n {hasMore && (\n <>\n <div ref={sentinel} aria-hidden style={{ height: 1, width: \"100%\" }} />\n <Button variant=\"secondary\" size=\"small\" onPress={onLoadMore} isDisabled={loadingMore}>\n {loadingMore ? \"Loading…\" : \"Load more\"}\n </Button>\n </>\n )}\n </Flex>\n );\n}\n\nexport function CommentInboxList() {\n const entityRoute = useRouteRef(entityRouteRef);\n const { show, sort, setShow, setSort } = useInboxFilters();\n const {\n built,\n items,\n openCount,\n unansweredCount,\n hasMore,\n loading,\n hasLoaded,\n loadingMore,\n error,\n loadMore,\n } = useInboxData({ show, sort });\n\n // Full-page spinner only on the very first load, when there's nothing to show.\n // A filter/sort change refetches with hasLoaded already true: keep the toolbar\n // and the previous results mounted while the new page loads (stale-while-\n // revalidate) so the page updates in place instead of blanking to a spinner.\n if (loading && !hasLoaded) return <Progress />;\n if (error) return <ErrorPanel error={error} />;\n if (!built) {\n return (\n <EmptyState\n missing=\"info\"\n title=\"Attribution still building…\"\n description=\"Comment ownership is being computed. This page will populate once the first build completes.\"\n />\n );\n }\n if (openCount === 0) {\n return (\n <EmptyState\n missing=\"content\"\n title=\"No open comments\"\n description=\"There are no open comments on docs your teams own.\"\n />\n );\n }\n\n return (\n // Centre the column to a reading measure. The filter is the page's lead\n // element; each date bucket is its own card below it.\n <div style={{ maxWidth: 900, marginInline: \"auto\" }}>\n <InboxToolbar\n show={show}\n sort={sort}\n allCount={openCount}\n unansweredCount={unansweredCount}\n onShowChange={setShow}\n onSortChange={setSort}\n />\n {items.length === 0 ? (\n <Text variant=\"body-small\" color=\"secondary\" style={{ padding: \"8px 12px\" }}>\n No unanswered threads — every open thread has at least one reply.\n </Text>\n ) : (\n // Date.now() is read at render (not memoised): bucket boundaries are\n // relative to \"now\", so a frozen value would mis-bucket threads as the\n // page stays open. items is already filtered + sorted by the backend, and\n // bucket order is derived from that item order (not the `sort` flag) so the\n // headers can't flip ahead of the rows during a sort-change refetch.\n <Flex direction=\"column\" gap=\"3\">\n {bucketByActivity(items, Date.now()).map((bucket) => (\n <Card key={bucket.key}>\n <CardHeader style={cardInset}>\n {/* The date period is the card's heading (h2 under the route's h1). */}\n <Text as=\"h2\" variant=\"title-small\" weight=\"bold\">\n {bucket.label}\n </Text>\n </CardHeader>\n <CardBody style={cardInset}>\n <Flex direction=\"column\" gap=\"0\">\n {bucket.items.map((item, index) => (\n <CommentInboxRow\n key={item.commentId}\n item={item}\n entityRoute={entityRoute}\n isLast={index === bucket.items.length - 1}\n />\n ))}\n </Flex>\n </CardBody>\n </Card>\n ))}\n </Flex>\n )}\n {/* Auto-load on scroll: the sentinel sits just below the last card; when it\n enters the viewport we fetch the next page. The button is the keyboard/no-JS\n fallback and the visible affordance. */}\n <InboxFooter\n shown={items.length}\n total={show === \"unanswered\" ? unansweredCount : openCount}\n hasMore={hasMore}\n loadingMore={loadingMore}\n onLoadMore={loadMore}\n />\n </div>\n );\n}\n"],"names":["CommentInboxRow"],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { Container } from '@backstage/ui';
3
+ import { CommentInboxList } from './CommentInboxList.esm.js';
4
+
5
+ function CommentInboxPage() {
6
+ return /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(CommentInboxList, {}) });
7
+ }
8
+
9
+ export { CommentInboxPage };
10
+ //# sourceMappingURL=CommentInboxPage.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentInboxPage.esm.js","sources":["../../src/components/CommentInboxPage.tsx"],"sourcesContent":["import { Container } from \"@backstage/ui\";\nimport { CommentInboxList } from \"./CommentInboxList\";\n\nexport function CommentInboxPage() {\n // No <Header>: this is the \"Comments\" tab body of the Docs page; the framework\n // PageLayout already renders the \"Docs\" header + tab strip for this route.\n // BUI's <Container> (page-level padding + max width) instead of core-components\n // <Page>/<Content>: the latter wrap the page in an inner overflow:auto pane that\n // pins the app-shell plugin header, whereas Container is a plain flow wrapper, so\n // — like the catalog — the page scrolls at the document level and the header\n // scrolls away with the content.\n return (\n <Container>\n <CommentInboxList />\n </Container>\n );\n}\n"],"names":[],"mappings":";;;;AAGO,SAAS,gBAAA,GAAmB;AAQjC,EAAA,uBACE,GAAA,CAAC,SAAA,EAAA,EACC,QAAA,kBAAA,GAAA,CAAC,gBAAA,EAAA,EAAiB,CAAA,EACpB,CAAA;AAEJ;;;;"}
@@ -9,7 +9,12 @@ import { useSectionRefResolver } from './useSectionRefResolver.esm.js';
9
9
  import { mountRw } from '@rwdocs/viewer';
10
10
  import '@rwdocs/viewer/embed.css';
11
11
 
12
- function RwDocsViewer({ apiBaseUrl, sectionRef, sourceEntityRef }) {
12
+ function RwDocsViewer({
13
+ apiBaseUrl,
14
+ sectionRef,
15
+ sourceEntityRef,
16
+ comments
17
+ }) {
13
18
  const ref = useRef(null);
14
19
  const rwApi = useApi(rwApiRef);
15
20
  const theme = useTheme();
@@ -61,7 +66,8 @@ function RwDocsViewer({ apiBaseUrl, sectionRef, sourceEntityRef }) {
61
66
  rwNavigatingRef.current = true;
62
67
  navigateRef.current(href, { replace: false });
63
68
  }
64
- }
69
+ },
70
+ ...comments ? { comments } : {}
65
71
  });
66
72
  } catch (err) {
67
73
  setError(err instanceof Error ? err : new Error(String(err)));
@@ -70,7 +76,7 @@ function RwDocsViewer({ apiBaseUrl, sectionRef, sourceEntityRef }) {
70
76
  instanceRef.current?.destroy();
71
77
  instanceRef.current = null;
72
78
  };
73
- }, [apiBaseUrl, sectionRef]);
79
+ }, [apiBaseUrl, sectionRef, comments]);
74
80
  useEffect(() => {
75
81
  instanceRef.current?.setColorScheme(theme.palette.type);
76
82
  }, [theme.palette.type]);
@@ -1 +1 @@
1
- {"version":3,"file":"RwDocsViewer.esm.js","sources":["../../src/components/RwDocsViewer.tsx"],"sourcesContent":["import { useRef, useEffect, useState, useCallback } from \"react\";\nimport { useApi } from \"@backstage/core-plugin-api\";\nimport { ErrorPanel } from \"@backstage/core-components\";\nimport { useTheme } from \"@material-ui/core/styles\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { rwApiRef } from \"../api/RwClient\";\nimport { useSectionRefResolver } from \"./useSectionRefResolver\";\nimport { mountRw } from \"@rwdocs/viewer\";\nimport type { RwInstance } from \"@rwdocs/viewer\";\nimport \"@rwdocs/viewer/embed.css\";\n\ninterface RwDocsViewerProps {\n apiBaseUrl: string;\n sectionRef: string;\n sourceEntityRef: string;\n}\n\nexport function RwDocsViewer({ apiBaseUrl, sectionRef, sourceEntityRef }: RwDocsViewerProps) {\n const ref = useRef<HTMLDivElement>(null);\n const rwApi = useApi(rwApiRef);\n const theme = useTheme();\n const [error, setError] = useState<Error | null>(null);\n const catalogResolver = useSectionRefResolver(sourceEntityRef);\n\n const location = useLocation();\n const navigate = useNavigate();\n const navigateRef = useRef(navigate);\n navigateRef.current = navigate;\n const { \"*\": subPath = \"\" } = useParams();\n\n const basePath = subPath ? location.pathname.slice(0, -(subPath.length + 1)) : location.pathname;\n const basePathRef = useRef(basePath);\n basePathRef.current = basePath;\n\n const resolveSectionRefs = useCallback(\n async (refs: string[]): Promise<Record<string, string>> => {\n const otherRefs = refs.filter((r) => r !== sectionRef);\n const result = otherRefs.length > 0 ? await catalogResolver(otherRefs) : {};\n if (refs.includes(sectionRef)) {\n result[sectionRef] = basePathRef.current;\n }\n return result;\n },\n [catalogResolver, sectionRef],\n );\n\n const instanceRef = useRef<RwInstance | null>(null);\n const prevSubPathRef = useRef(subPath);\n const rwNavigatingRef = useRef(false);\n\n useEffect(() => {\n if (!ref.current) {\n return undefined;\n }\n\n try {\n let initialPath = \"/\";\n if (subPath) {\n initialPath = `/${subPath}`;\n }\n if (location.hash) {\n initialPath += location.hash;\n }\n\n instanceRef.current = mountRw(ref.current, {\n apiBaseUrl,\n initialPath,\n sectionRef,\n fetchFn: rwApi.getFetch(),\n colorScheme: theme.palette.type,\n resolveSectionRefs,\n onNavigate: (href: string) => {\n if (window.location.pathname !== href) {\n rwNavigatingRef.current = true;\n navigateRef.current(href, { replace: false });\n }\n },\n });\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiBaseUrl, sectionRef]);\n\n useEffect(() => {\n instanceRef.current?.setColorScheme(theme.palette.type);\n }, [theme.palette.type]);\n\n useEffect(() => {\n if (subPath === prevSubPathRef.current) return;\n prevSubPathRef.current = subPath;\n\n if (rwNavigatingRef.current) {\n rwNavigatingRef.current = false;\n return;\n }\n\n const rwPath = subPath ? `/${subPath}` : \"/\";\n instanceRef.current?.navigateTo(rwPath);\n }, [subPath]);\n\n if (error) {\n return <ErrorPanel error={error} />;\n }\n\n return <div ref={ref} className=\"rw-root\" />;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RwDocsViewer.esm.js","sources":["../../src/components/RwDocsViewer.tsx"],"sourcesContent":["import { useRef, useEffect, useState, useCallback } from \"react\";\nimport { useApi } from \"@backstage/core-plugin-api\";\nimport { ErrorPanel } from \"@backstage/core-components\";\nimport { useTheme } from \"@material-ui/core/styles\";\nimport { useLocation, useNavigate, useParams } from \"react-router-dom\";\nimport { rwApiRef } from \"../api/RwClient\";\nimport { useSectionRefResolver } from \"./useSectionRefResolver\";\nimport { mountRw } from \"@rwdocs/viewer\";\nimport type { RwInstance, CommentApiClient } from \"@rwdocs/viewer\";\nimport \"@rwdocs/viewer/embed.css\";\n\ninterface RwDocsViewerProps {\n apiBaseUrl: string;\n sectionRef: string;\n sourceEntityRef: string;\n comments?: CommentApiClient;\n}\n\nexport function RwDocsViewer({\n apiBaseUrl,\n sectionRef,\n sourceEntityRef,\n comments,\n}: RwDocsViewerProps) {\n const ref = useRef<HTMLDivElement>(null);\n const rwApi = useApi(rwApiRef);\n const theme = useTheme();\n const [error, setError] = useState<Error | null>(null);\n const catalogResolver = useSectionRefResolver(sourceEntityRef);\n\n const location = useLocation();\n const navigate = useNavigate();\n const navigateRef = useRef(navigate);\n navigateRef.current = navigate;\n const { \"*\": subPath = \"\" } = useParams();\n\n const basePath = subPath ? location.pathname.slice(0, -(subPath.length + 1)) : location.pathname;\n const basePathRef = useRef(basePath);\n basePathRef.current = basePath;\n\n const resolveSectionRefs = useCallback(\n async (refs: string[]): Promise<Record<string, string>> => {\n const otherRefs = refs.filter((r) => r !== sectionRef);\n const result = otherRefs.length > 0 ? await catalogResolver(otherRefs) : {};\n if (refs.includes(sectionRef)) {\n result[sectionRef] = basePathRef.current;\n }\n return result;\n },\n [catalogResolver, sectionRef],\n );\n\n const instanceRef = useRef<RwInstance | null>(null);\n const prevSubPathRef = useRef(subPath);\n const rwNavigatingRef = useRef(false);\n\n useEffect(() => {\n if (!ref.current) {\n return undefined;\n }\n\n try {\n let initialPath = \"/\";\n if (subPath) {\n initialPath = `/${subPath}`;\n }\n if (location.hash) {\n initialPath += location.hash;\n }\n\n instanceRef.current = mountRw(ref.current, {\n apiBaseUrl,\n initialPath,\n sectionRef,\n fetchFn: rwApi.getFetch(),\n colorScheme: theme.palette.type,\n resolveSectionRefs,\n onNavigate: (href: string) => {\n if (window.location.pathname !== href) {\n rwNavigatingRef.current = true;\n navigateRef.current(href, { replace: false });\n }\n },\n ...(comments ? { comments } : {}),\n });\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n }\n\n return () => {\n instanceRef.current?.destroy();\n instanceRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [apiBaseUrl, sectionRef, comments]);\n\n useEffect(() => {\n instanceRef.current?.setColorScheme(theme.palette.type);\n }, [theme.palette.type]);\n\n useEffect(() => {\n if (subPath === prevSubPathRef.current) return;\n prevSubPathRef.current = subPath;\n\n if (rwNavigatingRef.current) {\n rwNavigatingRef.current = false;\n return;\n }\n\n const rwPath = subPath ? `/${subPath}` : \"/\";\n instanceRef.current?.navigateTo(rwPath);\n }, [subPath]);\n\n if (error) {\n return <ErrorPanel error={error} />;\n }\n\n return <div ref={ref} className=\"rw-root\" />;\n}\n"],"names":[],"mappings":"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"}
@@ -4,10 +4,9 @@ import { useApi } from '@backstage/core-plugin-api';
4
4
  import { useEntity } from '@backstage/plugin-catalog-react';
5
5
  import { getCompoundEntityRef } from '@backstage/catalog-model';
6
6
  import { ErrorPanel, Progress } from '@backstage/core-components';
7
+ import { toEntityPath, parseAnnotation } from '@rwdocs/backstage-plugin-rw-common';
7
8
  import { rwApiRef } from '../api/RwClient.esm.js';
8
- import { toEntityPath } from './entityPath.esm.js';
9
9
  import { ANNOTATION_KEY } from './constants.esm.js';
10
- import { parseAnnotation } from './parseAnnotation.esm.js';
11
10
  import { RwDocsViewer } from './RwDocsViewer.esm.js';
12
11
 
13
12
  function RwEntityDocsViewer() {
@@ -15,17 +14,38 @@ function RwEntityDocsViewer() {
15
14
  const rwApi = useApi(rwApiRef);
16
15
  const [apiBaseUrl, setApiBaseUrl] = useState(null);
17
16
  const [fetchError, setFetchError] = useState(null);
17
+ const [commentClient, setCommentClient] = useState(void 0);
18
+ const [commentsReady, setCommentsReady] = useState(false);
18
19
  const annotationValue = entity.metadata.annotations?.[ANNOTATION_KEY];
19
20
  const selfEntityRef = useMemo(() => toEntityPath(getCompoundEntityRef(entity)), [entity]);
20
21
  const parsed = parseAnnotation(annotationValue, selfEntityRef);
21
22
  useEffect(() => {
22
23
  if (!parsed) return void 0;
24
+ setApiBaseUrl(null);
25
+ setFetchError(null);
26
+ setCommentsReady(false);
27
+ setCommentClient(void 0);
23
28
  let cancelled = false;
24
- rwApi.getSiteBaseUrl(parsed.entityPath).then((url) => {
25
- if (!cancelled) setApiBaseUrl(url);
26
- }).catch((err) => {
27
- if (!cancelled) setFetchError(err);
28
- });
29
+ (async () => {
30
+ try {
31
+ const url = await rwApi.getSiteBaseUrl(parsed.entityPath);
32
+ if (cancelled) return;
33
+ setApiBaseUrl(url);
34
+ } catch (err) {
35
+ if (!cancelled) setFetchError(err instanceof Error ? err : new Error(String(err)));
36
+ return;
37
+ }
38
+ try {
39
+ const enabled = await rwApi.getCommentsEnabled();
40
+ if (cancelled) return;
41
+ setCommentClient(enabled ? rwApi.createCommentClient(parsed.entityRef) : void 0);
42
+ } catch (err) {
43
+ if (cancelled) return;
44
+ console.warn("rw: comments-enabled probe failed; comments disabled for this view", err);
45
+ setCommentClient(void 0);
46
+ }
47
+ if (!cancelled) setCommentsReady(true);
48
+ })();
29
49
  return () => {
30
50
  cancelled = true;
31
51
  };
@@ -36,7 +56,7 @@ function RwEntityDocsViewer() {
36
56
  if (fetchError) {
37
57
  return /* @__PURE__ */ jsx(ErrorPanel, { error: fetchError });
38
58
  }
39
- if (!apiBaseUrl) {
59
+ if (!apiBaseUrl || !commentsReady) {
40
60
  return /* @__PURE__ */ jsx(Progress, {});
41
61
  }
42
62
  const sectionRef = parsed.sectionRef ?? selfEntityRef;
@@ -45,7 +65,8 @@ function RwEntityDocsViewer() {
45
65
  {
46
66
  apiBaseUrl,
47
67
  sectionRef,
48
- sourceEntityRef: parsed.entityRef
68
+ sourceEntityRef: parsed.entityRef,
69
+ comments: commentClient
49
70
  }
50
71
  );
51
72
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RwEntityDocsViewer.esm.js","sources":["../../src/components/RwEntityDocsViewer.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { useApi } from \"@backstage/core-plugin-api\";\nimport { useEntity } from \"@backstage/plugin-catalog-react\";\nimport { getCompoundEntityRef } from \"@backstage/catalog-model\";\nimport { ErrorPanel, Progress } from \"@backstage/core-components\";\nimport { rwApiRef } from \"../api/RwClient\";\nimport { toEntityPath } from \"./entityPath\";\nimport { ANNOTATION_KEY } from \"./constants\";\nimport { parseAnnotation } from \"./parseAnnotation\";\nimport { RwDocsViewer } from \"./RwDocsViewer\";\n\nexport function RwEntityDocsViewer() {\n const { entity } = useEntity();\n const rwApi = useApi(rwApiRef);\n const [apiBaseUrl, setApiBaseUrl] = useState<string | null>(null);\n const [fetchError, setFetchError] = useState<Error | null>(null);\n\n const annotationValue = entity.metadata.annotations?.[ANNOTATION_KEY];\n const selfEntityRef = useMemo(() => toEntityPath(getCompoundEntityRef(entity)), [entity]);\n const parsed = parseAnnotation(annotationValue, selfEntityRef);\n\n useEffect(() => {\n if (!parsed) return undefined;\n\n let cancelled = false;\n rwApi\n .getSiteBaseUrl(parsed.entityPath)\n .then((url) => {\n if (!cancelled) setApiBaseUrl(url);\n })\n .catch((err) => {\n if (!cancelled) setFetchError(err);\n });\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- parsed is derived from annotationValue+selfEntityRef; using entityPath avoids object-identity churn\n }, [rwApi, parsed?.entityPath]);\n\n if (!parsed) {\n return <ErrorPanel error={new Error(`Entity is missing the \"${ANNOTATION_KEY}\" annotation`)} />;\n }\n\n if (fetchError) {\n return <ErrorPanel error={fetchError} />;\n }\n\n if (!apiBaseUrl) {\n return <Progress />;\n }\n\n const sectionRef = parsed.sectionRef ?? selfEntityRef;\n return (\n <RwDocsViewer\n apiBaseUrl={apiBaseUrl}\n sectionRef={sectionRef}\n sourceEntityRef={parsed.entityRef}\n />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAWO,SAAS,kBAAA,GAAqB;AACnC,EAAA,MAAM,EAAE,MAAA,EAAO,GAAI,SAAA,EAAU;AAC7B,EAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAwB,IAAI,CAAA;AAChE,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAuB,IAAI,CAAA;AAE/D,EAAA,MAAM,eAAA,GAAkB,MAAA,CAAO,QAAA,CAAS,WAAA,GAAc,cAAc,CAAA;AACpE,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,MAAM,YAAA,CAAa,oBAAA,CAAqB,MAAM,CAAC,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AACxF,EAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,eAAA,EAAiB,aAAa,CAAA;AAE7D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,QAAQ,OAAO,MAAA;AAEpB,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,KAAA,CACG,eAAe,MAAA,CAAO,UAAU,CAAA,CAChC,IAAA,CAAK,CAAC,GAAA,KAAQ;AACb,MAAA,IAAI,CAAC,SAAA,EAAW,aAAA,CAAc,GAAG,CAAA;AAAA,IACnC,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACd,MAAA,IAAI,CAAC,SAAA,EAAW,aAAA,CAAc,GAAG,CAAA;AAAA,IACnC,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EAEF,CAAA,EAAG,CAAC,KAAA,EAAO,MAAA,EAAQ,UAAU,CAAC,CAAA;AAE9B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,uBAAO,GAAA,CAAC,cAAW,KAAA,EAAO,IAAI,MAAM,CAAA,uBAAA,EAA0B,cAAc,cAAc,CAAA,EAAG,CAAA;AAAA,EAC/F;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,uBAAO,GAAA,CAAC,UAAA,EAAA,EAAW,KAAA,EAAO,UAAA,EAAY,CAAA;AAAA,EACxC;AAEA,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,2BAAQ,QAAA,EAAA,EAAS,CAAA;AAAA,EACnB;AAEA,EAAA,MAAM,UAAA,GAAa,OAAO,UAAA,IAAc,aAAA;AACxC,EAAA,uBACE,GAAA;AAAA,IAAC,YAAA;AAAA,IAAA;AAAA,MACC,UAAA;AAAA,MACA,UAAA;AAAA,MACA,iBAAiB,MAAA,CAAO;AAAA;AAAA,GAC1B;AAEJ;;;;"}
1
+ {"version":3,"file":"RwEntityDocsViewer.esm.js","sources":["../../src/components/RwEntityDocsViewer.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from \"react\";\nimport { useApi } from \"@backstage/core-plugin-api\";\nimport { useEntity } from \"@backstage/plugin-catalog-react\";\nimport { getCompoundEntityRef } from \"@backstage/catalog-model\";\nimport { ErrorPanel, Progress } from \"@backstage/core-components\";\nimport { toEntityPath, parseAnnotation } from \"@rwdocs/backstage-plugin-rw-common\";\nimport { rwApiRef } from \"../api/RwClient\";\nimport { ANNOTATION_KEY } from \"./constants\";\nimport { RwDocsViewer } from \"./RwDocsViewer\";\nimport type { CommentApiClient } from \"@rwdocs/viewer\";\n\nexport function RwEntityDocsViewer() {\n const { entity } = useEntity();\n const rwApi = useApi(rwApiRef);\n const [apiBaseUrl, setApiBaseUrl] = useState<string | null>(null);\n const [fetchError, setFetchError] = useState<Error | null>(null);\n const [commentClient, setCommentClient] = useState<CommentApiClient | undefined>(undefined);\n // Two gates before the viewer mounts: apiBaseUrl resolves first, then we wait\n // for the comments-enabled check so the viewer never mounts and immediately\n // remounts with a different comments prop.\n const [commentsReady, setCommentsReady] = useState(false);\n\n const annotationValue = entity.metadata.annotations?.[ANNOTATION_KEY];\n const selfEntityRef = useMemo(() => toEntityPath(getCompoundEntityRef(entity)), [entity]);\n const parsed = parseAnnotation(annotationValue, selfEntityRef);\n\n useEffect(() => {\n if (!parsed) return undefined;\n\n // Reset gate immediately so the viewer unmounts while we fetch the new entity's data.\n setApiBaseUrl(null);\n setFetchError(null);\n setCommentsReady(false);\n setCommentClient(undefined);\n\n let cancelled = false;\n (async () => {\n try {\n const url = await rwApi.getSiteBaseUrl(parsed.entityPath);\n if (cancelled) return;\n setApiBaseUrl(url);\n } catch (err) {\n if (!cancelled) setFetchError(err instanceof Error ? err : new Error(String(err)));\n return;\n }\n\n try {\n const enabled = await rwApi.getCommentsEnabled();\n if (cancelled) return;\n setCommentClient(enabled ? rwApi.createCommentClient(parsed.entityRef) : undefined);\n } catch (err) {\n if (cancelled) return;\n // eslint-disable-next-line no-console\n console.warn(\"rw: comments-enabled probe failed; comments disabled for this view\", err);\n setCommentClient(undefined);\n }\n\n if (!cancelled) setCommentsReady(true);\n })();\n return () => {\n cancelled = true;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps -- parsed is derived from annotationValue+selfEntityRef; using entityPath avoids object-identity churn\n }, [rwApi, parsed?.entityPath]);\n\n if (!parsed) {\n return <ErrorPanel error={new Error(`Entity is missing the \"${ANNOTATION_KEY}\" annotation`)} />;\n }\n\n if (fetchError) {\n return <ErrorPanel error={fetchError} />;\n }\n\n if (!apiBaseUrl || !commentsReady) {\n return <Progress />;\n }\n\n const sectionRef = parsed.sectionRef ?? selfEntityRef;\n return (\n <RwDocsViewer\n apiBaseUrl={apiBaseUrl}\n sectionRef={sectionRef}\n sourceEntityRef={parsed.entityRef}\n comments={commentClient}\n />\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;AAWO,SAAS,kBAAA,GAAqB;AACnC,EAAA,MAAM,EAAE,MAAA,EAAO,GAAI,SAAA,EAAU;AAC7B,EAAA,MAAM,KAAA,GAAQ,OAAO,QAAQ,CAAA;AAC7B,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAwB,IAAI,CAAA;AAChE,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAuB,IAAI,CAAA;AAC/D,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAI,SAAuC,MAAS,CAAA;AAI1F,EAAA,MAAM,CAAC,aAAA,EAAe,gBAAgB,CAAA,GAAI,SAAS,KAAK,CAAA;AAExD,EAAA,MAAM,eAAA,GAAkB,MAAA,CAAO,QAAA,CAAS,WAAA,GAAc,cAAc,CAAA;AACpE,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,MAAM,YAAA,CAAa,oBAAA,CAAqB,MAAM,CAAC,CAAA,EAAG,CAAC,MAAM,CAAC,CAAA;AACxF,EAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,eAAA,EAAiB,aAAa,CAAA;AAE7D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,QAAQ,OAAO,MAAA;AAGpB,IAAA,aAAA,CAAc,IAAI,CAAA;AAClB,IAAA,aAAA,CAAc,IAAI,CAAA;AAClB,IAAA,gBAAA,CAAiB,KAAK,CAAA;AACtB,IAAA,gBAAA,CAAiB,MAAS,CAAA;AAE1B,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,CAAC,YAAY;AACX,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,cAAA,CAAe,OAAO,UAAU,CAAA;AACxD,QAAA,IAAI,SAAA,EAAW;AACf,QAAA,aAAA,CAAc,GAAG,CAAA;AAAA,MACnB,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,CAAC,SAAA,EAAW,aAAA,CAAc,GAAA,YAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AACjF,QAAA;AAAA,MACF;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,kBAAA,EAAmB;AAC/C,QAAA,IAAI,SAAA,EAAW;AACf,QAAA,gBAAA,CAAiB,UAAU,KAAA,CAAM,mBAAA,CAAoB,MAAA,CAAO,SAAS,IAAI,KAAA,CAAS,CAAA;AAAA,MACpF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,SAAA,EAAW;AAEf,QAAA,OAAA,CAAQ,IAAA,CAAK,sEAAsE,GAAG,CAAA;AACtF,QAAA,gBAAA,CAAiB,MAAS,CAAA;AAAA,MAC5B;AAEA,MAAA,IAAI,CAAC,SAAA,EAAW,gBAAA,CAAiB,IAAI,CAAA;AAAA,IACvC,CAAA,GAAG;AACH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EAEF,CAAA,EAAG,CAAC,KAAA,EAAO,MAAA,EAAQ,UAAU,CAAC,CAAA;AAE9B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,uBAAO,GAAA,CAAC,cAAW,KAAA,EAAO,IAAI,MAAM,CAAA,uBAAA,EAA0B,cAAc,cAAc,CAAA,EAAG,CAAA;AAAA,EAC/F;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,uBAAO,GAAA,CAAC,UAAA,EAAA,EAAW,KAAA,EAAO,UAAA,EAAY,CAAA;AAAA,EACxC;AAEA,EAAA,IAAI,CAAC,UAAA,IAAc,CAAC,aAAA,EAAe;AACjC,IAAA,2BAAQ,QAAA,EAAA,EAAS,CAAA;AAAA,EACnB;AAEA,EAAA,MAAM,UAAA,GAAa,OAAO,UAAA,IAAc,aAAA;AACxC,EAAA,uBACE,GAAA;AAAA,IAAC,YAAA;AAAA,IAAA;AAAA,MACC,UAAA;AAAA,MACA,UAAA;AAAA,MACA,iBAAiB,MAAA,CAAO,SAAA;AAAA,MACxB,QAAA,EAAU;AAAA;AAAA,GACZ;AAEJ;;;;"}
@@ -1,5 +1,9 @@
1
1
  const ANNOTATION_KEY = "rwdocs.org/ref";
2
2
  const ROOT_SECTION_REF = "section:default/root";
3
+ const DOCS_PATH_SUFFIX = "/docs";
4
+ function entityDocsPath(entityRoute, ref) {
5
+ return entityRoute(ref) + DOCS_PATH_SUFFIX;
6
+ }
3
7
 
4
- export { ANNOTATION_KEY, ROOT_SECTION_REF };
8
+ export { ANNOTATION_KEY, DOCS_PATH_SUFFIX, ROOT_SECTION_REF, entityDocsPath };
5
9
  //# sourceMappingURL=constants.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.esm.js","sources":["../../src/components/constants.ts"],"sourcesContent":["export const ANNOTATION_KEY = \"rwdocs.org/ref\";\nexport const ROOT_SECTION_REF = \"section:default/root\";\n"],"names":[],"mappings":"AAAO,MAAM,cAAA,GAAiB;AACvB,MAAM,gBAAA,GAAmB;;;;"}
1
+ {"version":3,"file":"constants.esm.js","sources":["../../src/components/constants.ts"],"sourcesContent":["export const ANNOTATION_KEY = \"rwdocs.org/ref\";\nexport const ROOT_SECTION_REF = \"section:default/root\";\n\n/** The entity content-tab path segment that mounts the RW docs viewer (the\n * `rwEntityContent` EntityContentBlueprint's `path: \"docs\"`). */\nexport const DOCS_PATH_SUFFIX = \"/docs\";\n\n/** Base URL of an entity's RW docs tab: its catalog route + DOCS_PATH_SUFFIX.\n * Centralised so the '/docs' segment lives in one place — the inbox deep-link,\n * the cross-section resolver, and the docs icon-link all build on it. */\nexport function entityDocsPath(\n entityRoute: (ref: { kind: string; namespace: string; name: string }) => string,\n ref: { kind: string; namespace: string; name: string },\n): string {\n return entityRoute(ref) + DOCS_PATH_SUFFIX;\n}\n"],"names":[],"mappings":"AAAO,MAAM,cAAA,GAAiB;AACvB,MAAM,gBAAA,GAAmB;AAIzB,MAAM,gBAAA,GAAmB;AAKzB,SAAS,cAAA,CACd,aACA,GAAA,EACQ;AACR,EAAA,OAAO,WAAA,CAAY,GAAG,CAAA,GAAI,gBAAA;AAC5B;;;;"}
@@ -0,0 +1,38 @@
1
+ const DAY = 864e5;
2
+ const LABELS = {
3
+ today: "Today",
4
+ yesterday: "Yesterday",
5
+ previous7: "Previous 7 days",
6
+ earlier: "Earlier"
7
+ };
8
+ function calendarDaysAgo(now, then) {
9
+ const startOfDay = (ms) => {
10
+ const d = new Date(ms);
11
+ d.setHours(0, 0, 0, 0);
12
+ return d.getTime();
13
+ };
14
+ return Math.round((startOfDay(now) - startOfDay(then)) / DAY);
15
+ }
16
+ function bucketByActivity(items, now) {
17
+ const groups = {
18
+ today: [],
19
+ yesterday: [],
20
+ previous7: [],
21
+ earlier: []
22
+ };
23
+ const order = [];
24
+ for (const it of items) {
25
+ const days = calendarDaysAgo(now, new Date(it.updatedAt).getTime());
26
+ let key;
27
+ if (days <= 0) key = "today";
28
+ else if (days === 1) key = "yesterday";
29
+ else if (days <= 7) key = "previous7";
30
+ else key = "earlier";
31
+ if (groups[key].length === 0) order.push(key);
32
+ groups[key].push(it);
33
+ }
34
+ return order.map((key) => ({ key, label: LABELS[key], items: groups[key] }));
35
+ }
36
+
37
+ export { bucketByActivity };
38
+ //# sourceMappingURL=inboxBuckets.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboxBuckets.esm.js","sources":["../../src/components/inboxBuckets.ts"],"sourcesContent":["// plugins/rw/src/components/inboxBuckets.ts\nimport type { InboxItem } from \"../api/RwClient\";\n\nexport type BucketKey = \"today\" | \"yesterday\" | \"previous7\" | \"earlier\";\n\nexport interface InboxBucket {\n key: BucketKey;\n label: string;\n items: InboxItem[];\n}\n\nconst DAY = 86_400_000;\nconst LABELS: Record<BucketKey, string> = {\n today: \"Today\",\n yesterday: \"Yesterday\",\n previous7: \"Previous 7 days\",\n earlier: \"Earlier\",\n};\n\n/** Whole calendar days between two instants in the viewer's local timezone. */\nfunction calendarDaysAgo(now: number, then: number): number {\n const startOfDay = (ms: number): number => {\n const d = new Date(ms);\n d.setHours(0, 0, 0, 0);\n return d.getTime();\n };\n // Round so a DST-shortened/lengthened day (23 or 25h) still counts as one day.\n return Math.round((startOfDay(now) - startOfDay(then)) / DAY);\n}\n\n/**\n * Chunk an already-sorted inbox list into recency buckets by `updatedAt`.\n *\n * Buckets follow the macOS Finder convention: \"Today\"/\"Yesterday\" are real\n * calendar days in the viewer's local timezone (so \"Today\" means since local\n * midnight, not the last 24h), and \"Previous 7 days\" is an explicit rolling\n * window for the days before that — which sidesteps the Sunday-vs-Monday\n * ambiguity of a calendar \"this week\". Empty buckets are omitted.\n *\n * Bucket order follows the order the buckets first appear in `items`, which is\n * the server's sort (newest- or oldest-first). Deriving order from the items\n * shown — rather than a separate sort flag — keeps the headers consistent with\n * the rows by construction: during a sort toggle the previous page is still on\n * screen for one render (stale-while-revalidate), and ordering off a flag that\n * flips before the data would flash a reversed header before the new page lands.\n */\nexport function bucketByActivity(items: InboxItem[], now: number): InboxBucket[] {\n const groups: Record<BucketKey, InboxItem[]> = {\n today: [],\n yesterday: [],\n previous7: [],\n earlier: [],\n };\n // Record each bucket the first time an item lands in it; since `items` is sorted\n // by updatedAt and the bucket is a monotonic function of it, this first-seen\n // sequence is exactly the order the buckets should read in.\n const order: BucketKey[] = [];\n for (const it of items) {\n const days = calendarDaysAgo(now, new Date(it.updatedAt).getTime());\n let key: BucketKey;\n if (days <= 0) key = \"today\";\n else if (days === 1) key = \"yesterday\";\n else if (days <= 7) key = \"previous7\";\n else key = \"earlier\";\n if (groups[key].length === 0) order.push(key);\n groups[key].push(it);\n }\n return order.map((key) => ({ key, label: LABELS[key], items: groups[key] }));\n}\n"],"names":[],"mappings":"AAWA,MAAM,GAAA,GAAM,KAAA;AACZ,MAAM,MAAA,GAAoC;AAAA,EACxC,KAAA,EAAO,OAAA;AAAA,EACP,SAAA,EAAW,WAAA;AAAA,EACX,SAAA,EAAW,iBAAA;AAAA,EACX,OAAA,EAAS;AACX,CAAA;AAGA,SAAS,eAAA,CAAgB,KAAa,IAAA,EAAsB;AAC1D,EAAA,MAAM,UAAA,GAAa,CAAC,EAAA,KAAuB;AACzC,IAAA,MAAM,CAAA,GAAI,IAAI,IAAA,CAAK,EAAE,CAAA;AACrB,IAAA,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AACrB,IAAA,OAAO,EAAE,OAAA,EAAQ;AAAA,EACnB,CAAA;AAEA,EAAA,OAAO,IAAA,CAAK,OAAO,UAAA,CAAW,GAAG,IAAI,UAAA,CAAW,IAAI,KAAK,GAAG,CAAA;AAC9D;AAkBO,SAAS,gBAAA,CAAiB,OAAoB,GAAA,EAA4B;AAC/E,EAAA,MAAM,MAAA,GAAyC;AAAA,IAC7C,OAAO,EAAC;AAAA,IACR,WAAW,EAAC;AAAA,IACZ,WAAW,EAAC;AAAA,IACZ,SAAS;AAAC,GACZ;AAIA,EAAA,MAAM,QAAqB,EAAC;AAC5B,EAAA,KAAA,MAAW,MAAM,KAAA,EAAO;AACtB,IAAA,MAAM,IAAA,GAAO,gBAAgB,GAAA,EAAK,IAAI,KAAK,EAAA,CAAG,SAAS,CAAA,CAAE,OAAA,EAAS,CAAA;AAClE,IAAA,IAAI,GAAA;AACJ,IAAA,IAAI,IAAA,IAAQ,GAAG,GAAA,GAAM,OAAA;AAAA,SAAA,IACZ,IAAA,KAAS,GAAG,GAAA,GAAM,WAAA;AAAA,SAAA,IAClB,IAAA,IAAQ,GAAG,GAAA,GAAM,WAAA;AAAA,SACrB,GAAA,GAAM,SAAA;AACX,IAAA,IAAI,OAAO,GAAG,CAAA,CAAE,WAAW,CAAA,EAAG,KAAA,CAAM,KAAK,GAAG,CAAA;AAC5C,IAAA,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,CAAA;AAAA,EACrB;AACA,EAAA,OAAO,KAAA,CAAM,GAAA,CAAI,CAAC,GAAA,MAAS,EAAE,GAAA,EAAK,KAAA,EAAO,MAAA,CAAO,GAAG,CAAA,EAAG,KAAA,EAAO,MAAA,CAAO,GAAG,GAAE,CAAE,CAAA;AAC7E;;;;"}
@@ -0,0 +1,70 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { useApi } from '@backstage/core-plugin-api';
3
+ import { rwApiRef } from '../api/RwClient.esm.js';
4
+
5
+ const PAGE_LIMIT = 50;
6
+ function useInboxData(args) {
7
+ const api = useApi(rwApiRef);
8
+ const filter = args.show === "unanswered" ? "unanswered" : "open";
9
+ const { sort } = args;
10
+ const [built, setBuilt] = useState(false);
11
+ const [items, setItems] = useState([]);
12
+ const [openCount, setOpenCount] = useState(0);
13
+ const [unansweredCount, setUnansweredCount] = useState(0);
14
+ const [nextCursor, setNextCursor] = useState(void 0);
15
+ const [loading, setLoading] = useState(true);
16
+ const [hasLoaded, setHasLoaded] = useState(false);
17
+ const [loadingMore, setLoadingMore] = useState(false);
18
+ const [error, setError] = useState(void 0);
19
+ const genRef = useRef(0);
20
+ useEffect(() => {
21
+ const gen = ++genRef.current;
22
+ setLoading(true);
23
+ setLoadingMore(false);
24
+ setError(void 0);
25
+ api.getCommentInbox({ filter, sort, limit: PAGE_LIMIT }).then((res) => {
26
+ if (gen !== genRef.current) return;
27
+ setBuilt(res.built);
28
+ setItems(res.items);
29
+ setOpenCount(res.openCount);
30
+ setUnansweredCount(res.unansweredCount);
31
+ setNextCursor(res.pageInfo.nextCursor);
32
+ setLoading(false);
33
+ setHasLoaded(true);
34
+ }).catch((err) => {
35
+ if (gen !== genRef.current) return;
36
+ setError(err instanceof Error ? err : new Error(String(err)));
37
+ setLoading(false);
38
+ });
39
+ }, [api, filter, sort]);
40
+ const loadMore = useCallback(() => {
41
+ if (!nextCursor || loadingMore || loading) return;
42
+ const gen = genRef.current;
43
+ setLoadingMore(true);
44
+ api.getCommentInbox({ cursor: nextCursor, limit: PAGE_LIMIT }).then((res) => {
45
+ if (gen !== genRef.current) return;
46
+ setItems((prev) => [...prev, ...res.items]);
47
+ setNextCursor(res.pageInfo.nextCursor);
48
+ setLoadingMore(false);
49
+ }).catch((err) => {
50
+ if (gen !== genRef.current) return;
51
+ setError(err instanceof Error ? err : new Error(String(err)));
52
+ setLoadingMore(false);
53
+ });
54
+ }, [api, nextCursor, loadingMore, loading]);
55
+ return {
56
+ built,
57
+ items,
58
+ openCount,
59
+ unansweredCount,
60
+ hasMore: Boolean(nextCursor),
61
+ loading,
62
+ hasLoaded,
63
+ loadingMore,
64
+ error,
65
+ loadMore
66
+ };
67
+ }
68
+
69
+ export { useInboxData };
70
+ //# sourceMappingURL=useInboxData.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInboxData.esm.js","sources":["../../src/components/useInboxData.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useApi } from \"@backstage/core-plugin-api\";\nimport { rwApiRef } from \"../api/RwClient\";\nimport type { InboxItem } from \"../api/RwClient\";\nimport type { ShowFilter, SortOrder } from \"./useInboxFilters\";\n\nexport interface InboxData {\n built: boolean;\n items: InboxItem[];\n openCount: number;\n unansweredCount: number;\n hasMore: boolean;\n loading: boolean; // page-1 fetch in flight (initial load or a filter/sort refetch)\n hasLoaded: boolean; // a first page-1 fetch has resolved at least once\n loadingMore: boolean; // a follow-up page in flight\n error?: Error;\n loadMore: () => void;\n}\n\nconst PAGE_LIMIT = 50;\n\nexport function useInboxData(args: { show: ShowFilter; sort: SortOrder }): InboxData {\n const api = useApi(rwApiRef);\n const filter: \"open\" | \"unanswered\" = args.show === \"unanswered\" ? \"unanswered\" : \"open\";\n const { sort } = args;\n\n const [built, setBuilt] = useState(false);\n const [items, setItems] = useState<InboxItem[]>([]);\n const [openCount, setOpenCount] = useState(0);\n const [unansweredCount, setUnansweredCount] = useState(0);\n const [nextCursor, setNextCursor] = useState<string | undefined>(undefined);\n const [loading, setLoading] = useState(true);\n // Distinguishes the first load (nothing to show — render the full-page spinner)\n // from a filter/sort refetch (keep the prior results on screen while the new\n // page loads). Latches true on the first resolved page-1 fetch and never resets.\n const [hasLoaded, setHasLoaded] = useState(false);\n const [loadingMore, setLoadingMore] = useState(false);\n const [error, setError] = useState<Error | undefined>(undefined);\n\n // Identifies the current filter/sort generation; a stale in-flight response\n // (from a previous filter/sort) is discarded rather than appended.\n // The effect increments (++genRef.current) on each filter/sort change to\n // invalidate any in-flight page-1 fetch. loadMore reads but must NOT\n // increment: incrementing would discard the page-1 response that loadMore\n // is about to extend.\n const genRef = useRef(0);\n\n // (Re)load page 1 whenever filter or sort changes.\n useEffect(() => {\n const gen = ++genRef.current;\n setLoading(true);\n // A new generation invalidates any in-flight loadMore: its response will be\n // dropped at the genRef guard before it can clear loadingMore, so reset the\n // flag here or it latches true and dead-ends pagination for the new view.\n setLoadingMore(false);\n setError(undefined);\n api\n .getCommentInbox({ filter, sort, limit: PAGE_LIMIT })\n .then((res) => {\n if (gen !== genRef.current) return;\n setBuilt(res.built);\n setItems(res.items);\n setOpenCount(res.openCount);\n setUnansweredCount(res.unansweredCount);\n setNextCursor(res.pageInfo.nextCursor);\n setLoading(false);\n setHasLoaded(true);\n })\n .catch((err) => {\n if (gen !== genRef.current) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoading(false);\n });\n }, [api, filter, sort]);\n\n const loadMore = useCallback(() => {\n if (!nextCursor || loadingMore || loading) return;\n const gen = genRef.current;\n setLoadingMore(true);\n api\n .getCommentInbox({ cursor: nextCursor, limit: PAGE_LIMIT })\n .then((res) => {\n if (gen !== genRef.current) return; // filter/sort changed mid-flight\n setItems((prev) => [...prev, ...res.items]);\n setNextCursor(res.pageInfo.nextCursor);\n setLoadingMore(false);\n })\n .catch((err) => {\n if (gen !== genRef.current) return;\n setError(err instanceof Error ? err : new Error(String(err)));\n setLoadingMore(false);\n });\n }, [api, nextCursor, loadingMore, loading]);\n\n return {\n built,\n items,\n openCount,\n unansweredCount,\n hasMore: Boolean(nextCursor),\n loading,\n hasLoaded,\n loadingMore,\n error,\n loadMore,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import { useCallback } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+
4
+ function parseShow(value) {
5
+ return value === "unanswered" ? "unanswered" : "all";
6
+ }
7
+ function parseSort(value) {
8
+ return value === "oldest" ? "oldest" : "newest";
9
+ }
10
+ function useInboxFilters() {
11
+ const [searchParams, setSearchParams] = useSearchParams();
12
+ const show = parseShow(searchParams.get("show"));
13
+ const sort = parseSort(searchParams.get("sort"));
14
+ const setShow = useCallback(
15
+ (next) => {
16
+ setSearchParams((prev) => {
17
+ const params = new URLSearchParams(prev);
18
+ if (next === "all") params.delete("show");
19
+ else params.set("show", next);
20
+ return params;
21
+ });
22
+ },
23
+ [setSearchParams]
24
+ );
25
+ const setSort = useCallback(
26
+ (next) => {
27
+ setSearchParams((prev) => {
28
+ const params = new URLSearchParams(prev);
29
+ if (next === "newest") params.delete("sort");
30
+ else params.set("sort", next);
31
+ return params;
32
+ });
33
+ },
34
+ [setSearchParams]
35
+ );
36
+ return { show, sort, setShow, setSort };
37
+ }
38
+
39
+ export { useInboxFilters };
40
+ //# sourceMappingURL=useInboxFilters.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInboxFilters.esm.js","sources":["../../src/components/useInboxFilters.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useSearchParams } from \"react-router-dom\";\n\nexport type ShowFilter = \"all\" | \"unanswered\";\nexport type SortOrder = \"newest\" | \"oldest\";\n\nexport interface InboxFilters {\n show: ShowFilter;\n sort: SortOrder;\n setShow: (next: ShowFilter) => void;\n setSort: (next: SortOrder) => void;\n}\n\n// Unknown / absent values fall back to the default so the canonical clean URL\n// carries no param.\nfunction parseShow(value: string | null): ShowFilter {\n return value === \"unanswered\" ? \"unanswered\" : \"all\";\n}\n\nfunction parseSort(value: string | null): SortOrder {\n return value === \"oldest\" ? \"oldest\" : \"newest\";\n}\n\n/**\n * URL view-state for the inbox list, persisted in the URL query string so the\n * back-button and refresh restore the owner's filter/sort.\n */\nexport function useInboxFilters(): InboxFilters {\n const [searchParams, setSearchParams] = useSearchParams();\n const show = parseShow(searchParams.get(\"show\"));\n const sort = parseSort(searchParams.get(\"sort\"));\n\n const setShow = useCallback(\n (next: ShowFilter) => {\n setSearchParams((prev) => {\n const params = new URLSearchParams(prev);\n if (next === \"all\") params.delete(\"show\");\n else params.set(\"show\", next);\n return params;\n });\n },\n [setSearchParams],\n );\n\n const setSort = useCallback(\n (next: SortOrder) => {\n setSearchParams((prev) => {\n const params = new URLSearchParams(prev);\n if (next === \"newest\") params.delete(\"sort\");\n else params.set(\"sort\", next);\n return params;\n });\n },\n [setSearchParams],\n );\n\n return { show, sort, setShow, setSort };\n}\n"],"names":[],"mappings":";;;AAeA,SAAS,UAAU,KAAA,EAAkC;AACnD,EAAA,OAAO,KAAA,KAAU,eAAe,YAAA,GAAe,KAAA;AACjD;AAEA,SAAS,UAAU,KAAA,EAAiC;AAClD,EAAA,OAAO,KAAA,KAAU,WAAW,QAAA,GAAW,QAAA;AACzC;AAMO,SAAS,eAAA,GAAgC;AAC9C,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAI,eAAA,EAAgB;AACxD,EAAA,MAAM,IAAA,GAAO,SAAA,CAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAC,CAAA;AAC/C,EAAA,MAAM,IAAA,GAAO,SAAA,CAAU,YAAA,CAAa,GAAA,CAAI,MAAM,CAAC,CAAA;AAE/C,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,IAAA,KAAqB;AACpB,MAAA,eAAA,CAAgB,CAAC,IAAA,KAAS;AACxB,QAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,IAAI,CAAA;AACvC,QAAA,IAAI,IAAA,KAAS,KAAA,EAAO,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA;AAAA,aACnC,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAC5B,QAAA,OAAO,MAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,CAAC,eAAe;AAAA,GAClB;AAEA,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,IAAA,KAAoB;AACnB,MAAA,eAAA,CAAgB,CAAC,IAAA,KAAS;AACxB,QAAA,MAAM,MAAA,GAAS,IAAI,eAAA,CAAgB,IAAI,CAAA;AACvC,QAAA,IAAI,IAAA,KAAS,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA;AAAA,aACtC,MAAA,CAAO,GAAA,CAAI,MAAA,EAAQ,IAAI,CAAA;AAC5B,QAAA,OAAO,MAAA;AAAA,MACT,CAAC,CAAA;AAAA,IACH,CAAA;AAAA,IACA,CAAC,eAAe;AAAA,GAClB;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,OAAA,EAAS,OAAA,EAAQ;AACxC;;;;"}
@@ -2,9 +2,8 @@ import { useRef, useCallback } from 'react';
2
2
  import { useApi, useRouteRef } from '@backstage/core-plugin-api';
3
3
  import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
4
4
  import { parseEntityRef } from '@backstage/catalog-model';
5
- import { ROOT_SECTION_REF, ANNOTATION_KEY } from './constants.esm.js';
5
+ import { ROOT_SECTION_REF, entityDocsPath, ANNOTATION_KEY } from './constants.esm.js';
6
6
 
7
- const DOCS_PATH_SUFFIX = "/docs";
8
7
  function useSectionRefResolver(sourceEntityRef) {
9
8
  const catalogApi = useApi(catalogApiRef);
10
9
  const entityRoute = useRouteRef(entityRouteRef);
@@ -16,7 +15,7 @@ function useSectionRefResolver(sourceEntityRef) {
16
15
  for (const ref of unknown) {
17
16
  if (ref === ROOT_SECTION_REF) {
18
17
  const { kind, namespace, name } = parseEntityRef(sourceEntityRef);
19
- const routeUrl = entityRoute({ kind, namespace, name }) + DOCS_PATH_SUFFIX;
18
+ const routeUrl = entityDocsPath(entityRoute, { kind, namespace, name });
20
19
  cache.current.set(ref, routeUrl);
21
20
  } else {
22
21
  catalogRefs.push(ref);
@@ -30,7 +29,7 @@ function useSectionRefResolver(sourceEntityRef) {
30
29
  const entity = items[i];
31
30
  if (entity?.metadata.annotations?.[ANNOTATION_KEY]) {
32
31
  const { kind, namespace, name } = parseEntityRef(ref);
33
- const routeUrl = entityRoute({ kind, namespace, name }) + DOCS_PATH_SUFFIX;
32
+ const routeUrl = entityDocsPath(entityRoute, { kind, namespace, name });
34
33
  cache.current.set(ref, routeUrl);
35
34
  } else {
36
35
  cache.current.set(ref, null);
@@ -1 +1 @@
1
- {"version":3,"file":"useSectionRefResolver.esm.js","sources":["../../src/components/useSectionRefResolver.ts"],"sourcesContent":["import { useCallback, useRef } from \"react\";\nimport { useApi, useRouteRef } from \"@backstage/core-plugin-api\";\nimport { catalogApiRef, entityRouteRef } from \"@backstage/plugin-catalog-react\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { ANNOTATION_KEY, ROOT_SECTION_REF } from \"./constants\";\n\nconst DOCS_PATH_SUFFIX = \"/docs\";\n\nexport function useSectionRefResolver(\n sourceEntityRef: string,\n): (refs: string[]) => Promise<Record<string, string>> {\n const catalogApi = useApi(catalogApiRef);\n const entityRoute = useRouteRef(entityRouteRef);\n const cache = useRef(new Map<string, string | null>());\n\n return useCallback(\n async (refs: string[]): Promise<Record<string, string>> => {\n const unknown = refs.filter((r) => !cache.current.has(r));\n\n const catalogRefs: string[] = [];\n for (const ref of unknown) {\n if (ref === ROOT_SECTION_REF) {\n const { kind, namespace, name } = parseEntityRef(sourceEntityRef);\n const routeUrl = entityRoute({ kind, namespace, name }) + DOCS_PATH_SUFFIX;\n cache.current.set(ref, routeUrl);\n } else {\n catalogRefs.push(ref);\n }\n }\n\n if (catalogRefs.length > 0) {\n try {\n const { items } = await catalogApi.getEntitiesByRefs({ entityRefs: catalogRefs });\n for (let i = 0; i < catalogRefs.length; i++) {\n const ref = catalogRefs[i];\n const entity = items[i];\n if (entity?.metadata.annotations?.[ANNOTATION_KEY]) {\n const { kind, namespace, name } = parseEntityRef(ref);\n const routeUrl = entityRoute({ kind, namespace, name }) + DOCS_PATH_SUFFIX;\n cache.current.set(ref, routeUrl);\n } else {\n cache.current.set(ref, null);\n }\n }\n } catch {\n // On failure, leave uncached so they can be retried\n }\n }\n\n const result: Record<string, string> = {};\n for (const ref of refs) {\n const url = cache.current.get(ref);\n if (url !== null && url !== undefined) {\n result[ref] = url;\n }\n }\n return result;\n },\n [catalogApi, entityRoute, sourceEntityRef],\n );\n}\n"],"names":[],"mappings":";;;;;;AAMA,MAAM,gBAAA,GAAmB,OAAA;AAElB,SAAS,sBACd,eAAA,EACqD;AACrD,EAAA,MAAM,UAAA,GAAa,OAAO,aAAa,CAAA;AACvC,EAAA,MAAM,WAAA,GAAc,YAAY,cAAc,CAAA;AAC9C,EAAA,MAAM,KAAA,GAAQ,MAAA,iBAAO,IAAI,GAAA,EAA4B,CAAA;AAErD,EAAA,OAAO,WAAA;AAAA,IACL,OAAO,IAAA,KAAoD;AACzD,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAC,CAAA;AAExD,MAAA,MAAM,cAAwB,EAAC;AAC/B,MAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AACzB,QAAA,IAAI,QAAQ,gBAAA,EAAkB;AAC5B,UAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAK,GAAI,eAAe,eAAe,CAAA;AAChE,UAAA,MAAM,WAAW,WAAA,CAAY,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,CAAA,GAAI,gBAAA;AAC1D,UAAA,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,QAAQ,CAAA;AAAA,QACjC,CAAA,MAAO;AACL,UAAA,WAAA,CAAY,KAAK,GAAG,CAAA;AAAA,QACtB;AAAA,MACF;AAEA,MAAA,IAAI,WAAA,CAAY,SAAS,CAAA,EAAG;AAC1B,QAAA,IAAI;AACF,UAAA,MAAM,EAAE,OAAM,GAAI,MAAM,WAAW,iBAAA,CAAkB,EAAE,UAAA,EAAY,WAAA,EAAa,CAAA;AAChF,UAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,WAAA,CAAY,QAAQ,CAAA,EAAA,EAAK;AAC3C,YAAA,MAAM,GAAA,GAAM,YAAY,CAAC,CAAA;AACzB,YAAA,MAAM,MAAA,GAAS,MAAM,CAAC,CAAA;AACtB,YAAA,IAAI,MAAA,EAAQ,QAAA,CAAS,WAAA,GAAc,cAAc,CAAA,EAAG;AAClD,cAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAK,GAAI,eAAe,GAAG,CAAA;AACpD,cAAA,MAAM,WAAW,WAAA,CAAY,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,CAAA,GAAI,gBAAA;AAC1D,cAAA,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,QAAQ,CAAA;AAAA,YACjC,CAAA,MAAO;AACL,cAAA,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,IAAI,CAAA;AAAA,YAC7B;AAAA,UACF;AAAA,QACF,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAEA,MAAA,MAAM,SAAiC,EAAC;AACxC,MAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,QAAA,MAAM,GAAA,GAAM,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA;AACjC,QAAA,IAAI,GAAA,KAAQ,IAAA,IAAQ,GAAA,KAAQ,MAAA,EAAW;AACrC,UAAA,MAAA,CAAO,GAAG,CAAA,GAAI,GAAA;AAAA,QAChB;AAAA,MACF;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,CAAC,UAAA,EAAY,WAAA,EAAa,eAAe;AAAA,GAC3C;AACF;;;;"}
1
+ {"version":3,"file":"useSectionRefResolver.esm.js","sources":["../../src/components/useSectionRefResolver.ts"],"sourcesContent":["import { useCallback, useRef } from \"react\";\nimport { useApi, useRouteRef } from \"@backstage/core-plugin-api\";\nimport { catalogApiRef, entityRouteRef } from \"@backstage/plugin-catalog-react\";\nimport { parseEntityRef } from \"@backstage/catalog-model\";\nimport { ANNOTATION_KEY, ROOT_SECTION_REF, entityDocsPath } from \"./constants\";\n\nexport function useSectionRefResolver(\n sourceEntityRef: string,\n): (refs: string[]) => Promise<Record<string, string>> {\n const catalogApi = useApi(catalogApiRef);\n const entityRoute = useRouteRef(entityRouteRef);\n const cache = useRef(new Map<string, string | null>());\n\n return useCallback(\n async (refs: string[]): Promise<Record<string, string>> => {\n const unknown = refs.filter((r) => !cache.current.has(r));\n\n const catalogRefs: string[] = [];\n for (const ref of unknown) {\n if (ref === ROOT_SECTION_REF) {\n const { kind, namespace, name } = parseEntityRef(sourceEntityRef);\n const routeUrl = entityDocsPath(entityRoute, { kind, namespace, name });\n cache.current.set(ref, routeUrl);\n } else {\n catalogRefs.push(ref);\n }\n }\n\n if (catalogRefs.length > 0) {\n try {\n const { items } = await catalogApi.getEntitiesByRefs({ entityRefs: catalogRefs });\n for (let i = 0; i < catalogRefs.length; i++) {\n const ref = catalogRefs[i];\n const entity = items[i];\n if (entity?.metadata.annotations?.[ANNOTATION_KEY]) {\n const { kind, namespace, name } = parseEntityRef(ref);\n const routeUrl = entityDocsPath(entityRoute, { kind, namespace, name });\n cache.current.set(ref, routeUrl);\n } else {\n cache.current.set(ref, null);\n }\n }\n } catch {\n // On failure, leave uncached so they can be retried\n }\n }\n\n const result: Record<string, string> = {};\n for (const ref of refs) {\n const url = cache.current.get(ref);\n if (url !== null && url !== undefined) {\n result[ref] = url;\n }\n }\n return result;\n },\n [catalogApi, entityRoute, sourceEntityRef],\n );\n}\n"],"names":[],"mappings":";;;;;;AAMO,SAAS,sBACd,eAAA,EACqD;AACrD,EAAA,MAAM,UAAA,GAAa,OAAO,aAAa,CAAA;AACvC,EAAA,MAAM,WAAA,GAAc,YAAY,cAAc,CAAA;AAC9C,EAAA,MAAM,KAAA,GAAQ,MAAA,iBAAO,IAAI,GAAA,EAA4B,CAAA;AAErD,EAAA,OAAO,WAAA;AAAA,IACL,OAAO,IAAA,KAAoD;AACzD,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAC,CAAA;AAExD,MAAA,MAAM,cAAwB,EAAC;AAC/B,MAAA,KAAA,MAAW,OAAO,OAAA,EAAS;AACzB,QAAA,IAAI,QAAQ,gBAAA,EAAkB;AAC5B,UAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAK,GAAI,eAAe,eAAe,CAAA;AAChE,UAAA,MAAM,WAAW,cAAA,CAAe,WAAA,EAAa,EAAE,IAAA,EAAM,SAAA,EAAW,MAAM,CAAA;AACtE,UAAA,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,QAAQ,CAAA;AAAA,QACjC,CAAA,MAAO;AACL,UAAA,WAAA,CAAY,KAAK,GAAG,CAAA;AAAA,QACtB;AAAA,MACF;AAEA,MAAA,IAAI,WAAA,CAAY,SAAS,CAAA,EAAG;AAC1B,QAAA,IAAI;AACF,UAAA,MAAM,EAAE,OAAM,GAAI,MAAM,WAAW,iBAAA,CAAkB,EAAE,UAAA,EAAY,WAAA,EAAa,CAAA;AAChF,UAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,WAAA,CAAY,QAAQ,CAAA,EAAA,EAAK;AAC3C,YAAA,MAAM,GAAA,GAAM,YAAY,CAAC,CAAA;AACzB,YAAA,MAAM,MAAA,GAAS,MAAM,CAAC,CAAA;AACtB,YAAA,IAAI,MAAA,EAAQ,QAAA,CAAS,WAAA,GAAc,cAAc,CAAA,EAAG;AAClD,cAAA,MAAM,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAK,GAAI,eAAe,GAAG,CAAA;AACpD,cAAA,MAAM,WAAW,cAAA,CAAe,WAAA,EAAa,EAAE,IAAA,EAAM,SAAA,EAAW,MAAM,CAAA;AACtE,cAAA,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,QAAQ,CAAA;AAAA,YACjC,CAAA,MAAO;AACL,cAAA,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAA,EAAK,IAAI,CAAA;AAAA,YAC7B;AAAA,UACF;AAAA,QACF,CAAA,CAAA,MAAQ;AAAA,QAER;AAAA,MACF;AAEA,MAAA,MAAM,SAAiC,EAAC;AACxC,MAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,QAAA,MAAM,GAAA,GAAM,KAAA,CAAM,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA;AACjC,QAAA,IAAI,GAAA,KAAQ,IAAA,IAAQ,GAAA,KAAQ,MAAA,EAAW;AACrC,UAAA,MAAA,CAAO,GAAG,CAAA,GAAI,GAAA;AAAA,QAChB;AAAA,MACF;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,CAAC,UAAA,EAAY,WAAA,EAAa,eAAe;AAAA,GAC3C;AACF;;;;"}
@@ -2,6 +2,7 @@ import { jsx } from 'react/jsx-runtime';
2
2
  import DocsIcon from '@material-ui/icons/Description';
3
3
  import { useRouteRef } from '@backstage/core-plugin-api';
4
4
  import { useEntity, entityRouteRef } from '@backstage/plugin-catalog-react';
5
+ import { entityDocsPath } from '../components/constants.esm.js';
5
6
 
6
7
  const docsIcon = /* @__PURE__ */ jsx(DocsIcon, {});
7
8
  function useRwDocsIconLinkProps() {
@@ -14,7 +15,7 @@ function useRwDocsIconLinkProps() {
14
15
  label: "View Docs",
15
16
  disabled: !entityRoute,
16
17
  icon: docsIcon,
17
- href: entityRoute ? `${entityRoute({ kind, namespace, name })}/docs` : ""
18
+ href: entityRoute ? entityDocsPath(entityRoute, { kind, namespace, name }) : ""
18
19
  };
19
20
  }
20
21
 
@@ -1 +1 @@
1
- {"version":3,"file":"useRwDocsIconLinkProps.esm.js","sources":["../../src/hooks/useRwDocsIconLinkProps.tsx"],"sourcesContent":["import DocsIcon from \"@material-ui/icons/Description\";\nimport { useRouteRef } from \"@backstage/core-plugin-api\";\nimport { entityRouteRef, useEntity } from \"@backstage/plugin-catalog-react\";\n\nconst docsIcon = <DocsIcon />;\n\nexport function useRwDocsIconLinkProps() {\n const { entity } = useEntity();\n const entityRoute = useRouteRef(entityRouteRef);\n\n const kind = entity.kind.toLocaleLowerCase(\"en-US\");\n const namespace = entity.metadata.namespace?.toLocaleLowerCase(\"en-US\") ?? \"default\";\n const name = entity.metadata.name;\n\n return {\n label: \"View Docs\",\n disabled: !entityRoute,\n icon: docsIcon,\n href: entityRoute ? `${entityRoute({ kind, namespace, name })}/docs` : \"\",\n };\n}\n"],"names":[],"mappings":";;;;;AAIA,MAAM,QAAA,uBAAY,QAAA,EAAA,EAAS,CAAA;AAEpB,SAAS,sBAAA,GAAyB;AACvC,EAAA,MAAM,EAAE,MAAA,EAAO,GAAI,SAAA,EAAU;AAC7B,EAAA,MAAM,WAAA,GAAc,YAAY,cAAc,CAAA;AAE9C,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAA;AAClD,EAAA,MAAM,YAAY,MAAA,CAAO,QAAA,CAAS,SAAA,EAAW,iBAAA,CAAkB,OAAO,CAAA,IAAK,SAAA;AAC3E,EAAA,MAAM,IAAA,GAAO,OAAO,QAAA,CAAS,IAAA;AAE7B,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,WAAA;AAAA,IACP,UAAU,CAAC,WAAA;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,IAAA,EAAM,WAAA,GAAc,CAAA,EAAG,WAAA,CAAY,EAAE,MAAM,SAAA,EAAW,IAAA,EAAM,CAAC,CAAA,KAAA,CAAA,GAAU;AAAA,GACzE;AACF;;;;"}
1
+ {"version":3,"file":"useRwDocsIconLinkProps.esm.js","sources":["../../src/hooks/useRwDocsIconLinkProps.tsx"],"sourcesContent":["import DocsIcon from \"@material-ui/icons/Description\";\nimport { useRouteRef } from \"@backstage/core-plugin-api\";\nimport { entityRouteRef, useEntity } from \"@backstage/plugin-catalog-react\";\nimport { entityDocsPath } from \"../components/constants\";\n\nconst docsIcon = <DocsIcon />;\n\nexport function useRwDocsIconLinkProps() {\n const { entity } = useEntity();\n const entityRoute = useRouteRef(entityRouteRef);\n\n const kind = entity.kind.toLocaleLowerCase(\"en-US\");\n const namespace = entity.metadata.namespace?.toLocaleLowerCase(\"en-US\") ?? \"default\";\n const name = entity.metadata.name;\n\n return {\n label: \"View Docs\",\n disabled: !entityRoute,\n icon: docsIcon,\n href: entityRoute ? entityDocsPath(entityRoute, { kind, namespace, name }) : \"\",\n };\n}\n"],"names":[],"mappings":";;;;;;AAKA,MAAM,QAAA,uBAAY,QAAA,EAAA,EAAS,CAAA;AAEpB,SAAS,sBAAA,GAAyB;AACvC,EAAA,MAAM,EAAE,MAAA,EAAO,GAAI,SAAA,EAAU;AAC7B,EAAA,MAAM,WAAA,GAAc,YAAY,cAAc,CAAA;AAE9C,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,iBAAA,CAAkB,OAAO,CAAA;AAClD,EAAA,MAAM,YAAY,MAAA,CAAO,QAAA,CAAS,SAAA,EAAW,iBAAA,CAAkB,OAAO,CAAA,IAAK,SAAA;AAC3E,EAAA,MAAM,IAAA,GAAO,OAAO,QAAA,CAAS,IAAA;AAE7B,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,WAAA;AAAA,IACP,UAAU,CAAC,WAAA;AAAA,IACX,IAAA,EAAM,QAAA;AAAA,IACN,IAAA,EAAM,cAAc,cAAA,CAAe,WAAA,EAAa,EAAE,IAAA,EAAM,SAAA,EAAW,IAAA,EAAM,CAAA,GAAI;AAAA,GAC/E;AACF;;;;"}
package/dist/index.d.ts CHANGED
@@ -1,92 +1,18 @@
1
- import * as _backstage_core_components from '@backstage/core-components';
2
- import * as _backstage_catalog_model from '@backstage/catalog-model';
3
- import * as react from 'react';
4
- import * as _backstage_filter_predicates from '@backstage/filter-predicates';
5
1
  import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
2
+ import { FrontendPlugin } from '@backstage/frontend-plugin-api';
6
3
  import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
4
+ import { InboxQuery, InboxResponse } from '@rwdocs/backstage-plugin-rw-common';
5
+ import { CommentApiClient } from '@rwdocs/viewer';
7
6
 
8
- declare const rwPlugin: _backstage_frontend_plugin_api.OverridableFrontendPlugin<{}, {}, {
9
- "api:rw": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
10
- kind: "api";
11
- name: undefined;
12
- config: {};
13
- configInput: {};
14
- output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
15
- inputs: {};
16
- params: <TApi, TImpl extends TApi, TDeps extends { [name in string]: unknown; }>(params: _backstage_frontend_plugin_api.ApiFactory<TApi, TImpl, TDeps>) => _backstage_frontend_plugin_api.ExtensionBlueprintParams<_backstage_frontend_plugin_api.AnyApiFactory>;
17
- }>;
18
- "entity-content:rw": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
19
- kind: "entity-content";
20
- name: undefined;
21
- config: {
22
- path: string | undefined;
23
- title: string | undefined;
24
- filter: _backstage_filter_predicates.FilterPredicate | undefined;
25
- group: string | false | undefined;
26
- icon: string | undefined;
27
- };
28
- configInput: {
29
- filter?: _backstage_filter_predicates.FilterPredicate | undefined;
30
- title?: string | undefined;
31
- path?: string | undefined;
32
- group?: string | false | undefined;
33
- icon?: string | undefined;
34
- };
35
- output: _backstage_frontend_plugin_api.ExtensionDataRef<string, "core.routing.path", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.RouteRef<_backstage_frontend_plugin_api.AnyRouteRefParams>, "core.routing.ref", {
36
- optional: true;
37
- }> | _backstage_frontend_plugin_api.ExtensionDataRef<react.JSX.Element, "core.reactElement", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<(entity: _backstage_catalog_model.Entity) => boolean, "catalog.entity-filter-function", {
38
- optional: true;
39
- }> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-filter-expression", {
40
- optional: true;
41
- }> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-content-title", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-content-group", {
42
- optional: true;
43
- }> | _backstage_frontend_plugin_api.ExtensionDataRef<string | react.ReactElement<any, string | react.JSXElementConstructor<any>>, "catalog.entity-content-icon", {
44
- optional: true;
45
- }>;
46
- inputs: {};
47
- params: {
48
- defaultPath?: [Error: `Use the 'path' param instead`];
49
- path: string;
50
- defaultTitle?: [Error: `Use the 'title' param instead`];
51
- title: string;
52
- defaultGroup?: [Error: `Use the 'group' param instead`];
53
- group?: ("overview" | "documentation" | "development" | "deployment" | "operation" | "observability") | (string & {});
54
- icon?: string | react.ReactElement;
55
- loader: () => Promise<JSX.Element>;
56
- routeRef?: _backstage_frontend_plugin_api.RouteRef;
57
- filter?: string | _backstage_filter_predicates.FilterPredicate | ((entity: _backstage_catalog_model.Entity) => boolean);
58
- };
59
- }>;
60
- "entity-icon-link:rw/view-docs": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
61
- kind: "entity-icon-link";
62
- name: "view-docs";
63
- config: {
64
- label: string | undefined;
65
- title: string | undefined;
66
- filter: _backstage_filter_predicates.FilterPredicate | undefined;
67
- };
68
- configInput: {
69
- filter?: _backstage_filter_predicates.FilterPredicate | undefined;
70
- label?: string | undefined;
71
- title?: string | undefined;
72
- };
73
- output: _backstage_frontend_plugin_api.ExtensionDataRef<(entity: _backstage_catalog_model.Entity) => boolean, "catalog.entity-filter-function", {
74
- optional: true;
75
- }> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-filter-expression", {
76
- optional: true;
77
- }> | _backstage_frontend_plugin_api.ExtensionDataRef<() => _backstage_core_components.IconLinkVerticalProps, "entity-icon-link-props", {}>;
78
- inputs: {};
79
- params: {
80
- useProps: () => Omit<_backstage_core_components.IconLinkVerticalProps, "color">;
81
- filter?: _backstage_filter_predicates.FilterPredicate | ((entity: _backstage_catalog_model.Entity) => boolean);
82
- };
83
- }>;
84
- }>;
7
+ declare const rwPlugin: FrontendPlugin;
85
8
 
86
9
  interface RwApi {
87
10
  getBaseUrl(): Promise<string>;
88
11
  getSiteBaseUrl(entityRef: string): Promise<string>;
89
12
  getFetch(): typeof fetch;
13
+ getCommentsEnabled(): Promise<boolean>;
14
+ getCommentInbox(query?: InboxQuery): Promise<InboxResponse>;
15
+ createCommentClient(siteRef: string): CommentApiClient;
90
16
  }
91
17
  declare const rwApiRef: _backstage_frontend_plugin_api.ApiRef<RwApi>;
92
18
  declare class RwClient implements RwApi {
@@ -99,6 +25,9 @@ declare class RwClient implements RwApi {
99
25
  getBaseUrl(): Promise<string>;
100
26
  getSiteBaseUrl(entityRef: string): Promise<string>;
101
27
  getFetch(): typeof fetch;
28
+ getCommentsEnabled(): Promise<boolean>;
29
+ getCommentInbox(query?: InboxQuery): Promise<InboxResponse>;
30
+ createCommentClient(siteRef: string): CommentApiClient;
102
31
  }
103
32
 
104
33
  export { RwClient, rwPlugin as default, rwApiRef };
@@ -1,11 +1,16 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
- import { ApiBlueprint, createFrontendPlugin } from '@backstage/frontend-plugin-api';
2
+ import { createRouteRef, ApiBlueprint, PageBlueprint, SubPageBlueprint, createFrontendPlugin } from '@backstage/frontend-plugin-api';
3
3
  import { createApiFactory, fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
4
4
  import { EntityContentBlueprint, EntityIconLinkBlueprint } from '@backstage/plugin-catalog-react/alpha';
5
+ import { SearchFilterResultTypeBlueprint } from '@backstage/plugin-search-react/alpha';
6
+ import DocsIcon from '@material-ui/icons/Description';
7
+ import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
5
8
  import { RwClient, rwApiRef } from './api/RwClient.esm.js';
6
9
  import { ANNOTATION_KEY } from './components/constants.esm.js';
7
10
  import { useRwDocsIconLinkProps } from './hooks/useRwDocsIconLinkProps.esm.js';
8
11
 
12
+ const docsRouteRef = createRouteRef();
13
+ const commentInboxRouteRef = createRouteRef();
9
14
  const rwApi = ApiBlueprint.make({
10
15
  params: (defineParams) => defineParams(
11
16
  createApiFactory({
@@ -31,10 +36,43 @@ const rwEntityIconLink = EntityIconLinkBlueprint.make({
31
36
  useProps: useRwDocsIconLinkProps
32
37
  }
33
38
  });
39
+ const rwSearchResultType = SearchFilterResultTypeBlueprint.make({
40
+ params: {
41
+ value: "rw",
42
+ name: "Documentation",
43
+ icon: /* @__PURE__ */ jsx(DocsIcon, {})
44
+ }
45
+ });
46
+ const rwDocsPage = PageBlueprint.make({
47
+ name: "docs",
48
+ params: {
49
+ path: "/docs",
50
+ title: "Docs",
51
+ icon: /* @__PURE__ */ jsx(LibraryBooksIcon, {}),
52
+ routeRef: docsRouteRef
53
+ }
54
+ });
55
+ const rwCommentsSubPage = SubPageBlueprint.make({
56
+ name: "comments",
57
+ attachTo: { id: "page:rw/docs", input: "pages" },
58
+ params: {
59
+ path: "comments",
60
+ title: "Comments",
61
+ routeRef: commentInboxRouteRef,
62
+ loader: () => import('./components/CommentInboxPage.esm.js').then((m) => /* @__PURE__ */ jsx(m.CommentInboxPage, {}))
63
+ }
64
+ });
34
65
  const rwPlugin = createFrontendPlugin({
35
66
  pluginId: "rw",
36
- extensions: [rwApi, rwEntityContent, rwEntityIconLink]
67
+ extensions: [
68
+ rwApi,
69
+ rwEntityContent,
70
+ rwEntityIconLink,
71
+ rwSearchResultType,
72
+ rwDocsPage,
73
+ rwCommentsSubPage
74
+ ]
37
75
  });
38
76
 
39
- export { rwPlugin };
77
+ export { commentInboxRouteRef, docsRouteRef, rwPlugin };
40
78
  //# sourceMappingURL=plugin.esm.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import { createFrontendPlugin, ApiBlueprint } from \"@backstage/frontend-plugin-api\";\nimport { createApiFactory, discoveryApiRef, fetchApiRef } from \"@backstage/core-plugin-api\";\nimport {\n EntityContentBlueprint,\n EntityIconLinkBlueprint,\n} from \"@backstage/plugin-catalog-react/alpha\";\nimport { rwApiRef, RwClient } from \"./api/RwClient\";\nimport { ANNOTATION_KEY } from \"./components/constants\";\nimport { useRwDocsIconLinkProps } from \"./hooks/useRwDocsIconLinkProps\";\n\nconst rwApi = ApiBlueprint.make({\n params: (defineParams) =>\n defineParams(\n createApiFactory({\n api: rwApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) => new RwClient({ discoveryApi, fetchApi }),\n }),\n ),\n});\n\nconst rwEntityContent = EntityContentBlueprint.make({\n params: {\n path: \"docs\",\n title: \"Docs\",\n group: \"documentation\",\n filter: (entity) => Boolean(entity.metadata.annotations?.[ANNOTATION_KEY]),\n loader: () => import(\"./components/RwEntityDocsViewer\").then((m) => <m.RwEntityDocsViewer />),\n },\n});\n\nconst rwEntityIconLink = EntityIconLinkBlueprint.make({\n name: \"view-docs\",\n params: {\n filter: (entity) => Boolean(entity.metadata.annotations?.[ANNOTATION_KEY]),\n useProps: useRwDocsIconLinkProps,\n },\n});\n\nexport const rwPlugin = createFrontendPlugin({\n pluginId: \"rw\",\n extensions: [rwApi, rwEntityContent, rwEntityIconLink],\n});\n\nexport default rwPlugin;\n"],"names":[],"mappings":";;;;;;;;AAUA,MAAM,KAAA,GAAQ,aAAa,IAAA,CAAK;AAAA,EAC9B,MAAA,EAAQ,CAAC,YAAA,KACP,YAAA;AAAA,IACE,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,QAAA;AAAA,MACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,MAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,QAAA,EAAS,KAAM,IAAI,QAAA,CAAS,EAAE,YAAA,EAAc,QAAA,EAAU;AAAA,KACjF;AAAA;AAEP,CAAC,CAAA;AAED,MAAM,eAAA,GAAkB,uBAAuB,IAAA,CAAK;AAAA,EAClD,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,MAAA;AAAA,IACN,KAAA,EAAO,MAAA;AAAA,IACP,KAAA,EAAO,eAAA;AAAA,IACP,MAAA,EAAQ,CAAC,MAAA,KAAW,OAAA,CAAQ,OAAO,QAAA,CAAS,WAAA,GAAc,cAAc,CAAC,CAAA;AAAA,IACzE,MAAA,EAAQ,MAAM,OAAO,wCAAiC,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,qBAAM,GAAA,CAAC,CAAA,CAAE,kBAAA,EAAF,EAAqB,CAAE;AAAA;AAEhG,CAAC,CAAA;AAED,MAAM,gBAAA,GAAmB,wBAAwB,IAAA,CAAK;AAAA,EACpD,IAAA,EAAM,WAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,MAAA,EAAQ,CAAC,MAAA,KAAW,OAAA,CAAQ,OAAO,QAAA,CAAS,WAAA,GAAc,cAAc,CAAC,CAAA;AAAA,IACzE,QAAA,EAAU;AAAA;AAEd,CAAC,CAAA;AAEM,MAAM,WAAW,oBAAA,CAAqB;AAAA,EAC3C,QAAA,EAAU,IAAA;AAAA,EACV,UAAA,EAAY,CAAC,KAAA,EAAO,eAAA,EAAiB,gBAAgB;AACvD,CAAC;;;;"}
1
+ {"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import {\n createFrontendPlugin,\n ApiBlueprint,\n FrontendPlugin,\n PageBlueprint,\n SubPageBlueprint,\n createRouteRef,\n} from \"@backstage/frontend-plugin-api\";\nimport { createApiFactory, discoveryApiRef, fetchApiRef } from \"@backstage/core-plugin-api\";\nimport {\n EntityContentBlueprint,\n EntityIconLinkBlueprint,\n} from \"@backstage/plugin-catalog-react/alpha\";\nimport { SearchFilterResultTypeBlueprint } from \"@backstage/plugin-search-react/alpha\";\nimport DocsIcon from \"@material-ui/icons/Description\";\nimport LibraryBooksIcon from \"@material-ui/icons/LibraryBooks\";\nimport { rwApiRef, RwClient } from \"./api/RwClient\";\nimport { ANNOTATION_KEY } from \"./components/constants\";\nimport { useRwDocsIconLinkProps } from \"./hooks/useRwDocsIconLinkProps\";\n\n// Route ref for the parent \"Docs\" page. The sidebar nav item is auto-discovered\n// from this page's title/icon/routeRef — NavItemBlueprint was removed in\n// @backstage/frontend-plugin-api 0.17.0 — so no explicit nav extension is needed.\nexport const docsRouteRef = createRouteRef();\n\n// Route ref for the Comments tab (/docs/comments). Carried over from the former\n// standalone page so the ref stays resolvable at its new, nested location.\nexport const commentInboxRouteRef = createRouteRef();\n\nconst rwApi = ApiBlueprint.make({\n params: (defineParams) =>\n defineParams(\n createApiFactory({\n api: rwApiRef,\n deps: { discoveryApi: discoveryApiRef, fetchApi: fetchApiRef },\n factory: ({ discoveryApi, fetchApi }) => new RwClient({ discoveryApi, fetchApi }),\n }),\n ),\n});\n\nconst rwEntityContent = EntityContentBlueprint.make({\n params: {\n path: \"docs\",\n title: \"Docs\",\n group: \"documentation\",\n filter: (entity) => Boolean(entity.metadata.annotations?.[ANNOTATION_KEY]),\n loader: () => import(\"./components/RwEntityDocsViewer\").then((m) => <m.RwEntityDocsViewer />),\n },\n});\n\nconst rwEntityIconLink = EntityIconLinkBlueprint.make({\n name: \"view-docs\",\n params: {\n filter: (entity) => Boolean(entity.metadata.annotations?.[ANNOTATION_KEY]),\n useProps: useRwDocsIconLinkProps,\n },\n});\n\nconst rwSearchResultType = SearchFilterResultTypeBlueprint.make({\n params: {\n value: \"rw\",\n name: \"Documentation\",\n icon: <DocsIcon />,\n },\n});\n\n// Parent \"Docs\" page. With no loader and at least one attached sub-page, the\n// framework renders the plugin header (icon + title) with a tab strip built from\n// the sub-pages, and redirects the bare path to the first tab.\n//\n// `path` defaults to /docs and is overridable per Backstage instance through\n// app-config (no custom schema) — e.g. to avoid TechDocs, which also defaults to\n// /docs:\n//\n// app:\n// extensions:\n// - page:rw/docs:\n// config:\n// path: /rwdocs\nconst rwDocsPage = PageBlueprint.make({\n name: \"docs\",\n params: {\n path: \"/docs\",\n title: \"Docs\",\n icon: <LibraryBooksIcon />,\n routeRef: docsRouteRef,\n },\n});\n\n// The \"Comments\" tab — renders the doc-comment inbox at /docs/comments. Attached\n// explicitly to the Docs page's `pages` input by id. SubPageBlueprint's default\n// attachTo is { relative: { kind: \"page\" }, input: \"pages\" }, which resolves to\n// the un-named id \"page:rw\" — but the Docs page is named, so its id is\n// \"page:rw/docs\", which the default never matches. Hence the explicit page id.\nconst rwCommentsSubPage = SubPageBlueprint.make({\n name: \"comments\",\n attachTo: { id: \"page:rw/docs\", input: \"pages\" },\n params: {\n path: \"comments\",\n title: \"Comments\",\n routeRef: commentInboxRouteRef,\n loader: () => import(\"./components/CommentInboxPage\").then((m) => <m.CommentInboxPage />),\n },\n});\n\nexport const rwPlugin: FrontendPlugin = createFrontendPlugin({\n pluginId: \"rw\",\n extensions: [\n rwApi,\n rwEntityContent,\n rwEntityIconLink,\n rwSearchResultType,\n rwDocsPage,\n rwCommentsSubPage,\n ],\n});\n\nexport default rwPlugin;\n"],"names":[],"mappings":";;;;;;;;;;;AAuBO,MAAM,eAAe,cAAA;AAIrB,MAAM,uBAAuB,cAAA;AAEpC,MAAM,KAAA,GAAQ,aAAa,IAAA,CAAK;AAAA,EAC9B,MAAA,EAAQ,CAAC,YAAA,KACP,YAAA;AAAA,IACE,gBAAA,CAAiB;AAAA,MACf,GAAA,EAAK,QAAA;AAAA,MACL,IAAA,EAAM,EAAE,YAAA,EAAc,eAAA,EAAiB,UAAU,WAAA,EAAY;AAAA,MAC7D,OAAA,EAAS,CAAC,EAAE,YAAA,EAAc,QAAA,EAAS,KAAM,IAAI,QAAA,CAAS,EAAE,YAAA,EAAc,QAAA,EAAU;AAAA,KACjF;AAAA;AAEP,CAAC,CAAA;AAED,MAAM,eAAA,GAAkB,uBAAuB,IAAA,CAAK;AAAA,EAClD,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,MAAA;AAAA,IACN,KAAA,EAAO,MAAA;AAAA,IACP,KAAA,EAAO,eAAA;AAAA,IACP,MAAA,EAAQ,CAAC,MAAA,KAAW,OAAA,CAAQ,OAAO,QAAA,CAAS,WAAA,GAAc,cAAc,CAAC,CAAA;AAAA,IACzE,MAAA,EAAQ,MAAM,OAAO,wCAAiC,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,qBAAM,GAAA,CAAC,CAAA,CAAE,kBAAA,EAAF,EAAqB,CAAE;AAAA;AAEhG,CAAC,CAAA;AAED,MAAM,gBAAA,GAAmB,wBAAwB,IAAA,CAAK;AAAA,EACpD,IAAA,EAAM,WAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,MAAA,EAAQ,CAAC,MAAA,KAAW,OAAA,CAAQ,OAAO,QAAA,CAAS,WAAA,GAAc,cAAc,CAAC,CAAA;AAAA,IACzE,QAAA,EAAU;AAAA;AAEd,CAAC,CAAA;AAED,MAAM,kBAAA,GAAqB,gCAAgC,IAAA,CAAK;AAAA,EAC9D,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,IAAA;AAAA,IACP,IAAA,EAAM,eAAA;AAAA,IACN,IAAA,sBAAO,QAAA,EAAA,EAAS;AAAA;AAEpB,CAAC,CAAA;AAeD,MAAM,UAAA,GAAa,cAAc,IAAA,CAAK;AAAA,EACpC,IAAA,EAAM,MAAA;AAAA,EACN,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,OAAA;AAAA,IACN,KAAA,EAAO,MAAA;AAAA,IACP,IAAA,sBAAO,gBAAA,EAAA,EAAiB,CAAA;AAAA,IACxB,QAAA,EAAU;AAAA;AAEd,CAAC,CAAA;AAOD,MAAM,iBAAA,GAAoB,iBAAiB,IAAA,CAAK;AAAA,EAC9C,IAAA,EAAM,UAAA;AAAA,EACN,QAAA,EAAU,EAAE,EAAA,EAAI,cAAA,EAAgB,OAAO,OAAA,EAAQ;AAAA,EAC/C,MAAA,EAAQ;AAAA,IACN,IAAA,EAAM,UAAA;AAAA,IACN,KAAA,EAAO,UAAA;AAAA,IACP,QAAA,EAAU,oBAAA;AAAA,IACV,MAAA,EAAQ,MAAM,OAAO,sCAA+B,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,qBAAM,GAAA,CAAC,CAAA,CAAE,gBAAA,EAAF,EAAmB,CAAE;AAAA;AAE5F,CAAC,CAAA;AAEM,MAAM,WAA2B,oBAAA,CAAqB;AAAA,EAC3D,QAAA,EAAU,IAAA;AAAA,EACV,UAAA,EAAY;AAAA,IACV,KAAA;AAAA,IACA,eAAA;AAAA,IACA,gBAAA;AAAA,IACA,kBAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA;AAEJ,CAAC;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rwdocs/backstage-plugin-rw",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "MIT OR Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,15 +29,14 @@
29
29
  "pluginId": "rw",
30
30
  "pluginPackages": [
31
31
  "@rwdocs/backstage-plugin-rw",
32
- "@rwdocs/backstage-plugin-rw-backend"
32
+ "@rwdocs/backstage-plugin-rw-backend",
33
+ "@rwdocs/backstage-plugin-rw-common"
33
34
  ],
34
35
  "features": {
35
36
  ".": "@backstage/FrontendPlugin"
36
37
  }
37
38
  },
38
- "configSchema": "config.d.ts",
39
39
  "files": [
40
- "config.d.ts",
41
40
  "dist",
42
41
  "LICENSE-MIT",
43
42
  "LICENSE-APACHE"
@@ -55,14 +54,17 @@
55
54
  },
56
55
  "dependencies": {
57
56
  "@backstage/catalog-model": "^1.7.6",
57
+ "@backstage/ui": "^0.16.0",
58
58
  "@material-ui/icons": "^4.11.3",
59
- "@rwdocs/viewer": "^0.1.21"
59
+ "@rwdocs/backstage-plugin-rw-common": "^0.1.6",
60
+ "@rwdocs/viewer": "^0.1.27"
60
61
  },
61
62
  "peerDependencies": {
62
63
  "@backstage/core-components": "^0.18.0",
63
64
  "@backstage/core-plugin-api": "^1.0.0",
64
- "@backstage/frontend-plugin-api": "^0.14.0 || ^0.15.0",
65
- "@backstage/plugin-catalog-react": "^2.0.0",
65
+ "@backstage/frontend-plugin-api": "^0.14.0 || ^0.15.0 || ^0.17.0",
66
+ "@backstage/plugin-catalog-react": "^2.0.0 || ^3.0.0",
67
+ "@backstage/plugin-search-react": "^1.11.0",
66
68
  "@material-ui/core": "^4.12.2",
67
69
  "react": "^18.0.0",
68
70
  "react-dom": "^18.0.0",
@@ -78,8 +80,9 @@
78
80
  "@backstage/config": "^1.3.6",
79
81
  "@backstage/core-components": "^0.18.0",
80
82
  "@backstage/core-plugin-api": "^1.0.0",
81
- "@backstage/frontend-plugin-api": "^0.15.0",
82
- "@backstage/plugin-catalog-react": "^2.0.0",
83
+ "@backstage/frontend-plugin-api": "^0.17.0",
84
+ "@backstage/plugin-catalog-react": "^3.0.0",
85
+ "@backstage/plugin-search-react": "^1.11.0",
83
86
  "@backstage/test-utils": "^1.7.0",
84
87
  "@testing-library/dom": "^10.0.0",
85
88
  "@testing-library/jest-dom": "^6.0.0",
package/config.d.ts DELETED
@@ -1 +0,0 @@
1
- export interface Config {}
@@ -1,10 +0,0 @@
1
- import { parseEntityRef } from '@backstage/catalog-model';
2
-
3
- function toEntityPath(ref) {
4
- const parsed = typeof ref === "string" ? parseEntityRef(ref) : ref;
5
- const ns = parsed.namespace ?? "default";
6
- return `${ns}/${parsed.kind}/${parsed.name}`.toLocaleLowerCase("en-US");
7
- }
8
-
9
- export { toEntityPath };
10
- //# sourceMappingURL=entityPath.esm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entityPath.esm.js","sources":["../../src/components/entityPath.ts"],"sourcesContent":["import { parseEntityRef } from \"@backstage/catalog-model\";\n\n/**\n * Converts an entity ref (e.g. \"component:default/arch\") or a compound ref\n * object to the slash-delimited, lowercased path used in API URLs\n * (e.g. \"default/component/arch\").\n *\n * Uses namespace/kind/name ordering to match Backstage catalog URL convention.\n *\n * NOTE: The backend plugin has a similar utility at\n * plugins/rw-backend/src/entityPath.ts — keep in sync if changing logic.\n */\nexport function toEntityPath(\n ref: string | { kind: string; namespace?: string; name: string },\n): string {\n const parsed = typeof ref === \"string\" ? parseEntityRef(ref) : ref;\n const ns = parsed.namespace ?? \"default\";\n return `${ns}/${parsed.kind}/${parsed.name}`.toLocaleLowerCase(\"en-US\");\n}\n"],"names":[],"mappings":";;AAYO,SAAS,aACd,GAAA,EACQ;AACR,EAAA,MAAM,SAAS,OAAO,GAAA,KAAQ,QAAA,GAAW,cAAA,CAAe,GAAG,CAAA,GAAI,GAAA;AAC/D,EAAA,MAAM,EAAA,GAAK,OAAO,SAAA,IAAa,SAAA;AAC/B,EAAA,OAAO,CAAA,EAAG,EAAE,CAAA,CAAA,EAAI,MAAA,CAAO,IAAI,IAAI,MAAA,CAAO,IAAI,CAAA,CAAA,CAAG,iBAAA,CAAkB,OAAO,CAAA;AACxE;;;;"}
@@ -1,35 +0,0 @@
1
- import { stringifyEntityRef, parseEntityRef } from '@backstage/catalog-model';
2
- import { toEntityPath } from './entityPath.esm.js';
3
-
4
- function parseAnnotation(value, selfEntityRef) {
5
- if (!value) return void 0;
6
- const hashIndex = value.indexOf("#");
7
- let entity;
8
- let sectionRef;
9
- if (hashIndex === -1) {
10
- entity = value;
11
- sectionRef = void 0;
12
- } else {
13
- entity = value.slice(0, hashIndex);
14
- sectionRef = value.slice(hashIndex + 1) || void 0;
15
- }
16
- if (entity === ".") {
17
- return { entityPath: selfEntityRef, entityRef: fromEntityPath(selfEntityRef), sectionRef };
18
- }
19
- try {
20
- return {
21
- entityPath: toEntityPath(entity),
22
- entityRef: stringifyEntityRef(parseEntityRef(entity)),
23
- sectionRef
24
- };
25
- } catch {
26
- return void 0;
27
- }
28
- }
29
- function fromEntityPath(path) {
30
- const [namespace, kind, name] = path.split("/");
31
- return stringifyEntityRef({ kind, namespace, name });
32
- }
33
-
34
- export { parseAnnotation };
35
- //# sourceMappingURL=parseAnnotation.esm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parseAnnotation.esm.js","sources":["../../src/components/parseAnnotation.ts"],"sourcesContent":["import { parseEntityRef, stringifyEntityRef } from \"@backstage/catalog-model\";\nimport { toEntityPath } from \"./entityPath\";\n\nexport interface ParsedAnnotation {\n /** Slash-delimited path for API URLs (e.g. \"default/component/arch\"). */\n entityPath: string;\n /** Standard Backstage entity ref (e.g. \"component:default/arch\"). */\n entityRef: string;\n sectionRef: string | undefined;\n}\n\nexport function parseAnnotation(\n value: string | undefined,\n selfEntityRef: string,\n): ParsedAnnotation | undefined {\n if (!value) return undefined;\n\n const hashIndex = value.indexOf(\"#\");\n let entity: string;\n let sectionRef: string | undefined;\n\n if (hashIndex === -1) {\n entity = value;\n sectionRef = undefined;\n } else {\n entity = value.slice(0, hashIndex);\n sectionRef = value.slice(hashIndex + 1) || undefined;\n }\n\n if (entity === \".\") {\n return { entityPath: selfEntityRef, entityRef: fromEntityPath(selfEntityRef), sectionRef };\n }\n\n try {\n return {\n entityPath: toEntityPath(entity),\n entityRef: stringifyEntityRef(parseEntityRef(entity)),\n sectionRef,\n };\n } catch {\n return undefined;\n }\n}\n\n/** Convert slash-delimited path (namespace/kind/name) back to colon-format entity ref. */\nfunction fromEntityPath(path: string): string {\n const [namespace, kind, name] = path.split(\"/\");\n return stringifyEntityRef({ kind, namespace, name });\n}\n"],"names":[],"mappings":";;;AAWO,SAAS,eAAA,CACd,OACA,aAAA,EAC8B;AAC9B,EAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AAEnB,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,OAAA,CAAQ,GAAG,CAAA;AACnC,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI,UAAA;AAEJ,EAAA,IAAI,cAAc,EAAA,EAAI;AACpB,IAAA,MAAA,GAAS,KAAA;AACT,IAAA,UAAA,GAAa,MAAA;AAAA,EACf,CAAA,MAAO;AACL,IAAA,MAAA,GAAS,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,SAAS,CAAA;AACjC,IAAA,UAAA,GAAa,KAAA,CAAM,KAAA,CAAM,SAAA,GAAY,CAAC,CAAA,IAAK,MAAA;AAAA,EAC7C;AAEA,EAAA,IAAI,WAAW,GAAA,EAAK;AAClB,IAAA,OAAO,EAAE,UAAA,EAAY,aAAA,EAAe,WAAW,cAAA,CAAe,aAAa,GAAG,UAAA,EAAW;AAAA,EAC3F;AAEA,EAAA,IAAI;AACF,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,aAAa,MAAM,CAAA;AAAA,MAC/B,SAAA,EAAW,kBAAA,CAAmB,cAAA,CAAe,MAAM,CAAC,CAAA;AAAA,MACpD;AAAA,KACF;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;AAGA,SAAS,eAAe,IAAA,EAAsB;AAC5C,EAAA,MAAM,CAAC,SAAA,EAAW,IAAA,EAAM,IAAI,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AAC9C,EAAA,OAAO,kBAAA,CAAmB,EAAE,IAAA,EAAM,SAAA,EAAW,MAAM,CAAA;AACrD;;;;"}