@webority/support-react 0.50.1 → 0.51.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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @webority/support-react
2
2
 
3
- Customer support for a React portal: a ticket centre, a report-an-issue button that attaches a
4
- screenshot of the page, and support contact details. It talks to the Webority Support API that
3
+ Customer support for a React portal: a ticket centre, a report-an-issue button that can attach a
4
+ screenshot of the page when the person asks for one, and support contact details. It talks to the Webority Support API that
5
5
  the product's backend hosts, and it is built on `@webority/ui-react` and `@webority/theme`, so it
6
6
  looks like the rest of the portal with no extra CSS.
7
7
 
@@ -101,9 +101,9 @@ one message, in the package's words.
101
101
 
102
102
  ### `<ReportIssue />`
103
103
 
104
- A button that takes a screenshot of the current page, then opens a dialog to describe the
105
- problem. The person can remove the screenshot and add more images by picking, dropping or
106
- pasting them. The ticket records the page address, the browser and the product version.
104
+ A button that opens a dialog at once to describe the problem. The person can add images by
105
+ picking, dropping or pasting them. The ticket records the page address, the browser and the
106
+ product version.
107
107
 
108
108
  ```jsx
109
109
  <ReportIssue floating appVersion={version} getTicketHref={(id) => `/support?ticket=${id}`} />
@@ -142,8 +142,16 @@ item inside the stack, which is not itself fixed.
142
142
  <ReportIssue floating compact appVersion={version} />
143
143
  ```
144
144
 
145
- The screenshot is taken before the dialog opens, and the button itself is left out of it.
146
- Anything else the page marks with `data-support-capture="exclude"` is left out too.
145
+ Nothing on the page is captured unless the person asks. The dialog has an "Add a screenshot"
146
+ button, with the line "Only what is on your screen now. You can remove it before sending." under
147
+ it. Pressing it hides the dialog and its backdrop, shows a small "Taking a screenshot" status,
148
+ takes a picture of the page, and brings the dialog back with the picture as a thumbnail. What
149
+ they typed is kept, and focus returns to the screenshot buttons. They can then retake it or remove
150
+ it. If the capture fails, the dialog says so and the report can still be sent without one. There
151
+ is no prop to capture on open.
152
+
153
+ The report button, the dialog and the status are left out of the picture. Anything else the page
154
+ marks with `data-support-capture="exclude"` is left out too.
147
155
 
148
156
  ### `<SupportContact />`
149
157
 
