@peers-app/peers-ui 0.19.7 → 0.19.9

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.
@@ -1,7 +1,11 @@
1
1
  import { type DataFilter, type SortBy, type Table } from "@peers-app/peers-sdk";
2
2
  interface IProps<T extends Record<string, unknown>> {
3
3
  table: Table<T>;
4
- newRecord: (text: string) => Promise<T>;
4
+ /**
5
+ * Creates a new record from the search text when the user presses Enter. When omitted,
6
+ * the list is browse/search only (no create) and the placeholder drops "or create".
7
+ */
8
+ newRecord?: (text: string) => Promise<T>;
5
9
  getFilter?: (text: string) => DataFilter<T>;
6
10
  sortBy?: SortBy<T>;
7
11
  renderItem: (record: T) => JSX.Element;
@@ -93,13 +93,15 @@ function ListScreen(props) {
93
93
  async function searchSubmit(evt) {
94
94
  if (evt.key !== "Enter")
95
95
  return;
96
+ if (!newRecord)
97
+ return;
96
98
  const name = searchText.trim();
97
99
  if (!name)
98
100
  return;
99
101
  searchTextObs("");
100
102
  await newRecord(name);
101
103
  }
102
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-group mt-3 mb-3", children: (0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: `Search or create ${props.placeholderName || table.metaData.name}`, autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }) }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: searchText, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (notes) => {
104
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsx)("div", { className: "input-group mt-3 mb-3", children: (0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: `${newRecord ? "Search or create" : "Search"} ${props.placeholderName || table.metaData.name}`, autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }) }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: searchText, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (notes) => {
103
105
  return notes.map(props.renderItem);
104
106
  }, loadingIndicator: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 }, children: (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}) }), endOfList: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 } }) }) })] }));
105
107
  }
@@ -59,15 +59,58 @@ const VariableValue = (props) => {
59
59
  const { pvar } = props;
60
60
  const [isSecret] = (0, hooks_1.useObservable)(pvar.qs.isSecret);
61
61
  const scalarValue = (0, peers_sdk_1.observable)(pvar.value.value);
62
- // WARNING: we're assuming this is a string
62
+ // WARNING: the editable string path assumes the value is a string
63
63
  (0, hooks_1.useSubscription)(pvar.qs.value, (value) => {
64
64
  scalarValue(value?.value);
65
65
  });
66
66
  (0, hooks_1.useSubscription)(scalarValue, (value) => {
67
67
  pvar.value = { value };
68
68
  });
69
+ const valueLabel = ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-between", children: [(0, jsx_runtime_1.jsx)("small", { children: "Value:" }), (0, jsx_runtime_1.jsx)(CopyValueButton, { pvar: pvar })] }));
69
70
  if (isSecret) {
70
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("small", { children: "Value:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: scalarValue, type: "password", className: "form-control mb-3 p-0 ps-2", placeholder: "Variable value", title: "Variable value" })] }));
71
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [valueLabel, (0, jsx_runtime_1.jsx)(input_1.Input, { value: scalarValue, type: "password", className: "form-control mb-3 p-0 ps-2", placeholder: "Variable value", title: "Variable value" })] }));
71
72
  }
