convex-feedback-ui 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/native/expo/ExpoFeedbackBody.d.ts +1 -1
- package/dist/native/expo/ExpoFeedbackBody.d.ts.map +1 -1
- package/dist/native/expo/ExpoFeedbackBody.js +2 -2
- package/dist/native/expo/ExpoFeedbackBody.js.map +1 -1
- package/dist/native/expo/FeedbackCreateStackLayout.d.ts +3 -0
- package/dist/native/expo/FeedbackCreateStackLayout.d.ts.map +1 -0
- package/dist/native/expo/FeedbackCreateStackLayout.js +35 -0
- package/dist/native/expo/FeedbackCreateStackLayout.js.map +1 -0
- package/dist/native/expo/FeedbackRoutes.d.ts +4 -0
- package/dist/native/expo/FeedbackRoutes.d.ts.map +1 -0
- package/dist/native/expo/FeedbackRoutes.js +76 -0
- package/dist/native/expo/FeedbackRoutes.js.map +1 -0
- package/dist/native/expo/FeedbackScreen.d.ts +1 -1
- package/dist/native/expo/FeedbackScreen.d.ts.map +1 -1
- package/dist/native/expo/FeedbackScreen.js +3 -2
- package/dist/native/expo/FeedbackScreen.js.map +1 -1
- package/dist/native/expo/FeedbackStack.d.ts +1 -1
- package/dist/native/expo/FeedbackStack.d.ts.map +1 -1
- package/dist/native/expo/FeedbackStack.js +4 -2
- package/dist/native/expo/FeedbackStack.js.map +1 -1
- package/dist/native/expo/FeedbackStackLayout.d.ts +3 -0
- package/dist/native/expo/FeedbackStackLayout.d.ts.map +1 -0
- package/dist/native/expo/FeedbackStackLayout.js +54 -0
- package/dist/native/expo/FeedbackStackLayout.js.map +1 -0
- package/dist/native/expo/RoutedFeedbackContext.d.ts +4 -0
- package/dist/native/expo/RoutedFeedbackContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedFeedbackContext.js +11 -0
- package/dist/native/expo/RoutedFeedbackContext.js.map +1 -0
- package/dist/native/expo/RoutedFeedbackModalContext.d.ts +7 -0
- package/dist/native/expo/RoutedFeedbackModalContext.d.ts.map +1 -0
- package/dist/native/expo/RoutedFeedbackModalContext.js +7 -0
- package/dist/native/expo/RoutedFeedbackModalContext.js.map +1 -0
- package/dist/native/expo/index.d.ts +6 -0
- package/dist/native/expo/index.d.ts.map +1 -1
- package/dist/native/expo/index.js +6 -0
- package/dist/native/expo/index.js.map +1 -1
- package/dist/native/expo/metadata.d.ts +4 -0
- package/dist/native/expo/metadata.d.ts.map +1 -0
- package/dist/native/expo/metadata.js +44 -0
- package/dist/native/expo/metadata.js.map +1 -0
- package/dist/native/expo/routes.d.ts +16 -0
- package/dist/native/expo/routes.d.ts.map +1 -0
- package/dist/native/expo/routes.js +23 -0
- package/dist/native/expo/routes.js.map +1 -0
- package/dist/native/expo/types.d.ts +51 -1
- package/dist/native/expo/types.d.ts.map +1 -1
- package/dist/native/native/FeedbackScreen.d.ts +1 -1
- package/dist/native/native/FeedbackScreen.d.ts.map +1 -1
- package/dist/native/native/FeedbackScreen.js +3 -2
- package/dist/native/native/FeedbackScreen.js.map +1 -1
- package/dist/native/native/index.d.ts +2 -0
- package/dist/native/native/index.d.ts.map +1 -1
- package/dist/native/native/index.js +2 -0
- package/dist/native/native/index.js.map +1 -1
- package/dist/native/shared/metadata.d.ts +5 -0
- package/dist/native/shared/metadata.d.ts.map +1 -0
- package/dist/native/shared/metadata.js +31 -0
- package/dist/native/shared/metadata.js.map +1 -0
- package/dist/native/shared/ui/EntryDetail.d.ts.map +1 -1
- package/dist/native/shared/ui/EntryDetail.js +3 -1
- package/dist/native/shared/ui/EntryDetail.js.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.d.ts.map +1 -1
- package/dist/native/shared/ui/FeedbackScreenList.js +2 -2
- package/dist/native/shared/ui/FeedbackScreenList.js.map +1 -1
- package/dist/native/shared/ui/MetadataModal.d.ts +6 -0
- package/dist/native/shared/ui/MetadataModal.d.ts.map +1 -0
- package/dist/native/shared/ui/MetadataModal.js +39 -0
- package/dist/native/shared/ui/MetadataModal.js.map +1 -0
- package/dist/native/shared/ui/NewEntry.d.ts +10 -0
- package/dist/native/shared/ui/NewEntry.d.ts.map +1 -1
- package/dist/native/shared/ui/NewEntry.js +11 -4
- package/dist/native/shared/ui/NewEntry.js.map +1 -1
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/metadata.d.ts +4 -0
- package/dist/react/metadata.d.ts.map +1 -0
- package/dist/react/metadata.js +40 -0
- package/dist/react/metadata.js.map +1 -0
- package/dist/react/screen.d.ts +1 -1
- package/dist/react/screen.d.ts.map +1 -1
- package/dist/react/screen.js +18 -4
- package/dist/react/screen.js.map +1 -1
- package/dist/react/styles.css +35 -0
- package/dist/shared/context/FeedbackBodyProvider.d.ts +1 -1
- package/dist/shared/context/FeedbackBodyProvider.d.ts.map +1 -1
- package/dist/shared/context/FeedbackBodyProvider.js +5 -1
- package/dist/shared/context/FeedbackBodyProvider.js.map +1 -1
- package/dist/shared/messages.d.ts.map +1 -1
- package/dist/shared/messages.js +9 -0
- package/dist/shared/messages.js.map +1 -1
- package/dist/shared/metadata.d.ts +6 -0
- package/dist/shared/metadata.d.ts.map +1 -0
- package/dist/shared/metadata.js +54 -0
- package/dist/shared/metadata.js.map +1 -0
- package/dist/shared/types/index.d.ts +1 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/shared/types/messages.d.ts +17 -0
- package/dist/shared/types/messages.d.ts.map +1 -1
- package/dist/shared/types/metadata.d.ts +22 -0
- package/dist/shared/types/metadata.d.ts.map +1 -0
- package/dist/shared/types/metadata.js +2 -0
- package/dist/shared/types/metadata.js.map +1 -0
- package/dist/shared/types/screen.d.ts +14 -1
- package/dist/shared/types/screen.d.ts.map +1 -1
- package/package.json +12 -8
- package/src/native/expo/ExpoFeedbackBody.tsx +6 -1
- package/src/native/expo/FeedbackCreateStackLayout.tsx +62 -0
- package/src/native/expo/FeedbackRoutes.tsx +204 -0
- package/src/native/expo/FeedbackScreen.tsx +4 -0
- package/src/native/expo/FeedbackStack.tsx +16 -4
- package/src/native/expo/FeedbackStackLayout.tsx +116 -0
- package/src/native/expo/README.md +24 -0
- package/src/native/expo/RoutedFeedbackContext.tsx +20 -0
- package/src/native/expo/RoutedFeedbackModalContext.tsx +14 -0
- package/src/native/expo/index.ts +6 -0
- package/src/native/expo/metadata.ts +62 -0
- package/src/native/expo/routes.ts +41 -0
- package/src/native/expo/types.ts +63 -1
- package/src/native/native/FeedbackScreen.tsx +4 -0
- package/src/native/native/index.ts +2 -0
- package/src/native/shared/metadata.ts +39 -0
- package/src/native/shared/ui/EntryDetail.tsx +14 -0
- package/src/native/shared/ui/FeedbackScreenList.tsx +2 -1
- package/src/native/shared/ui/MetadataModal.tsx +112 -0
- package/src/native/shared/ui/NewEntry.tsx +18 -4
- package/src/react/index.ts +2 -0
- package/src/react/metadata.ts +51 -0
- package/src/react/screen.tsx +89 -2
- package/src/react/styles.css +35 -0
- package/src/shared/context/FeedbackBodyProvider.tsx +8 -0
- package/src/shared/messages.ts +9 -0
- package/src/shared/metadata.ts +84 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/messages.ts +24 -0
- package/src/shared/types/metadata.ts +55 -0
- package/src/shared/types/screen.ts +17 -2
package/src/native/expo/types.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { StackScreenProps } from "expo-router";
|
|
2
|
+
import type { ImageSourcePropType } from "react-native";
|
|
2
3
|
import type { SearchBarCommands } from "react-native-screens";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
FeedbackColorProps,
|
|
6
|
+
FeedbackScreenRootProps,
|
|
7
|
+
} from "../../shared/types";
|
|
4
8
|
|
|
5
9
|
interface FeedbackScreenWithStack {
|
|
6
10
|
useStack?: true;
|
|
7
11
|
StackOptions?: StackScreenProps["options"];
|
|
12
|
+
/** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
|
|
13
|
+
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
8
14
|
}
|
|
9
15
|
interface FeedbackScreenWithoutStack {
|
|
10
16
|
useStack?: false;
|
|
11
17
|
StackOptions?: never;
|
|
18
|
+
androidToolbarIcons?: never;
|
|
12
19
|
}
|
|
13
20
|
export type FeedbackScreenStackProps =
|
|
14
21
|
FeedbackScreenWithStack | FeedbackScreenWithoutStack;
|
|
@@ -19,5 +26,60 @@ export type ExpoFeedbackScreenProps = FeedbackScreenStackProps &
|
|
|
19
26
|
export interface FeedbackStackProps {
|
|
20
27
|
searchRef: React.RefObject<SearchBarCommands | null>;
|
|
21
28
|
stackOptions?: StackScreenProps["options"];
|
|
29
|
+
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
22
30
|
children: React.ReactNode;
|
|
23
31
|
}
|
|
32
|
+
|
|
33
|
+
/** File names used by the routed feedback stack. */
|
|
34
|
+
export interface FeedbackRouteNames {
|
|
35
|
+
/** Board route. @default "index" */
|
|
36
|
+
board: string;
|
|
37
|
+
/** Entry route. Must contain `[entryId]`. @default "[entryId]" */
|
|
38
|
+
entry: string;
|
|
39
|
+
/** Create-entry modal route. @default "new" */
|
|
40
|
+
create: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type FeedbackStackScreenOptions = Exclude<
|
|
44
|
+
NonNullable<StackScreenProps["options"]>,
|
|
45
|
+
(...args: never[]) => unknown
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
export interface FeedbackAndroidToolbarIcons {
|
|
49
|
+
/** Android image source for the create-entry action. */
|
|
50
|
+
create?: ImageSourcePropType;
|
|
51
|
+
/** Android image source for the back action. */
|
|
52
|
+
back?: ImageSourcePropType;
|
|
53
|
+
/** Android image source for modal close actions. */
|
|
54
|
+
close?: ImageSourcePropType;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FeedbackStackLayoutProps extends FeedbackScreenRootProps {
|
|
58
|
+
/** Partial overrides for the prescribed route file names. */
|
|
59
|
+
routes?: Partial<FeedbackRouteNames>;
|
|
60
|
+
/** Options shared by every feedback screen. */
|
|
61
|
+
screenOptions?: FeedbackStackScreenOptions;
|
|
62
|
+
/** Board screen option overrides. */
|
|
63
|
+
boardOptions?: FeedbackStackScreenOptions;
|
|
64
|
+
/** Entry-detail screen option overrides. */
|
|
65
|
+
entryOptions?: FeedbackStackScreenOptions;
|
|
66
|
+
/** Create-entry modal option overrides. */
|
|
67
|
+
createOptions?: FeedbackStackScreenOptions;
|
|
68
|
+
/** Android image sources for toolbar actions. iOS continues to use SF Symbols. */
|
|
69
|
+
androidToolbarIcons?: FeedbackAndroidToolbarIcons;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface FeedbackCreateStackLayoutProps {
|
|
73
|
+
/** Options shared by the create form and suggested-entry screens. */
|
|
74
|
+
screenOptions?: FeedbackStackScreenOptions;
|
|
75
|
+
/** Create-form screen option overrides. */
|
|
76
|
+
createOptions?: FeedbackStackScreenOptions;
|
|
77
|
+
/** Suggested-entry screen option overrides. */
|
|
78
|
+
entryOptions?: FeedbackStackScreenOptions;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface RoutedFeedbackContextValue {
|
|
82
|
+
routes: FeedbackRouteNames;
|
|
83
|
+
colors: FeedbackColorProps;
|
|
84
|
+
androidToolbarIcons: FeedbackAndroidToolbarIcons;
|
|
85
|
+
}
|
|
@@ -3,6 +3,7 @@ import { FeedbackProvider } from "../../shared/context/FeedbackProvider";
|
|
|
3
3
|
import { kinds } from "../../shared/helpers";
|
|
4
4
|
import type { FeedbackScreenRootProps } from "../../shared/types";
|
|
5
5
|
import { FeedbackScreenContent } from "../shared/ui/BodyContent";
|
|
6
|
+
import { collectNativeMetadata } from "../shared/metadata";
|
|
6
7
|
|
|
7
8
|
export function FeedbackScreen({
|
|
8
9
|
hooks,
|
|
@@ -14,6 +15,7 @@ export function FeedbackScreen({
|
|
|
14
15
|
enabledKinds = kinds,
|
|
15
16
|
maxCommentDepth = 5,
|
|
16
17
|
debounceDuration = 300,
|
|
18
|
+
collectMetadata,
|
|
17
19
|
transformComments,
|
|
18
20
|
renderActor,
|
|
19
21
|
...props
|
|
@@ -27,6 +29,8 @@ export function FeedbackScreen({
|
|
|
27
29
|
enabledKinds={enabledKinds}
|
|
28
30
|
maxCommentDepth={maxCommentDepth}
|
|
29
31
|
debounceDuration={debounceDuration}
|
|
32
|
+
collectMetadata={collectMetadata}
|
|
33
|
+
collectStandardMetadata={collectNativeMetadata}
|
|
30
34
|
transformComments={transformComments}
|
|
31
35
|
renderActor={renderActor}
|
|
32
36
|
>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from "../../shared/context/FeedbackBodyProvider.js";
|
|
2
2
|
export * from "../../shared/context/FeedbackProvider.js";
|
|
3
3
|
export * from "../../shared/messages.js";
|
|
4
|
+
export * from "../../shared/metadata.js";
|
|
4
5
|
export * from "../../shared/theme.js";
|
|
5
6
|
export type * from "../../shared/types/index.js";
|
|
6
7
|
export * from "../shared/ui/primitives.js";
|
|
8
|
+
export * from "../shared/metadata.js";
|
|
7
9
|
export * from "./FeedbackScreen.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FeedbackMetadataValue } from "convex-feedback";
|
|
2
|
+
import { Dimensions, PixelRatio, Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
StandardMetadataKey,
|
|
6
|
+
EnabledStandardMetadataSelection,
|
|
7
|
+
} from "../../shared/types/metadata.js";
|
|
8
|
+
|
|
9
|
+
export const defaultNativeMetadataKeys: readonly StandardMetadataKey[] = [
|
|
10
|
+
"platform",
|
|
11
|
+
"osVersion",
|
|
12
|
+
"deviceModel",
|
|
13
|
+
"screenWidth",
|
|
14
|
+
"screenHeight",
|
|
15
|
+
"pixelRatio",
|
|
16
|
+
"fontScale",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function collectNativeMetadata(
|
|
20
|
+
selection: EnabledStandardMetadataSelection,
|
|
21
|
+
): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
|
|
22
|
+
const keys = new Set(
|
|
23
|
+
selection === true ? defaultNativeMetadataKeys : selection,
|
|
24
|
+
);
|
|
25
|
+
const metadata: Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> =
|
|
26
|
+
{};
|
|
27
|
+
const screen = Dimensions.get("screen");
|
|
28
|
+
|
|
29
|
+
if (keys.has("platform")) metadata.platform = Platform.OS;
|
|
30
|
+
if (keys.has("osVersion")) metadata.osVersion = Platform.Version;
|
|
31
|
+
if (keys.has("deviceModel") && Platform.OS === "android")
|
|
32
|
+
metadata.deviceModel = Platform.constants.Model;
|
|
33
|
+
if (keys.has("screenWidth")) metadata.screenWidth = screen.width;
|
|
34
|
+
if (keys.has("screenHeight")) metadata.screenHeight = screen.height;
|
|
35
|
+
if (keys.has("pixelRatio")) metadata.pixelRatio = PixelRatio.get();
|
|
36
|
+
if (keys.has("fontScale")) metadata.fontScale = PixelRatio.getFontScale();
|
|
37
|
+
|
|
38
|
+
return metadata;
|
|
39
|
+
}
|
|
@@ -7,6 +7,7 @@ import type { FeedbackScreenEntryDetailProps } from "../../../shared/types";
|
|
|
7
7
|
import { Button } from "./Button";
|
|
8
8
|
import { CommentBranch } from "./CommentBranch";
|
|
9
9
|
import { FeedbackEntry, FeedbackForm } from "./primitives";
|
|
10
|
+
import { MetadataModal } from "./MetadataModal";
|
|
10
11
|
|
|
11
12
|
export function EntryDetail({
|
|
12
13
|
entryId,
|
|
@@ -21,6 +22,7 @@ export function EntryDetail({
|
|
|
21
22
|
const setUpvote = hooks.useSetEntryUpvote();
|
|
22
23
|
const createComment = hooks.useCreateComment();
|
|
23
24
|
const [body, setBody] = useState("");
|
|
25
|
+
const [showMetadata, setShowMetadata] = useState(false);
|
|
24
26
|
const visible = useMemo(
|
|
25
27
|
() => transformComments?.(comments.results) ?? comments.results,
|
|
26
28
|
[comments.results, transformComments],
|
|
@@ -66,6 +68,18 @@ export function EntryDetail({
|
|
|
66
68
|
<FeedbackEntry.CommentCount />
|
|
67
69
|
</FeedbackEntry.Content>
|
|
68
70
|
</FeedbackEntry.Root>
|
|
71
|
+
{entry.metadata !== undefined && (
|
|
72
|
+
<Button
|
|
73
|
+
label={messages.metadata.view}
|
|
74
|
+
onPress={() => setShowMetadata(true)}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
{showMetadata && entry.metadata !== undefined && (
|
|
78
|
+
<MetadataModal
|
|
79
|
+
metadata={entry.metadata}
|
|
80
|
+
onRequestClose={() => setShowMetadata(false)}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
69
83
|
<Text
|
|
70
84
|
style={{ color: theme.colors.text, fontSize: 18, fontWeight: "700" }}
|
|
71
85
|
>
|
|
@@ -8,6 +8,7 @@ import { FeedbackBoard } from "./primitives.js";
|
|
|
8
8
|
|
|
9
9
|
export function FeedbackScreenList({
|
|
10
10
|
hideBackButton = false,
|
|
11
|
+
showSelectedEntry = true,
|
|
11
12
|
onEntryOpen,
|
|
12
13
|
}: FeedbackScreenListProps) {
|
|
13
14
|
const {
|
|
@@ -32,7 +33,7 @@ export function FeedbackScreenList({
|
|
|
32
33
|
return (
|
|
33
34
|
<>
|
|
34
35
|
<FeedbackBoard.List style={{ padding: theme.spacing }}>
|
|
35
|
-
{selectedEntryId ? (
|
|
36
|
+
{showSelectedEntry && selectedEntryId ? (
|
|
36
37
|
<EntryDetail
|
|
37
38
|
entryId={selectedEntryId}
|
|
38
39
|
hideBackButton={hideBackButton}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { FeedbackMetadata } from "convex-feedback";
|
|
2
|
+
import { Modal, Pressable, ScrollView, Text, View } from "react-native";
|
|
3
|
+
|
|
4
|
+
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
|
|
5
|
+
import { formatMetadataKey } from "../../../shared/metadata";
|
|
6
|
+
import { Button } from "./Button";
|
|
7
|
+
|
|
8
|
+
export function MetadataModal({
|
|
9
|
+
metadata,
|
|
10
|
+
onRequestClose,
|
|
11
|
+
}: {
|
|
12
|
+
metadata: FeedbackMetadata;
|
|
13
|
+
onRequestClose: () => void;
|
|
14
|
+
}) {
|
|
15
|
+
const { messages, theme } = useFeedbackUi();
|
|
16
|
+
const sections = [
|
|
17
|
+
[messages.metadata.standard, metadata.standard],
|
|
18
|
+
[messages.metadata.additional, metadata.additional],
|
|
19
|
+
] as const;
|
|
20
|
+
const hasValues = sections.some(
|
|
21
|
+
([, values]) => values !== undefined && Object.keys(values).length > 0,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Modal
|
|
26
|
+
visible
|
|
27
|
+
transparent
|
|
28
|
+
animationType="fade"
|
|
29
|
+
onRequestClose={onRequestClose}
|
|
30
|
+
role="dialog"
|
|
31
|
+
>
|
|
32
|
+
<Pressable
|
|
33
|
+
accessible={false}
|
|
34
|
+
focusable={false}
|
|
35
|
+
onPress={onRequestClose}
|
|
36
|
+
style={{
|
|
37
|
+
flex: 1,
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
padding: 20,
|
|
40
|
+
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<Pressable
|
|
44
|
+
accessible={false}
|
|
45
|
+
focusable={false}
|
|
46
|
+
onPress={(event) => event.stopPropagation()}
|
|
47
|
+
style={{
|
|
48
|
+
maxHeight: "80%",
|
|
49
|
+
gap: 14,
|
|
50
|
+
padding: 18,
|
|
51
|
+
borderRadius: theme.radius,
|
|
52
|
+
backgroundColor: theme.colors.surface,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<Text
|
|
56
|
+
style={{
|
|
57
|
+
color: theme.colors.text,
|
|
58
|
+
fontSize: 18,
|
|
59
|
+
fontWeight: "700",
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{messages.metadata.title}
|
|
63
|
+
</Text>
|
|
64
|
+
<ScrollView contentContainerStyle={{ gap: 16 }}>
|
|
65
|
+
{hasValues ? (
|
|
66
|
+
sections.map(([label, values]) =>
|
|
67
|
+
values === undefined ||
|
|
68
|
+
Object.keys(values).length === 0 ? null : (
|
|
69
|
+
<View key={label} style={{ gap: 8 }}>
|
|
70
|
+
<Text
|
|
71
|
+
style={{ color: theme.colors.text, fontWeight: "700" }}
|
|
72
|
+
>
|
|
73
|
+
{label}
|
|
74
|
+
</Text>
|
|
75
|
+
{Object.entries(values).map(([key, value]) => (
|
|
76
|
+
<View
|
|
77
|
+
key={key}
|
|
78
|
+
style={{
|
|
79
|
+
gap: 3,
|
|
80
|
+
paddingBottom: 8,
|
|
81
|
+
borderBottomWidth: 1,
|
|
82
|
+
borderBottomColor: theme.colors.border,
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<Text
|
|
86
|
+
style={{
|
|
87
|
+
color: theme.colors.mutedText,
|
|
88
|
+
fontWeight: "600",
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
{formatMetadataKey(key)}
|
|
92
|
+
</Text>
|
|
93
|
+
<Text selectable style={{ color: theme.colors.text }}>
|
|
94
|
+
{String(value)}
|
|
95
|
+
</Text>
|
|
96
|
+
</View>
|
|
97
|
+
))}
|
|
98
|
+
</View>
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
) : (
|
|
102
|
+
<Text style={{ color: theme.colors.mutedText }}>
|
|
103
|
+
{messages.metadata.empty}
|
|
104
|
+
</Text>
|
|
105
|
+
)}
|
|
106
|
+
</ScrollView>
|
|
107
|
+
<Button label={messages.metadata.close} onPress={onRequestClose} />
|
|
108
|
+
</Pressable>
|
|
109
|
+
</Pressable>
|
|
110
|
+
</Modal>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { useFeedbackBody } from "../../../shared/context/FeedbackBodyProvider";
|
|
14
14
|
import { useFeedbackUi } from "../../../shared/context/FeedbackProvider";
|
|
15
15
|
import type { FeedbackScreenEntryModalProps } from "../../../shared/types";
|
|
16
|
+
import { collectEntryMetadata } from "../../../shared/metadata";
|
|
16
17
|
import { EntryDetail } from "./EntryDetail";
|
|
17
18
|
import { FeedbackForm } from "./primitives";
|
|
18
19
|
|
|
@@ -130,7 +131,7 @@ export function CreateEntryModal({
|
|
|
130
131
|
onBack={() => setPreviewEntryId(null)}
|
|
131
132
|
/>
|
|
132
133
|
) : (
|
|
133
|
-
<
|
|
134
|
+
<CreateEntryForm
|
|
134
135
|
kind={kind}
|
|
135
136
|
onKindChange={setKind}
|
|
136
137
|
title={title}
|
|
@@ -148,7 +149,7 @@ export function CreateEntryModal({
|
|
|
148
149
|
);
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
function
|
|
152
|
+
export function CreateEntryForm({
|
|
152
153
|
kind,
|
|
153
154
|
onKindChange,
|
|
154
155
|
title,
|
|
@@ -166,7 +167,8 @@ function CreateForm({
|
|
|
166
167
|
onBodyChange: (body: string) => void;
|
|
167
168
|
onOpenSuggestion: (id: string) => void;
|
|
168
169
|
}) {
|
|
169
|
-
const { hooks, enabledKinds } =
|
|
170
|
+
const { hooks, enabledKinds, collectMetadata, collectStandardMetadata } =
|
|
171
|
+
useFeedbackBody();
|
|
170
172
|
const { messages, theme } = useFeedbackUi();
|
|
171
173
|
const [submitting, setSubmitting] = useState(false);
|
|
172
174
|
const create = hooks.useCreateEntry();
|
|
@@ -185,7 +187,19 @@ function CreateForm({
|
|
|
185
187
|
setSubmitting(true);
|
|
186
188
|
|
|
187
189
|
try {
|
|
188
|
-
|
|
190
|
+
const metadata = await collectEntryMetadata(
|
|
191
|
+
collectMetadata,
|
|
192
|
+
kind,
|
|
193
|
+
collectStandardMetadata,
|
|
194
|
+
);
|
|
195
|
+
onCreated(
|
|
196
|
+
await create({
|
|
197
|
+
kind,
|
|
198
|
+
title,
|
|
199
|
+
body,
|
|
200
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
189
203
|
} finally {
|
|
190
204
|
setSubmitting(false);
|
|
191
205
|
}
|
package/src/react/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "../shared/messages.js";
|
|
2
|
+
export * from "../shared/metadata.js";
|
|
2
3
|
export * from "../shared/theme.js";
|
|
3
4
|
export type * from "../shared/types";
|
|
4
5
|
export * from "./css.js";
|
|
6
|
+
export * from "./metadata.js";
|
|
5
7
|
export * from "./primitives.js";
|
|
6
8
|
export * from "./screen.js";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { FeedbackMetadataValue } from "convex-feedback";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
StandardMetadataKey,
|
|
5
|
+
EnabledStandardMetadataSelection,
|
|
6
|
+
} from "../shared/types/metadata.js";
|
|
7
|
+
|
|
8
|
+
const defaultWebMetadataKeys: readonly StandardMetadataKey[] = [
|
|
9
|
+
"platform",
|
|
10
|
+
"userAgent",
|
|
11
|
+
"language",
|
|
12
|
+
"timezone",
|
|
13
|
+
"screenWidth",
|
|
14
|
+
"screenHeight",
|
|
15
|
+
"viewportWidth",
|
|
16
|
+
"viewportHeight",
|
|
17
|
+
"devicePixelRatio",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function collectWebMetadata(
|
|
21
|
+
selection: EnabledStandardMetadataSelection,
|
|
22
|
+
): Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> {
|
|
23
|
+
const keys = new Set(selection === true ? defaultWebMetadataKeys : selection);
|
|
24
|
+
const metadata: Partial<Record<StandardMetadataKey, FeedbackMetadataValue>> =
|
|
25
|
+
{};
|
|
26
|
+
const browserNavigator =
|
|
27
|
+
typeof navigator === "undefined" ? undefined : navigator;
|
|
28
|
+
const browserWindow = typeof window === "undefined" ? undefined : window;
|
|
29
|
+
|
|
30
|
+
if (keys.has("platform")) metadata.platform = "web";
|
|
31
|
+
if (keys.has("userAgent") && browserNavigator?.userAgent)
|
|
32
|
+
metadata.userAgent = browserNavigator.userAgent;
|
|
33
|
+
if (keys.has("language") && browserNavigator?.language)
|
|
34
|
+
metadata.language = browserNavigator.language;
|
|
35
|
+
if (keys.has("timezone")) {
|
|
36
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
37
|
+
if (timezone) metadata.timezone = timezone;
|
|
38
|
+
}
|
|
39
|
+
if (keys.has("screenWidth") && browserWindow?.screen)
|
|
40
|
+
metadata.screenWidth = browserWindow.screen.width;
|
|
41
|
+
if (keys.has("screenHeight") && browserWindow?.screen)
|
|
42
|
+
metadata.screenHeight = browserWindow.screen.height;
|
|
43
|
+
if (keys.has("viewportWidth") && browserWindow)
|
|
44
|
+
metadata.viewportWidth = browserWindow.innerWidth;
|
|
45
|
+
if (keys.has("viewportHeight") && browserWindow)
|
|
46
|
+
metadata.viewportHeight = browserWindow.innerHeight;
|
|
47
|
+
if (keys.has("devicePixelRatio") && browserWindow)
|
|
48
|
+
metadata.devicePixelRatio = browserWindow.devicePixelRatio;
|
|
49
|
+
|
|
50
|
+
return metadata;
|
|
51
|
+
}
|
package/src/react/screen.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { EntryKind } from "convex-feedback";
|
|
3
|
+
import type { EntryKind, FeedbackMetadata } from "convex-feedback";
|
|
4
4
|
import { useMemo, useState, type SyntheticEvent } from "react";
|
|
5
5
|
|
|
6
6
|
import {
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
FeedbackEntry,
|
|
27
27
|
FeedbackForm,
|
|
28
28
|
} from "./primitives.js";
|
|
29
|
+
import { collectWebMetadata } from "./metadata.js";
|
|
30
|
+
import { collectEntryMetadata, formatMetadataKey } from "../shared/metadata.js";
|
|
29
31
|
|
|
30
32
|
const entryKinds: readonly EntryKind[] = [
|
|
31
33
|
"feedback",
|
|
@@ -49,6 +51,7 @@ export function FeedbackScreen({
|
|
|
49
51
|
transformComments,
|
|
50
52
|
renderActor,
|
|
51
53
|
debounceDuration = 300,
|
|
54
|
+
collectMetadata,
|
|
52
55
|
...props
|
|
53
56
|
}: FeedbackScreenProps) {
|
|
54
57
|
return (
|
|
@@ -60,6 +63,8 @@ export function FeedbackScreen({
|
|
|
60
63
|
enabledKinds={enabledKinds}
|
|
61
64
|
maxCommentDepth={maxCommentDepth}
|
|
62
65
|
debounceDuration={debounceDuration}
|
|
66
|
+
collectMetadata={collectMetadata}
|
|
67
|
+
collectStandardMetadata={collectWebMetadata}
|
|
63
68
|
transformComments={transformComments}
|
|
64
69
|
renderActor={renderActor}
|
|
65
70
|
>
|
|
@@ -207,7 +212,8 @@ function EntryCard({ entry, hooks, onOpen }: FeedbackScreenEntryCardProps) {
|
|
|
207
212
|
}
|
|
208
213
|
|
|
209
214
|
function CreateEntryForm({ onCreated }: FeedbackScreenEntryModalProps) {
|
|
210
|
-
const { hooks, enabledKinds } =
|
|
215
|
+
const { hooks, enabledKinds, collectMetadata, collectStandardMetadata } =
|
|
216
|
+
useFeedbackBody();
|
|
211
217
|
const { messages } = useFeedbackUi();
|
|
212
218
|
const [kind, setKind] = useState<EntryKind>(enabledKinds[0] ?? "feedback");
|
|
213
219
|
const [title, setTitle] = useState("");
|
|
@@ -242,10 +248,16 @@ function CreateEntryForm({ onCreated }: FeedbackScreenEntryModalProps) {
|
|
|
242
248
|
setSubmitting(true);
|
|
243
249
|
|
|
244
250
|
try {
|
|
251
|
+
const metadata = await collectEntryMetadata(
|
|
252
|
+
collectMetadata,
|
|
253
|
+
kind,
|
|
254
|
+
collectStandardMetadata,
|
|
255
|
+
);
|
|
245
256
|
const entryId = await createEntry({
|
|
246
257
|
kind,
|
|
247
258
|
title,
|
|
248
259
|
body,
|
|
260
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
249
261
|
});
|
|
250
262
|
|
|
251
263
|
onCreated(entryId);
|
|
@@ -382,6 +394,7 @@ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
|
|
|
382
394
|
const comments = hooks.useComments({ entryId, sort: commentSort });
|
|
383
395
|
const createComment = hooks.useCreateComment();
|
|
384
396
|
const [body, setBody] = useState("");
|
|
397
|
+
const [showMetadata, setShowMetadata] = useState(false);
|
|
385
398
|
|
|
386
399
|
const visibleComments = useMemo(
|
|
387
400
|
() => transformComments?.(comments.results) ?? comments.results,
|
|
@@ -422,6 +435,25 @@ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
|
|
|
422
435
|
</FeedbackEntry.Content>
|
|
423
436
|
</FeedbackEntry.Root>
|
|
424
437
|
|
|
438
|
+
{entry.metadata !== undefined && (
|
|
439
|
+
<div>
|
|
440
|
+
<button
|
|
441
|
+
type="button"
|
|
442
|
+
className="cf-button"
|
|
443
|
+
onClick={() => setShowMetadata(true)}
|
|
444
|
+
>
|
|
445
|
+
{messages.metadata.view}
|
|
446
|
+
</button>
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{showMetadata && entry.metadata !== undefined && (
|
|
451
|
+
<MetadataDialog
|
|
452
|
+
metadata={entry.metadata}
|
|
453
|
+
onClose={() => setShowMetadata(false)}
|
|
454
|
+
/>
|
|
455
|
+
)}
|
|
456
|
+
|
|
425
457
|
<section className="cf-discussion">
|
|
426
458
|
<h3>{messages.comments.title}</h3>
|
|
427
459
|
<FeedbackForm.Root
|
|
@@ -473,6 +505,61 @@ function EntryDetail({ entryId, onBack }: FeedbackScreenEntryDetailProps) {
|
|
|
473
505
|
);
|
|
474
506
|
}
|
|
475
507
|
|
|
508
|
+
function MetadataDialog({
|
|
509
|
+
metadata,
|
|
510
|
+
onClose,
|
|
511
|
+
}: {
|
|
512
|
+
metadata: FeedbackMetadata;
|
|
513
|
+
onClose: () => void;
|
|
514
|
+
}) {
|
|
515
|
+
const { messages } = useFeedbackUi();
|
|
516
|
+
const sections = [
|
|
517
|
+
[messages.metadata.standard, metadata.standard],
|
|
518
|
+
[messages.metadata.additional, metadata.additional],
|
|
519
|
+
] as const;
|
|
520
|
+
const hasValues = sections.some(
|
|
521
|
+
([, values]) => values !== undefined && Object.keys(values).length > 0,
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<div className="cf-confirm-backdrop" onMouseDown={onClose}>
|
|
526
|
+
<div
|
|
527
|
+
className="cf-confirm cf-metadata-dialog"
|
|
528
|
+
role="dialog"
|
|
529
|
+
aria-modal="true"
|
|
530
|
+
aria-labelledby="cf-metadata-title"
|
|
531
|
+
onMouseDown={(event) => event.stopPropagation()}
|
|
532
|
+
>
|
|
533
|
+
<h3 id="cf-metadata-title">{messages.metadata.title}</h3>
|
|
534
|
+
{hasValues ? (
|
|
535
|
+
sections.map(([label, values]) =>
|
|
536
|
+
values === undefined || Object.keys(values).length === 0 ? null : (
|
|
537
|
+
<section key={label} className="cf-metadata-section">
|
|
538
|
+
<h4>{label}</h4>
|
|
539
|
+
<dl>
|
|
540
|
+
{Object.entries(values).map(([key, value]) => (
|
|
541
|
+
<div key={key} className="cf-metadata-row">
|
|
542
|
+
<dt>{formatMetadataKey(key)}</dt>
|
|
543
|
+
<dd>{String(value)}</dd>
|
|
544
|
+
</div>
|
|
545
|
+
))}
|
|
546
|
+
</dl>
|
|
547
|
+
</section>
|
|
548
|
+
),
|
|
549
|
+
)
|
|
550
|
+
) : (
|
|
551
|
+
<p>{messages.metadata.empty}</p>
|
|
552
|
+
)}
|
|
553
|
+
<div className="cf-inline-actions">
|
|
554
|
+
<button type="button" className="cf-button" onClick={onClose}>
|
|
555
|
+
{messages.metadata.close}
|
|
556
|
+
</button>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
476
563
|
function CommentBranch({ comment, entryId }: FeedbackScreenCommentBranchProps) {
|
|
477
564
|
const { hooks, maxCommentDepth, renderActor } = useFeedbackBody();
|
|
478
565
|
const { messages } = useFeedbackUi();
|
package/src/react/styles.css
CHANGED
|
@@ -297,3 +297,38 @@
|
|
|
297
297
|
color: var(--cf-muted);
|
|
298
298
|
line-height: 1.5;
|
|
299
299
|
}
|
|
300
|
+
|
|
301
|
+
:where(.cf-board) :where(.cf-metadata-dialog) {
|
|
302
|
+
width: min(100%, 560px);
|
|
303
|
+
max-height: min(80vh, 720px);
|
|
304
|
+
overflow: auto;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
:where(.cf-board) :where(.cf-metadata-dialog h3),
|
|
308
|
+
:where(.cf-board) :where(.cf-metadata-dialog h4),
|
|
309
|
+
:where(.cf-board) :where(.cf-metadata-dialog dl) {
|
|
310
|
+
margin: 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
:where(.cf-board) :where(.cf-metadata-section) {
|
|
314
|
+
display: grid;
|
|
315
|
+
gap: 8px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
:where(.cf-board) :where(.cf-metadata-row) {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1.2fr);
|
|
321
|
+
gap: 12px;
|
|
322
|
+
padding: 8px 0;
|
|
323
|
+
border-bottom: 1px solid var(--cf-border);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
:where(.cf-board) :where(.cf-metadata-row dt) {
|
|
327
|
+
color: var(--cf-muted);
|
|
328
|
+
font-weight: 600;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
:where(.cf-board) :where(.cf-metadata-row dd) {
|
|
332
|
+
min-width: 0;
|
|
333
|
+
overflow-wrap: anywhere;
|
|
334
|
+
}
|
|
@@ -22,6 +22,10 @@ export function FeedbackBodyProvider({
|
|
|
22
22
|
enabledKinds,
|
|
23
23
|
maxCommentDepth,
|
|
24
24
|
debounceDuration,
|
|
25
|
+
collectMetadata,
|
|
26
|
+
collectStandardMetadata,
|
|
27
|
+
transformComments,
|
|
28
|
+
renderActor,
|
|
25
29
|
children,
|
|
26
30
|
}: PropsWithChildren<FeedbackScreenProviderProps>) {
|
|
27
31
|
const [selectedEntryId, setSelectedEntryId] = useState<string | null>(null);
|
|
@@ -60,6 +64,10 @@ export function FeedbackBodyProvider({
|
|
|
60
64
|
enabledKinds,
|
|
61
65
|
maxCommentDepth,
|
|
62
66
|
debounceDuration,
|
|
67
|
+
collectMetadata,
|
|
68
|
+
collectStandardMetadata,
|
|
69
|
+
transformComments,
|
|
70
|
+
renderActor,
|
|
63
71
|
} satisfies FeedbackScreenBodyContextValue;
|
|
64
72
|
|
|
65
73
|
return (
|
package/src/shared/messages.ts
CHANGED
|
@@ -41,6 +41,14 @@ export const englishFeedbackMessages: FeedbackMessages = {
|
|
|
41
41
|
open: "Open",
|
|
42
42
|
back: "Go back",
|
|
43
43
|
},
|
|
44
|
+
metadata: {
|
|
45
|
+
view: "View metadata",
|
|
46
|
+
title: "Diagnostic metadata",
|
|
47
|
+
standard: "Standard",
|
|
48
|
+
additional: "Additional",
|
|
49
|
+
empty: "No metadata values were collected.",
|
|
50
|
+
close: "Close",
|
|
51
|
+
},
|
|
44
52
|
form: {
|
|
45
53
|
kind: "Type",
|
|
46
54
|
title: "Title",
|
|
@@ -94,6 +102,7 @@ export function mergeFeedbackMessages(
|
|
|
94
102
|
...overrides.statuses,
|
|
95
103
|
},
|
|
96
104
|
entry: { ...englishFeedbackMessages.entry, ...overrides.entry },
|
|
105
|
+
metadata: { ...englishFeedbackMessages.metadata, ...overrides.metadata },
|
|
97
106
|
form: { ...englishFeedbackMessages.form, ...overrides.form },
|
|
98
107
|
comments: { ...englishFeedbackMessages.comments, ...overrides.comments },
|
|
99
108
|
};
|