@fewchurs/react 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core.d.mts +5 -4
- package/dist/core.d.ts +5 -4
- package/dist/core.js +52 -29
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +51 -29
- package/dist/core.mjs.map +1 -1
- package/dist/index.d.mts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +152 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -70
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +41 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(src_exports, {
|
|
|
38
38
|
FewchursProvider: () => FewchursProvider,
|
|
39
39
|
FollowForm: () => FollowForm,
|
|
40
40
|
LIMITS: () => LIMITS,
|
|
41
|
+
NOT_CONFIGURED_MESSAGE: () => NOT_CONFIGURED_MESSAGE,
|
|
41
42
|
ROADMAP_STATUSES: () => ROADMAP_STATUSES,
|
|
42
43
|
SDK_HEADER: () => SDK_HEADER,
|
|
43
44
|
SDK_VERSION: () => SDK_VERSION,
|
|
@@ -85,7 +86,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
85
86
|
var import_react = require("react");
|
|
86
87
|
|
|
87
88
|
// src/client/version.ts
|
|
88
|
-
var SDK_VERSION = "1.
|
|
89
|
+
var SDK_VERSION = "1.5.0";
|
|
89
90
|
|
|
90
91
|
// src/client/constants.ts
|
|
91
92
|
var SDK_HEADER = `react/${SDK_VERSION}`;
|
|
@@ -103,17 +104,28 @@ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
103
104
|
var ROADMAP_STATUSES = ["planned", "in_progress", "done"];
|
|
104
105
|
|
|
105
106
|
// src/client/errors.ts
|
|
107
|
+
var NOT_CONFIGURED_MESSAGE = "Fewchurs is not configured.";
|
|
106
108
|
var USER_MESSAGES = {
|
|
107
|
-
invalid_key: "
|
|
108
|
-
validation_failed: "
|
|
109
|
-
rate_limited: "You
|
|
110
|
-
not_found: "
|
|
111
|
-
limit_reached: "
|
|
112
|
-
server: "Something went wrong.
|
|
113
|
-
network: "
|
|
109
|
+
invalid_key: "Invalid API key. Check the key in your Fewchurs dashboard.",
|
|
110
|
+
validation_failed: "Invalid input.",
|
|
111
|
+
rate_limited: "You've reached today's limit. Try again tomorrow.",
|
|
112
|
+
not_found: "This request no longer exists.",
|
|
113
|
+
limit_reached: "This board isn't accepting requests right now.",
|
|
114
|
+
server: "Something went wrong. Try again.",
|
|
115
|
+
network: "You're offline."
|
|
114
116
|
};
|
|
117
|
+
function isWrongApp(error) {
|
|
118
|
+
return error.code === "invalid_key" && error.status === 403;
|
|
119
|
+
}
|
|
120
|
+
function isNotConfigured(error) {
|
|
121
|
+
return error.code === "server" && error.message === NOT_CONFIGURED_MESSAGE;
|
|
122
|
+
}
|
|
123
|
+
function notConfiguredError() {
|
|
124
|
+
return { code: "server", status: 500, message: NOT_CONFIGURED_MESSAGE };
|
|
125
|
+
}
|
|
115
126
|
function userMessage(error) {
|
|
116
127
|
if (error.code === "validation_failed" && error.message) return error.message;
|
|
128
|
+
if (isWrongApp(error) && error.message) return error.message;
|
|
117
129
|
return USER_MESSAGES[error.code];
|
|
118
130
|
}
|
|
119
131
|
function networkError(message = "Network request failed.") {
|
|
@@ -124,17 +136,17 @@ function validationError(message) {
|
|
|
124
136
|
}
|
|
125
137
|
function errorFromResponse(status, body) {
|
|
126
138
|
const code = body?.error?.code;
|
|
127
|
-
const
|
|
139
|
+
const serverMessage = body?.error?.message;
|
|
140
|
+
const message = serverMessage ?? `Request failed (${status}).`;
|
|
128
141
|
switch (code) {
|
|
129
142
|
case "invalid_key":
|
|
130
143
|
return { code: "invalid_key", status: 401, message };
|
|
131
144
|
case "wrong_app":
|
|
132
145
|
return { code: "invalid_key", status: 403, message };
|
|
133
|
-
// missing_device_id is a client mistake the caller fixes the same way a bad field is fixed,
|
|
134
|
-
// so it collapses into validation_failed rather than widening the public union.
|
|
135
146
|
case "missing_device_id":
|
|
147
|
+
return { code: "server", status: 400, message };
|
|
136
148
|
case "validation_failed":
|
|
137
|
-
return { code: "validation_failed", status: 400, message };
|
|
149
|
+
return { code: "validation_failed", status: 400, message: serverMessage ?? "Invalid input." };
|
|
138
150
|
case "rate_limited":
|
|
139
151
|
return { code: "rate_limited", status: 429, message };
|
|
140
152
|
case "not_found":
|
|
@@ -228,11 +240,8 @@ function normalizeVote(value) {
|
|
|
228
240
|
// src/client/validate.ts
|
|
229
241
|
function validateFeatureInput(input) {
|
|
230
242
|
const title = input.title.trim();
|
|
231
|
-
if (title.length < LIMITS.titleMin) {
|
|
232
|
-
return validationError(`Title must be
|
|
233
|
-
}
|
|
234
|
-
if (title.length > LIMITS.titleMax) {
|
|
235
|
-
return validationError(`Title must be at most ${LIMITS.titleMax} characters.`);
|
|
243
|
+
if (title.length < LIMITS.titleMin || title.length > LIMITS.titleMax) {
|
|
244
|
+
return validationError(`Title must be ${LIMITS.titleMin}\u2013${LIMITS.titleMax} characters.`);
|
|
236
245
|
}
|
|
237
246
|
if ((input.description ?? "").trim().length > LIMITS.descriptionMax) {
|
|
238
247
|
return validationError(`Description must be at most ${LIMITS.descriptionMax} characters.`);
|
|
@@ -244,9 +253,8 @@ function validateFeatureInput(input) {
|
|
|
244
253
|
}
|
|
245
254
|
function validateCommentInput(input) {
|
|
246
255
|
const text = input.text.trim();
|
|
247
|
-
if (text.length < LIMITS.commentMin
|
|
248
|
-
|
|
249
|
-
return validationError(`Comment must be at most ${LIMITS.commentMax} characters.`);
|
|
256
|
+
if (text.length < LIMITS.commentMin || text.length > LIMITS.commentMax) {
|
|
257
|
+
return validationError(`Comment must be ${LIMITS.commentMin}\u2013${LIMITS.commentMax} characters.`);
|
|
250
258
|
}
|
|
251
259
|
if ((input.authorName ?? "").trim().length > LIMITS.authorNameMax) {
|
|
252
260
|
return validationError(`Name must be at most ${LIMITS.authorNameMax} characters.`);
|
|
@@ -266,6 +274,7 @@ function createFewchursClient(options = {}) {
|
|
|
266
274
|
const doFetch = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
267
275
|
const retries = options.retries ?? 1;
|
|
268
276
|
const retryDelayMs = options.retryDelayMs ?? 250;
|
|
277
|
+
if (options.apiKey && !options.apiKey.startsWith("fr_")) logOnce(BAD_KEY_PREFIX);
|
|
269
278
|
function getDeviceId() {
|
|
270
279
|
return typeof options.deviceId === "function" ? options.deviceId() : options.deviceId ?? null;
|
|
271
280
|
}
|
|
@@ -292,6 +301,11 @@ function createFewchursClient(options = {}) {
|
|
|
292
301
|
}
|
|
293
302
|
async function request(path, map, { method = "GET", body, query, signal } = {}) {
|
|
294
303
|
if (!doFetch) return { ok: false, error: networkError("No fetch implementation available.") };
|
|
304
|
+
if (!options.apiKey && options.baseUrl === void 0) {
|
|
305
|
+
const failure = notConfiguredError();
|
|
306
|
+
logError(failure);
|
|
307
|
+
return { ok: false, error: failure };
|
|
308
|
+
}
|
|
295
309
|
const target = url(path, query);
|
|
296
310
|
const init = { method, headers: headers(body !== void 0) };
|
|
297
311
|
if (body !== void 0) init.body = JSON.stringify(body);
|
|
@@ -307,7 +321,9 @@ function createFewchursClient(options = {}) {
|
|
|
307
321
|
await delay(retryDelayMs);
|
|
308
322
|
continue;
|
|
309
323
|
}
|
|
310
|
-
|
|
324
|
+
const failure2 = networkError(messageOf(error));
|
|
325
|
+
logError(failure2);
|
|
326
|
+
return { ok: false, error: failure2 };
|
|
311
327
|
}
|
|
312
328
|
if (response.ok) {
|
|
313
329
|
const payload = await readJson(response);
|
|
@@ -318,7 +334,7 @@ function createFewchursClient(options = {}) {
|
|
|
318
334
|
continue;
|
|
319
335
|
}
|
|
320
336
|
const failure = errorFromResponse(response.status, await readJson(response));
|
|
321
|
-
|
|
337
|
+
logError(failure);
|
|
322
338
|
return { ok: false, error: failure };
|
|
323
339
|
}
|
|
324
340
|
return { ok: false, error: networkError() };
|
|
@@ -409,14 +425,20 @@ function delay(ms) {
|
|
|
409
425
|
function messageOf(error) {
|
|
410
426
|
return error instanceof Error ? error.message : "Network request failed.";
|
|
411
427
|
}
|
|
412
|
-
var
|
|
413
|
-
|
|
414
|
-
|
|
428
|
+
var INVALID_KEY_HELP = "Fewchurs rejected the API key. Check the key in your dashboard (Apps \u2192 API keys).";
|
|
429
|
+
var NOT_CONFIGURED_HELP = "Fewchurs is not configured. Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.";
|
|
430
|
+
var BAD_KEY_PREFIX = 'The Fewchurs API key should start with "fr_". Copy it from your dashboard.';
|
|
431
|
+
var loggedErrors = /* @__PURE__ */ new Set();
|
|
432
|
+
function logOnce(detail) {
|
|
415
433
|
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") return;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
434
|
+
if (loggedErrors.has(detail)) return;
|
|
435
|
+
loggedErrors.add(detail);
|
|
436
|
+
console.warn(`[fewchurs] : ${detail}`);
|
|
437
|
+
}
|
|
438
|
+
function logError(error) {
|
|
439
|
+
if (isNotConfigured(error)) return logOnce(NOT_CONFIGURED_HELP);
|
|
440
|
+
const isInvalidKey = error.code === "invalid_key" && error.status === 401;
|
|
441
|
+
logOnce(isInvalidKey ? INVALID_KEY_HELP : error.message);
|
|
420
442
|
}
|
|
421
443
|
|
|
422
444
|
// src/identity/deviceId.ts
|
|
@@ -846,6 +868,20 @@ var DEFAULT_LABELS = {
|
|
|
846
868
|
loading: "Loading\u2026",
|
|
847
869
|
loadMore: "Load more",
|
|
848
870
|
retry: "Try again",
|
|
871
|
+
errorInvalidKeyTitle: "Invalid API key",
|
|
872
|
+
errorInvalidKeyDetail: "Copy the key from your Fewchurs dashboard (Apps \u2192 API keys) and pass it to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
873
|
+
errorWrongAppTitle: "Invalid API key",
|
|
874
|
+
errorWrongAppDetail: "",
|
|
875
|
+
errorTitle: "Couldn't load requests",
|
|
876
|
+
errorDetail: "Something went wrong. Try again.",
|
|
877
|
+
errorOfflineTitle: "You're offline",
|
|
878
|
+
errorOfflineDetail: "Check your connection and try again.",
|
|
879
|
+
errorNotSetUpTitle: "Fewchurs isn't set up",
|
|
880
|
+
errorNotSetUpDetail: "Pass apiKey to FewchursProvider, or set FEWCHURS_API_KEY on your server.",
|
|
881
|
+
errorLoadMore: "Couldn't load more.",
|
|
882
|
+
errorComments: "Couldn't load comments.",
|
|
883
|
+
fieldTitleTooShort: "At least 3 characters.",
|
|
884
|
+
fieldEmailInvalid: "Enter a valid email address.",
|
|
849
885
|
comments: "Comments",
|
|
850
886
|
commentPlaceholder: "Add a comment",
|
|
851
887
|
postComment: "Post",
|
|
@@ -1044,6 +1080,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
1044
1080
|
store.ensureComments(featureId);
|
|
1045
1081
|
}, [store, featureId, enabled, isReady]);
|
|
1046
1082
|
const loadMore = (0, import_react9.useCallback)(() => store.loadMoreComments(featureId), [store, featureId]);
|
|
1083
|
+
const refresh = (0, import_react9.useCallback)(() => store.refreshComments(featureId), [store, featureId]);
|
|
1047
1084
|
const add = (0, import_react9.useCallback)(
|
|
1048
1085
|
async (text, authorName) => {
|
|
1049
1086
|
setIsSubmitting(true);
|
|
@@ -1064,6 +1101,7 @@ function useComments(featureId, { enabled = true } = {}) {
|
|
|
1064
1101
|
submitError,
|
|
1065
1102
|
hasMore: comments.cursor !== null,
|
|
1066
1103
|
loadMore,
|
|
1104
|
+
refresh,
|
|
1067
1105
|
add
|
|
1068
1106
|
};
|
|
1069
1107
|
}
|
|
@@ -1083,10 +1121,15 @@ function formatDate(iso) {
|
|
|
1083
1121
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
1084
1122
|
function CommentList({ featureId }) {
|
|
1085
1123
|
const { labels, cx } = useBoard();
|
|
1086
|
-
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore } = useComments(featureId);
|
|
1124
|
+
const { comments, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } = useComments(featureId);
|
|
1087
1125
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-comments"), children: [
|
|
1088
1126
|
isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.loading }),
|
|
1089
|
-
error && /* @__PURE__ */ (0, import_jsx_runtime3.
|
|
1127
|
+
error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cx("fw-empty", "fw-empty--compact"), role: "alert", children: [
|
|
1128
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__title"), children: labels.errorComments }),
|
|
1129
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-empty__detail"), children: isNotConfigured(error) ? labels.errorNotSetUpTitle : userMessage(error) }),
|
|
1130
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1131
|
+
] }),
|
|
1132
|
+
error && comments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1090
1133
|
!isLoading && !error && comments.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-muted"), children: labels.noComments }),
|
|
1091
1134
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { className: cx("fw-comment-list"), children: comments.map((comment) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { className: cx("fw-comment"), children: [
|
|
1092
1135
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: cx("fw-comment__author"), children: comment.authorName }),
|
|
@@ -1157,7 +1200,7 @@ function CommentForm({ featureId }) {
|
|
|
1157
1200
|
}
|
|
1158
1201
|
)
|
|
1159
1202
|
] }),
|
|
1160
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting, children: labels.postComment }),
|
|
1203
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isSubmitting || !text.trim(), children: labels.postComment }),
|
|
1161
1204
|
submitError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(submitError) })
|
|
1162
1205
|
]
|
|
1163
1206
|
}
|
|
@@ -1173,20 +1216,24 @@ function FollowForm({ feature }) {
|
|
|
1173
1216
|
const [email, setEmail] = (0, import_react11.useState)(userEmail ?? "");
|
|
1174
1217
|
const [isOpen, setIsOpen] = (0, import_react11.useState)(false);
|
|
1175
1218
|
if (isFollowing) {
|
|
1176
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1219
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
1220
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
1221
|
+
"button",
|
|
1222
|
+
{
|
|
1223
|
+
type: "button",
|
|
1224
|
+
className: cx("fw-link"),
|
|
1225
|
+
disabled: isPending,
|
|
1226
|
+
onClick: () => void unfollow(),
|
|
1227
|
+
children: labels.following
|
|
1228
|
+
}
|
|
1229
|
+
),
|
|
1230
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1231
|
+
] });
|
|
1186
1232
|
}
|
|
1187
1233
|
if (!isOpen) {
|
|
1188
1234
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: cx("fw-link"), onClick: () => setIsOpen(true), children: labels.follow });
|
|
1189
1235
|
}
|
|
1236
|
+
const isEmailValid = EMAIL_PATTERN.test(email.trim());
|
|
1190
1237
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
1191
1238
|
"form",
|
|
1192
1239
|
{
|
|
@@ -1210,7 +1257,8 @@ function FollowForm({ feature }) {
|
|
|
1210
1257
|
required: true
|
|
1211
1258
|
}
|
|
1212
1259
|
),
|
|
1213
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("
|
|
1260
|
+
email !== "" && !isEmailValid && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), children: labels.fieldEmailInvalid }),
|
|
1261
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !isEmailValid, children: labels.follow }),
|
|
1214
1262
|
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) })
|
|
1215
1263
|
]
|
|
1216
1264
|
}
|
|
@@ -1253,7 +1301,7 @@ function FeatureCard({
|
|
|
1253
1301
|
defaultCommentsOpen = false
|
|
1254
1302
|
}) {
|
|
1255
1303
|
const { labels, allowComments, cx } = useBoard();
|
|
1256
|
-
const { isPending, toggle } = useVote(feature.id);
|
|
1304
|
+
const { isPending, toggle, error: voteError } = useVote(feature.id);
|
|
1257
1305
|
const [showComments, setShowComments] = (0, import_react12.useState)(defaultCommentsOpen);
|
|
1258
1306
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { className: cx("fw-card"), children: [
|
|
1259
1307
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(VoteButton, { feature, isPending, onToggle: () => void toggle() }),
|
|
@@ -1281,6 +1329,7 @@ function FeatureCard({
|
|
|
1281
1329
|
),
|
|
1282
1330
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(FollowForm, { feature })
|
|
1283
1331
|
] }),
|
|
1332
|
+
voteError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(voteError) }),
|
|
1284
1333
|
allowComments && showComments && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { id: `fw-comments-${feature.id}`, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CommentList, { featureId: feature.id }) })
|
|
1285
1334
|
] })
|
|
1286
1335
|
] });
|
|
@@ -1321,6 +1370,27 @@ function Bar({ width, height }) {
|
|
|
1321
1370
|
|
|
1322
1371
|
// src/components/FeatureList.tsx
|
|
1323
1372
|
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
1373
|
+
function errorCopy(error, labels) {
|
|
1374
|
+
if (isNotConfigured(error)) {
|
|
1375
|
+
return { title: labels.errorNotSetUpTitle, detail: labels.errorNotSetUpDetail };
|
|
1376
|
+
}
|
|
1377
|
+
if (error.code === "network") {
|
|
1378
|
+
return { title: labels.errorOfflineTitle, detail: labels.errorOfflineDetail };
|
|
1379
|
+
}
|
|
1380
|
+
if (isWrongApp(error)) {
|
|
1381
|
+
return {
|
|
1382
|
+
title: labels.errorWrongAppTitle,
|
|
1383
|
+
detail: labels.errorWrongAppDetail || userMessage(error)
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
if (error.code === "invalid_key") {
|
|
1387
|
+
return { title: labels.errorInvalidKeyTitle, detail: labels.errorInvalidKeyDetail };
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
title: labels.errorTitle,
|
|
1391
|
+
detail: error.code === "server" ? labels.errorDetail : userMessage(error)
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1324
1394
|
function FeatureList() {
|
|
1325
1395
|
const { initialFeatureId } = useBoard();
|
|
1326
1396
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
@@ -1330,8 +1400,11 @@ function FeatureList() {
|
|
|
1330
1400
|
}
|
|
1331
1401
|
function PinnedFeature({ featureId }) {
|
|
1332
1402
|
const { cx } = useBoard();
|
|
1333
|
-
const { feature, isLoading } = useFeature(featureId);
|
|
1403
|
+
const { feature, isLoading, error } = useFeature(featureId);
|
|
1334
1404
|
if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureListSkeleton, { rows: 1 });
|
|
1405
|
+
if (!feature && error) {
|
|
1406
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-error", "fw-pinned"), role: "alert", children: userMessage(error) });
|
|
1407
|
+
}
|
|
1335
1408
|
if (!feature) return null;
|
|
1336
1409
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pinned"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { className: cx("fw-list"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(FeatureCard, { feature, defaultCommentsOpen: true }) }) });
|
|
1337
1410
|
}
|
|
@@ -1350,17 +1423,11 @@ function Listing({ excludeId }) {
|
|
|
1350
1423
|
return showDeclined || feature.status !== "declined";
|
|
1351
1424
|
});
|
|
1352
1425
|
if (error && visible.length === 0) {
|
|
1426
|
+
const { title, detail } = errorCopy(error, labels);
|
|
1353
1427
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-empty"), role: "alert", children: [
|
|
1354
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", {
|
|
1355
|
-
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1356
|
-
|
|
1357
|
-
{
|
|
1358
|
-
type: "button",
|
|
1359
|
-
className: cx("fw-btn", "fw-btn--ghost"),
|
|
1360
|
-
onClick: () => void refresh(),
|
|
1361
|
-
children: labels.retry
|
|
1362
|
-
}
|
|
1363
|
-
)
|
|
1428
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__title"), children: title }),
|
|
1429
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: cx("fw-empty__detail"), children: detail }),
|
|
1430
|
+
!isNotConfigured(error) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void refresh(), children: labels.retry })
|
|
1364
1431
|
] });
|
|
1365
1432
|
}
|
|
1366
1433
|
if (isLoading && visible.length === 0) {
|
|
@@ -1375,7 +1442,11 @@ function Listing({ excludeId }) {
|
|
|
1375
1442
|
}
|
|
1376
1443
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
|
|
1377
1444
|
isRoadmap ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(RoadmapColumns, { features: visible }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Rows2, { features: visible }),
|
|
1378
|
-
hasMore && /* @__PURE__ */ (0, import_jsx_runtime9.
|
|
1445
|
+
hasMore && error && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: cx("fw-pagination"), role: "alert", children: [
|
|
1446
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: labels.errorLoadMore }),
|
|
1447
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: cx("fw-btn-text"), onClick: () => void loadMore(), children: labels.retry })
|
|
1448
|
+
] }),
|
|
1449
|
+
hasMore && !error && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: cx("fw-pagination"), children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
1379
1450
|
"button",
|
|
1380
1451
|
{
|
|
1381
1452
|
type: "button",
|
|
@@ -1456,6 +1527,10 @@ function SubmitForm() {
|
|
|
1456
1527
|
const [title, setTitle] = (0, import_react14.useState)("");
|
|
1457
1528
|
const [description, setDescription] = (0, import_react14.useState)("");
|
|
1458
1529
|
const [email, setEmail] = (0, import_react14.useState)(userEmail ?? "");
|
|
1530
|
+
const isTitleValid = title.trim().length >= LIMITS.titleMin;
|
|
1531
|
+
const isEmailInvalid = emailField !== "off" && email !== "" && !EMAIL_PATTERN.test(email.trim());
|
|
1532
|
+
const isEmailMissing = emailField === "required" && !email.trim();
|
|
1533
|
+
const canSubmit = isTitleValid && !isEmailInvalid && !isEmailMissing;
|
|
1459
1534
|
return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
|
|
1460
1535
|
"form",
|
|
1461
1536
|
{
|
|
@@ -1489,7 +1564,15 @@ function SubmitForm() {
|
|
|
1489
1564
|
onChange: (event) => setTitle(event.target.value),
|
|
1490
1565
|
required: true
|
|
1491
1566
|
}
|
|
1492
|
-
)
|
|
1567
|
+
),
|
|
1568
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
|
|
1569
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: cx("fw-error"), children: title !== "" && !isTitleValid ? labels.fieldTitleTooShort : "" }),
|
|
1570
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
|
|
1571
|
+
title.length,
|
|
1572
|
+
"/",
|
|
1573
|
+
LIMITS.titleMax
|
|
1574
|
+
] })
|
|
1575
|
+
] })
|
|
1493
1576
|
] }),
|
|
1494
1577
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
|
|
1495
1578
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-description", children: labels.fieldDescription }),
|
|
@@ -1503,7 +1586,15 @@ function SubmitForm() {
|
|
|
1503
1586
|
placeholder: labels.fieldDescriptionPlaceholder,
|
|
1504
1587
|
onChange: (event) => setDescription(event.target.value)
|
|
1505
1588
|
}
|
|
1506
|
-
)
|
|
1589
|
+
),
|
|
1590
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field__foot"), children: [
|
|
1591
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", {}),
|
|
1592
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: cx("fw-muted"), children: [
|
|
1593
|
+
description.length,
|
|
1594
|
+
"/",
|
|
1595
|
+
LIMITS.descriptionMax
|
|
1596
|
+
] })
|
|
1597
|
+
] })
|
|
1507
1598
|
] }),
|
|
1508
1599
|
emailField !== "off" && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-field"), children: [
|
|
1509
1600
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("label", { className: cx("fw-label"), htmlFor: "fw-email", children: labels.fieldEmail }),
|
|
@@ -1520,11 +1611,11 @@ function SubmitForm() {
|
|
|
1520
1611
|
required: emailField === "required"
|
|
1521
1612
|
}
|
|
1522
1613
|
),
|
|
1523
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1614
|
+
isEmailInvalid ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-error"), children: labels.fieldEmailInvalid }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { id: "fw-email-hint", className: cx("fw-muted"), children: labels.fieldEmailHint })
|
|
1524
1615
|
] }),
|
|
1525
1616
|
error && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: cx("fw-error"), role: "alert", children: userMessage(error) }),
|
|
1526
1617
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: cx("fw-form__actions"), children: [
|
|
1527
|
-
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending, children: isPending ? labels.submitting : labels.submit }),
|
|
1618
|
+
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "submit", className: cx("fw-btn"), disabled: isPending || !canSubmit, children: isPending ? labels.submitting : labels.submit }),
|
|
1528
1619
|
/* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
|
|
1529
1620
|
"button",
|
|
1530
1621
|
{
|
|
@@ -1837,6 +1928,7 @@ function FewchursButton({
|
|
|
1837
1928
|
FewchursProvider,
|
|
1838
1929
|
FollowForm,
|
|
1839
1930
|
LIMITS,
|
|
1931
|
+
NOT_CONFIGURED_MESSAGE,
|
|
1840
1932
|
ROADMAP_STATUSES,
|
|
1841
1933
|
SDK_HEADER,
|
|
1842
1934
|
SDK_VERSION,
|