72
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("small", { children: "Value:" }), (0, jsx_runtime_1.jsx)(editor_inline_1.MarkdownEditorInline, { value: scalarValue, hideToolbar: true }), (0, jsx_runtime_1.jsx)("br", {})] }));
73
+ // The markdown editor only handles strings. System/package pvars may hold booleans,
74
+ // numbers, or objects, so render those read-only (still copyable via the button).
75
+ // Read directly (no subscription) to avoid a render/write-back loop with scalarValue.
76
+ const scalar = pvar.value?.value;
77
+ if (typeof scalar !== "string") {
78
+ const isObject = scalar !== null && typeof scalar === "object";
79
+ const display = scalar === undefined || scalar === null
80
+ ? ""
81
+ : isObject
82
+ ? JSON.stringify(scalar, null, 2)
83
+ : String(scalar);
84
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [valueLabel, isObject ? ((0, jsx_runtime_1.jsx)("pre", { className: "form-control mb-1 p-2", style: { whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: 300 }, children: display })) : ((0, jsx_runtime_1.jsx)("input", { className: "form-control mb-1 p-0 ps-2", value: display, readOnly: true, title: "Variable value" })), (0, jsx_runtime_1.jsxs)("div", { className: "form-text text-muted mb-3", children: ["This ", typeof scalar, " value is read-only here \u2014 use Copy Value to copy it."] })] }));
85
+ }
86
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [valueLabel, (0, jsx_runtime_1.jsx)(editor_inline_1.MarkdownEditorInline, { value: scalarValue, hideToolbar: true }), (0, jsx_runtime_1.jsx)("br", {})] }));
87
+ };
88
+ /**
89
+ * Button that copies a persistent variable's value to the clipboard. Decryption (for
90
+ * secret values) and the clipboard write happen entirely on the server/main process via
91
+ * {@link rpcServerCalls.copyPvarValueToClipboard}, so the plaintext is never exposed to
92
+ * the UI. Shows transient success/error feedback on the icon.
93
+ */
94
+ const CopyValueButton = (props) => {
95
+ const { pvar } = props;
96
+ const [status, setStatus] = (0, react_1.useState)("idle");
97
+ async function onCopy() {
98
+ try {
99
+ const userContext = await (0, peers_sdk_1.getUserContext)();
100
+ const groupId = userContext.currentlyActiveGroupId();
101
+ const result = await peers_sdk_1.rpcServerCalls.copyPvarValueToClipboard(pvar.persistentVarId, groupId);
102
+ setStatus(result.success ? "copied" : "error");
103
+ }
104
+ catch (err) {
105
+ console.error("Failed to copy variable value", err);
106
+ setStatus("error");
107
+ }
108
+ setTimeout(() => setStatus("idle"), 1500);
109
+ }
110
+ const iconClass = status === "copied"
111
+ ? "bi-check-lg text-success"
112
+ : status === "error"
113
+ ? "bi-exclamation-triangle text-danger"
114
+ : "bi-clipboard";
115
+ return ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-sm btn-outline-secondary", onClick: onCopy, title: "Copy value to clipboard", children: [(0, jsx_runtime_1.jsx)("i", { className: iconClass }), (0, jsx_runtime_1.jsx)("span", { className: "ms-1", children: "Copy Value" })] }));
73
116
  };
@@ -5,9 +5,14 @@ const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const peers_sdk_1 = require("@peers-app/peers-sdk");
6
6
  const list_screen_1 = require("../../components/list-screen");
7
7
  const markdown_with_mentions_1 = require("../../components/markdown-with-mentions");
8
+ const tabs_1 = require("../../components/tabs");
8
9
  const globals_1 = require("../../globals");
9
10
  const ui_loader_1 = require("../../ui-router/ui-loader");