@@ -1 +1 @@
1
- import{createContext as e,useCallback as t,useContext as n,useEffect as r,useId as i,useMemo as a,useRef as o,useState as s}from"react";import{AppAlert as c,AppAvatar as l,AppBadge as u,AppButton as d,AppCard as f,AppCopyButton as p,AppDataTable as m,AppFileAttachment as h,AppIcon as g,AppInfoList as _,AppInput as v,AppLink as y,AppModal as b,AppSearch as x,AppSelect as S,AppSkeleton as C,AppSpinner as w,AppStatusBadge as T,AppTableEmptyState as E,AppTabs as D,AppTextarea as O,useFloatingStack as k}from"@webority/ui-react";import{toBlob as A}from"html-to-image";import{createPortal as j}from"react-dom";import{Fragment as ee,jsx as M,jsxs as N}from"react/jsx-runtime";var P=`Something went wrong. Please try again.`,F=class extends Error{constructor(e,t,n){super(t),this.name=`SupportApiError`,this.code=e,this.status=n}};async function I(e){if(e&&typeof Blob<`u`&&e instanceof Blob)try{return JSON.parse(await e.text())}catch{return null}if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return e??null}async function L(e){if(e instanceof F)return e;let t=e?.response;if(!t)return new F(`network`,P);let n=await I(t.data);return new F(typeof n?.code==`string`&&n.code?n.code:`unknown`,typeof n?.message==`string`&&n.message?n.message:P,t.status)}async function R(e){try{return(await e()).data}catch(e){throw await L(e)}}function z(e,t,n){n!=null&&String(n).trim()!==``&&e.append(t,String(n))}function B(e,t,n,r){(n??[]).forEach((n,i)=>{let a=n?.name||`${r}-${i+1}.png`;e.append(t,n,a)})}function te(e,t=`/api/support`){let n=(typeof t==`string`?t:t?.base??`/api/support`).replace(/\/+$/,``),r=e=>`${n}/tickets/${encodeURIComponent(e)}`;return{getConfig:()=>R(()=>e.get(`${n}/settings`)),listTickets:({status:t,search:r,page:i,pageSize:a}={})=>{let o=new URLSearchParams;z(o,`status`,t),z(o,`search`,r?.trim()),z(o,`page`,i),z(o,`pageSize`,a);let s=o.toString();return R(()=>e.get(`${n}/tickets${s?`?${s}`:``}`))},getTicketCounts:()=>R(()=>e.get(`${n}/tickets/counts`)),getTicket:t=>R(()=>e.get(r(t))),createTicket:({category:t,subject:r,body:i,pageUrl:a,userAgent:o,appVersion:s,files:c,screenshots:l})=>{let u=new FormData;return u.append(`category`,t??``),u.append(`subject`,r??``),u.append(`body`,i??``),z(u,`pageUrl`,a),z(u,`userAgent`,o),z(u,`appVersion`,s),B(u,`files`,c,`file`),B(u,`screenshots`,l,`screenshot`),R(()=>e.post(`${n}/tickets`,u))},reply:(t,{body:n,files:i,screenshots:a})=>{let o=new FormData;return o.append(`body`,n??``),B(o,`files`,i,`file`),B(o,`screenshots`,a,`screenshot`),R(()=>e.post(`${r(t)}/replies`,o))},reopen:t=>R(()=>e.post(`${r(t)}/reopen`)),rate:(t,{satisfaction:n,comment:i})=>{let a={satisfaction:n};return i!=null&&i.trim()!==``&&(a.comment=i.trim()),R(()=>e.post(`${r(t)}/rating`,a))},fetchAttachment:t=>R(()=>e.get(`${n}/attachments/${encodeURIComponent(t)}`,{responseType:`blob`})),confirmAction:t=>R(()=>e.post(`${n}/actions/confirm`,{token:t}))}}var V=e(null);function H(){let e=n(V);if(!e)throw Error(`Support components and hooks must be used inside <SupportProvider>.`);return e}function U(){return H().client}function W(){return H().timeZone}function G(e,n,i=!0){let[a,c]=s({data:null,loading:i,error:null}),[l,u]=s(0),d=o(e);d.current=e;let f=o(n);r(()=>{let e=f.current!==n;if(f.current=n,!i){c({data:null,loading:!1,error:null});return}let t=!0;return c(t=>({data:e?null:t.data,loading:!0,error:null})),d.current(l>0).then(e=>t&&c({data:e,loading:!1,error:null}),e=>t&&c(t=>({...t,loading:!1,error:e}))),()=>{t=!1}},[n,i,l]);let p=t(()=>u(e=>e+1),[]),m=t(e=>c(t=>({...t,data:e})),[]);return{...a,reload:p,setData:m}}function K(){let{loadConfig:e}=H(),{data:t,loading:n,error:r,reload:i}=G(e,`config`);return{config:t,loading:n,error:r,reload:i}}function ne(e={}){let t=U(),{status:n,search:r,page:i,pageSize:a}=e,{data:o,loading:s,error:c,reload:l}=G(()=>t.listTickets({status:n,search:r,page:i,pageSize:a}),JSON.stringify([`tickets`,n??null,r??null,i??null,a??null]));return{tickets:o?.items??[],totalCount:o?.totalCount??0,totalPages:o?.totalPages??0,loading:s,error:c,reload:l}}function q(){let e=U(),{data:t,loading:n,error:r,reload:i}=G(()=>e.getTicketCounts(),`ticket-counts`);return{counts:t,loading:n,error:r,reload:i}}function re(e){let n=U(),{data:r,loading:i,error:a,reload:o,setData:s}=G(()=>n.getTicket(e),e??``,!!e),c=t(async e=>{let t=await e();return s(t),t},[s]);return{ticket:r,loading:i,error:a,reload:o,reply:t(t=>c(()=>n.reply(e,t)),[c,n,e]),reopen:t(()=>c(()=>n.reopen(e)),[c,n,e]),rate:t(t=>c(()=>n.rate(e,t)),[c,n,e])}}var J=`Asia/Kolkata`,ie=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],ae=new Map;function oe(e){let t=ae.get(e);return t||(t=new Intl.DateTimeFormat(`en-IN`,{day:`2-digit`,month:`numeric`,year:`numeric`,hour:`numeric`,minute:`2-digit`,hour12:!0,timeZone:e}),ae.set(e,t)),t}function Y(e,t=J){if(!e)return`-`;let n=new Date(e);if(Number.isNaN(n.getTime()))return`-`;let r=Object.fromEntries(oe(t).formatToParts(n).map(e=>[e.type,e.value]));return`${r.day}-${ie[Number(r.month)-1]}-${r.year}, ${r.hour}:${r.minute} ${r.dayPeriod.toUpperCase()}`}function se(e=J){return new Intl.DateTimeFormat(`en-IN`,{timeZone:e,timeZoneName:`short`}).formatToParts(new Date).find(e=>e.type===`timeZoneName`)?.value??e}function ce(e){if(!Number.isFinite(e)||e<0)return`-`;if(e<1024)return`${e} B`;if(e<1024*1024)return`${Math.round(e/1024)} KB`;let t=e/(1024*1024);return`${Number.isInteger(t)?t:t.toFixed(1)} MB`}function le(e){return(e?.allowedContentTypes??[]).join(`,`)}function ue(e,t){if(t.length===0)return!0;let n=(e||``).toLowerCase();return t.some(e=>{let t=e.toLowerCase();return t.endsWith(`/*`)?n.startsWith(t.slice(0,-1)):n===t})}function X(e,t){let n=e??[],r=t?.maxAttachmentsPerMessage??0,i=t?.maxAttachmentBytes??0,a=t?.allowedContentTypes??[];if(r>0&&n.length>r)return r===1?`You can attach one file.`:`You can attach up to ${r} files.`;for(let e of n){let t=e.name||`This file`;if(!ue(e.type,a))return`${t} is a file type we cannot accept.`;if(i>0&&e.size>i)return`${t} is larger than ${ce(i)}.`}return null}function de(e){let t=Array.from(e.clipboardData?.items??[]),n=Date.now();return t.filter(e=>e.kind===`file`&&e.type.startsWith(`image/`)).map(e=>e.getAsFile()).filter(Boolean).map((e,t)=>{let r=(e.type.split(`/`)[1]||`png`).replace(`jpeg`,`jpg`);return new File([e],`pasted-image-${n}-${t+1}.${r}`,{type:e.type})})}function fe({id:e,label:t=`Attachments`,files:n,onChange:r,config:i,error:a}){let o=i?.maxAttachmentBytes??0,s=o>0?Math.max(.1,Math.round(o/(1024*1024)*10)/10):0;return M(h,{id:e,label:t,optional:!0,multiple:!0,value:n,onChange:r,accept:le(i),maxSizeMb:s,validate:!1,error:a||void 0})}function pe(e,t){return t&&e.includes(t)?t:e[0]??``}function me({id:e,config:t,defaultCategory:n,extraFiles:r=[],onSubmit:i,onBusyChange:a,children:o}){let l=t?.categories??[],[u,d]=s(()=>pe(l,n)),[f,p]=s(``),[m,h]=s(``),[g,_]=s([]),[y,b]=s({}),[x,C]=s(``),w=e=>{_(e),b(n=>({...n,files:X([...r,...e],t)??void 0}))},T=e=>{let t=de(e);t.length>0&&w([...g,...t])};function E(){let e={};u||(e.category=`Choose a category.`),f.trim()||(e.subject=`Give your ticket a subject.`),m.trim()||(e.body=`Tell us what happened.`);let n=X([...r,...g],t);return n&&(e.files=n),b(e),Object.keys(e).length===0}async function D(e){if(e.preventDefault(),C(``),E()){a?.(!0);try{await i({category:u,subject:f.trim(),body:m.trim(),files:g})}catch(e){C(e?.message||`Your ticket did not send. Please try again.`)}finally{a?.(!1)}}}return N(`form`,{id:e,onSubmit:D,noValidate:!0,className:`d-flex flex-column gap-3`,children:[x&&M(c,{variant:`danger`,children:x}),M(S,{id:`${e}-category`,label:`Category`,required:!0,value:u,onChange:d,options:l.map(e=>({value:e,label:e})),placeholder:`Choose a category`,error:y.category}),M(v,{id:`${e}-subject`,label:`Subject`,required:!0,maxLength:t?.subjectMaxLength,placeholder:`A short summary`,value:f,onChange:e=>p(e.target.value),error:y.subject}),M(O,{id:`${e}-body`,label:`Message`,required:!0,rows:6,maxLength:t?.bodyMaxLength,placeholder:`What happened, and what did you expect to happen?`,value:m,onChange:e=>h(e.target.value),onPaste:T,hint:`You can paste an image into the message.`,error:y.body}),o,M(fe,{id:`${e}-files`,files:g,onChange:w,config:t,error:y.files})]})}var he=`supportCapture`,ge={zIndex:`var(--wui-z-fixed)`,bottom:0,insetInlineEnd:0},_e=e=>e?.dataset?.[he]!==`exclude`;async function ve(){let e=await A(document.body,{filter:_e,pixelRatio:Math.min(window.devicePixelRatio||1,2)});if(!e)throw Error(`The page capture came back empty.`);return new File([e],`screenshot.png`,{type:`image/png`})}function ye({file:e,onRemove:t}){let[n,i]=s(null);return r(()=>{let t=URL.createObjectURL(e);return i(t),()=>URL.revokeObjectURL(t)},[e]),N(`figure`,{className:`mb-0 d-flex flex-column gap-2`,children:[M(`figcaption`,{className:`wui-text-sm fw-semibold`,children:`Screenshot of this page`}),n&&M(`img`,{src:n,alt:`Screenshot of the page you were on`,className:`d-block w-100 border rounded`}),M(d,{variant:`ghost`,size:`sm`,leftIcon:`x`,onClick:t,className:`align-self-start`,children:`Remove screenshot`})]})}function be({label:e=`Report an issue`,floating:t=!1,compact:n=!1,defaultCategory:r=`Bug report`,appVersion:a,getTicketHref:o,onOpenTicket:l,onCreated:u,className:f,style:p}){let m=U(),{config:h,error:_}=K(),v=`support-report-${i().replace(/[^a-zA-Z0-9-]/g,``)}`,[y,x]=s(!1),[S,C]=s(!1),[w,T]=s(null),[E,D]=s(!1),[O,A]=s(!1),[P,F]=s(null),I=k(`end`,t&&n);async function L(){C(!0),F(null);try{T(await ve()),D(!1)}catch{T(null),D(!0)}finally{C(!1),x(!0)}}let R=()=>{O||(x(!1),T(null),F(null))};async function z(e){let t=await m.createTicket({...e,pageUrl:window.location.href,userAgent:navigator.userAgent,appVersion:a,screenshots:w?[w]:[]});F(t),T(null),u?.(t)}let B=P&&o?o(P.id):void 0,te=P?N(`div`,{className:`d-flex justify-content-end gap-2 w-100`,children:[M(d,{variant:`secondary`,onClick:R,children:`Close`}),P&&(B||l)?M(d,{href:B,onClick:B?void 0:()=>{x(!1),F(null),l(P)},children:`View ticket`}):null]}):N(`div`,{className:`d-flex justify-content-end gap-2 w-100`,children:[M(d,{variant:`secondary`,onClick:R,disabled:O,children:`Cancel`}),M(d,{type:`submit`,form:v,loading:O,disabled:!h,children:`Send report`})]}),V=n?M(d,{variant:`secondary`,className:`wui-floating-btn wui-floating-btn--secondary`,loading:S,onClick:L,"aria-haspopup":`dialog`,"aria-label":e,tooltip:e,tooltipPlacement:`start`,children:M(g,{name:`bug`,size:22})}):M(d,{leftIcon:`bug`,variant:t?`primary`:`outline`,loading:S,onClick:L,"aria-haspopup":`dialog`,children:e});return N(ee,{children:[t&&n?I&&j(M(`span`,{className:[`wui-floating-stack-item wui-floating-stack-item--secondary`,f].filter(Boolean).join(` `),style:p,children:V}),I):M(`span`,{"data-support-capture":`exclude`,className:[t?`position-fixed m-3`:`d-inline-block`,f].filter(Boolean).join(` `),style:t?{...ge,...p}:p,children:V}),M(b,{open:y,size:`lg`,title:`Report an issue`,subtitle:`Tell us what went wrong. We attach a screenshot of this page to help us see it.`,headerIcon:`bug`,staticBackdrop:!0,onClose:R,footer:te,children:P?M(c,{variant:`success`,role:`status`,title:`Ticket ${P.number} is with the support team`,children:`We reply by email. You can follow the ticket on your support page.`}):h?M(me,{id:v,config:h,defaultCategory:r,extraFiles:w?[w]:[],onSubmit:z,onBusyChange:A,children:w?M(ye,{file:w,onRemove:()=>T(null)}):E?M(c,{variant:`info`,children:`We could not take a screenshot of this page. You can attach an image instead.`}):null}):M(c,{variant:_?`danger`:`info`,children:_?_.message||`We could not load the support settings.`:`Loading the form.`})})]})}function xe({open:e,onClose:t,onCreated:n,config:r,defaultCategory:a,appVersion:o}){let c=U(),l=`support-new-${i().replace(/[^a-zA-Z0-9-]/g,``)}`,[u,f]=s(!1);async function p(e){n(await c.createTicket({...e,userAgent:typeof navigator>`u`?void 0:navigator.userAgent,appVersion:o}))}return M(b,{open:e,size:`lg`,title:`New ticket`,subtitle:`Tell us what you need. We reply by email and on this page.`,headerIcon:`message-square`,staticBackdrop:!0,onClose:()=>{u||t()},footer:N(`div`,{className:`d-flex justify-content-end gap-2 w-100`,children:[M(d,{variant:`secondary`,onClick:t,disabled:u,children:`Cancel`}),M(d,{type:`submit`,form:l,loading:u,children:`Send ticket`})]}),children:e&&M(me,{id:l,config:r,defaultCategory:a,onSubmit:p,onBusyChange:f})})}var Se={Open:`warning`,InProgress:`info`,Resolved:`success`,Closed:`neutral`};function Ce(e){return Se[e]??`neutral`}var we={width:140,maxWidth:`100%`},Te={height:84};function Ee({attachment:e}){let t=U(),n=(e.contentType||``).startsWith(`image/`),[i,a]=s(null),[o,c]=s(!1);r(()=>{let n=!0,r=null;return a(null),c(!1),t.fetchAttachment(e.id).then(e=>{r=URL.createObjectURL(e),n?a(r):URL.revokeObjectURL(r)},()=>n&&c(!0)),()=>{n=!1,r&&URL.revokeObjectURL(r)}},[t,e.id]);let l=`Open ${e.fileName}`,u=N(`span`,{className:`d-block text-truncate px-2 py-1 wui-text-xs text-body-secondary`,children:[e.fileName,`, `,ce(e.sizeBytes)]});return o?N(`span`,{className:`d-inline-flex flex-column border rounded overflow-hidden`,style:we,children:[N(`span`,{className:`d-flex align-items-center justify-content-center gap-1 p-2 wui-text-xs text-body-secondary`,children:[M(g,{name:`x-circle`,size:14}),`Could not load this file.`]}),u]}):N(`a`,{className:`d-inline-flex flex-column border rounded overflow-hidden text-decoration-none`,style:we,href:i??void 0,target:`_blank`,rel:`noopener noreferrer`,"aria-label":l,"aria-busy":!i||void 0,children:[M(`span`,{className:`d-flex align-items-center justify-content-center bg-body-tertiary`,style:Te,children:i?n?M(`img`,{src:i,alt:e.fileName,className:`d-block w-100 h-100 object-fit-cover`}):M(g,{name:`file-text`,size:28}):M(w,{size:`sm`,label:`Loading ${e.fileName}`})}),u]})}var De={whiteSpace:`pre-wrap`,overflowWrap:`anywhere`};function Oe({messages:e}){let t=W();return M(`ol`,{className:`list-unstyled d-flex flex-column gap-3 mb-0`,"aria-label":`Conversation`,children:e.map(e=>{let n=M(`time`,{dateTime:e.createdDateTimeUtc,className:`wui-text-xs text-body-secondary text-nowrap`,children:Y(e.createdDateTimeUtc,t)});if(e.authorKind===`System`)return N(`li`,{className:`text-center wui-text-xs text-body-secondary`,"data-author":`system`,children:[M(`span`,{className:`d-block`,children:e.body}),n]},e.id);let r=e.authorKind===`Customer`,i=e.authorName||(r?`You`:`Support team`);return N(`li`,{className:`d-flex align-items-start gap-2${r?` flex-row-reverse`:``}`,"data-author":r?`customer`:`agent`,children:[M(l,{name:i,size:32,tone:r?`neutral`:`primary`}),N(f,{className:`flex-grow-1 border`,padding:`sm`,variant:r?`default`:`accent`,style:{minWidth:0},children:[N(`div`,{className:`d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2`,children:[N(`span`,{className:`d-inline-flex flex-wrap align-items-center gap-2 fw-semibold wui-text-sm text-break`,children:[i,!r&&M(u,{tone:`primary`,children:`Support`})]}),n]}),M(`div`,{className:`wui-text-sm`,style:De,children:e.body}),e.attachments?.length>0&&M(`div`,{className:`d-flex flex-wrap gap-2 mt-2`,children:e.attachments.map(e=>M(Ee,{attachment:e},e.id))})]})]},e.id)})})}function ke({satisfaction:e,comment:t,canRate:n=!1,commentMaxLength:r,onRate:a}){let o=i(),[l,u]=s(``),[p,m]=s(null),[h,g]=s(``);if(e)return N(c,{variant:e===`Satisfied`?`success`:`info`,role:`status`,children:[e===`Satisfied`?`You told us this solved your problem.`:`You told us this did not solve your problem.`,t?N(`span`,{className:`d-block mt-1`,children:[`Your comment: `,t]}):null]});if(!n)return null;async function _(e){g(``),m(e);try{await a({satisfaction:e,comment:l})}catch(e){g(e?.message||`Your rating did not send. Please try again.`)}finally{m(null)}}return M(f,{padding:`sm`,className:`border`,title:`Did this solve your problem?`,children:N(`div`,{className:`d-flex flex-column gap-2`,children:[h&&M(c,{variant:`danger`,children:h}),M(O,{id:`${o}-comment`,label:`Anything to add?`,rows:2,maxLength:r,value:l,onChange:e=>u(e.target.value),hint:`Optional. Your comment goes to the support team.`}),N(`div`,{className:`d-flex flex-wrap gap-2`,children:[M(d,{variant:`outline`,leftIcon:`check-circle`,loading:p===`Satisfied`,disabled:p!==null,onClick:()=>_(`Satisfied`),children:`Yes, it is solved`}),M(d,{variant:`outline`,leftIcon:`x-circle`,loading:p===`Unsatisfied`,disabled:p!==null,onClick:()=>_(`Unsatisfied`),children:`No, it is not`})]})]})})}function Ae({onSend:e,config:t,reopens:n=!1}){let r=i(),[a,o]=s(``),[l,u]=s([]),[f,p]=s(``),[m,h]=s(``),[g,_]=s(!1),v=e=>{u(e),p(X(e,t)??``)},y=e=>{let t=de(e);t.length>0&&v([...l,...t])};async function b(n){n.preventDefault();let r=a.trim();if(!r){h(`Write a message before you send it.`);return}let i=X(l,t);if(i){p(i);return}h(``),_(!0);try{await e({body:r,files:l}),o(``),u([]),p(``)}catch(e){h(e?.message||`Your reply did not send. Please try again.`)}finally{_(!1)}}return N(`form`,{onSubmit:b,noValidate:!0,className:`d-flex flex-column gap-2`,"aria-label":`Reply to this ticket`,children:[m&&M(c,{variant:`danger`,children:m}),M(O,{id:`${r}-body`,label:`Your reply`,rows:4,autoGrow:!0,maxLength:t?.bodyMaxLength,value:a,onChange:e=>o(e.target.value),onPaste:y,hint:n?`Sending a reply reopens this ticket. You can paste an image into the message.`:`You can paste an image into the message.`,required:!0}),M(fe,{id:`${r}-files`,files:l,onChange:v,config:t,error:f}),M(`div`,{className:`d-flex justify-content-end`,children:M(d,{type:`submit`,leftIcon:`send`,loading:g,children:`Send reply`})})]})}function Z({label:e,children:t}){return N(`div`,{className:`d-flex flex-column`,children:[M(`span`,{className:`wui-text-xs fw-semibold text-body-secondary`,children:e}),M(`span`,{className:`wui-text-sm`,children:t})]})}function je({ticketId:e,onBack:t,onChanged:n}){let{config:r}=K(),i=W(),{ticket:a,loading:o,error:l,reload:u,reply:f,reopen:p,rate:m}=re(e),[h,g]=s(!1),[_,v]=s(``),y=async e=>{let t=await e();return n?.(),t};async function b(){v(``),g(!0);try{await y(p)}catch(e){v(e?.message||`The ticket did not reopen. Please try again.`)}finally{g(!1)}}let x=M(d,{variant:`ghost`,size:`sm`,leftIcon:`arrow-left`,onClick:t,className:`align-self-start`,children:`Back to tickets`});if(!a)return N(`section`,{className:`d-flex flex-column gap-3`,"aria-busy":o||void 0,children:[x,l?N(c,{variant:`danger`,children:[l.message||`We could not load this ticket.`,` `,M(d,{variant:`ghost`,size:`sm`,onClick:u,children:`Try again`})]}):M(C,{height:160})]});let S=a.status===`Resolved`||a.status===`Closed`;return N(`section`,{className:`d-flex flex-column gap-3`,"aria-labelledby":`support-ticket-${a.id}`,children:[x,N(`header`,{className:`d-flex flex-column gap-2`,children:[N(`div`,{className:`d-flex flex-wrap align-items-center gap-2`,children:[M(`h2`,{id:`support-ticket-${a.id}`,className:`h5 mb-0 text-break`,children:a.subject}),a.statusLabel&&M(T,{label:a.statusLabel,tone:Ce(a.status)})]}),N(`div`,{className:`row row-cols-2 row-cols-md-4 g-3`,children:[M(Z,{label:`Ticket`,children:a.number}),M(Z,{label:`Category`,children:a.category||`-`}),M(Z,{label:`Opened`,children:M(`time`,{dateTime:a.createdDateTimeUtc,children:Y(a.createdDateTimeUtc,i)})}),M(Z,{label:`Last update`,children:M(`time`,{dateTime:a.updatedDateTimeUtc,children:Y(a.updatedDateTimeUtc,i)})})]}),N(`p`,{className:`wui-text-xs text-body-secondary mb-0`,children:[`Times are shown in `,se(i),`.`]})]}),_&&M(c,{variant:`danger`,children:_}),S&&M(c,{variant:`success`,role:`status`,children:`If you still need help, reopen the ticket or send a reply.`}),a.canReopen&&M(d,{variant:`secondary`,leftIcon:`history`,loading:h,onClick:b,className:`align-self-start`,children:`Reopen ticket`}),M(ke,{satisfaction:a.satisfaction,comment:a.satisfactionComment,canRate:a.canRate,commentMaxLength:r?.satisfactionCommentMaxLength,onRate:e=>y(()=>m(e))}),M(Oe,{messages:a.messages??[]}),M(Ae,{config:r,reopens:S,onSend:e=>y(()=>f(e))})]})}var Q=`ticket`;function Me(){return typeof window>`u`?null:new URLSearchParams(window.location.search).get(Q)}function Ne(e){if(typeof window>`u`)return e?`?${Q}=${encodeURIComponent(e)}`:`?`;let t=new URL(window.location.href);return e?t.searchParams.set(Q,e):t.searchParams.delete(Q),`${t.pathname}${t.search}${t.hash}`}function Pe(e){if(typeof window>`u`)return;let t=Ne(e);t!==`${window.location.pathname}${window.location.search}${window.location.hash}`&&window.history.replaceState(window.history.state,``,t)}var Fe=[{key:`all`,label:`All`,countKey:`total`},{key:`Open`,label:`Open`,countKey:`open`},{key:`InProgress`,label:`In progress`,countKey:`inProgress`},{key:`Resolved`,label:`Resolved`,countKey:`resolved`},{key:`Closed`,label:`Closed`,countKey:`closed`}];function Ie({tickets:e,loading:t,error:n,onRetry:r,onOpen:i,status:a,onStatusChange:o,counts:s,search:l,onSearchChange:u,pagination:f}){let p=W();return N(`div`,{className:`d-flex flex-column gap-3`,children:[n&&N(c,{variant:`danger`,children:[n.message||`We could not load your tickets.`,` `,M(d,{variant:`ghost`,size:`sm`,onClick:r,children:`Try again`})]}),N(`div`,{className:`d-flex flex-column flex-md-row gap-2 align-items-md-center justify-content-between`,children:[M(D,{tabs:Fe.map(({key:e,label:t,countKey:n})=>({key:e,label:t,count:s?.[n]})),value:a,onChange:o,ariaLabel:`Filter tickets by status`,className:`flex-grow-1`}),M(x,{value:l,onChange:u,placeholder:`Search tickets`,ariaLabel:`Search tickets`})]}),M(m,{columns:[{key:`subject`,label:`Subject`,render:e=>N(`div`,{className:`d-flex flex-column`,style:{minWidth:0},children:[M(y,{href:Ne(e.id),animation:`none`,className:`fw-semibold text-break`,onClick:t=>{t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||(t.preventDefault(),i(e.id))},children:e.subject}),N(`span`,{className:`wui-text-xs text-body-secondary`,children:[e.number,`, `,e.category,e.awaitingCustomer?`. Waiting for your reply`:``]})]})},{key:`status`,label:`Status`,render:e=>e.statusLabel?M(T,{label:e.statusLabel,tone:Ce(e.status)}):`-`},{key:`messageCount`,label:`Messages`,cellClassName:`wui-text-sm text-body-secondary`,render:e=>e.messageCount},{key:`updatedDateTimeUtc`,label:`Last update`,cellClassName:`wui-text-sm text-body-secondary text-nowrap`,render:e=>M(`time`,{dateTime:e.updatedDateTimeUtc,children:Y(e.updatedDateTimeUtc,p)})}],rows:e,loading:t,skeletonRows:4,ariaLabel:`Support tickets`,sourceCount:s?.total??0,pagination:f,empty:M(E,{icon:`message-square`,title:`No tickets yet`,text:`When you raise a ticket, it shows here with its status.`}),emptyFiltered:M(E,{icon:`search`,title:`No tickets match`,text:`Try another search or status.`})})]})}function Le(e,t){let[n,i]=s(e);return r(()=>{let n=setTimeout(()=>i(e),t);return()=>clearTimeout(n)},[e,t]),n}var Re=300,ze=10;function Be({initialTicketId:e,defaultCategory:t,appVersion:n}){let{config:i,error:a,reload:o}=K(),[l,u]=s(`all`),[f,p]=s(``),m=Le(f.trim(),Re),[h,g]=s(ze),_=`${l}|${m}|${h}`,[v,y]=s({key:_,page:1}),b=v.key===_?v.page:1,x=e=>y({key:_,page:e}),S=ne({status:l===`all`?void 0:l,search:m||void 0,page:b,pageSize:h}),{counts:C,reload:w}=q(),[T,E]=s(()=>e||Me()),[D,O]=s(!1),k=S.totalPages;r(()=>{k>0&&b>k&&y({key:_,page:k})},[k,b,_]);let A=()=>{S.reload(),w()},j=e=>{E(e),Pe(e)};if(T)return M(je,{ticketId:T,onBack:()=>j(null),onChanged:A},T);let ee={page:b,totalPages:Math.max(1,S.totalPages),pageSize:h,total:S.totalCount,setPage:x,setPageSize:g};return N(`section`,{className:`d-flex flex-column gap-3`,"aria-labelledby":`support-center-title`,children:[N(`div`,{className:`d-flex flex-wrap align-items-center justify-content-between gap-2`,children:[M(`h2`,{id:`support-center-title`,className:`h5 mb-0`,children:`Your tickets`}),M(d,{leftIcon:`plus`,onClick:()=>O(!0),disabled:!i,children:`New ticket`})]}),a&&N(c,{variant:`warning`,children:[a.message||`We could not load the support settings.`,` `,M(d,{variant:`ghost`,size:`sm`,onClick:o,children:`Try again`})]}),M(Ie,{tickets:S.tickets,loading:S.loading,error:S.error,onRetry:A,onOpen:j,status:l,onStatusChange:u,counts:C,search:f,onSearchChange:p,pagination:ee}),i&&M(xe,{open:D,onClose:()=>O(!1),config:i,defaultCategory:t,appVersion:n,onCreated:e=>{O(!1),A(),j(e.id)}})]})}var $=e=>typeof e==`string`&&e.trim()!==``;function Ve({className:e}){let{config:t}=K();if(!t)return null;let n=[];return $(t.supportEmail)&&n.push({key:`email`,icon:`mail`,label:`Email`,value:N(`span`,{className:`d-inline-flex flex-wrap align-items-center gap-2`,children:[M(`a`,{href:`mailto:${t.supportEmail}`,className:`text-break`,children:t.supportEmail}),M(p,{text:t.supportEmail,size:`sm`,label:`Copy email`,copiedLabel:`Email copied`})]})}),$(t.supportPhone)&&n.push({key:`phone`,icon:`phone`,label:`Phone`,value:t.supportPhone,href:`tel:${t.supportPhone.replace(/[^\d+]/g,``)}`}),$(t.supportHours)&&n.push({key:`hours`,icon:`clock`,label:`Hours`,value:t.supportHours}),n.length===0?null:N(`section`,{className:e,"aria-label":$(t.supportName)?`Contact ${t.supportName}`:`Contact support`,children:[$(t.supportName)&&M(`p`,{className:`fw-semibold mb-2`,children:t.supportName}),M(_,{items:n})]})}function He({client:e,timeZone:n=J,children:r}){let i=o(null),s=t((t=!1)=>{let n=i.current;if(t||!n||n.client!==e){let t=e.getConfig(),n={client:e,pending:t};return i.current=n,t.catch(()=>{i.current===n&&(i.current=null)}),t}return n.pending},[e]),c=a(()=>({client:e,loadConfig:s,timeZone:n}),[e,s,n]);return M(V.Provider,{value:c,children:r})}export{be as ReportIssue,F as SupportApiError,Be as SupportCenter,Ve as SupportContact,He as SupportProvider,te as createSupportClient,U as useSupportClient,K as useSupportConfig,re as useSupportTicket,q as useSupportTicketCounts,ne as useSupportTickets};
1
+ import{createContext as e,useCallback as t,useContext as n,useEffect as r,useId as i,useMemo as a,useRef as o,useState as s}from"react";import{AppAlert as c,AppAvatar as l,AppBadge as u,AppButton as d,AppCard as f,AppCopyButton as p,AppDataTable as m,AppFileAttachment as h,AppIcon as g,AppInfoList as _,AppInput as v,AppLink as y,AppModal as b,AppSearch as x,AppSelect as S,AppSkeleton as C,AppSpinner as w,AppStatusBadge as T,AppTableEmptyState as E,AppTabs as D,AppTextarea as O,useFloatingStack as k}from"@webority/ui-react";import{toBlob as A}from"html-to-image";import{createPortal as j}from"react-dom";import{Fragment as ee,jsx as M,jsxs as N}from"react/jsx-runtime";var P=`Something went wrong. Please try again.`,F=class extends Error{constructor(e,t,n){super(t),this.name=`SupportApiError`,this.code=e,this.status=n}};async function I(e){if(e&&typeof Blob<`u`&&e instanceof Blob)try{return JSON.parse(await e.text())}catch{return null}if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return e??null}async function L(e){if(e instanceof F)return e;let t=e?.response;if(!t)return new F(`network`,P);let n=await I(t.data);return new F(typeof n?.code==`string`&&n.code?n.code:`unknown`,typeof n?.message==`string`&&n.message?n.message:P,t.status)}async function R(e){try{return(await e()).data}catch(e){throw await L(e)}}function z(e,t,n){n!=null&&String(n).trim()!==``&&e.append(t,String(n))}function B(e,t,n,r){(n??[]).forEach((n,i)=>{let a=n?.name||`${r}-${i+1}.png`;e.append(t,n,a)})}function V(e,t=`/api/support`){let n=(typeof t==`string`?t:t?.base??`/api/support`).replace(/\/+$/,``),r=e=>`${n}/tickets/${encodeURIComponent(e)}`;return{getConfig:()=>R(()=>e.get(`${n}/settings`)),listTickets:({status:t,search:r,page:i,pageSize:a}={})=>{let o=new URLSearchParams;z(o,`status`,t),z(o,`search`,r?.trim()),z(o,`page`,i),z(o,`pageSize`,a);let s=o.toString();return R(()=>e.get(`${n}/tickets${s?`?${s}`:``}`))},getTicketCounts:()=>R(()=>e.get(`${n}/tickets/counts`)),getTicket:t=>R(()=>e.get(r(t))),createTicket:({category:t,subject:r,body:i,pageUrl:a,userAgent:o,appVersion:s,files:c,screenshots:l})=>{let u=new FormData;return u.append(`category`,t??``),u.append(`subject`,r??``),u.append(`body`,i??``),z(u,`pageUrl`,a),z(u,`userAgent`,o),z(u,`appVersion`,s),B(u,`files`,c,`file`),B(u,`screenshots`,l,`screenshot`),R(()=>e.post(`${n}/tickets`,u))},reply:(t,{body:n,files:i,screenshots:a})=>{let o=new FormData;return o.append(`body`,n??``),B(o,`files`,i,`file`),B(o,`screenshots`,a,`screenshot`),R(()=>e.post(`${r(t)}/replies`,o))},reopen:t=>R(()=>e.post(`${r(t)}/reopen`)),rate:(t,{satisfaction:n,comment:i})=>{let a={satisfaction:n};return i!=null&&i.trim()!==``&&(a.comment=i.trim()),R(()=>e.post(`${r(t)}/rating`,a))},fetchAttachment:t=>R(()=>e.get(`${n}/attachments/${encodeURIComponent(t)}`,{responseType:`blob`})),confirmAction:t=>R(()=>e.post(`${n}/actions/confirm`,{token:t}))}}var te=e(null);function H(){let e=n(te);if(!e)throw Error(`Support components and hooks must be used inside <SupportProvider>.`);return e}function U(){return H().client}function W(){return H().timeZone}function G(e,n,i=!0){let[a,c]=s({data:null,loading:i,error:null}),[l,u]=s(0),d=o(e);d.current=e;let f=o(n);r(()=>{let e=f.current!==n;if(f.current=n,!i){c({data:null,loading:!1,error:null});return}let t=!0;return c(t=>({data:e?null:t.data,loading:!0,error:null})),d.current(l>0).then(e=>t&&c({data:e,loading:!1,error:null}),e=>t&&c(t=>({...t,loading:!1,error:e}))),()=>{t=!1}},[n,i,l]);let p=t(()=>u(e=>e+1),[]),m=t(e=>c(t=>({...t,data:e})),[]);return{...a,reload:p,setData:m}}function K(){let{loadConfig:e}=H(),{data:t,loading:n,error:r,reload:i}=G(e,`config`);return{config:t,loading:n,error:r,reload:i}}function q(e={}){let t=U(),{status:n,search:r,page:i,pageSize:a}=e,{data:o,loading:s,error:c,reload:l}=G(()=>t.listTickets({status:n,search:r,page:i,pageSize:a}),JSON.stringify([`tickets`,n??null,r??null,i??null,a??null]));return{tickets:o?.items??[],totalCount:o?.totalCount??0,totalPages:o?.totalPages??0,loading:s,error:c,reload:l}}function ne(){let e=U(),{data:t,loading:n,error:r,reload:i}=G(()=>e.getTicketCounts(),`ticket-counts`);return{counts:t,loading:n,error:r,reload:i}}function J(e){let n=U(),{data:r,loading:i,error:a,reload:o,setData:s}=G(()=>n.getTicket(e),e??``,!!e),c=t(async e=>{let t=await e();return s(t),t},[s]);return{ticket:r,loading:i,error:a,reload:o,reply:t(t=>c(()=>n.reply(e,t)),[c,n,e]),reopen:t(()=>c(()=>n.reopen(e)),[c,n,e]),rate:t(t=>c(()=>n.rate(e,t)),[c,n,e])}}var Y=`Asia/Kolkata`,re=[`Jan`,`Feb`,`Mar`,`Apr`,`May`,`Jun`,`Jul`,`Aug`,`Sep`,`Oct`,`Nov`,`Dec`],ie=new Map;function ae(e){let t=ie.get(e);return t||(t=new Intl.DateTimeFormat(`en-IN`,{day:`2-digit`,month:`numeric`,year:`numeric`,hour:`numeric`,minute:`2-digit`,hour12:!0,timeZone:e}),ie.set(e,t)),t}function X(e,t=Y){if(!e)return`-`;let n=new Date(e);if(Number.isNaN(n.getTime()))return`-`;let r=Object.fromEntries(ae(t).formatToParts(n).map(e=>[e.type,e.value]));return`${r.day}-${re[Number(r.month)-1]}-${r.year}, ${r.hour}:${r.minute} ${r.dayPeriod.toUpperCase()}`}function oe(e=Y){return new Intl.DateTimeFormat(`en-IN`,{timeZone:e,timeZoneName:`short`}).formatToParts(new Date).find(e=>e.type===`timeZoneName`)?.value??e}function se(e){if(!Number.isFinite(e)||e<0)return`-`;if(e<1024)return`${e} B`;if(e<1024*1024)return`${Math.round(e/1024)} KB`;let t=e/(1024*1024);return`${Number.isInteger(t)?t:t.toFixed(1)} MB`}function ce(e){return(e?.allowedContentTypes??[]).join(`,`)}function le(e,t){if(t.length===0)return!0;let n=(e||``).toLowerCase();return t.some(e=>{let t=e.toLowerCase();return t.endsWith(`/*`)?n.startsWith(t.slice(0,-1)):n===t})}function Z(e,t){let n=e??[],r=t?.maxAttachmentsPerMessage??0,i=t?.maxAttachmentBytes??0,a=t?.allowedContentTypes??[];if(r>0&&n.length>r)return r===1?`You can attach one file.`:`You can attach up to ${r} files.`;for(let e of n){let t=e.name||`This file`;if(!le(e.type,a))return`${t} is a file type we cannot accept.`;if(i>0&&e.size>i)return`${t} is larger than ${se(i)}.`}return null}function ue(e){let t=Array.from(e.clipboardData?.items??[]),n=Date.now();return t.filter(e=>e.kind===`file`&&e.type.startsWith(`image/`)).map(e=>e.getAsFile()).filter(Boolean).map((e,t)=>{let r=(e.type.split(`/`)[1]||`png`).replace(`jpeg`,`jpg`);return new File([e],`pasted-image-${n}-${t+1}.${r}`,{type:e.type})})}function de({id:e,label:t=`Attachments`,files:n,onChange:r,config:i,error:a}){let o=i?.maxAttachmentBytes??0,s=o>0?Math.max(.1,Math.round(o/(1024*1024)*10)/10):0;return M(h,{id:e,label:t,optional:!0,multiple:!0,value:n,onChange:r,accept:ce(i),maxSizeMb:s,validate:!1,error:a||void 0})}function fe(e,t){return t&&e.includes(t)?t:e[0]??``}function pe({id:e,config:t,defaultCategory:n,initialValues:i,onValuesChange:a,extraFiles:o=[],onSubmit:l,onBusyChange:u,children:d}){let f=t?.categories??[],[p,m]=s(()=>i?.category??fe(f,n)),[h,g]=s(i?.subject??``),[_,y]=s(i?.body??``),[b,x]=s(i?.files??[]),[C,w]=s({}),[T,E]=s(``);r(()=>{a?.({category:p,subject:h,body:_,files:b})},[p,h,_,b,a]);let D=e=>{x(e),w(n=>({...n,files:Z([...o,...e],t)??void 0}))},k=e=>{let t=ue(e);t.length>0&&D([...b,...t])};function A(){let e={};p||(e.category=`Choose a category.`),h.trim()||(e.subject=`Give your ticket a subject.`),_.trim()||(e.body=`Tell us what happened.`);let n=Z([...o,...b],t);return n&&(e.files=n),w(e),Object.keys(e).length===0}async function j(e){if(e.preventDefault(),E(``),A()){u?.(!0);try{await l({category:p,subject:h.trim(),body:_.trim(),files:b})}catch(e){E(e?.message||`Your ticket did not send. Please try again.`)}finally{u?.(!1)}}}return N(`form`,{id:e,onSubmit:j,noValidate:!0,className:`d-flex flex-column gap-3`,children:[T&&M(c,{variant:`danger`,children:T}),M(S,{id:`${e}-category`,label:`Category`,required:!0,value:p,onChange:m,options:f.map(e=>({value:e,label:e})),placeholder:`Choose a category`,error:C.category}),M(v,{id:`${e}-subject`,label:`Subject`,required:!0,maxLength:t?.subjectMaxLength,placeholder:`A short summary`,value:h,onChange:e=>g(e.target.value),error:C.subject}),M(O,{id:`${e}-body`,label:`Message`,required:!0,rows:6,maxLength:t?.bodyMaxLength,placeholder:`What happened, and what did you expect to happen?`,value:_,onChange:e=>y(e.target.value),onPaste:k,hint:`You can paste an image into the message.`,error:C.body}),d,M(de,{id:`${e}-files`,files:b,onChange:D,config:t,error:C.files})]})}var me=`supportCapture`,he={zIndex:`var(--wui-z-fixed)`,bottom:0,insetInlineEnd:0},ge={zIndex:`var(--wui-z-fixed)`},_e={maxWidth:`20rem`},ve=1500,ye=e=>e?.dataset?.[me]!==`exclude`,be=()=>new Promise(e=>requestAnimationFrame(()=>e()));async function xe(){let e=await A(document.body,{filter:ye,pixelRatio:Math.min(window.devicePixelRatio||1,2)});if(!e)throw Error(`The page capture came back empty.`);return new File([e],`screenshot.png`,{type:`image/png`})}function Se({file:e,failed:t,disabled:n,hintId:i,controlRef:a,onAdd:o,onRemove:l}){let[u,f]=s(null);return r(()=>{if(!e)return;let t=URL.createObjectURL(e);return f(t),()=>URL.revokeObjectURL(t)},[e]),N(`div`,{className:`d-flex flex-column gap-2`,children:[M(`span`,{className:`wui-text-sm fw-semibold`,children:`Screenshot`}),e&&u&&M(`img`,{src:u,alt:`Screenshot of the page you were on`,className:`d-block w-100 border rounded`,style:_e}),M(`div`,{ref:a,className:`d-flex flex-wrap gap-2`,children:e?N(ee,{children:[M(d,{variant:`outline`,size:`sm`,leftIcon:`refresh-cw`,onClick:o,disabled:n,"aria-describedby":i,children:`Retake`}),M(d,{variant:`ghost`,size:`sm`,leftIcon:`x`,onClick:l,disabled:n,children:`Remove screenshot`})]}):M(d,{variant:`outline`,size:`sm`,leftIcon:`camera`,onClick:o,disabled:n,"aria-describedby":i,children:`Add a screenshot`})}),M(`span`,{id:i,className:`wui-text-sm text-body-secondary`,children:`Only what is on your screen now. You can remove it before sending.`}),t&&M(c,{variant:`info`,children:`We could not take a screenshot of this page. You can attach an image instead, or send the report without one.`})]})}function Ce({label:e=`Report an issue`,floating:t=!1,compact:n=!1,defaultCategory:a=`Bug report`,appVersion:l,getTicketHref:u,onOpenTicket:f,onCreated:p,className:m,style:h}){let _=U(),{config:v,error:y}=K(),x=`support-report-${i().replace(/[^a-zA-Z0-9-]/g,``)}`,[S,C]=s(!1),[w,T]=s(!1),[E,D]=s(null),[O,A]=s(!1),[P,F]=s(!1),[I,L]=s(null),[R,z]=s(null),B=k(`end`,t&&n),V=o(null),te=o(null),H=o(!1);r(()=>{!S||!H.current||(H.current=!1,te.current?.querySelector(`button`)?.focus())},[S]);function W(){L(null),D(null),A(!1),z(null),C(!0)}async function G(){T(!0),A(!1),await new Promise(e=>{let t=setTimeout(e,ve);V.current=()=>{clearTimeout(t),e()},C(!1)}),V.current=null,await be(),await be();try{D(await xe())}catch{D(null),A(!0)}finally{H.current=!0,T(!1),C(!0)}}let q=()=>{P||(C(!1),D(null),A(!1),L(null),z(null))};async function ne(e){let t=await _.createTicket({...e,pageUrl:window.location.href,userAgent:navigator.userAgent,appVersion:l,screenshots:E?[E]:[]});L(t),D(null),p?.(t)}let J=I&&u?u(I.id):void 0,Y=I?N(`div`,{className:`d-flex justify-content-end gap-2 w-100`,children:[M(d,{variant:`secondary`,onClick:q,children:`Close`}),I&&(J||f)?M(d,{href:J,onClick:J?void 0:()=>{C(!1),L(null),f(I)},children:`View ticket`}):null]}):N(`div`,{className:`d-flex justify-content-end gap-2 w-100`,children:[M(d,{variant:`secondary`,onClick:q,disabled:P,children:`Cancel`}),M(d,{type:`submit`,form:x,loading:P,disabled:!v,children:`Submit`})]}),re=n?M(d,{variant:`secondary`,className:`wui-floating-btn wui-floating-btn--secondary`,disabled:w,onClick:W,"aria-haspopup":`dialog`,"aria-label":e,tooltip:e,tooltipPlacement:`start`,children:M(g,{name:`bug`,size:22})}):M(d,{leftIcon:`bug`,variant:t?`primary`:`outline`,disabled:w,onClick:W,"aria-haspopup":`dialog`,children:e});return N(ee,{children:[t&&n?B&&j(M(`span`,{className:[`wui-floating-stack-item wui-floating-stack-item--secondary`,m].filter(Boolean).join(` `),style:h,children:re}),B):M(`span`,{"data-support-capture":`exclude`,className:[t?`position-fixed m-3`:`d-inline-block`,m].filter(Boolean).join(` `),style:t?{...he,...h}:h,children:re}),w&&N(`div`,{"data-support-capture":`exclude`,role:`status`,className:`position-fixed top-0 start-50 translate-middle-x mt-3 d-flex align-items-center gap-2 px-3 py-2 bg-body border rounded-pill shadow-sm wui-text-sm`,style:ge,children:[M(`span`,{className:`spinner-border spinner-border-sm`,"aria-hidden":`true`}),`Taking a screenshot`]}),M(b,{open:S,size:`lg`,title:`Report an issue`,subtitle:`Tell us what went wrong. You can add a screenshot of this page if it helps.`,headerIcon:`bug`,staticBackdrop:!0,onClose:q,onExited:()=>V.current?.(),footer:Y,"data-support-capture":`exclude`,children:I?M(c,{variant:`success`,role:`status`,title:`Ticket ${I.number} is with the support team`,children:`We reply by email. You can follow the ticket on your support page.`}):v?M(pe,{id:x,config:v,defaultCategory:a,initialValues:R,onValuesChange:z,extraFiles:E?[E]:[],onSubmit:ne,onBusyChange:F,children:M(Se,{file:E,failed:O,disabled:P||w,hintId:`${x}-screenshot-hint`,controlRef:te,onAdd:G,onRemove:()=>D(null)})}):M(c,{variant:y?`danger`:`info`,children:y?y.message||`We could not load the support settings.`:`Loading the form.`})})]})}function we({open:e,onClose:t,onCreated:n,config:r,defaultCategory:a,appVersion:o}){let c=U(),l=`support-new-${i().replace(/[^a-zA-Z0-9-]/g,``)}`,[u,f]=s(!1);async function p(e){n(await c.createTicket({...e,userAgent:typeof navigator>`u`?void 0:navigator.userAgent,appVersion:o}))}return M(b,{open:e,size:`lg`,title:`New ticket`,subtitle:`Tell us what you need. We reply by email and on this page.`,headerIcon:`message-square`,staticBackdrop:!0,onClose:()=>{u||t()},footer:N(`div`,{className:`d-flex justify-content-end gap-2 w-100`,children:[M(d,{variant:`secondary`,onClick:t,disabled:u,children:`Cancel`}),M(d,{type:`submit`,form:l,loading:u,children:`Send ticket`})]}),children:e&&M(pe,{id:l,config:r,defaultCategory:a,onSubmit:p,onBusyChange:f})})}var Te={Open:`warning`,InProgress:`info`,Resolved:`success`,Closed:`neutral`};function Ee(e){return Te[e]??`neutral`}var De={width:140,maxWidth:`100%`},Oe={height:84};function ke({attachment:e}){let t=U(),n=(e.contentType||``).startsWith(`image/`),[i,a]=s(null),[o,c]=s(!1);r(()=>{let n=!0,r=null;return a(null),c(!1),t.fetchAttachment(e.id).then(e=>{r=URL.createObjectURL(e),n?a(r):URL.revokeObjectURL(r)},()=>n&&c(!0)),()=>{n=!1,r&&URL.revokeObjectURL(r)}},[t,e.id]);let l=`Open ${e.fileName}`,u=N(`span`,{className:`d-block text-truncate px-2 py-1 wui-text-xs text-body-secondary`,children:[e.fileName,`, `,se(e.sizeBytes)]});return o?N(`span`,{className:`d-inline-flex flex-column border rounded overflow-hidden`,style:De,children:[N(`span`,{className:`d-flex align-items-center justify-content-center gap-1 p-2 wui-text-xs text-body-secondary`,children:[M(g,{name:`x-circle`,size:14}),`Could not load this file.`]}),u]}):N(`a`,{className:`d-inline-flex flex-column border rounded overflow-hidden text-decoration-none`,style:De,href:i??void 0,target:`_blank`,rel:`noopener noreferrer`,"aria-label":l,"aria-busy":!i||void 0,children:[M(`span`,{className:`d-flex align-items-center justify-content-center bg-body-tertiary`,style:Oe,children:i?n?M(`img`,{src:i,alt:e.fileName,className:`d-block w-100 h-100 object-fit-cover`}):M(g,{name:`file-text`,size:28}):M(w,{size:`sm`,label:`Loading ${e.fileName}`})}),u]})}var Ae={whiteSpace:`pre-wrap`,overflowWrap:`anywhere`};function je({messages:e}){let t=W();return M(`ol`,{className:`list-unstyled d-flex flex-column gap-3 mb-0`,"aria-label":`Conversation`,children:e.map(e=>{let n=M(`time`,{dateTime:e.createdDateTimeUtc,className:`wui-text-xs text-body-secondary text-nowrap`,children:X(e.createdDateTimeUtc,t)});if(e.authorKind===`System`)return N(`li`,{className:`text-center wui-text-xs text-body-secondary`,"data-author":`system`,children:[M(`span`,{className:`d-block`,children:e.body}),n]},e.id);let r=e.authorKind===`Customer`,i=e.authorName||(r?`You`:`Support team`);return N(`li`,{className:`d-flex align-items-start gap-2${r?` flex-row-reverse`:``}`,"data-author":r?`customer`:`agent`,children:[M(l,{name:i,size:32,tone:r?`neutral`:`primary`}),N(f,{className:`flex-grow-1 border`,padding:`sm`,variant:r?`default`:`accent`,style:{minWidth:0},children:[N(`div`,{className:`d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2`,children:[N(`span`,{className:`d-inline-flex flex-wrap align-items-center gap-2 fw-semibold wui-text-sm text-break`,children:[i,!r&&M(u,{tone:`primary`,children:`Support`})]}),n]}),M(`div`,{className:`wui-text-sm`,style:Ae,children:e.body}),e.attachments?.length>0&&M(`div`,{className:`d-flex flex-wrap gap-2 mt-2`,children:e.attachments.map(e=>M(ke,{attachment:e},e.id))})]})]},e.id)})})}function Me({satisfaction:e,comment:t,canRate:n=!1,commentMaxLength:r,onRate:a}){let o=i(),[l,u]=s(``),[p,m]=s(null),[h,g]=s(``);if(e)return N(c,{variant:e===`Satisfied`?`success`:`info`,role:`status`,children:[e===`Satisfied`?`You told us this solved your problem.`:`You told us this did not solve your problem.`,t?N(`span`,{className:`d-block mt-1`,children:[`Your comment: `,t]}):null]});if(!n)return null;async function _(e){g(``),m(e);try{await a({satisfaction:e,comment:l})}catch(e){g(e?.message||`Your rating did not send. Please try again.`)}finally{m(null)}}return M(f,{padding:`sm`,className:`border`,title:`Did this solve your problem?`,children:N(`div`,{className:`d-flex flex-column gap-2`,children:[h&&M(c,{variant:`danger`,children:h}),M(O,{id:`${o}-comment`,label:`Anything to add?`,rows:2,maxLength:r,value:l,onChange:e=>u(e.target.value),hint:`Optional. Your comment goes to the support team.`}),N(`div`,{className:`d-flex flex-wrap gap-2`,children:[M(d,{variant:`outline`,leftIcon:`check-circle`,loading:p===`Satisfied`,disabled:p!==null,onClick:()=>_(`Satisfied`),children:`Yes, it is solved`}),M(d,{variant:`outline`,leftIcon:`x-circle`,loading:p===`Unsatisfied`,disabled:p!==null,onClick:()=>_(`Unsatisfied`),children:`No, it is not`})]})]})})}function Ne({onSend:e,config:t,reopens:n=!1}){let r=i(),[a,o]=s(``),[l,u]=s([]),[f,p]=s(``),[m,h]=s(``),[g,_]=s(!1),v=e=>{u(e),p(Z(e,t)??``)},y=e=>{let t=ue(e);t.length>0&&v([...l,...t])};async function b(n){n.preventDefault();let r=a.trim();if(!r){h(`Write a message before you send it.`);return}let i=Z(l,t);if(i){p(i);return}h(``),_(!0);try{await e({body:r,files:l}),o(``),u([]),p(``)}catch(e){h(e?.message||`Your reply did not send. Please try again.`)}finally{_(!1)}}return N(`form`,{onSubmit:b,noValidate:!0,className:`d-flex flex-column gap-2`,"aria-label":`Reply to this ticket`,children:[m&&M(c,{variant:`danger`,children:m}),M(O,{id:`${r}-body`,label:`Your reply`,rows:4,autoGrow:!0,maxLength:t?.bodyMaxLength,value:a,onChange:e=>o(e.target.value),onPaste:y,hint:n?`Sending a reply reopens this ticket. You can paste an image into the message.`:`You can paste an image into the message.`,required:!0}),M(de,{id:`${r}-files`,files:l,onChange:v,config:t,error:f}),M(`div`,{className:`d-flex justify-content-end`,children:M(d,{type:`submit`,leftIcon:`send`,loading:g,children:`Send reply`})})]})}function Q({label:e,children:t}){return N(`div`,{className:`d-flex flex-column`,children:[M(`span`,{className:`wui-text-xs fw-semibold text-body-secondary`,children:e}),M(`span`,{className:`wui-text-sm`,children:t})]})}function Pe({ticketId:e,onBack:t,onChanged:n}){let{config:r}=K(),i=W(),{ticket:a,loading:o,error:l,reload:u,reply:f,reopen:p,rate:m}=J(e),[h,g]=s(!1),[_,v]=s(``),y=async e=>{let t=await e();return n?.(),t};async function b(){v(``),g(!0);try{await y(p)}catch(e){v(e?.message||`The ticket did not reopen. Please try again.`)}finally{g(!1)}}let x=M(d,{variant:`ghost`,size:`sm`,leftIcon:`arrow-left`,onClick:t,className:`align-self-start`,children:`Back to tickets`});if(!a)return N(`section`,{className:`d-flex flex-column gap-3`,"aria-busy":o||void 0,children:[x,l?N(c,{variant:`danger`,children:[l.message||`We could not load this ticket.`,` `,M(d,{variant:`ghost`,size:`sm`,onClick:u,children:`Try again`})]}):M(C,{height:160})]});let S=a.status===`Resolved`||a.status===`Closed`;return N(`section`,{className:`d-flex flex-column gap-3`,"aria-labelledby":`support-ticket-${a.id}`,children:[x,N(`header`,{className:`d-flex flex-column gap-2`,children:[N(`div`,{className:`d-flex flex-wrap align-items-center gap-2`,children:[M(`h2`,{id:`support-ticket-${a.id}`,className:`h5 mb-0 text-break`,children:a.subject}),a.statusLabel&&M(T,{label:a.statusLabel,tone:Ee(a.status)})]}),N(`div`,{className:`row row-cols-2 row-cols-md-4 g-3`,children:[M(Q,{label:`Ticket`,children:a.number}),M(Q,{label:`Category`,children:a.category||`-`}),M(Q,{label:`Opened`,children:M(`time`,{dateTime:a.createdDateTimeUtc,children:X(a.createdDateTimeUtc,i)})}),M(Q,{label:`Last update`,children:M(`time`,{dateTime:a.updatedDateTimeUtc,children:X(a.updatedDateTimeUtc,i)})})]}),N(`p`,{className:`wui-text-xs text-body-secondary mb-0`,children:[`Times are shown in `,oe(i),`.`]})]}),_&&M(c,{variant:`danger`,children:_}),S&&M(c,{variant:`success`,role:`status`,children:`If you still need help, reopen the ticket or send a reply.`}),a.canReopen&&M(d,{variant:`secondary`,leftIcon:`history`,loading:h,onClick:b,className:`align-self-start`,children:`Reopen ticket`}),M(Me,{satisfaction:a.satisfaction,comment:a.satisfactionComment,canRate:a.canRate,commentMaxLength:r?.satisfactionCommentMaxLength,onRate:e=>y(()=>m(e))}),M(je,{messages:a.messages??[]}),M(Ne,{config:r,reopens:S,onSend:e=>y(()=>f(e))})]})}var Fe=`ticket`;function Ie(){return typeof window>`u`?null:new URLSearchParams(window.location.search).get(Fe)}function Le(e){if(typeof window>`u`)return e?`?${Fe}=${encodeURIComponent(e)}`:`?`;let t=new URL(window.location.href);return e?t.searchParams.set(Fe,e):t.searchParams.delete(Fe),`${t.pathname}${t.search}${t.hash}`}function Re(e){if(typeof window>`u`)return;let t=Le(e);t!==`${window.location.pathname}${window.location.search}${window.location.hash}`&&window.history.replaceState(window.history.state,``,t)}var ze=[{key:`all`,label:`All`,countKey:`total`},{key:`Open`,label:`Open`,countKey:`open`},{key:`InProgress`,label:`In progress`,countKey:`inProgress`},{key:`Resolved`,label:`Resolved`,countKey:`resolved`},{key:`Closed`,label:`Closed`,countKey:`closed`}];function Be({tickets:e,loading:t,error:n,onRetry:r,onOpen:i,status:a,onStatusChange:o,counts:s,search:l,onSearchChange:u,pagination:f}){let p=W();return N(`div`,{className:`d-flex flex-column gap-3`,children:[n&&N(c,{variant:`danger`,children:[n.message||`We could not load your tickets.`,` `,M(d,{variant:`ghost`,size:`sm`,onClick:r,children:`Try again`})]}),N(`div`,{className:`d-flex flex-column flex-md-row gap-2 align-items-md-center justify-content-between`,children:[M(D,{tabs:ze.map(({key:e,label:t,countKey:n})=>({key:e,label:t,count:s?.[n]})),value:a,onChange:o,ariaLabel:`Filter tickets by status`,className:`flex-grow-1`}),M(x,{value:l,onChange:u,placeholder:`Search tickets`,ariaLabel:`Search tickets`})]}),M(m,{columns:[{key:`subject`,label:`Subject`,render:e=>N(`div`,{className:`d-flex flex-column`,style:{minWidth:0},children:[M(y,{href:Le(e.id),animation:`none`,className:`fw-semibold text-break`,onClick:t=>{t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||(t.preventDefault(),i(e.id))},children:e.subject}),N(`span`,{className:`wui-text-xs text-body-secondary`,children:[e.number,`, `,e.category,e.awaitingCustomer?`. Waiting for your reply`:``]})]})},{key:`status`,label:`Status`,render:e=>e.statusLabel?M(T,{label:e.statusLabel,tone:Ee(e.status)}):`-`},{key:`messageCount`,label:`Messages`,cellClassName:`wui-text-sm text-body-secondary`,render:e=>e.messageCount},{key:`updatedDateTimeUtc`,label:`Last update`,cellClassName:`wui-text-sm text-body-secondary text-nowrap`,render:e=>M(`time`,{dateTime:e.updatedDateTimeUtc,children:X(e.updatedDateTimeUtc,p)})}],rows:e,loading:t,skeletonRows:4,ariaLabel:`Support tickets`,sourceCount:s?.total??0,pagination:f,empty:M(E,{icon:`message-square`,title:`No tickets yet`,text:`When you raise a ticket, it shows here with its status.`}),emptyFiltered:M(E,{icon:`search`,title:`No tickets match`,text:`Try another search or status.`})})]})}function Ve(e,t){let[n,i]=s(e);return r(()=>{let n=setTimeout(()=>i(e),t);return()=>clearTimeout(n)},[e,t]),n}var He=300,Ue=10;function We({initialTicketId:e,defaultCategory:t,appVersion:n}){let{config:i,error:a,reload:o}=K(),[l,u]=s(`all`),[f,p]=s(``),m=Ve(f.trim(),He),[h,g]=s(Ue),_=`${l}|${m}|${h}`,[v,y]=s({key:_,page:1}),b=v.key===_?v.page:1,x=e=>y({key:_,page:e}),S=q({status:l===`all`?void 0:l,search:m||void 0,page:b,pageSize:h}),{counts:C,reload:w}=ne(),[T,E]=s(()=>e||Ie()),[D,O]=s(!1),k=S.totalPages;r(()=>{k>0&&b>k&&y({key:_,page:k})},[k,b,_]);let A=()=>{S.reload(),w()},j=e=>{E(e),Re(e)};if(T)return M(Pe,{ticketId:T,onBack:()=>j(null),onChanged:A},T);let ee={page:b,totalPages:Math.max(1,S.totalPages),pageSize:h,total:S.totalCount,setPage:x,setPageSize:g};return N(`section`,{className:`d-flex flex-column gap-3`,"aria-labelledby":`support-center-title`,children:[N(`div`,{className:`d-flex flex-wrap align-items-center justify-content-between gap-2`,children:[M(`h2`,{id:`support-center-title`,className:`h5 mb-0`,children:`Your tickets`}),M(d,{leftIcon:`plus`,onClick:()=>O(!0),disabled:!i,children:`New ticket`})]}),a&&N(c,{variant:`warning`,children:[a.message||`We could not load the support settings.`,` `,M(d,{variant:`ghost`,size:`sm`,onClick:o,children:`Try again`})]}),M(Be,{tickets:S.tickets,loading:S.loading,error:S.error,onRetry:A,onOpen:j,status:l,onStatusChange:u,counts:C,search:f,onSearchChange:p,pagination:ee}),i&&M(we,{open:D,onClose:()=>O(!1),config:i,defaultCategory:t,appVersion:n,onCreated:e=>{O(!1),A(),j(e.id)}})]})}var $=e=>typeof e==`string`&&e.trim()!==``;function Ge({className:e}){let{config:t}=K();if(!t)return null;let n=[];return $(t.supportEmail)&&n.push({key:`email`,icon:`mail`,label:`Email`,value:N(`span`,{className:`d-inline-flex flex-wrap align-items-center gap-2`,children:[M(`a`,{href:`mailto:${t.supportEmail}`,className:`text-break`,children:t.supportEmail}),M(p,{text:t.supportEmail,size:`sm`,label:`Copy email`,copiedLabel:`Email copied`})]})}),$(t.supportPhone)&&n.push({key:`phone`,icon:`phone`,label:`Phone`,value:t.supportPhone,href:`tel:${t.supportPhone.replace(/[^\d+]/g,``)}`}),$(t.supportHours)&&n.push({key:`hours`,icon:`clock`,label:`Hours`,value:t.supportHours}),n.length===0?null:N(`section`,{className:e,"aria-label":$(t.supportName)?`Contact ${t.supportName}`:`Contact support`,children:[$(t.supportName)&&M(`p`,{className:`fw-semibold mb-2`,children:t.supportName}),M(_,{items:n})]})}function Ke({client:e,timeZone:n=Y,children:r}){let i=o(null),s=t((t=!1)=>{let n=i.current;if(t||!n||n.client!==e){let t=e.getConfig(),n={client:e,pending:t};return i.current=n,t.catch(()=>{i.current===n&&(i.current=null)}),t}return n.pending},[e]),c=a(()=>({client:e,loadConfig:s,timeZone:n}),[e,s,n]);return M(te.Provider,{value:c,children:r})}export{Ce as ReportIssue,F as SupportApiError,We as SupportCenter,Ge as SupportContact,Ke as SupportProvider,V as createSupportClient,U as useSupportClient,K as useSupportConfig,J as useSupportTicket,ne as useSupportTicketCounts,q as useSupportTickets};
@@ -1,7 +1,9 @@
1
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.
2
+ * A "Report an issue" button. It opens a dialog at once, where the person describes the problem
3
+ * and adds images. Nothing is captured unless they ask: "Add a screenshot" hides the dialog, takes
4
+ * a picture of the page, and brings the dialog back with the picture attached, what they typed
5
+ * kept, and the choice to retake or remove it. The ticket carries the page address, the browser
6
+ * and the product version.
5
7
  *
