@webority/support-react 0.13.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.
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A "Report an issue" button. It takes a screenshot of the page first, then opens a dialog
3
+ * where the person describes the problem, reviews or removes the screenshot, and adds more
4
+ * images. The ticket carries the page address, the browser and the product version.
5
+ *
6
+ * @param {object} props
7
+ * @param {string} [props.label="Report an issue"] The button text.
8
+ * @param {boolean} [props.floating=false] Pin the button to the bottom corner of the screen.
9
+ * @param {string} [props.defaultCategory="Bug report"] Preselected when the config lists it.
10
+ * @param {string} [props.appVersion] The product build, recorded on the ticket.
11
+ * @param {(id: string) => string} [props.getTicketHref] Where the new ticket can be opened; shows a View ticket link.
12
+ * @param {(ticket: any) => void} [props.onOpenTicket] Called by the View ticket button when there is no href.
13
+ * @param {(ticket: any) => void} [props.onCreated] Called once the ticket is raised.
14
+ */
15
+ export default function ReportIssue({ label, floating, defaultCategory, appVersion, getTicketHref, onOpenTicket, onCreated, }: {
16
+ label?: string;
17
+ floating?: boolean;
18
+ defaultCategory?: string;
19
+ appVersion?: string;
20
+ getTicketHref?: (id: string) => string;
21
+ onOpenTicket?: (ticket: any) => void;
22
+ onCreated?: (ticket: any) => void;
23
+ }): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The customer's support page: their tickets, one ticket's conversation, and a New ticket
3
+ * dialog. Opens the ticket named by `?ticket=<id>` in the address (support emails link there),
4
+ * or `initialTicketId` when given, and keeps that parameter in step as tickets open and close.
5
+ *
6
+ * @param {object} props
7
+ * @param {string} [props.initialTicketId] Open this ticket first. Wins over the address.
8
+ * @param {string} [props.defaultCategory] Preselected in the New ticket form when the config lists it.
9
+ * @param {string} [props.appVersion] The product build, recorded on new tickets.
10
+ */
11
+ export default function SupportCenter({ initialTicketId, defaultCategory, appVersion }: {
12
+ initialTicketId?: string;
13
+ defaultCategory?: string;
14
+ appVersion?: string;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * How to reach support: the email address with a copy button, the phone number as a call link,
3
+ * and the hours. A field the product has not set is left out; nothing renders when none is set.
4
+ *
5
+ * @param {object} props
6
+ * @param {string} [props.className]
7
+ */
8
+ export default function SupportContact({ className }: {
9
+ className?: string;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Makes a support client available to every support component and hook below it.
3
+ *
4
+ * @param {object} props
5
+ * @param {import('./client.js').SupportClient} props.client From `createSupportClient(http)`.
6
+ * @param {import('react').ReactNode} props.children
7
+ */
8
+ export default function SupportProvider({ client, children }: {
9
+ client: import('./client.js').SupportClient;
10
+ children: import('react').ReactNode;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A failed support request. `message` is written for the person and is safe to show as-is.
3
+ */
4
+ export declare class SupportApiError extends Error {
5
+ code: string;
6
+ status: number;
7
+ /**
8
+ * @param {string} code Machine-readable reason from the API, or "network" when no response arrived.
9
+ * @param {string} message The sentence to show the person.
10
+ * @param {number} [status] HTTP status, when a response arrived.
11
+ */
12
+ constructor(code: string, message: string, status?: number);
13
+ }
14
+ export type SupportHttp = {
15
+ get: (url: string, config?: object) => Promise<{
16
+ data: any;
17
+ }>;
18
+ post: (url: string, data?: any, config?: object) => Promise<{
19
+ data: any;
20
+ }>;
21
+ };
22
+ export type NewTicketInput = {
23
+ category: string;
24
+ subject: string;
25
+ body: string;
26
+ pageUrl?: string;
27
+ userAgent?: string;
28
+ appVersion?: string;
29
+ /**
30
+ * Files the person picked, pasted or dropped.
31
+ */
32
+ files?: Blob[];
33
+ /**
34
+ * Page captures taken by the report-an-issue widget.
35
+ */
36
+ screenshots?: Blob[];
37
+ };
38
+ export type ReplyInput = {
39
+ body: string;
40
+ files?: Blob[];
41
+ screenshots?: Blob[];
42
+ };
43
+ export type RatingInput = {
44
+ satisfaction: "Satisfied" | "Unsatisfied";
45
+ comment?: string;
46
+ };
47
+ export type SupportClient = {
48
+ getConfig: () => Promise<any>;
49
+ listTickets: () => Promise<any[]>;
50
+ getTicket: (id: string) => Promise<any>;
51
+ createTicket: (input: NewTicketInput) => Promise<any>;
52
+ reply: (id: string, input: ReplyInput) => Promise<any>;
53
+ reopen: (id: string) => Promise<any>;
54
+ rate: (id: string, input: RatingInput) => Promise<any>;
55
+ fetchAttachment: (id: string) => Promise<Blob>;
56
+ };
57
+ /**
58
+ * @typedef {object} SupportHttp An axios-like HTTP client that already carries the bearer token.
59
+ * @property {(url: string, config?: object) => Promise<{ data: any }>} get
60
+ * @property {(url: string, data?: any, config?: object) => Promise<{ data: any }>} post
61
+ */
62
+ /**
63
+ * @typedef {object} NewTicketInput
64
+ * @property {string} category
65
+ * @property {string} subject
66
+ * @property {string} body
67
+ * @property {string} [pageUrl]
68
+ * @property {string} [userAgent]
69
+ * @property {string} [appVersion]
70
+ * @property {Blob[]} [files] Files the person picked, pasted or dropped.
71
+ * @property {Blob[]} [screenshots] Page captures taken by the report-an-issue widget.
72
+ */
73
+ /**
74
+ * @typedef {object} ReplyInput
75
+ * @property {string} body
76
+ * @property {Blob[]} [files]
77
+ * @property {Blob[]} [screenshots]
78
+ */
79
+ /**
80
+ * @typedef {object} RatingInput
81
+ * @property {"Satisfied"|"Unsatisfied"} satisfaction
82
+ * @property {string} [comment]
83
+ */
84
+ /**
85
+ * @typedef {object} SupportClient
86
+ * @property {() => Promise<any>} getConfig
87
+ * @property {() => Promise<any[]>} listTickets
88
+ * @property {(id: string) => Promise<any>} getTicket
89
+ * @property {(input: NewTicketInput) => Promise<any>} createTicket
90
+ * @property {(id: string, input: ReplyInput) => Promise<any>} reply
91
+ * @property {(id: string) => Promise<any>} reopen
92
+ * @property {(id: string, input: RatingInput) => Promise<any>} rate
93
+ * @property {(id: string) => Promise<Blob>} fetchAttachment
94
+ */
95
+ /**
96
+ * Binds the support HTTP contract to the product's own authenticated client.
97
+ *
98
+ * @param {SupportHttp} http The product's axios-like instance (bearer token and refresh already wired).
99
+ * @param {string} [basePath="/api/support"] The prefix the API maps the support endpoints under.
100
+ * @returns {SupportClient}
101
+ */
102
+ export declare function createSupportClient(http: SupportHttp, basePath?: string): SupportClient;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Picked, dropped or pasted files for a message, limited to what the support config allows.
3
+ *
4
+ * @param {object} props
5
+ * @param {string} [props.id]
6
+ * @param {string} [props.label="Attachments"]
7
+ * @param {File[]} props.files
8
+ * @param {(files: File[]) => void} props.onChange
9
+ * @param {{ maxAttachmentBytes?: number, allowedContentTypes?: string[] }|null} [props.config]
10
+ * @param {string} [props.error]
11
+ */
12
+ export default function AttachmentField({ id, label, files, onChange, config, error }: {
13
+ id?: string;
14
+ label?: string;
15
+ files: File[];
16
+ onChange: (files: File[]) => void;
17
+ config?: {
18
+ maxAttachmentBytes?: number;
19
+ allowedContentTypes?: string[];
20
+ } | null;
21
+ error?: string;
22
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * One attachment on a message. The bytes need the bearer token, so they are fetched through the
3
+ * support client into an object URL that is revoked when the thumbnail goes away.
4
+ *
5
+ * @param {object} props
6
+ * @param {{ id: string, fileName: string, contentType: string, sizeBytes: number, kind: string }} props.attachment
7
+ */
8
+ export default function AttachmentThumb({ attachment }: {
9
+ attachment: {
10
+ id: string;
11
+ fileName: string;
12
+ contentType: string;
13
+ sizeBytes: number;
14
+ kind: string;
15
+ };
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A ticket's conversation. The customer's own messages sit on the right, the support team's on
3
+ * the left, and status or assignment events show as small centred lines.
4
+ *
5
+ * @param {object} props
6
+ * @param {{ id: string, authorKind: string, authorName: string|null, body: string, createdDateTimeUtc: string, attachments: any[] }[]} props.messages
7
+ */
8
+ export default function MessageThread({ messages }: {
9
+ messages: {
10
+ id: string;
11
+ authorKind: string;
12
+ authorName: string | null;
13
+ body: string;
14
+ createdDateTimeUtc: string;
15
+ attachments: any[];
16
+ }[];
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The New ticket dialog.
3
+ *
4
+ * @param {object} props
5
+ * @param {boolean} props.open
6
+ * @param {() => void} props.onClose
7
+ * @param {(ticket: any) => void} props.onCreated Receives the new ticket.
8
+ * @param {any} props.config
9
+ * @param {string} [props.defaultCategory]
10
+ * @param {string} [props.appVersion] Recorded on the ticket so support knows which build you were on.
11
+ */
12
+ export default function NewTicketModal({ open, onClose, onCreated, config, defaultCategory, appVersion }: {
13
+ open: boolean;
14
+ onClose: () => void;
15
+ onCreated: (ticket: any) => void;
16
+ config: any;
17
+ defaultCategory?: string;
18
+ appVersion?: string;
19
+ }): import("react").JSX.Element;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Asks whether a settled ticket solved the problem, or shows the answer already given.
3
+ *
4
+ * @param {object} props
5
+ * @param {"Satisfied"|"Unsatisfied"|null} [props.satisfaction] The rating already given, if any.
6
+ * @param {string|null} [props.comment] The comment given with it.
7
+ * @param {boolean} [props.canRate=false]
8
+ * @param {number} [props.commentMaxLength] From the support config.
9
+ * @param {(input: { satisfaction: "Satisfied"|"Unsatisfied", comment: string }) => Promise<unknown>} props.onRate
10
+ */
11
+ export default function RatingPanel({ satisfaction, comment, canRate, commentMaxLength, onRate }: {
12
+ satisfaction?: "Satisfied" | "Unsatisfied" | null;
13
+ comment?: string | null;
14
+ canRate?: boolean;
15
+ commentMaxLength?: number;
16
+ onRate: (input: {
17
+ satisfaction: "Satisfied" | "Unsatisfied";
18
+ comment: string;
19
+ }) => Promise<unknown>;
20
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The customer's reply to a ticket, with attachments. Images pasted into the message are attached.
3
+ *
4
+ * @param {object} props
5
+ * @param {(input: { body: string, files: File[] }) => Promise<unknown>} props.onSend Rejects with a SupportApiError on failure.
6
+ * @param {any} [props.config] The support config, for the text and attachment limits.
7
+ * @param {boolean} [props.reopens=false] The ticket is settled, so a reply reopens it.
8
+ */
9
+ export default function ReplyBox({ onSend, config, reopens }: {
10
+ onSend: (input: {
11
+ body: string;
12
+ files: File[];
13
+ }) => Promise<unknown>;
14
+ config?: any;
15
+ reopens?: boolean;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One ticket: its facts, the conversation, and what the customer can do next.
3
+ *
4
+ * @param {object} props
5
+ * @param {string} props.ticketId
6
+ * @param {() => void} props.onBack Return to the ticket list.
7
+ * @param {() => void} [props.onChanged] The ticket changed (reply, reopen, rating), so a list can refresh.
8
+ */
9
+ export default function TicketDetail({ ticketId, onBack, onChanged }: {
10
+ ticketId: string;
11
+ onBack: () => void;
12
+ onChanged?: () => void;
13
+ }): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The fields of a new ticket: category, subject, message and attachments. Validates against the
3
+ * support config before anything uploads. The submit button lives outside (a modal footer), tied
4
+ * to this form by `id`.
5
+ *
6
+ * @param {object} props
7
+ * @param {string} props.id The form's id, for a submit button outside it.
8
+ * @param {any} props.config The support config: categories, text limits and attachment limits.
9
+ * @param {string} [props.defaultCategory] Preselected when the config lists it.
10
+ * @param {Blob[]} [props.extraFiles] Files that ride along from elsewhere (a page capture), counted against the limits.
11
+ * @param {(values: { category: string, subject: string, body: string, files: File[] }) => Promise<unknown>} props.onSubmit Rejects with a SupportApiError on failure.
12
+ * @param {(busy: boolean) => void} [props.onBusyChange]
13
+ * @param {import('react').ReactNode} [props.children] Shown above the attachments, such as a capture preview.
14
+ */
15
+ export default function TicketForm({ id, config, defaultCategory, extraFiles, onSubmit, onBusyChange, children }: {
16
+ id: string;
17
+ config: any;
18
+ defaultCategory?: string;
19
+ extraFiles?: Blob[];
20
+ onSubmit: (values: {
21
+ category: string;
22
+ subject: string;
23
+ body: string;
24
+ files: File[];
25
+ }) => Promise<unknown>;
26
+ onBusyChange?: (busy: boolean) => void;
27
+ children?: import('react').ReactNode;
28
+ }): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The customer's tickets with status tabs and a search box.
3
+ *
4
+ * @param {object} props
5
+ * @param {any[]} props.tickets
6
+ * @param {boolean} props.loading
7
+ * @param {any} [props.error]
8
+ * @param {() => void} props.onRetry
9
+ * @param {(id: string) => void} props.onOpen
10
+ */
11
+ export default function TicketList({ tickets, loading, error, onRetry, onOpen }: {
12
+ tickets: any[];
13
+ loading: boolean;
14
+ error?: any;
15
+ onRetry: () => void;
16
+ onOpen: (id: string) => void;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,54 @@
1
+ /** @type {import('react').Context<{ client: import('./client.js').SupportClient, loadConfig: (fresh?: boolean) => Promise<any> } | null>} */
2
+ export declare const SupportContext: import('react').Context<{
3
+ client: import('./client.js').SupportClient;
4
+ loadConfig: (fresh?: boolean) => Promise<any>;
5
+ } | null>;
6
+ /**
7
+ * The support client the nearest SupportProvider holds.
8
+ *
9
+ * @returns {import('./client.js').SupportClient}
10
+ */
11
+ export declare function useSupportClient(): import('./client.js').SupportClient;
12
+ /**
13
+ * What the customer may send and how to reach support. Loaded once per provider and shared.
14
+ *
15
+ * @returns {{ config: any, loading: boolean, error: any, reload: () => void }}
16
+ */
17
+ export declare function useSupportConfig(): {
18
+ config: any;
19
+ loading: boolean;
20
+ error: any;
21
+ reload: () => void;
22
+ };
23
+ /**
24
+ * The signed-in customer's tickets.
25
+ *
26
+ * @returns {{ tickets: any[], loading: boolean, error: any, reload: () => void }}
27
+ */
28
+ export declare function useSupportTickets(): {
29
+ tickets: any[];
30
+ loading: boolean;
31
+ error: any;
32
+ reload: () => void;
33
+ };
34
+ /**
35
+ * One ticket with its thread, plus the actions a customer can take on it. Each action returns
36
+ * the updated ticket and replaces the one held here.
37
+ *
38
+ * @param {string|null|undefined} id The ticket's id; nothing loads while it is empty.
39
+ * @returns {{
40
+ * ticket: any, loading: boolean, error: any, reload: () => void,
41
+ * reply: (input: import('./client.js').ReplyInput) => Promise<any>,
42
+ * reopen: () => Promise<any>,
43
+ * rate: (input: import('./client.js').RatingInput) => Promise<any>
44
+ * }}
45
+ */
46
+ export declare function useSupportTicket(id: string | null | undefined): {
47
+ ticket: any;
48
+ loading: boolean;
49
+ error: any;
50
+ reload: () => void;
51
+ reply: (input: import('./client.js').ReplyInput) => Promise<any>;
52
+ reopen: () => Promise<any>;
53
+ rate: (input: import('./client.js').RatingInput) => Promise<any>;
54
+ };
@@ -0,0 +1,6 @@
1
+ export { createSupportClient, SupportApiError } from "./client.js";
2
+ export { useSupportClient, useSupportConfig, useSupportTicket, useSupportTickets } from "./context.js";
3
+ export { default as ReportIssue } from "./ReportIssue.jsx";
4
+ export { default as SupportCenter } from "./SupportCenter.jsx";
5
+ export { default as SupportContact } from "./SupportContact.jsx";
6
+ export { default as SupportProvider } from "./SupportProvider.jsx";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The `accept` attribute for a file input, from the config's allowed content types.
3
+ *
4
+ * @param {{ allowedContentTypes?: string[] }|null|undefined} config
5
+ * @returns {string|undefined}
6
+ */
7
+ export declare function acceptOf(config: {
8
+ allowedContentTypes?: string[];
9
+ } | null | undefined): string | undefined;
10
+ /**
11
+ * Checks files against the config before anything uploads. Returns the first problem as a
12
+ * sentence for the person, or null when every file can go.
13
+ *
14
+ * @param {Blob[]} files Every file on the message, screenshots included.
15
+ * @param {{ maxAttachmentBytes?: number, maxAttachmentsPerMessage?: number, allowedContentTypes?: string[] }|null|undefined} config
16
+ * @returns {string|null}
17
+ */
18
+ export declare function validateAttachments(files: Blob[], config: {
19
+ maxAttachmentBytes?: number;
20
+ maxAttachmentsPerMessage?: number;
21
+ allowedContentTypes?: string[];
22
+ } | null | undefined): string | null;
23
+ /**
24
+ * Image files from a paste event, named so they read well on the ticket.
25
+ *
26
+ * @param {ClipboardEvent|{ clipboardData?: DataTransfer|null }} event
27
+ * @returns {File[]}
28
+ */
29
+ export declare function imagesFromPaste(event: ClipboardEvent | {
30
+ clipboardData?: DataTransfer | null;
31
+ }): File[];
@@ -0,0 +1,13 @@
1
+ /**
2
+ * A timestamp in the viewer's own time zone, with the zone named so the reader knows which clock
3
+ * it is. Returns a plain hyphen for a missing or unreadable value.
4
+ *
5
+ * @param {string|null|undefined} iso
6
+ * @returns {string}
7
+ */
8
+ export declare function formatDateTime(iso: string | null | undefined): string;
9
+ /**
10
+ * @param {number} bytes
11
+ * @returns {string}
12
+ */
13
+ export declare function formatBytes(bytes: number): string;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @param {string} status The ticket's status as the API sends it.
3
+ * @returns {string}
4
+ */
5
+ export declare function statusTone(status: string): string;
@@ -0,0 +1,20 @@
1
+ /** The query parameter a support email links with to open one ticket. */
2
+ export declare const TICKET_PARAM = "ticket";
3
+ /**
4
+ * @returns {string|null} The ticket id in the current address, if any.
5
+ */
6
+ export declare function ticketFromLocation(): string | null;
7
+ /**
8
+ * The current page's address with the ticket parameter set, or removed when `id` is empty.
9
+ *
10
+ * @param {string|null|undefined} id
11
+ * @returns {string}
12
+ */
13
+ export declare function ticketHref(id: string | null | undefined): string;
14
+ /**
15
+ * Records the open ticket in the address without a navigation, keeping the router's own history
16
+ * state, so a refresh or a shared link lands on the same ticket.
17
+ *
18
+ * @param {string|null|undefined} id
19
+ */
20
+ export declare function replaceTicketInLocation(id: string | null | undefined): void;
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@webority/support-react",
3
+ "version": "0.13.0",
4
+ "description": "Webority customer support for React portals: ticket centre, report-an-issue with a page screenshot, and support contact details, built on @webority/ui-react.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org/"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Webority/webority-ui.git",
14
+ "directory": "packages/support-react"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "main": "./dist/support-react.js",
20
+ "module": "./dist/support-react.js",
21
+ "types": "./dist/types/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/types/index.d.ts",
25
+ "default": "./dist/support-react.js"
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "sideEffects": false,
30
+ "peerDependencies": {
31
+ "bootstrap": "5.3.3",
32
+ "react": "^19.0.0",
33
+ "react-bootstrap": "^2.10.0",
34
+ "react-dom": "^19.0.0",
35
+ "@webority/ui-react": "0.13.0"
36
+ },
37
+ "devDependencies": {
38
+ "@testing-library/dom": "10.4.2",
39
+ "@testing-library/jest-dom": "7.0.1",
40
+ "@testing-library/react": "^16.3.0",
41
+ "@testing-library/user-event": "^14.6.1",
42
+ "@types/react": "^19.2.17",
43
+ "@vitejs/plugin-react": "^6.0.1",
44
+ "@vitest/coverage-v8": "5.0.1",
45
+ "bootstrap": "5.3.3",
46
+ "jsdom": "30.1.1",
47
+ "react": "^19.2.6",
48
+ "react-bootstrap": "^2.10.6",
49
+ "react-dom": "^19.2.6",
50
+ "typescript": "^7.0.2",
51
+ "vite": "^8.0.12",
52
+ "vitest": "5.0.1",
53
+ "@webority/ui-react": "0.13.0"
54
+ },
55
+ "dependencies": {
56
+ "html-to-image": "^1.11.13"
57
+ },
58
+ "scripts": {
59
+ "build": "vite build && tsc -p tsconfig.json",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest",
62
+ "test:coverage": "vitest run --coverage"
63
+ }
64
+ }