@rwdocs/backstage-plugin-rw 0.1.5 → 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.
- package/dist/api/RwClient.esm.js +65 -0
- package/dist/api/RwClient.esm.js.map +1 -1
- package/dist/components/CommentInboxList.esm.js +262 -0
- package/dist/components/CommentInboxList.esm.js.map +1 -0
- package/dist/components/CommentInboxPage.esm.js +10 -0
- package/dist/components/CommentInboxPage.esm.js.map +1 -0
- package/dist/components/RwDocsViewer.esm.js +9 -3
- package/dist/components/RwDocsViewer.esm.js.map +1 -1
- package/dist/components/RwEntityDocsViewer.esm.js +29 -7
- package/dist/components/RwEntityDocsViewer.esm.js.map +1 -1
- package/dist/components/constants.esm.js +5 -1
- package/dist/components/constants.esm.js.map +1 -1
- package/dist/components/inboxBuckets.esm.js +38 -0
- package/dist/components/inboxBuckets.esm.js.map +1 -0
- package/dist/components/useInboxData.esm.js +70 -0
- package/dist/components/useInboxData.esm.js.map +1 -0
- package/dist/components/useInboxFilters.esm.js +40 -0
- package/dist/components/useInboxFilters.esm.js.map +1 -0
- package/dist/components/useSectionRefResolver.esm.js +3 -4
- package/dist/components/useSectionRefResolver.esm.js.map +1 -1
- package/dist/hooks/useRwDocsIconLinkProps.esm.js +2 -1
- package/dist/hooks/useRwDocsIconLinkProps.esm.js.map +1 -1
- package/dist/index.d.ts +10 -95
- package/dist/plugin.esm.js +32 -3
- package/dist/plugin.esm.js.map +1 -1
- package/package.json +8 -7
package/dist/api/RwClient.esm.js
CHANGED
|
@@ -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":";;
|
|
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({
|
|
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({
|
|
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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"}
|
|
@@ -14,17 +14,38 @@ function RwEntityDocsViewer() {
|
|
|
14
14
|
const rwApi = useApi(rwApiRef);
|
|
15
15
|
const [apiBaseUrl, setApiBaseUrl] = useState(null);
|
|
16
16
|
const [fetchError, setFetchError] = useState(null);
|
|
17
|
+
const [commentClient, setCommentClient] = useState(void 0);
|
|
18
|
+
const [commentsReady, setCommentsReady] = useState(false);
|
|
17
19
|
const annotationValue = entity.metadata.annotations?.[ANNOTATION_KEY];
|
|
18
20
|
const selfEntityRef = useMemo(() => toEntityPath(getCompoundEntityRef(entity)), [entity]);
|
|
19
21
|
const parsed = parseAnnotation(annotationValue, selfEntityRef);
|
|
20
22
|
useEffect(() => {
|
|
21
23
|
if (!parsed) return void 0;
|
|
24
|
+
setApiBaseUrl(null);
|
|
25
|
+
setFetchError(null);
|
|
26
|
+
setCommentsReady(false);
|
|
27
|
+
setCommentClient(void 0);
|
|
22
28
|
let cancelled = false;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
})();
|
|
28
49
|
return () => {
|
|
29
50
|
cancelled = true;
|
|
30
51
|
};
|
|
@@ -35,7 +56,7 @@ function RwEntityDocsViewer() {
|
|
|
35
56
|
if (fetchError) {
|
|
36
57
|
return /* @__PURE__ */ jsx(ErrorPanel, { error: fetchError });
|
|
37
58
|
}
|
|
38
|
-
if (!apiBaseUrl) {
|
|
59
|
+
if (!apiBaseUrl || !commentsReady) {
|
|
39
60
|
return /* @__PURE__ */ jsx(Progress, {});
|
|
40
61
|
}
|
|
41
62
|
const sectionRef = parsed.sectionRef ?? selfEntityRef;
|
|
@@ -44,7 +65,8 @@ function RwEntityDocsViewer() {
|
|
|
44
65
|
{
|
|
45
66
|
apiBaseUrl,
|
|
46
67
|
sectionRef,
|
|
47
|
-
sourceEntityRef: parsed.entityRef
|
|
68
|
+
sourceEntityRef: parsed.entityRef,
|
|
69
|
+
comments: commentClient
|
|
48
70
|
}
|
|
49
71
|
);
|
|
50
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 { toEntityPath, parseAnnotation } from \"@rwdocs/backstage-plugin-rw-common\";\nimport { rwApiRef } from \"../api/RwClient\";\nimport { ANNOTATION_KEY } from \"./constants\";\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
|
|
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":"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"}
|
|
@@ -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
|
|
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
|
|
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\
|
|
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 ?
|
|
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 ?
|
|
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,106 +1,18 @@
|
|
|
1
|
-
import * as _backstage_plugin_search_react_alpha from '@backstage/plugin-search-react/alpha';
|
|
2
|
-
import * as _backstage_core_components from '@backstage/core-components';
|
|
3
|
-
import * as _backstage_catalog_model from '@backstage/catalog-model';
|
|
4
|
-
import * as react from 'react';
|
|
5
|
-
import * as _backstage_filter_predicates from '@backstage/filter-predicates';
|
|
6
1
|
import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
|
|
2
|
+
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
|
|
7
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';
|
|
8
6
|
|
|
9
|
-
declare const rwPlugin:
|
|
10
|
-
"api:rw": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
11
|
-
kind: "api";
|
|
12
|
-
name: undefined;
|
|
13
|
-
config: {};
|
|
14
|
-
configInput: {};
|
|
15
|
-
output: _backstage_frontend_plugin_api.ExtensionDataRef<_backstage_frontend_plugin_api.AnyApiFactory, "core.api.factory", {}>;
|
|
16
|
-
inputs: {};
|
|
17
|
-
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>;
|
|
18
|
-
}>;
|
|
19
|
-
"entity-content:rw": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
20
|
-
kind: "entity-content";
|
|
21
|
-
name: undefined;
|
|
22
|
-
config: {
|
|
23
|
-
path: string | undefined;
|
|
24
|
-
title: string | undefined;
|
|
25
|
-
filter: _backstage_filter_predicates.FilterPredicate | undefined;
|
|
26
|
-
group: string | false | undefined;
|
|
27
|
-
icon: string | undefined;
|
|
28
|
-
};
|
|
29
|
-
configInput: {
|
|
30
|
-
filter?: _backstage_filter_predicates.FilterPredicate | undefined;
|
|
31
|
-
title?: string | undefined;
|
|
32
|
-
path?: string | undefined;
|
|
33
|
-
group?: string | false | undefined;
|
|
34
|
-
icon?: string | undefined;
|
|
35
|
-
};
|
|
36
|
-
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", {
|
|
37
|
-
optional: true;
|
|
38
|
-
}> | _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", {
|
|
39
|
-
optional: true;
|
|
40
|
-
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-filter-expression", {
|
|
41
|
-
optional: true;
|
|
42
|
-
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-content-title", {}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-content-group", {
|
|
43
|
-
optional: true;
|
|
44
|
-
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string | react.ReactElement<any, string | react.JSXElementConstructor<any>>, "catalog.entity-content-icon", {
|
|
45
|
-
optional: true;
|
|
46
|
-
}>;
|
|
47
|
-
inputs: {};
|
|
48
|
-
params: {
|
|
49
|
-
defaultPath?: [Error: `Use the 'path' param instead`];
|
|
50
|
-
path: string;
|
|
51
|
-
defaultTitle?: [Error: `Use the 'title' param instead`];
|
|
52
|
-
title: string;
|
|
53
|
-
defaultGroup?: [Error: `Use the 'group' param instead`];
|
|
54
|
-
group?: ("overview" | "documentation" | "development" | "deployment" | "operation" | "observability") | (string & {});
|
|
55
|
-
icon?: string | react.ReactElement;
|
|
56
|
-
loader: () => Promise<JSX.Element>;
|
|
57
|
-
routeRef?: _backstage_frontend_plugin_api.RouteRef;
|
|
58
|
-
filter?: string | _backstage_filter_predicates.FilterPredicate | ((entity: _backstage_catalog_model.Entity) => boolean);
|
|
59
|
-
};
|
|
60
|
-
}>;
|
|
61
|
-
"entity-icon-link:rw/view-docs": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
62
|
-
kind: "entity-icon-link";
|
|
63
|
-
name: "view-docs";
|
|
64
|
-
config: {
|
|
65
|
-
label: string | undefined;
|
|
66
|
-
title: string | undefined;
|
|
67
|
-
filter: _backstage_filter_predicates.FilterPredicate | undefined;
|
|
68
|
-
};
|
|
69
|
-
configInput: {
|
|
70
|
-
filter?: _backstage_filter_predicates.FilterPredicate | undefined;
|
|
71
|
-
label?: string | undefined;
|
|
72
|
-
title?: string | undefined;
|
|
73
|
-
};
|
|
74
|
-
output: _backstage_frontend_plugin_api.ExtensionDataRef<(entity: _backstage_catalog_model.Entity) => boolean, "catalog.entity-filter-function", {
|
|
75
|
-
optional: true;
|
|
76
|
-
}> | _backstage_frontend_plugin_api.ExtensionDataRef<string, "catalog.entity-filter-expression", {
|
|
77
|
-
optional: true;
|
|
78
|
-
}> | _backstage_frontend_plugin_api.ExtensionDataRef<() => _backstage_core_components.IconLinkVerticalProps, "entity-icon-link-props", {}>;
|
|
79
|
-
inputs: {};
|
|
80
|
-
params: {
|
|
81
|
-
useProps: () => Omit<_backstage_core_components.IconLinkVerticalProps, "color">;
|
|
82
|
-
filter?: _backstage_filter_predicates.FilterPredicate | ((entity: _backstage_catalog_model.Entity) => boolean);
|
|
83
|
-
};
|
|
84
|
-
}>;
|
|
85
|
-
"search-filter-result-type:rw": _backstage_frontend_plugin_api.OverridableExtensionDefinition<{
|
|
86
|
-
kind: "search-filter-result-type";
|
|
87
|
-
name: undefined;
|
|
88
|
-
config: {};
|
|
89
|
-
configInput: {};
|
|
90
|
-
output: _backstage_frontend_plugin_api.ExtensionDataRef<{
|
|
91
|
-
value: string;
|
|
92
|
-
name: string;
|
|
93
|
-
icon: react.JSX.Element;
|
|
94
|
-
}, "search.filters.result-types.type", {}>;
|
|
95
|
-
inputs: {};
|
|
96
|
-
params: _backstage_plugin_search_react_alpha.SearchFilterResultTypeBlueprintParams;
|
|
97
|
-
}>;
|
|
98
|
-
}>;
|
|
7
|
+
declare const rwPlugin: FrontendPlugin;
|
|
99
8
|
|
|
100
9
|
interface RwApi {
|
|
101
10
|
getBaseUrl(): Promise<string>;
|
|
102
11
|
getSiteBaseUrl(entityRef: string): Promise<string>;
|
|
103
12
|
getFetch(): typeof fetch;
|
|
13
|
+
getCommentsEnabled(): Promise<boolean>;
|
|
14
|
+
getCommentInbox(query?: InboxQuery): Promise<InboxResponse>;
|
|
15
|
+
createCommentClient(siteRef: string): CommentApiClient;
|
|
104
16
|
}
|
|
105
17
|
declare const rwApiRef: _backstage_frontend_plugin_api.ApiRef<RwApi>;
|
|
106
18
|
declare class RwClient implements RwApi {
|
|
@@ -113,6 +25,9 @@ declare class RwClient implements RwApi {
|
|
|
113
25
|
getBaseUrl(): Promise<string>;
|
|
114
26
|
getSiteBaseUrl(entityRef: string): Promise<string>;
|
|
115
27
|
getFetch(): typeof fetch;
|
|
28
|
+
getCommentsEnabled(): Promise<boolean>;
|
|
29
|
+
getCommentInbox(query?: InboxQuery): Promise<InboxResponse>;
|
|
30
|
+
createCommentClient(siteRef: string): CommentApiClient;
|
|
116
31
|
}
|
|
117
32
|
|
|
118
33
|
export { RwClient, rwPlugin as default, rwApiRef };
|
package/dist/plugin.esm.js
CHANGED
|
@@ -1,13 +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
5
|
import { SearchFilterResultTypeBlueprint } from '@backstage/plugin-search-react/alpha';
|
|
6
6
|
import DocsIcon from '@material-ui/icons/Description';
|
|
7
|
+
import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
|
|
7
8
|
import { RwClient, rwApiRef } from './api/RwClient.esm.js';
|
|
8
9
|
import { ANNOTATION_KEY } from './components/constants.esm.js';
|
|
9
10
|
import { useRwDocsIconLinkProps } from './hooks/useRwDocsIconLinkProps.esm.js';
|
|
10
11
|
|
|
12
|
+
const docsRouteRef = createRouteRef();
|
|
13
|
+
const commentInboxRouteRef = createRouteRef();
|
|
11
14
|
const rwApi = ApiBlueprint.make({
|
|
12
15
|
params: (defineParams) => defineParams(
|
|
13
16
|
createApiFactory({
|
|
@@ -40,10 +43,36 @@ const rwSearchResultType = SearchFilterResultTypeBlueprint.make({
|
|
|
40
43
|
icon: /* @__PURE__ */ jsx(DocsIcon, {})
|
|
41
44
|
}
|
|
42
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
|
+
});
|
|
43
65
|
const rwPlugin = createFrontendPlugin({
|
|
44
66
|
pluginId: "rw",
|
|
45
|
-
extensions: [
|
|
67
|
+
extensions: [
|
|
68
|
+
rwApi,
|
|
69
|
+
rwEntityContent,
|
|
70
|
+
rwEntityIconLink,
|
|
71
|
+
rwSearchResultType,
|
|
72
|
+
rwDocsPage,
|
|
73
|
+
rwCommentsSubPage
|
|
74
|
+
]
|
|
46
75
|
});
|
|
47
76
|
|
|
48
|
-
export { rwPlugin };
|
|
77
|
+
export { commentInboxRouteRef, docsRouteRef, rwPlugin };
|
|
49
78
|
//# sourceMappingURL=plugin.esm.js.map
|
package/dist/plugin.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.esm.js","sources":["../src/plugin.tsx"],"sourcesContent":["import {
|
|
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.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"license": "MIT OR Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -54,15 +54,16 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@backstage/catalog-model": "^1.7.6",
|
|
57
|
+
"@backstage/ui": "^0.16.0",
|
|
57
58
|
"@material-ui/icons": "^4.11.3",
|
|
58
|
-
"@rwdocs/backstage-plugin-rw-common": "^0.1.
|
|
59
|
-
"@rwdocs/viewer": "^0.1.
|
|
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",
|
|
66
67
|
"@backstage/plugin-search-react": "^1.11.0",
|
|
67
68
|
"@material-ui/core": "^4.12.2",
|
|
68
69
|
"react": "^18.0.0",
|
|
@@ -79,8 +80,8 @@
|
|
|
79
80
|
"@backstage/config": "^1.3.6",
|
|
80
81
|
"@backstage/core-components": "^0.18.0",
|
|
81
82
|
"@backstage/core-plugin-api": "^1.0.0",
|
|
82
|
-
"@backstage/frontend-plugin-api": "^0.
|
|
83
|
-
"@backstage/plugin-catalog-react": "^
|
|
83
|
+
"@backstage/frontend-plugin-api": "^0.17.0",
|
|
84
|
+
"@backstage/plugin-catalog-react": "^3.0.0",
|
|
84
85
|
"@backstage/plugin-search-react": "^1.11.0",
|
|
85
86
|
"@backstage/test-utils": "^1.7.0",
|
|
86
87
|
"@testing-library/dom": "^10.0.0",
|