10
- function VariableList() {
11
+ function renderItem(persistentVar) {
12
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4", children: [(0, jsx_runtime_1.jsx)("i", { className: (0, peers_sdk_1.getIconClassName)((0, peers_sdk_1.PersistentVars)()) }), "\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#variables/${persistentVar.persistentVarId}`, children: persistentVar.name || "<empty-name>" }), (0, jsx_runtime_1.jsx)("div", { style: { paddingLeft: "20px" }, children: (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: persistentVar.description || "" }) })] }, persistentVar.persistentVarId));
13
+ }
14
+ /** List of user-created variables, with search-to-create. */
15
+ function UserVariableList() {
11
16
  async function newRecord(name) {
12
17
  const userContext = await (0, peers_sdk_1.getUserContext)();
13
18
  const isPersonal = userContext.currentlyActiveGroupId() === userContext.userId;
@@ -29,11 +34,28 @@ function VariableList() {
29
34
  userCreated: true,
30
35
  };
31
36
  }
32
- function renderItem(persistentVar) {
33
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4", children: [(0, jsx_runtime_1.jsx)("i", { className: (0, peers_sdk_1.getIconClassName)((0, peers_sdk_1.PersistentVars)()) }), "\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#variables/${persistentVar.persistentVarId}`, children: persistentVar.name || "<empty-name>" }), (0, jsx_runtime_1.jsx)("div", { style: { paddingLeft: "20px" }, children: (0, jsx_runtime_1.jsx)(markdown_with_mentions_1.MarkdownWithMentions, { content: persistentVar.description || "" }) })] }, persistentVar.persistentVarId));
34
- }
35
37
  return ((0, jsx_runtime_1.jsx)(list_screen_1.ListScreen, { table: (0, peers_sdk_1.PersistentVars)(), newRecord: newRecord, getFilter: getFilter, sortBy: ["name"], renderItem: renderItem, placeholderName: "environment variable" }));
36
38
  }
39
+ /**
40
+ * Browse/search list of non-user-created variables (system and package-owned pvars).
41
+ * These are read-only here (no create); selecting one opens the same detail screen.
42
+ */
43
+ function SystemVariableList() {
44
+ function getFilter(searchText) {
45
+ return {
46
+ $or: [{ name: { $matchWords: searchText } }, { description: { $matchWords: searchText } }],
47
+ // $ne: true matches both `false` and unset (system/package pvars omit userCreated)
48
+ userCreated: { $ne: true },
49
+ };
50
+ }
51
+ return ((0, jsx_runtime_1.jsx)(list_screen_1.ListScreen, { table: (0, peers_sdk_1.PersistentVars)(), getFilter: getFilter, sortBy: ["name"], renderItem: renderItem, placeholderName: "system variable" }));
52
+ }
53
+ function VariableList() {
54
+ return ((0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: [
55
+ { name: "User", content: (0, jsx_runtime_1.jsx)(UserVariableList, {}) },
56
+ { name: "System", content: (0, jsx_runtime_1.jsx)(SystemVariableList, {}) },
57
+ ] }));
58
+ }
37
59
  (0, ui_loader_1.registerInternalPeersUI)({
38
60
  peersUIId: "00m5fshz2g6ea23v8z6y0a1ci",
39
61
  component: VariableList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.19.7",
3
+ "version": "0.19.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.19.7",
31
+ "@peers-app/peers-sdk": "^0.19.9",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-env": "^7.24.5",
40
40
  "@babel/preset-react": "^7.24.1",
41
41
  "@babel/preset-typescript": "^7.27.1",
42
- "@peers-app/peers-sdk": "0.19.7",
42
+ "@peers-app/peers-sdk": "0.19.9",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
@@ -16,7 +16,11 @@ import { LoadingIndicator } from "./loading-indicator";
16
16
 
17
17
  interface IProps<T extends Record<string, unknown>> {
18
18
  table: Table<T>;
19
- newRecord: (text: string) => Promise<T>;
19
+ /**
20
+ * Creates a new record from the search text when the user presses Enter. When omitted,
21
+ * the list is browse/search only (no create) and the placeholder drops "or create".
22
+ */
23
+ newRecord?: (text: string) => Promise<T>;
20
24
  getFilter?: (text: string) => DataFilter<T>;
21
25
  sortBy?: SortBy<T>;
22
26
  renderItem: (record: T) => JSX.Element;
@@ -78,6 +82,7 @@ export function ListScreen<T extends Record<string, unknown>>(props: IProps<T>)
78
82
 
79
83
  async function searchSubmit(evt: React.KeyboardEvent<HTMLInputElement>) {
80
84
  if (evt.key !== "Enter") return;
85
+ if (!newRecord) return;
81
86
  const name = searchText.trim();
82
87
  if (!name) return;
83
88
  searchTextObs("");
@@ -90,7 +95,7 @@ export function ListScreen<T extends Record<string, unknown>>(props: IProps<T>)
90
95
  <Input
91
96
  value={searchTextObs}
92
97
  className="form-control"
93
- placeholder={`Search or create ${props.placeholderName || table.metaData.name}`}
98
+ placeholder={`${newRecord ? "Search or create" : "Search"} ${props.placeholderName || table.metaData.name}`}
94
99
  autoFocus={!!isDesktop()}
95
100
  onKeyUp={(evt) => searchSubmit(evt)}
96
101
  />
@@ -1,13 +1,15 @@
1
1
  import {
2
2
  camelCaseToSpaces,
3
3
  getIconClassName,
4
+ getUserContext,
4
5
  type IDoc,
5
6
  type IPersistentVar,
6
7
  type Observable,
7
8
  observable,
8
9
  PersistentVars,
10
+ rpcServerCalls,
9
11
  } from "@peers-app/peers-sdk";
10
- import { useEffect } from "react";
12
+ import { useEffect, useState } from "react";
11
13
  import { Checkbox } from "../../components/checkbox";
12
14
  import { Input } from "../../components/input";
13
15
  import { LoadingIndicator } from "../../components/loading-indicator";
@@ -162,7 +164,7 @@ const VariableValue = (props: { pvar: IDoc<IPersistentVar> }) => {
162
164
 
163
165
  const [isSecret] = useObservable(pvar.qs.isSecret);
164
166
  const scalarValue = observable(pvar.value.value);
165
- // WARNING: we're assuming this is a string
167
+ // WARNING: the editable string path assumes the value is a string
166
168
  useSubscription(pvar.qs.value, (value) => {
167
169
  scalarValue(value?.value);
168
170
  });
@@ -170,10 +172,17 @@ const VariableValue = (props: { pvar: IDoc<IPersistentVar> }) => {
170
172
  pvar.value = { value };
171
173
  });
172
174
 
175
+ const valueLabel = (
176
+ <div className="d-flex align-items-center justify-content-between">
177
+ <small>Value:</small>
178
+ <CopyValueButton pvar={pvar} />
179
+ </div>
180
+ );
181
+
173
182
  if (isSecret) {
174
183
  return (
175
184
  <>
176
- <small>Value:</small>
185
+ {valueLabel}
177
186
  <Input
178
187
  value={scalarValue}
179
188
  type="password"
@@ -185,11 +194,91 @@ const VariableValue = (props: { pvar: IDoc<IPersistentVar> }) => {
185
194
  );
186
195
  }
187
196
 
197
+ // The markdown editor only handles strings. System/package pvars may hold booleans,
198
+ // numbers, or objects, so render those read-only (still copyable via the button).
199
+ // Read directly (no subscription) to avoid a render/write-back loop with scalarValue.
200
+ const scalar = pvar.value?.value;
201
+ if (typeof scalar !== "string") {
202
+ const isObject = scalar !== null && typeof scalar === "object";
203
+ const display =
204
+ scalar === undefined || scalar === null
205
+ ? ""
206
+ : isObject
207
+ ? JSON.stringify(scalar, null, 2)
208
+ : String(scalar);
209
+ return (
210
+ <>
211
+ {valueLabel}
212
+ {isObject ? (
213
+ <pre
214
+ className="form-control mb-1 p-2"
215
+ style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: 300 }}
216
+ >
217
+ {display}
218
+ </pre>
219
+ ) : (
220
+ <input
221
+ className="form-control mb-1 p-0 ps-2"
222
+ value={display}
223
+ readOnly
224
+ title="Variable value"
225
+ />
226
+ )}
227
+ <div className="form-text text-muted mb-3">
228
+ This {typeof scalar} value is read-only here — use Copy Value to copy it.
229
+ </div>
230
+ </>
231
+ );
232
+ }
233
+
188
234
  return (
189
235
  <>
190
- <small>Value:</small>
236
+ {valueLabel}
191
237
  <MarkdownEditorInline value={scalarValue} hideToolbar />
192
238
  <br />
193
239
  </>
194
240
  );
195
241
  };
242
+
243
+ /**
244
+ * Button that copies a persistent variable's value to the clipboard. Decryption (for
245
+ * secret values) and the clipboard write happen entirely on the server/main process via
246
+ * {@link rpcServerCalls.copyPvarValueToClipboard}, so the plaintext is never exposed to
247
+ * the UI. Shows transient success/error feedback on the icon.
248
+ */
249
+ const CopyValueButton = (props: { pvar: IDoc<IPersistentVar> }) => {
250
+ const { pvar } = props;
251
+ const [status, setStatus] = useState<"idle" | "copied" | "error">("idle");
252
+
253
+ async function onCopy() {
254
+ try {
255
+ const userContext = await getUserContext();
256
+ const groupId = userContext.currentlyActiveGroupId();
257
+ const result = await rpcServerCalls.copyPvarValueToClipboard(pvar.persistentVarId, groupId);
258
+ setStatus(result.success ? "copied" : "error");
259
+ } catch (err) {
260
+ console.error("Failed to copy variable value", err);
261
+ setStatus("error");
262
+ }
263
+ setTimeout(() => setStatus("idle"), 1500);
264
+ }
265
+
266
+ const iconClass =
267
+ status === "copied"
268
+ ? "bi-check-lg text-success"
269
+ : status === "error"
270
+ ? "bi-exclamation-triangle text-danger"
271
+ : "bi-clipboard";
272
+
273
+ return (
274
+ <button
275
+ type="button"
276
+ className="btn btn-sm btn-outline-secondary"
277
+ onClick={onCopy}
278
+ title="Copy value to clipboard"
279
+ >
280
+ <i className={iconClass}></i>
281
+ <span className="ms-1">Copy Value</span>
282
+ </button>
283
+ );
284
+ };
@@ -7,10 +7,26 @@ import {
7
7
  } from "@peers-app/peers-sdk";
8
8
  import { ListScreen } from "../../components/list-screen";
9
9
  import { MarkdownWithMentions } from "../../components/markdown-with-mentions";
10
+ import { Tabs } from "../../components/tabs";
10
11
  import { mainContentPath } from "../../globals";
11
12
  import { registerInternalPeersUI } from "../../ui-router/ui-loader";
12
13
 
13
- export function VariableList() {
14
+ function renderItem(persistentVar: IPersistentVar) {
15
+ return (
16
+ <div key={persistentVar.persistentVarId} className="container-fluid pb-4">
17
+ <i className={getIconClassName(PersistentVars())}></i>&nbsp;
18
+ <a href={`#variables/${persistentVar.persistentVarId}`}>
19
+ {persistentVar.name || "<empty-name>"}
20
+ </a>
21
+ <div style={{ paddingLeft: "20px" }}>
22
+ <MarkdownWithMentions content={persistentVar.description || ""} />
23
+ </div>
24
+ </div>
25
+ );
26
+ }
27
+
28
+ /** List of user-created variables, with search-to-create. */
29
+ function UserVariableList() {
14
30
  async function newRecord(name: string) {
15
31
  const userContext = await getUserContext();
16
32
  const isPersonal = userContext.currentlyActiveGroupId() === userContext.userId;
@@ -34,20 +50,6 @@ export function VariableList() {
34
50
  };
35
51
  }
36
52
 
37
- function renderItem(persistentVar: IPersistentVar) {
38
- return (
39
- <div key={persistentVar.persistentVarId} className="container-fluid pb-4">
40
- <i className={getIconClassName(PersistentVars())}></i>&nbsp;
41
- <a href={`#variables/${persistentVar.persistentVarId}`}>
42
- {persistentVar.name || "<empty-name>"}
43
- </a>
44
- <div style={{ paddingLeft: "20px" }}>
45
- <MarkdownWithMentions content={persistentVar.description || ""} />
46
- </div>
47
- </div>
48
- );
49
- }
50
-
51
53
  return (
52
54
  <ListScreen
53
55
  table={PersistentVars()}
@@ -60,6 +62,41 @@ export function VariableList() {
60
62
  );
61
63
  }
62
64
 
65
+ /**
66
+ * Browse/search list of non-user-created variables (system and package-owned pvars).
67
+ * These are read-only here (no create); selecting one opens the same detail screen.
68
+ */
69
+ function SystemVariableList() {
70
+ function getFilter(searchText: string) {
71
+ return {
72
+ $or: [{ name: { $matchWords: searchText } }, { description: { $matchWords: searchText } }],
73
+ // $ne: true matches both `false` and unset (system/package pvars omit userCreated)
74
+ userCreated: { $ne: true },
75
+ };
76
+ }
77
+
78
+ return (
79
+ <ListScreen
80
+ table={PersistentVars()}
81
+ getFilter={getFilter}
82
+ sortBy={["name"]}
83
+ renderItem={renderItem}
84
+ placeholderName="system variable"
85
+ />
86
+ );
87
+ }
88
+
89
+ export function VariableList() {
90
+ return (
91
+ <Tabs
92
+ tabs={[
93
+ { name: "User", content: <UserVariableList /> },
94
+ { name: "System", content: <SystemVariableList /> },
95
+ ]}
96
+ />
97
+ );
98
+ }
99
+
63
100
  registerInternalPeersUI({
64
101
  peersUIId: "00m5fshz2g6ea23v8z6y0a1ci",
65
102
  component: VariableList,