6
8
  * @param {object} props
7
9
  * @param {string} [props.label="Report an issue"] The button text.
@@ -7,15 +7,31 @@
7
7
  * @param {string} props.id The form's id, for a submit button outside it.
8
8
  * @param {any} props.config The support config: categories, text limits and attachment limits.
9
9
  * @param {string} [props.defaultCategory] Preselected when the config lists it.
10
+ * @param {{ category: string, subject: string, body: string, files: File[] }} [props.initialValues] Values to start from,
11
+ * so a form that unmounts and mounts again (a dialog hidden for a page capture) keeps what was typed.
12
+ * @param {(values: { category: string, subject: string, body: string, files: File[] }) => void} [props.onValuesChange]
13
+ * Called with every change, and once on mount; pass a stable function.
10
14
  * @param {Blob[]} [props.extraFiles] Files that ride along from elsewhere (a page capture), counted against the limits.
11
15
  * @param {(values: { category: string, subject: string, body: string, files: File[] }) => Promise<unknown>} props.onSubmit Rejects with a SupportApiError on failure.
12
16
  * @param {(busy: boolean) => void} [props.onBusyChange]
13
17
  * @param {import('react').ReactNode} [props.children] Shown above the attachments, such as a capture preview.
14
18
  */
15
- export default function TicketForm({ id, config, defaultCategory, extraFiles, onSubmit, onBusyChange, children }: {
19
+ export default function TicketForm({ id, config, defaultCategory, initialValues, onValuesChange, extraFiles, onSubmit, onBusyChange, children, }: {
16
20
  id: string;
17
21
  config: any;
18
22
  defaultCategory?: string;
23
+ initialValues?: {
24
+ category: string;
25
+ subject: string;
26
+ body: string;
27
+ files: File[];
28
+ };
29
+ onValuesChange?: (values: {
30
+ category: string;
31
+ subject: string;
32
+ body: string;
33
+ files: File[];
34
+ }) => void;
19
35
  extraFiles?: Blob[];
20
36
  onSubmit: (values: {
21
37
  category: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/support-react",
3
- "version": "0.50.1",
3
+ "version": "0.51.0",
4
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
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "react": "^19.0.0",
31
31
  "react-bootstrap": "^2.10.0",
32
32
  "react-dom": "^19.0.0",
33
- "@webority/ui-react": "0.50.1"
33
+ "@webority/ui-react": "0.51.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@testing-library/dom": "10.4.2",
@@ -48,7 +48,7 @@
48
48
  "typescript": "^7.0.2",
49
49
  "vite": "^8.0.12",
50
50
  "vitest": "5.0.1",
51
- "@webority/ui-react": "0.50.1"
51
+ "@webority/ui-react": "0.51.0"
52
52
  },
53
53
  "dependencies": {
54
54
  "html-to-image": "^1.11.13"