codicent-app-sdk 0.6.7 → 0.6.8

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.
@@ -13,6 +13,55 @@ export interface ListPageProps {
13
13
  /** Optional data filter applied after fetch. Useful for app-specific filtering (e.g. empty notes). */
14
14
  filterData?: (data: DataMessage[], tag: string) => DataMessage[];
15
15
  }
16
+ /**
17
+ * ListPage — data fetch & cache behaviour
18
+ *
19
+ * ## Fetch key guard
20
+ * A `fetchKey` string (`tag-contentTitle-loadAll-refreshKey-debouncedSearch`) is stored in
21
+ * `lastFetchKey` (ref). The fetch effect bails out early if the key hasn't changed, preventing
22
+ * duplicate calls when unrelated state updates re-render the component.
23
+ * The ref is reset to `""` on effect cleanup (unmount / dep change) so that re-navigation
24
+ * always triggers a fresh fetch.
25
+ *
26
+ * ## Race-condition protection
27
+ * `activeFetchId` (ref) is incremented each time a new fetch starts. Each in-flight promise
28
+ * captures its own `myFetchId` and checks `isActive()` before writing state. A superseded
29
+ * fetch (e.g. from rapid search typing or quick navigation) silently discards its result
30
+ * without calling `setSearching(false)`, so the spinner stays visible until the winning fetch
31
+ * completes.
32
+ *
33
+ * ## Cache strategy
34
+ * | Visit | Cache present? | Behaviour |
35
+ * |-------|---------------|-----------|
36
+ * | First | No | `searching=true` → API call → populate cache → show list |
37
+ * | First | Yes | Instant render from cache (no spinner) → background delta-fetch for records newer than the latest cached timestamp → silently prepend new rows |
38
+ * | Back-navigation | Yes | Unmount cleanup resets fetchKey → same as "First, cache present" |
39
+ * | Back-navigation | Expired | Same as "First, no cache" |
40
+ * | Refresh button | Any | Clears cache for tag + resets fetchKey + increments `refreshKey` → full API call |
41
+ *
42
+ * ## Search / filter flow
43
+ * - Typing triggers a 500 ms debounce before `debouncedSearch` updates.
44
+ * - `debouncedSearch` change → new fetchKey → new `activeFetchId` → previous in-flight
45
+ * fetch is superseded and its result discarded.
46
+ * - `filter_<field>=<value>` URL params are applied client-side after fetch (not sent to API)
47
+ * except for `filter_<tag>=<value>` which seeds `debouncedSearch` for server-side filtering.
48
+ * - `q=` URL param is kept in sync with the search box via a debounced `setSearchParams`
49
+ * call that preserves all other existing params (`canAdd`, `canEdit`, `filter_*`, etc.).
50
+ *
51
+ * ## URL params (all optional)
52
+ * | Param | Type | Default | Purpose |
53
+ * |-------|------|---------|---------|
54
+ * | `tag` | string | `"logbook"` | Data tag to query |
55
+ * | `title` | string | `"Loggbok"` | Page heading |
56
+ * | `contentTitle` | string | — | If set, fetches raw messages and maps this field as the display column |
57
+ * | `loadAll` | `"true"` | — | Removes PAGE_SIZE cap; disables Load-more |
58
+ * | `canAdd` | `"true"` | — | Shows Add Record button (opens RecordModal) |
59
+ * | `canEdit` | `"true"` | — | Shows per-row Edit/Delete buttons |
60
+ * | `canExport` | `"false"` to hide | visible | Shows Export to Excel button |
61
+ * | `addUrl` | string | — | External URL opened on Add click (alternative to modal) |
62
+ * | `q` | string | — | Initial search term; kept in sync with search box |
63
+ * | `filter_<field>` | string | — | Client-side row filter; `%USERNAME%` resolves to current user |
64
+ */
16
65
  export declare const ListPage: ({ state, getColumnDefs, getListConfig, filterData }: ListPageProps) => import("react/jsx-runtime").JSX.Element;
17
66
  export default ListPage;
18
67
  //# sourceMappingURL=ListPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ListPage.d.ts","sourceRoot":"","sources":["../../../src/pages/ListPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAuB3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAkE3D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,+FAA+F;IAC/F,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,gBAAgB,EAAE,CAAC;IACpD;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;IACxD,sGAAsG;IACtG,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC;CAClE;AAED,eAAO,MAAM,QAAQ,wDAAyD,aAAa,4CAsmB1F,CAAC;AAEF,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"ListPage.d.ts","sourceRoot":"","sources":["../../../src/pages/ListPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAuB3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAkE3D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,+FAA+F;IAC/F,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,gBAAgB,EAAE,CAAC;IACpD;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;IACxD,sGAAsG;IACtG,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC;CAClE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,QAAQ,wDAAyD,aAAa,4CA+mB1F,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react/jsx-runtime"),t=require("react");require("../components/Markdown.js"),require("../components/Textarea.js"),require("../components/Button.js"),require("../components/CompoundButton.js");var r=require("@fluentui/react-components");require("../components/Spinner.js");var n=require("../components/TextHeader.js");require("../components/TypingIndicator.js"),require("../components/Dialog.js"),require("../components/ChatInput.js"),require("../components/CombinedPlaceholderDialog.js"),require("../components/ChatMessage.js"),require("../components/Header.js");var a=require("@fluentui/react-icons");require("../services/codicent.js");var s=require("../services/dataCache.js"),i=require("../utils/MessageContent.js");require("../node_modules/tinycolor2/esm/tinycolor.js"),require("../_virtual/index.js"),require("../config/index.js"),require("../utils/cacheManager.js"),require("../lib/wavtools/lib/wav_packer.js"),require("../lib/wavtools/lib/analysis/audio_analysis.js"),require("../lib/wavtools/lib/wav_stream_player.js"),require("../lib/wavtools/lib/wav_recorder.js");var o=require("../utils/excelExport.js");require("./AppFrame.js"),require("./Canvas.js"),require("./Chat.js"),require("./Compose.js"),require("./Snap.js"),require("./Search.js"),require("./Menu.js"),require("./Log.js"),require("./Login.js"),require("./Home.js"),require("./CrmPage.js"),require("./CrmPagePersistent.js"),require("./ImageView.js"),require("./FormInvite.js"),require("./FormAccept.js"),require("./Sales.js");var c=require("react-router-dom");require("./Purchase.js"),require("../components/Content.js");var l=require("../components/Page.js");require("./QrScan.js");var u=require("../hooks/useLocalization.js");require("../components/FileThumbnail.js"),require("react-dom/client"),require("../node_modules/@auth0/auth0-react/dist/auth0-react.esm.js"),require("../hooks/useAppStyles.js");var d=require("../hooks/useUserRoles.js");require("../components/MessageInput.js"),require("../components/UploadFile.js"),require("../components/SnapFooter.js"),require("../components/Profile.js"),require("../components/MessageItem.js"),require("../components/AiInput.js");var p=require("../components/SearchBox.js");require("../components/DataMessagePicker.js"),require("../components/HtmlView.js"),require("../components/Footer.js"),require("../components/QrCodeDialog.js"),require("../components/QrScanner.js"),require("../components/OfflineMessage.js"),require("../components/LanguageSelector.js");var m=require("../components/ListView.js"),g=require("../components/RecordModal.js");require("../components/BulkUploadDialog.js"),require("../components/CookieBanner.js"),require("../components/audit/AuditCircularProgress.js"),require("../components/audit/AuditHorizontalProgress.js"),require("../components/audit/AuditRoleIndicator.js"),require("../components/audit/AuditUnitSwitcher.js"),require("../components/audit/AuditAnswerCell.js"),require("../components/audit/AuditSearchBar.js"),require("../components/audit/AuditFilterChips.js"),require("../components/audit/AuditFilterBar.js"),require("../components/audit/AuditGroupsProgress.js"),require("../components/audit/AuditSummaryDashboard.js"),require("../components/audit/AuditRequirementDialog.js"),require("../components/audit/AuditUnitExportDialog.js"),require("../components/audit/AuditUnitImportDialog.js"),require("../components/audit/AuditBulkExportDialog.js"),require("../components/audit/AuditBulkUploadDialog.js"),require("../components/audit/AuditSortPresets.js");var f=require("../node_modules/lodash/lodash.js");const h=r.makeStyles({container:{width:"100%",flex:1,maxWidth:"100%",height:"100%",minHeight:0,backgroundColor:"#ffffff",display:"flex",flexDirection:"column",margin:"0 auto",overflow:"hidden","@media (max-width: 768px)":{maxWidth:"100%",borderRadius:"0"},touchAction:"pan-y",paddingLeft:"0",paddingRight:"0"},listContainer:{overflow:"hidden",flex:1,minHeight:0,marginTop:"10px"},headerRow:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"10px",flexWrap:"wrap"},buttonGroup:{display:"flex",gap:"8px",flexWrap:"wrap"},addButton:{minWidth:"auto",whiteSpace:"nowrap"},recordsCount:{fontSize:"14px",color:"#666",marginTop:"8px"},backButton:{marginBottom:"10px"},searchBox:{marginTop:"8px"},loadMoreContainer:{display:"flex",justifyContent:"center",padding:"16px 0",borderTop:"1px solid #e0e0e0",marginTop:"4px"}}),j=500,q=({state:q,getColumnDefs:x,getListConfig:C,filterData:S})=>{const[y,w]=c.useSearchParams(),A=y.get("tag")||"logbook",v=y.get("title")||"",k=y.get("contentTitle"),b="true"===y.get("loadAll"),D="true"===y.get("canAdd"),M="true"===y.get("canEdit"),R="false"!==y.get("canExport"),B=y.get("addUrl")||"",E=y.get("q")||"",[I,P]=t.useState(E),[_,T]=t.useState(E),{service:$}=q,[N,L]=t.useState([]),[U,F]=t.useState(!1),[O,H]=t.useState(!1),[W,z]=t.useState(!1),V=t.useRef(null),G=t.useRef(E),J=t.useRef(""),Q=t.useRef(0),K=t.useRef($);K.current=$;const X=!!$,[Y,Z]=t.useState(0),[ee,te]=t.useState(null),re=h(),{t:ne}=u.default(),ae=c.useNavigate(),{getCurrentUserName:se}=d.useUserRoles($,q.context.nickname,q.context.selectedApp),ie=t.useMemo((()=>(k?[{key:k,title:k,maxWidth:250}]:C?C(A)?.columns??[]:x?x(A):[]).map((e=>({...e,title:e.title?ne(e.title):void 0})))),[k,A,ne,x,C]),[oe,ce]=t.useState(!1),[le,ue]=t.useState(null),[de,pe]=t.useState(!1),[me,ge]=t.useState(null),fe=t.useMemo((()=>`listpage_searches_${A}`),[A]),he=e=>{try{const t=localStorage.getItem(e);return t?JSON.parse(t):[]}catch{return[]}},[je,qe]=t.useState((()=>he(fe)));t.useEffect((()=>{qe(he(fe))}),[fe]);const xe=t.useCallback((e=>{const t=e.trim();t&&qe((e=>{const r=[t,...e.filter((e=>e!==t))].slice(0,20);try{localStorage.setItem(fe,JSON.stringify(r))}catch{}return r}))}),[fe]),Ce=t.useCallback((e=>{qe((t=>{const r=t.filter((t=>t!==e));try{localStorage.setItem(fe,JSON.stringify(r))}catch{}return r}))}),[fe]),Se=t.useMemo((()=>f.lodashExports.debounce((e=>T(e)),500)),[T]);t.useEffect((()=>()=>{Se.cancel()}),[Se]);const ye=t.useCallback((e=>{P(e),Se(e)}),[Se]),we=t.useCallback((e=>{xe(e)}),[xe]);t.useEffect((()=>{if(!y.get("q")){const e=y.get(`filter_${A}`);if(e){let t=e;if("%USERNAME%"===t){const e=se();if(!e)return;t=e}P(t),T(t),G.current=t}}}),[A,y,se]),t.useEffect((()=>{const e=y.get("q")||"";e!==G.current&&(P(e),T(e),G.current=e,J.current="")}),[y]),t.useEffect((()=>(V.current&&clearTimeout(V.current),V.current=setTimeout((()=>{if(G.current!==I){const e=new URLSearchParams(y);I?e.set("q",I):e.delete("q"),w(e,{replace:!0}),G.current=I}}),500),()=>{V.current&&clearTimeout(V.current)})),[I,y,w]),t.useEffect((()=>{if(!K.current)return;const e=`${A}-${k}-${b}-${Y}-${_}`;if(e===J.current)return;J.current=e;const t=++Q.current,r=()=>t===Q.current,n=k?`${A}-${_}-${k}`:`${A}-${_||"all"}`,a=s.dataCache.get(n);if(a){if(L(a),H(!b&&!k&&a.length>=j),!k&&!_&&a.length>0){const e=a.reduce(((e,t)=>t.createdAt>e?t.createdAt:e),a[0].createdAt);K.current.readDataMessages(A,"",void 0,void 0,void 0,e).then((e=>{r()&&L((t=>{const r=new Set(t.map((e=>e.id))),a=e.filter((e=>!r.has(e.id)));if(0===a.length)return t;const i=[...a,...t];return s.dataCache.set(n,i),i}))})).catch(console.warn)}}else if(H(!1),F(!0),k)K.current.getMessagesFast([A],_).then((e=>{if(!r())return;const t=e.map((e=>{const t=new Date(e.createdAt).toLocaleString();return{id:e.id,data:{[k]:new i.default(e.content).content,createdAt:t},fileId:null,fileIds:[],tags:[A],mentions:[K.current.codicent],createdAt:t}}));s.dataCache.set(n,t),L(t)})).catch(console.warn).finally((()=>{r()&&F(!1)}));else{const e=_||"",t=b||e?void 0:j;K.current.readDataMessages(A,e,void 0,0,t).then((t=>{r()&&(s.dataCache.set(n,t),L(t),H(!b&&!e&&t.length===j))})).catch(console.warn).finally((()=>{r()&&F(!1)}))}}),[A,k,b,_,Y,X]);const Ae=t.useMemo((()=>{const e={};y.forEach(((t,r)=>{r.startsWith("filter_")&&(e[r.replace("filter_","")]=t)}));let t=N.map(((e,t)=>({item:e,index:t})));for(const[r,n]of Object.entries(e)){const e=n.replace("%USERNAME%",se()),a=q.context.nickname||"";t=t.filter((({item:t})=>{const n=t.data[r];return null!=n&&(String(n).includes(e)||String(n).includes(a))}))}if(S){const e=S(t.map((({item:e})=>e)),A),r=new Set(e.map((e=>e.id)));t=t.filter((({item:e})=>r.has(e.id)))}const r=t.map((({item:e,index:t})=>({...e.data,createdAt:e.createdAt,_id:e.id,originalMessageId:e.originalMessageId||e.id,_fileIds:e.fileIds??[],_index:t}))),n=C?.(A)?.deduplicateBy;if(n){const e=Array.isArray(n)?n:[n],t=t=>{for(const r of e){const e=t[r];if(null!=e&&""!==String(e).trim())return String(e)}},a=new Set;return r.filter((e=>{const r=t(e);return void 0===r||!a.has(r)&&(a.add(r),!0)}))}return r}),[N,y,q.context.nickname,se,S,A,C]),ve=t.useMemo((()=>{const e=(I||"").trim().toLowerCase();if(!e)return Ae;const t=ie.filter((e=>!e.hidden)).map((e=>e.key));return Ae.filter((r=>{for(const n of t)if(Array.isArray(n))for(const t of n){const n=r[t];if(null!=n&&String(n).toLowerCase().includes(e))return!0}else{const t=r[n];if(null!=t&&String(t).toLowerCase().includes(e))return!0}return!1}))}),[Ae,I,ie]),ke=t.useCallback((()=>{s.dataCache.clearPattern(A),J.current="",Z((e=>e+1))}),[A]),be=t.useCallback((async()=>{z(!0);try{const e=await K.current.readDataMessages(A,"",void 0,N.length,j),t=[...N,...e];L(t),s.dataCache.set(`${A}-all`,t),H(e.length===j)}catch(e){console.warn(e)}finally{z(!1)}}),[N,A]),De=t.useCallback((e=>{te(e)}),[]),Me=()=>{pe(!1),ge(null)},Re=t.useCallback((async()=>{const e=`${A}_${(new Date).toISOString().split("T")[0]}`.replace(/[/\\:*?"<>|]/g,"_");const t=null!==ee?ee:ve;await o.exportToExcel(t,ie,e,ne)||console.warn("No data to export")}),[ee,ve,ie,A,ne]),Be=t.useMemo((()=>null!==ee?ee.length:ve.length),[ee,ve]);return e.jsx(l.Page,{hideHeader:!0,children:e.jsxs("div",{className:re.container,children:[e.jsx("div",{className:re.backButton,children:e.jsx(r.Button,{appearance:"subtle",icon:e.jsx(a.ArrowLeft24Regular,{}),onClick:()=>ae(-1),children:ne("Tillbaka")})}),e.jsxs("div",{className:re.headerRow,children:[e.jsx(n.default,{title:ne(v||"Loggbok")}),e.jsxs("div",{className:re.buttonGroup,children:[e.jsx(r.Button,{appearance:"subtle",icon:e.jsx(a.ArrowClockwise24Regular,{}),onClick:ke,title:ne("Refresh"),children:ne("Refresh")}),R&&e.jsx(r.Button,{appearance:"subtle",icon:e.jsx(a.ArrowDownload24Regular,{}),onClick:Re,title:ne("Export to Excel"),children:ne("Export to Excel")}),(D||B)&&e.jsx(r.Button,{appearance:"primary",icon:e.jsx(a.Add24Regular,{}),onClick:D?()=>{ue(null),ce(!0)}:()=>window.open(B,"_blank"),className:re.addButton,children:ne("Add Record")})]})]}),e.jsx("div",{className:re.searchBox,children:e.jsx(p.default,{placeholder:ne("Sök")+"...",value:I,onChange:ye,recentSearches:je,onCommitSearch:we,onRemoveRecentSearch:Ce})}),!U&&e.jsxs("div",{className:re.recordsCount,children:[ne("Total"),": ",Be," ",ne("records"),Be!==N.length&&` (${ne("filtered from")} ${N.length})`]}),e.jsxs("div",{className:re.listContainer,children:[U&&e.jsx("p",{children:ne("Söker")+"..."}),!U&&e.jsx(m.default,{data:ve,columns:ie,canEdit:M,onEdit:(e,t)=>{const r=N[t];r&&(ue({data:{...r.data,_fileIds:r.fileIds??[]},index:t}),ce(!0))},onDelete:(e,t)=>{const r=N[t];r&&(ge({index:t,id:r.id}),pe(!0))},onToggleCheckbox:async(e,t,r,n)=>{const a=N[n];if(a){const e=Array.isArray(t)?t[0]:t,i={...a.data,[e]:String(r)},o=await $.updateDataMessage(a.id,i);L((e=>{const t=[...e];return t[n]={...a,id:o,data:i},t})),s.dataCache.clearPattern(A)}},onEnumChange:async(e,t,r,n)=>{const a=N[n];if(a){const e=Array.isArray(t)?t[0]:t,i={...a.data,[e]:r},o=await $.updateDataMessage(a.id,i);L((e=>{const t=[...e];return t[n]={...a,id:o,data:i},t})),s.dataCache.clearPattern(A)}},onFilteredDataChange:De})]}),O&&!I.trim()&&e.jsx("div",{className:re.loadMoreContainer,children:e.jsx(r.Button,{appearance:"primary",size:"medium",onClick:be,disabled:W,children:W?ne("Laddar..."):ne("Visa fler")+` (${N.length} ${ne("laddade")})`})}),e.jsx(g.RecordModal,{open:oe,onClose:()=>ce(!1),onSave:async e=>{const t=new Set(ie.filter((e=>"thumbnail"===e.type)).map((e=>Array.isArray(e.key)?e.key[0]:e.key)));if(le){const r=N[le.index];if(r){const n={};for(const r in e){if(t.has(r))continue;const a=e[r];n[r]=null==a?"":String(a)}const a=await $.updateDataMessage(r.id,n);L((e=>{const t=[...e];return t[le.index]={...r,id:a,data:n},t})),s.dataCache.clearPattern(A)}}else{const r={};for(const n in e){if(t.has(n))continue;const a=e[n];r[n]=null==a?"":String(a)}const n={id:await $.createDataMessage(A,r),data:r,createdAt:(new Date).toISOString(),fileId:null,fileIds:[],tags:[A],mentions:[$.codicent]};L((e=>[n,...e])),s.dataCache.clearPattern(A)}},columns:ie,initialData:le?.data}),e.jsx(r.Dialog,{open:de,onOpenChange:(e,t)=>!t.open&&Me(),children:e.jsx(r.DialogSurface,{children:e.jsxs(r.DialogBody,{children:[e.jsx(r.DialogTitle,{children:ne("Confirm Delete")}),e.jsx(r.DialogContent,{children:ne("Are you sure you want to delete this record? This action cannot be undone.")}),e.jsxs(r.DialogActions,{children:[e.jsx(r.Button,{appearance:"secondary",onClick:Me,children:ne("Cancel")}),e.jsx(r.Button,{appearance:"primary",onClick:async()=>{if(me)try{await $.deleteDataMessage(me.id),L((e=>e.filter(((e,t)=>t!==me.index)))),s.dataCache.clearPattern(A)}catch(e){console.error("Failed to delete record:",e)}finally{pe(!1),ge(null)}},children:ne("Delete")})]})]})})})]})})};exports.ListPage=q,exports.default=q;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("react/jsx-runtime"),t=require("react");require("../components/Markdown.js"),require("../components/Textarea.js"),require("../components/Button.js"),require("../components/CompoundButton.js");var r=require("@fluentui/react-components");require("../components/Spinner.js");var n=require("../components/TextHeader.js");require("../components/TypingIndicator.js"),require("../components/Dialog.js"),require("../components/ChatInput.js"),require("../components/CombinedPlaceholderDialog.js"),require("../components/ChatMessage.js"),require("../components/Header.js");var a=require("@fluentui/react-icons");require("../services/codicent.js");var s=require("../services/dataCache.js"),i=require("../utils/MessageContent.js");require("../node_modules/tinycolor2/esm/tinycolor.js"),require("../_virtual/index.js"),require("../config/index.js"),require("../utils/cacheManager.js"),require("../lib/wavtools/lib/wav_packer.js"),require("../lib/wavtools/lib/analysis/audio_analysis.js"),require("../lib/wavtools/lib/wav_stream_player.js"),require("../lib/wavtools/lib/wav_recorder.js");var o=require("../utils/excelExport.js");require("./AppFrame.js"),require("./Canvas.js"),require("./Chat.js"),require("./Compose.js"),require("./Snap.js"),require("./Search.js"),require("./Menu.js"),require("./Log.js"),require("./Login.js"),require("./Home.js"),require("./CrmPage.js"),require("./CrmPagePersistent.js"),require("./ImageView.js"),require("./FormInvite.js"),require("./FormAccept.js"),require("./Sales.js");var c=require("react-router-dom");require("./Purchase.js"),require("../components/Content.js");var l=require("../components/Page.js");require("./QrScan.js");var u=require("../hooks/useLocalization.js");require("../components/FileThumbnail.js"),require("react-dom/client"),require("../node_modules/@auth0/auth0-react/dist/auth0-react.esm.js"),require("../hooks/useAppStyles.js");var d=require("../hooks/useUserRoles.js");require("../components/MessageInput.js"),require("../components/UploadFile.js"),require("../components/SnapFooter.js"),require("../components/Profile.js"),require("../components/MessageItem.js"),require("../components/AiInput.js");var p=require("../components/SearchBox.js");require("../components/DataMessagePicker.js"),require("../components/HtmlView.js"),require("../components/Footer.js"),require("../components/QrCodeDialog.js"),require("../components/QrScanner.js"),require("../components/OfflineMessage.js"),require("../components/LanguageSelector.js");var m=require("../components/ListView.js"),g=require("../components/RecordModal.js");require("../components/BulkUploadDialog.js"),require("../components/CookieBanner.js"),require("../components/audit/AuditCircularProgress.js"),require("../components/audit/AuditHorizontalProgress.js"),require("../components/audit/AuditRoleIndicator.js"),require("../components/audit/AuditUnitSwitcher.js"),require("../components/audit/AuditAnswerCell.js"),require("../components/audit/AuditSearchBar.js"),require("../components/audit/AuditFilterChips.js"),require("../components/audit/AuditFilterBar.js"),require("../components/audit/AuditGroupsProgress.js"),require("../components/audit/AuditSummaryDashboard.js"),require("../components/audit/AuditRequirementDialog.js"),require("../components/audit/AuditUnitExportDialog.js"),require("../components/audit/AuditUnitImportDialog.js"),require("../components/audit/AuditBulkExportDialog.js"),require("../components/audit/AuditBulkUploadDialog.js"),require("../components/audit/AuditSortPresets.js");var f=require("../node_modules/lodash/lodash.js");const h=r.makeStyles({container:{width:"100%",flex:1,maxWidth:"100%",height:"100%",minHeight:0,backgroundColor:"#ffffff",display:"flex",flexDirection:"column",margin:"0 auto",overflow:"hidden","@media (max-width: 768px)":{maxWidth:"100%",borderRadius:"0"},touchAction:"pan-y",paddingLeft:"0",paddingRight:"0"},listContainer:{overflow:"hidden",flex:1,minHeight:0,marginTop:"10px"},headerRow:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"10px",flexWrap:"wrap"},buttonGroup:{display:"flex",gap:"8px",flexWrap:"wrap"},addButton:{minWidth:"auto",whiteSpace:"nowrap"},recordsCount:{fontSize:"14px",color:"#666",marginTop:"8px"},backButton:{marginBottom:"10px"},searchBox:{marginTop:"8px"},loadMoreContainer:{display:"flex",justifyContent:"center",padding:"16px 0",borderTop:"1px solid #e0e0e0",marginTop:"4px"}}),j=500,q=({state:q,getColumnDefs:x,getListConfig:C,filterData:S})=>{const[y,w]=c.useSearchParams(),A=y.get("tag")||"logbook",v=y.get("title")||"",k=y.get("contentTitle"),b="true"===y.get("loadAll"),D="true"===y.get("canAdd"),M="true"===y.get("canEdit"),R="false"!==y.get("canExport"),B=y.get("addUrl")||"",E=y.get("q")||"",[I,P]=t.useState(E),[_,T]=t.useState(E),{service:$}=q,[N,L]=t.useState([]),[U,F]=t.useState(!1),[O,H]=t.useState(!1),[W,z]=t.useState(!1),V=t.useRef(null),G=t.useRef(E),J=t.useRef(""),Q=t.useRef(0),K=t.useRef($);K.current=$;const X=!!$,[Y,Z]=t.useState(0),[ee,te]=t.useState(null),re=h(),{t:ne}=u.default(),ae=c.useNavigate(),{getCurrentUserName:se}=d.useUserRoles($,q.context.nickname,q.context.selectedApp),ie=t.useMemo((()=>(k?[{key:k,title:k,maxWidth:250}]:C?C(A)?.columns??[]:x?x(A):[]).map((e=>({...e,title:e.title?ne(e.title):void 0})))),[k,A,ne,x,C]),[oe,ce]=t.useState(!1),[le,ue]=t.useState(null),[de,pe]=t.useState(!1),[me,ge]=t.useState(null),fe=t.useMemo((()=>`listpage_searches_${A}`),[A]),he=e=>{try{const t=localStorage.getItem(e);return t?JSON.parse(t):[]}catch{return[]}},[je,qe]=t.useState((()=>he(fe)));t.useEffect((()=>{qe(he(fe))}),[fe]);const xe=t.useCallback((e=>{const t=e.trim();t&&qe((e=>{const r=[t,...e.filter((e=>e!==t))].slice(0,20);try{localStorage.setItem(fe,JSON.stringify(r))}catch{}return r}))}),[fe]),Ce=t.useCallback((e=>{qe((t=>{const r=t.filter((t=>t!==e));try{localStorage.setItem(fe,JSON.stringify(r))}catch{}return r}))}),[fe]),Se=t.useMemo((()=>f.lodashExports.debounce((e=>T(e)),500)),[T]);t.useEffect((()=>()=>{Se.cancel()}),[Se]);const ye=t.useCallback((e=>{P(e),Se(e)}),[Se]),we=t.useCallback((e=>{xe(e)}),[xe]);t.useEffect((()=>{if(!y.get("q")){const e=y.get(`filter_${A}`);if(e){let t=e;if("%USERNAME%"===t){const e=se();if(!e)return;t=e}P(t),T(t),G.current=t}}}),[A,y,se]),t.useEffect((()=>{const e=y.get("q")||"";e!==G.current&&e!==_&&(P(e),T(e),G.current=e,J.current="")}),[y]),t.useEffect((()=>(V.current&&clearTimeout(V.current),V.current=setTimeout((()=>{if(G.current!==I){const e=new URLSearchParams(y);I?e.set("q",I):e.delete("q"),w(e,{replace:!0}),G.current=I}}),500),()=>{V.current&&clearTimeout(V.current)})),[I,y,w]),t.useEffect((()=>{if(!K.current)return;const e=`${A}-${k}-${b}-${Y}-${_}`;if(e===J.current)return;J.current=e;const t=++Q.current,r=()=>t===Q.current,n=k?`${A}-${_}-${k}`:`${A}-${_||"all"}`,a=s.dataCache.get(n);if(!a){if(H(!1),F(!0),k)K.current.getMessagesFast([A],_).then((e=>{if(!r())return;const t=e.map((e=>{const t=new Date(e.createdAt).toLocaleString();return{id:e.id,data:{[k]:new i.default(e.content).content,createdAt:t},fileId:null,fileIds:[],tags:[A],mentions:[K.current.codicent],createdAt:t}}));s.dataCache.set(n,t),L(t)})).catch(console.warn).finally((()=>{r()&&F(!1)}));else{const e=_||"",t=b||e?void 0:j;K.current.readDataMessages(A,e,void 0,0,t).then((t=>{r()&&(s.dataCache.set(n,t),L(t),H(!b&&!e&&t.length===j))})).catch(console.warn).finally((()=>{r()&&F(!1)}))}return()=>{J.current=""}}if(L(a),H(!b&&!k&&a.length>=j),!k&&!_&&a.length>0){const e=a.reduce(((e,t)=>t.createdAt>e?t.createdAt:e),a[0].createdAt);K.current.readDataMessages(A,"",void 0,void 0,void 0,e).then((e=>{r()&&L((t=>{const r=new Set(t.map((e=>e.id))),a=e.filter((e=>!r.has(e.id)));if(0===a.length)return t;const i=[...a,...t];return s.dataCache.set(n,i),i}))})).catch(console.warn)}}),[A,k,b,_,Y,X]);const Ae=t.useMemo((()=>{const e={};y.forEach(((t,r)=>{r.startsWith("filter_")&&(e[r.replace("filter_","")]=t)}));let t=N.map(((e,t)=>({item:e,index:t})));for(const[r,n]of Object.entries(e)){const e=n.replace("%USERNAME%",se()),a=q.context.nickname||"";t=t.filter((({item:t})=>{const n=t.data[r];return null!=n&&(String(n).includes(e)||String(n).includes(a))}))}if(S){const e=S(t.map((({item:e})=>e)),A),r=new Set(e.map((e=>e.id)));t=t.filter((({item:e})=>r.has(e.id)))}const r=t.map((({item:e,index:t})=>({...e.data,createdAt:e.createdAt,_id:e.id,originalMessageId:e.originalMessageId||e.id,_fileIds:e.fileIds??[],_index:t}))),n=C?.(A)?.deduplicateBy;if(n){const e=Array.isArray(n)?n:[n],t=t=>{for(const r of e){const e=t[r];if(null!=e&&""!==String(e).trim())return String(e)}},a=new Set;return r.filter((e=>{const r=t(e);return void 0===r||!a.has(r)&&(a.add(r),!0)}))}return r}),[N,y,q.context.nickname,se,S,A,C]),ve=t.useMemo((()=>{const e=(I||"").trim().toLowerCase();if(!e)return Ae;const t=ie.filter((e=>!e.hidden)).map((e=>e.key));return Ae.filter((r=>{for(const n of t)if(Array.isArray(n))for(const t of n){const n=r[t];if(null!=n&&String(n).toLowerCase().includes(e))return!0}else{const t=r[n];if(null!=t&&String(t).toLowerCase().includes(e))return!0}return!1}))}),[Ae,I,ie]),ke=t.useCallback((()=>{s.dataCache.clearPattern(A),J.current="",Z((e=>e+1))}),[A]),be=t.useCallback((async()=>{z(!0);try{const e=await K.current.readDataMessages(A,"",void 0,N.length,j),t=[...N,...e];L(t),s.dataCache.set(`${A}-all`,t),H(e.length===j)}catch(e){console.warn(e)}finally{z(!1)}}),[N,A]),De=t.useCallback((e=>{te(e)}),[]),Me=()=>{pe(!1),ge(null)},Re=t.useCallback((async()=>{const e=`${A}_${(new Date).toISOString().split("T")[0]}`.replace(/[/\\:*?"<>|]/g,"_");const t=null!==ee?ee:ve;await o.exportToExcel(t,ie,e,ne)||console.warn("No data to export")}),[ee,ve,ie,A,ne]),Be=t.useMemo((()=>null!==ee?ee.length:ve.length),[ee,ve]);return e.jsx(l.Page,{hideHeader:!0,children:e.jsxs("div",{className:re.container,children:[e.jsx("div",{className:re.backButton,children:e.jsx(r.Button,{appearance:"subtle",icon:e.jsx(a.ArrowLeft24Regular,{}),onClick:()=>ae(-1),children:ne("Tillbaka")})}),e.jsxs("div",{className:re.headerRow,children:[e.jsx(n.default,{title:ne(v||"Loggbok")}),e.jsxs("div",{className:re.buttonGroup,children:[e.jsx(r.Button,{appearance:"subtle",icon:e.jsx(a.ArrowClockwise24Regular,{}),onClick:ke,title:ne("Refresh"),children:ne("Refresh")}),R&&e.jsx(r.Button,{appearance:"subtle",icon:e.jsx(a.ArrowDownload24Regular,{}),onClick:Re,title:ne("Export to Excel"),children:ne("Export to Excel")}),(D||B)&&e.jsx(r.Button,{appearance:"primary",icon:e.jsx(a.Add24Regular,{}),onClick:D?()=>{ue(null),ce(!0)}:()=>window.open(B,"_blank"),className:re.addButton,children:ne("Add Record")})]})]}),e.jsx("div",{className:re.searchBox,children:e.jsx(p.default,{placeholder:ne("Sök")+"...",value:I,onChange:ye,recentSearches:je,onCommitSearch:we,onRemoveRecentSearch:Ce})}),!U&&e.jsxs("div",{className:re.recordsCount,children:[ne("Total"),": ",Be," ",ne("records"),Be!==N.length&&` (${ne("filtered from")} ${N.length})`]}),e.jsxs("div",{className:re.listContainer,children:[U&&e.jsx("p",{children:ne("Söker")+"..."}),!U&&e.jsx(m.default,{data:ve,columns:ie,canEdit:M,onEdit:(e,t)=>{const r=N[t];r&&(ue({data:{...r.data,_fileIds:r.fileIds??[]},index:t}),ce(!0))},onDelete:(e,t)=>{const r=N[t];r&&(ge({index:t,id:r.id}),pe(!0))},onToggleCheckbox:async(e,t,r,n)=>{const a=N[n];if(a){const e=Array.isArray(t)?t[0]:t,i={...a.data,[e]:String(r)},o=await $.updateDataMessage(a.id,i);L((e=>{const t=[...e];return t[n]={...a,id:o,data:i},t})),s.dataCache.clearPattern(A)}},onEnumChange:async(e,t,r,n)=>{const a=N[n];if(a){const e=Array.isArray(t)?t[0]:t,i={...a.data,[e]:r},o=await $.updateDataMessage(a.id,i);L((e=>{const t=[...e];return t[n]={...a,id:o,data:i},t})),s.dataCache.clearPattern(A)}},onFilteredDataChange:De})]}),O&&!I.trim()&&e.jsx("div",{className:re.loadMoreContainer,children:e.jsx(r.Button,{appearance:"primary",size:"medium",onClick:be,disabled:W,children:W?ne("Laddar..."):ne("Visa fler")+` (${N.length} ${ne("laddade")})`})}),e.jsx(g.RecordModal,{open:oe,onClose:()=>ce(!1),onSave:async e=>{const t=new Set(ie.filter((e=>"thumbnail"===e.type)).map((e=>Array.isArray(e.key)?e.key[0]:e.key)));if(le){const r=N[le.index];if(r){const n={};for(const r in e){if(t.has(r))continue;const a=e[r];n[r]=null==a?"":String(a)}const a=await $.updateDataMessage(r.id,n);L((e=>{const t=[...e];return t[le.index]={...r,id:a,data:n},t})),s.dataCache.clearPattern(A)}}else{const r={};for(const n in e){if(t.has(n))continue;const a=e[n];r[n]=null==a?"":String(a)}const n={id:await $.createDataMessage(A,r),data:r,createdAt:(new Date).toISOString(),fileId:null,fileIds:[],tags:[A],mentions:[$.codicent]};L((e=>[n,...e])),s.dataCache.clearPattern(A)}},columns:ie,initialData:le?.data}),e.jsx(r.Dialog,{open:de,onOpenChange:(e,t)=>!t.open&&Me(),children:e.jsx(r.DialogSurface,{children:e.jsxs(r.DialogBody,{children:[e.jsx(r.DialogTitle,{children:ne("Confirm Delete")}),e.jsx(r.DialogContent,{children:ne("Are you sure you want to delete this record? This action cannot be undone.")}),e.jsxs(r.DialogActions,{children:[e.jsx(r.Button,{appearance:"secondary",onClick:Me,children:ne("Cancel")}),e.jsx(r.Button,{appearance:"primary",onClick:async()=>{if(me)try{await $.deleteDataMessage(me.id),L((e=>e.filter(((e,t)=>t!==me.index)))),s.dataCache.clearPattern(A)}catch(e){console.error("Failed to delete record:",e)}finally{pe(!1),ge(null)}},children:ne("Delete")})]})]})})})]})})};exports.ListPage=q,exports.default=q;
@@ -13,6 +13,55 @@ export interface ListPageProps {
13
13
  /** Optional data filter applied after fetch. Useful for app-specific filtering (e.g. empty notes). */
14
14
  filterData?: (data: DataMessage[], tag: string) => DataMessage[];
15
15
  }
16
+ /**
17
+ * ListPage — data fetch & cache behaviour
18
+ *
19
+ * ## Fetch key guard
20
+ * A `fetchKey` string (`tag-contentTitle-loadAll-refreshKey-debouncedSearch`) is stored in
21
+ * `lastFetchKey` (ref). The fetch effect bails out early if the key hasn't changed, preventing
22
+ * duplicate calls when unrelated state updates re-render the component.
23
+ * The ref is reset to `""` on effect cleanup (unmount / dep change) so that re-navigation
24
+ * always triggers a fresh fetch.
25
+ *
26
+ * ## Race-condition protection
27
+ * `activeFetchId` (ref) is incremented each time a new fetch starts. Each in-flight promise
28
+ * captures its own `myFetchId` and checks `isActive()` before writing state. A superseded
29
+ * fetch (e.g. from rapid search typing or quick navigation) silently discards its result
30
+ * without calling `setSearching(false)`, so the spinner stays visible until the winning fetch
31
+ * completes.
32
+ *
33
+ * ## Cache strategy
34
+ * | Visit | Cache present? | Behaviour |
35
+ * |-------|---------------|-----------|
36
+ * | First | No | `searching=true` → API call → populate cache → show list |
37
+ * | First | Yes | Instant render from cache (no spinner) → background delta-fetch for records newer than the latest cached timestamp → silently prepend new rows |
38
+ * | Back-navigation | Yes | Unmount cleanup resets fetchKey → same as "First, cache present" |
39
+ * | Back-navigation | Expired | Same as "First, no cache" |
40
+ * | Refresh button | Any | Clears cache for tag + resets fetchKey + increments `refreshKey` → full API call |
41
+ *
42
+ * ## Search / filter flow
43
+ * - Typing triggers a 500 ms debounce before `debouncedSearch` updates.
44
+ * - `debouncedSearch` change → new fetchKey → new `activeFetchId` → previous in-flight
45
+ * fetch is superseded and its result discarded.
46
+ * - `filter_<field>=<value>` URL params are applied client-side after fetch (not sent to API)
47
+ * except for `filter_<tag>=<value>` which seeds `debouncedSearch` for server-side filtering.
48
+ * - `q=` URL param is kept in sync with the search box via a debounced `setSearchParams`
49
+ * call that preserves all other existing params (`canAdd`, `canEdit`, `filter_*`, etc.).
50
+ *
51
+ * ## URL params (all optional)
52
+ * | Param | Type | Default | Purpose |
53
+ * |-------|------|---------|---------|
54
+ * | `tag` | string | `"logbook"` | Data tag to query |
55
+ * | `title` | string | `"Loggbok"` | Page heading |
56
+ * | `contentTitle` | string | — | If set, fetches raw messages and maps this field as the display column |
57
+ * | `loadAll` | `"true"` | — | Removes PAGE_SIZE cap; disables Load-more |
58
+ * | `canAdd` | `"true"` | — | Shows Add Record button (opens RecordModal) |
59
+ * | `canEdit` | `"true"` | — | Shows per-row Edit/Delete buttons |
60
+ * | `canExport` | `"false"` to hide | visible | Shows Export to Excel button |
61
+ * | `addUrl` | string | — | External URL opened on Add click (alternative to modal) |
62
+ * | `q` | string | — | Initial search term; kept in sync with search box |
63
+ * | `filter_<field>` | string | — | Client-side row filter; `%USERNAME%` resolves to current user |
64
+ */
16
65
  export declare const ListPage: ({ state, getColumnDefs, getListConfig, filterData }: ListPageProps) => import("react/jsx-runtime").JSX.Element;
17
66
  export default ListPage;
18
67
  //# sourceMappingURL=ListPage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ListPage.d.ts","sourceRoot":"","sources":["../../../src/pages/ListPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAuB3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAkE3D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,+FAA+F;IAC/F,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,gBAAgB,EAAE,CAAC;IACpD;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;IACxD,sGAAsG;IACtG,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC;CAClE;AAED,eAAO,MAAM,QAAQ,wDAAyD,aAAa,4CAsmB1F,CAAC;AAEF,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"ListPage.d.ts","sourceRoot":"","sources":["../../../src/pages/ListPage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAuB3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAkE3D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,+FAA+F;IAC/F,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,gBAAgB,EAAE,CAAC;IACpD;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;IACxD,sGAAsG;IACtG,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC;CAClE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,eAAO,MAAM,QAAQ,wDAAyD,aAAa,4CA+mB1F,CAAC;AAEF,eAAe,QAAQ,CAAC"}
@@ -1 +1 @@
1
- import{jsx as t,jsxs as e}from"react/jsx-runtime";import{useState as o,useRef as n,useMemo as r,useEffect as i,useCallback as a}from"react";import"../components/Markdown.js";import"../components/Textarea.js";import"../components/Button.js";import"../components/CompoundButton.js";import{makeStyles as s,Button as c,Dialog as l,DialogSurface as d,DialogBody as m,DialogTitle as p,DialogContent as u,DialogActions as g}from"@fluentui/react-components";import"../components/Spinner.js";import f from"../components/TextHeader.js";import"../components/TypingIndicator.js";import"../components/Dialog.js";import"../components/ChatInput.js";import"../components/CombinedPlaceholderDialog.js";import"../components/ChatMessage.js";import"../components/Header.js";import{ArrowLeft24Regular as h,ArrowClockwise24Regular as j,ArrowDownload24Regular as x,Add24Regular as y}from"@fluentui/react-icons";import"../services/codicent.js";import{dataCache as w}from"../services/dataCache.js";import S from"../utils/MessageContent.js";import"../node_modules/tinycolor2/esm/tinycolor.js";import"../_virtual/index.js";import"../config/index.js";import"../utils/cacheManager.js";import"../lib/wavtools/lib/wav_packer.js";import"../lib/wavtools/lib/analysis/audio_analysis.js";import"../lib/wavtools/lib/wav_stream_player.js";import"../lib/wavtools/lib/wav_recorder.js";import{exportToExcel as A}from"../utils/excelExport.js";import"./AppFrame.js";import"./Canvas.js";import"./Chat.js";import"./Compose.js";import"./Snap.js";import"./Search.js";import"./Menu.js";import"./Log.js";import"./Login.js";import"./Home.js";import"./CrmPage.js";import"./CrmPagePersistent.js";import"./ImageView.js";import"./FormInvite.js";import"./FormAccept.js";import"./Sales.js";import{useSearchParams as C,useNavigate as k}from"react-router-dom";import"./Purchase.js";import"../components/Content.js";import{Page as v}from"../components/Page.js";import"./QrScan.js";import b from"../hooks/useLocalization.js";import"../components/FileThumbnail.js";import"react-dom/client";import"../node_modules/@auth0/auth0-react/dist/auth0-react.esm.js";import"../hooks/useAppStyles.js";import{useUserRoles as D}from"../hooks/useUserRoles.js";import"../components/MessageInput.js";import"../components/UploadFile.js";import"../components/SnapFooter.js";import"../components/Profile.js";import"../components/MessageItem.js";import"../components/AiInput.js";import I from"../components/SearchBox.js";import"../components/DataMessagePicker.js";import"../components/HtmlView.js";import"../components/Footer.js";import"../components/QrCodeDialog.js";import"../components/QrScanner.js";import"../components/OfflineMessage.js";import"../components/LanguageSelector.js";import M from"../components/ListView.js";import{RecordModal as _}from"../components/RecordModal.js";import"../components/BulkUploadDialog.js";import"../components/CookieBanner.js";import"../components/audit/AuditCircularProgress.js";import"../components/audit/AuditHorizontalProgress.js";import"../components/audit/AuditRoleIndicator.js";import"../components/audit/AuditUnitSwitcher.js";import"../components/audit/AuditAnswerCell.js";import"../components/audit/AuditSearchBar.js";import"../components/audit/AuditFilterChips.js";import"../components/audit/AuditFilterBar.js";import"../components/audit/AuditGroupsProgress.js";import"../components/audit/AuditSummaryDashboard.js";import"../components/audit/AuditRequirementDialog.js";import"../components/audit/AuditUnitExportDialog.js";import"../components/audit/AuditUnitImportDialog.js";import"../components/audit/AuditBulkExportDialog.js";import"../components/audit/AuditBulkUploadDialog.js";import"../components/audit/AuditSortPresets.js";import{l as P}from"../node_modules/lodash/lodash.js";const $=s({container:{width:"100%",flex:1,maxWidth:"100%",height:"100%",minHeight:0,backgroundColor:"#ffffff",display:"flex",flexDirection:"column",margin:"0 auto",overflow:"hidden","@media (max-width: 768px)":{maxWidth:"100%",borderRadius:"0"},touchAction:"pan-y",paddingLeft:"0",paddingRight:"0"},listContainer:{overflow:"hidden",flex:1,minHeight:0,marginTop:"10px"},headerRow:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"10px",flexWrap:"wrap"},buttonGroup:{display:"flex",gap:"8px",flexWrap:"wrap"},addButton:{minWidth:"auto",whiteSpace:"nowrap"},recordsCount:{fontSize:"14px",color:"#666",marginTop:"8px"},backButton:{marginBottom:"10px"},searchBox:{marginTop:"8px"},loadMoreContainer:{display:"flex",justifyContent:"center",padding:"16px 0",borderTop:"1px solid #e0e0e0",marginTop:"4px"}}),T=500,B=({state:s,getColumnDefs:B,getListConfig:E,filterData:N})=>{const[R,L]=C(),F=R.get("tag")||"logbook",U=R.get("title")||"",H=R.get("contentTitle"),O="true"===R.get("loadAll"),W="true"===R.get("canAdd"),q="true"===R.get("canEdit"),z="false"!==R.get("canExport"),V=R.get("addUrl")||"",G=R.get("q")||"",[J,Q]=o(G),[K,X]=o(G),{service:Y}=s,[Z,tt]=o([]),[et,ot]=o(!1),[nt,rt]=o(!1),[it,at]=o(!1),st=n(null),ct=n(G),lt=n(""),dt=n(0),mt=n(Y);mt.current=Y;const pt=!!Y,[ut,gt]=o(0),[ft,ht]=o(null),jt=$(),{t:xt}=b(),yt=k(),{getCurrentUserName:wt}=D(Y,s.context.nickname,s.context.selectedApp),St=r((()=>(H?[{key:H,title:H,maxWidth:250}]:E?E(F)?.columns??[]:B?B(F):[]).map((t=>({...t,title:t.title?xt(t.title):void 0})))),[H,F,xt,B,E]),[At,Ct]=o(!1),[kt,vt]=o(null),[bt,Dt]=o(!1),[It,Mt]=o(null),_t=r((()=>`listpage_searches_${F}`),[F]),Pt=t=>{try{const e=localStorage.getItem(t);return e?JSON.parse(e):[]}catch{return[]}},[$t,Tt]=o((()=>Pt(_t)));i((()=>{Tt(Pt(_t))}),[_t]);const Bt=a((t=>{const e=t.trim();e&&Tt((t=>{const o=[e,...t.filter((t=>t!==e))].slice(0,20);try{localStorage.setItem(_t,JSON.stringify(o))}catch{}return o}))}),[_t]),Et=a((t=>{Tt((e=>{const o=e.filter((e=>e!==t));try{localStorage.setItem(_t,JSON.stringify(o))}catch{}return o}))}),[_t]),Nt=r((()=>P.debounce((t=>X(t)),500)),[X]);i((()=>()=>{Nt.cancel()}),[Nt]);const Rt=a((t=>{Q(t),Nt(t)}),[Nt]),Lt=a((t=>{Bt(t)}),[Bt]);i((()=>{if(!R.get("q")){const t=R.get(`filter_${F}`);if(t){let e=t;if("%USERNAME%"===e){const t=wt();if(!t)return;e=t}Q(e),X(e),ct.current=e}}}),[F,R,wt]),i((()=>{const t=R.get("q")||"";t!==ct.current&&(Q(t),X(t),ct.current=t,lt.current="")}),[R]),i((()=>(st.current&&clearTimeout(st.current),st.current=setTimeout((()=>{if(ct.current!==J){const t=new URLSearchParams(R);J?t.set("q",J):t.delete("q"),L(t,{replace:!0}),ct.current=J}}),500),()=>{st.current&&clearTimeout(st.current)})),[J,R,L]),i((()=>{if(!mt.current)return;const t=`${F}-${H}-${O}-${ut}-${K}`;if(t===lt.current)return;lt.current=t;const e=++dt.current,o=()=>e===dt.current,n=H?`${F}-${K}-${H}`:`${F}-${K||"all"}`,r=w.get(n);if(r){if(tt(r),rt(!O&&!H&&r.length>=T),!H&&!K&&r.length>0){const t=r.reduce(((t,e)=>e.createdAt>t?e.createdAt:t),r[0].createdAt);mt.current.readDataMessages(F,"",void 0,void 0,void 0,t).then((t=>{o()&&tt((e=>{const o=new Set(e.map((t=>t.id))),r=t.filter((t=>!o.has(t.id)));if(0===r.length)return e;const i=[...r,...e];return w.set(n,i),i}))})).catch(console.warn)}}else if(rt(!1),ot(!0),H)mt.current.getMessagesFast([F],K).then((t=>{if(!o())return;const e=t.map((t=>{const e=new Date(t.createdAt).toLocaleString();return{id:t.id,data:{[H]:new S(t.content).content,createdAt:e},fileId:null,fileIds:[],tags:[F],mentions:[mt.current.codicent],createdAt:e}}));w.set(n,e),tt(e)})).catch(console.warn).finally((()=>{o()&&ot(!1)}));else{const t=K||"",e=O||t?void 0:T;mt.current.readDataMessages(F,t,void 0,0,e).then((e=>{o()&&(w.set(n,e),tt(e),rt(!O&&!t&&e.length===T))})).catch(console.warn).finally((()=>{o()&&ot(!1)}))}}),[F,H,O,K,ut,pt]);const Ft=r((()=>{const t={};R.forEach(((e,o)=>{o.startsWith("filter_")&&(t[o.replace("filter_","")]=e)}));let e=Z.map(((t,e)=>({item:t,index:e})));for(const[o,n]of Object.entries(t)){const t=n.replace("%USERNAME%",wt()),r=s.context.nickname||"";e=e.filter((({item:e})=>{const n=e.data[o];return null!=n&&(String(n).includes(t)||String(n).includes(r))}))}if(N){const t=N(e.map((({item:t})=>t)),F),o=new Set(t.map((t=>t.id)));e=e.filter((({item:t})=>o.has(t.id)))}const o=e.map((({item:t,index:e})=>({...t.data,createdAt:t.createdAt,_id:t.id,originalMessageId:t.originalMessageId||t.id,_fileIds:t.fileIds??[],_index:e}))),n=E?.(F)?.deduplicateBy;if(n){const t=Array.isArray(n)?n:[n],e=e=>{for(const o of t){const t=e[o];if(null!=t&&""!==String(t).trim())return String(t)}},r=new Set;return o.filter((t=>{const o=e(t);return void 0===o||!r.has(o)&&(r.add(o),!0)}))}return o}),[Z,R,s.context.nickname,wt,N,F,E]),Ut=r((()=>{const t=(J||"").trim().toLowerCase();if(!t)return Ft;const e=St.filter((t=>!t.hidden)).map((t=>t.key));return Ft.filter((o=>{for(const n of e)if(Array.isArray(n))for(const e of n){const n=o[e];if(null!=n&&String(n).toLowerCase().includes(t))return!0}else{const e=o[n];if(null!=e&&String(e).toLowerCase().includes(t))return!0}return!1}))}),[Ft,J,St]),Ht=a((()=>{w.clearPattern(F),lt.current="",gt((t=>t+1))}),[F]),Ot=a((async()=>{at(!0);try{const t=await mt.current.readDataMessages(F,"",void 0,Z.length,T),e=[...Z,...t];tt(e),w.set(`${F}-all`,e),rt(t.length===T)}catch(t){console.warn(t)}finally{at(!1)}}),[Z,F]),Wt=a((t=>{ht(t)}),[]),qt=()=>{Dt(!1),Mt(null)},zt=a((async()=>{const t=`${F}_${(new Date).toISOString().split("T")[0]}`.replace(/[/\\:*?"<>|]/g,"_");const e=null!==ft?ft:Ut;await A(e,St,t,xt)||console.warn("No data to export")}),[ft,Ut,St,F,xt]),Vt=r((()=>null!==ft?ft.length:Ut.length),[ft,Ut]);return t(v,{hideHeader:!0,children:e("div",{className:jt.container,children:[t("div",{className:jt.backButton,children:t(c,{appearance:"subtle",icon:t(h,{}),onClick:()=>yt(-1),children:xt("Tillbaka")})}),e("div",{className:jt.headerRow,children:[t(f,{title:xt(U||"Loggbok")}),e("div",{className:jt.buttonGroup,children:[t(c,{appearance:"subtle",icon:t(j,{}),onClick:Ht,title:xt("Refresh"),children:xt("Refresh")}),z&&t(c,{appearance:"subtle",icon:t(x,{}),onClick:zt,title:xt("Export to Excel"),children:xt("Export to Excel")}),(W||V)&&t(c,{appearance:"primary",icon:t(y,{}),onClick:W?()=>{vt(null),Ct(!0)}:()=>window.open(V,"_blank"),className:jt.addButton,children:xt("Add Record")})]})]}),t("div",{className:jt.searchBox,children:t(I,{placeholder:xt("Sök")+"...",value:J,onChange:Rt,recentSearches:$t,onCommitSearch:Lt,onRemoveRecentSearch:Et})}),!et&&e("div",{className:jt.recordsCount,children:[xt("Total"),": ",Vt," ",xt("records"),Vt!==Z.length&&` (${xt("filtered from")} ${Z.length})`]}),e("div",{className:jt.listContainer,children:[et&&t("p",{children:xt("Söker")+"..."}),!et&&t(M,{data:Ut,columns:St,canEdit:q,onEdit:(t,e)=>{const o=Z[e];o&&(vt({data:{...o.data,_fileIds:o.fileIds??[]},index:e}),Ct(!0))},onDelete:(t,e)=>{const o=Z[e];o&&(Mt({index:e,id:o.id}),Dt(!0))},onToggleCheckbox:async(t,e,o,n)=>{const r=Z[n];if(r){const t=Array.isArray(e)?e[0]:e,i={...r.data,[t]:String(o)},a=await Y.updateDataMessage(r.id,i);tt((t=>{const e=[...t];return e[n]={...r,id:a,data:i},e})),w.clearPattern(F)}},onEnumChange:async(t,e,o,n)=>{const r=Z[n];if(r){const t=Array.isArray(e)?e[0]:e,i={...r.data,[t]:o},a=await Y.updateDataMessage(r.id,i);tt((t=>{const e=[...t];return e[n]={...r,id:a,data:i},e})),w.clearPattern(F)}},onFilteredDataChange:Wt})]}),nt&&!J.trim()&&t("div",{className:jt.loadMoreContainer,children:t(c,{appearance:"primary",size:"medium",onClick:Ot,disabled:it,children:it?xt("Laddar..."):xt("Visa fler")+` (${Z.length} ${xt("laddade")})`})}),t(_,{open:At,onClose:()=>Ct(!1),onSave:async t=>{const e=new Set(St.filter((t=>"thumbnail"===t.type)).map((t=>Array.isArray(t.key)?t.key[0]:t.key)));if(kt){const o=Z[kt.index];if(o){const n={};for(const o in t){if(e.has(o))continue;const r=t[o];n[o]=null==r?"":String(r)}const r=await Y.updateDataMessage(o.id,n);tt((t=>{const e=[...t];return e[kt.index]={...o,id:r,data:n},e})),w.clearPattern(F)}}else{const o={};for(const n in t){if(e.has(n))continue;const r=t[n];o[n]=null==r?"":String(r)}const n={id:await Y.createDataMessage(F,o),data:o,createdAt:(new Date).toISOString(),fileId:null,fileIds:[],tags:[F],mentions:[Y.codicent]};tt((t=>[n,...t])),w.clearPattern(F)}},columns:St,initialData:kt?.data}),t(l,{open:bt,onOpenChange:(t,e)=>!e.open&&qt(),children:t(d,{children:e(m,{children:[t(p,{children:xt("Confirm Delete")}),t(u,{children:xt("Are you sure you want to delete this record? This action cannot be undone.")}),e(g,{children:[t(c,{appearance:"secondary",onClick:qt,children:xt("Cancel")}),t(c,{appearance:"primary",onClick:async()=>{if(It)try{await Y.deleteDataMessage(It.id),tt((t=>t.filter(((t,e)=>e!==It.index)))),w.clearPattern(F)}catch(t){console.error("Failed to delete record:",t)}finally{Dt(!1),Mt(null)}},children:xt("Delete")})]})]})})})]})})};export{B as ListPage,B as default};
1
+ import{jsx as t,jsxs as e}from"react/jsx-runtime";import{useState as o,useRef as n,useMemo as r,useEffect as i,useCallback as a}from"react";import"../components/Markdown.js";import"../components/Textarea.js";import"../components/Button.js";import"../components/CompoundButton.js";import{makeStyles as s,Button as c,Dialog as l,DialogSurface as d,DialogBody as m,DialogTitle as p,DialogContent as u,DialogActions as g}from"@fluentui/react-components";import"../components/Spinner.js";import f from"../components/TextHeader.js";import"../components/TypingIndicator.js";import"../components/Dialog.js";import"../components/ChatInput.js";import"../components/CombinedPlaceholderDialog.js";import"../components/ChatMessage.js";import"../components/Header.js";import{ArrowLeft24Regular as h,ArrowClockwise24Regular as j,ArrowDownload24Regular as x,Add24Regular as y}from"@fluentui/react-icons";import"../services/codicent.js";import{dataCache as w}from"../services/dataCache.js";import S from"../utils/MessageContent.js";import"../node_modules/tinycolor2/esm/tinycolor.js";import"../_virtual/index.js";import"../config/index.js";import"../utils/cacheManager.js";import"../lib/wavtools/lib/wav_packer.js";import"../lib/wavtools/lib/analysis/audio_analysis.js";import"../lib/wavtools/lib/wav_stream_player.js";import"../lib/wavtools/lib/wav_recorder.js";import{exportToExcel as A}from"../utils/excelExport.js";import"./AppFrame.js";import"./Canvas.js";import"./Chat.js";import"./Compose.js";import"./Snap.js";import"./Search.js";import"./Menu.js";import"./Log.js";import"./Login.js";import"./Home.js";import"./CrmPage.js";import"./CrmPagePersistent.js";import"./ImageView.js";import"./FormInvite.js";import"./FormAccept.js";import"./Sales.js";import{useSearchParams as C,useNavigate as k}from"react-router-dom";import"./Purchase.js";import"../components/Content.js";import{Page as v}from"../components/Page.js";import"./QrScan.js";import b from"../hooks/useLocalization.js";import"../components/FileThumbnail.js";import"react-dom/client";import"../node_modules/@auth0/auth0-react/dist/auth0-react.esm.js";import"../hooks/useAppStyles.js";import{useUserRoles as D}from"../hooks/useUserRoles.js";import"../components/MessageInput.js";import"../components/UploadFile.js";import"../components/SnapFooter.js";import"../components/Profile.js";import"../components/MessageItem.js";import"../components/AiInput.js";import I from"../components/SearchBox.js";import"../components/DataMessagePicker.js";import"../components/HtmlView.js";import"../components/Footer.js";import"../components/QrCodeDialog.js";import"../components/QrScanner.js";import"../components/OfflineMessage.js";import"../components/LanguageSelector.js";import M from"../components/ListView.js";import{RecordModal as _}from"../components/RecordModal.js";import"../components/BulkUploadDialog.js";import"../components/CookieBanner.js";import"../components/audit/AuditCircularProgress.js";import"../components/audit/AuditHorizontalProgress.js";import"../components/audit/AuditRoleIndicator.js";import"../components/audit/AuditUnitSwitcher.js";import"../components/audit/AuditAnswerCell.js";import"../components/audit/AuditSearchBar.js";import"../components/audit/AuditFilterChips.js";import"../components/audit/AuditFilterBar.js";import"../components/audit/AuditGroupsProgress.js";import"../components/audit/AuditSummaryDashboard.js";import"../components/audit/AuditRequirementDialog.js";import"../components/audit/AuditUnitExportDialog.js";import"../components/audit/AuditUnitImportDialog.js";import"../components/audit/AuditBulkExportDialog.js";import"../components/audit/AuditBulkUploadDialog.js";import"../components/audit/AuditSortPresets.js";import{l as P}from"../node_modules/lodash/lodash.js";const $=s({container:{width:"100%",flex:1,maxWidth:"100%",height:"100%",minHeight:0,backgroundColor:"#ffffff",display:"flex",flexDirection:"column",margin:"0 auto",overflow:"hidden","@media (max-width: 768px)":{maxWidth:"100%",borderRadius:"0"},touchAction:"pan-y",paddingLeft:"0",paddingRight:"0"},listContainer:{overflow:"hidden",flex:1,minHeight:0,marginTop:"10px"},headerRow:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:"10px",flexWrap:"wrap"},buttonGroup:{display:"flex",gap:"8px",flexWrap:"wrap"},addButton:{minWidth:"auto",whiteSpace:"nowrap"},recordsCount:{fontSize:"14px",color:"#666",marginTop:"8px"},backButton:{marginBottom:"10px"},searchBox:{marginTop:"8px"},loadMoreContainer:{display:"flex",justifyContent:"center",padding:"16px 0",borderTop:"1px solid #e0e0e0",marginTop:"4px"}}),T=500,B=({state:s,getColumnDefs:B,getListConfig:E,filterData:N})=>{const[R,L]=C(),F=R.get("tag")||"logbook",U=R.get("title")||"",H=R.get("contentTitle"),O="true"===R.get("loadAll"),W="true"===R.get("canAdd"),q="true"===R.get("canEdit"),z="false"!==R.get("canExport"),V=R.get("addUrl")||"",G=R.get("q")||"",[J,Q]=o(G),[K,X]=o(G),{service:Y}=s,[Z,tt]=o([]),[et,ot]=o(!1),[nt,rt]=o(!1),[it,at]=o(!1),st=n(null),ct=n(G),lt=n(""),dt=n(0),mt=n(Y);mt.current=Y;const pt=!!Y,[ut,gt]=o(0),[ft,ht]=o(null),jt=$(),{t:xt}=b(),yt=k(),{getCurrentUserName:wt}=D(Y,s.context.nickname,s.context.selectedApp),St=r((()=>(H?[{key:H,title:H,maxWidth:250}]:E?E(F)?.columns??[]:B?B(F):[]).map((t=>({...t,title:t.title?xt(t.title):void 0})))),[H,F,xt,B,E]),[At,Ct]=o(!1),[kt,vt]=o(null),[bt,Dt]=o(!1),[It,Mt]=o(null),_t=r((()=>`listpage_searches_${F}`),[F]),Pt=t=>{try{const e=localStorage.getItem(t);return e?JSON.parse(e):[]}catch{return[]}},[$t,Tt]=o((()=>Pt(_t)));i((()=>{Tt(Pt(_t))}),[_t]);const Bt=a((t=>{const e=t.trim();e&&Tt((t=>{const o=[e,...t.filter((t=>t!==e))].slice(0,20);try{localStorage.setItem(_t,JSON.stringify(o))}catch{}return o}))}),[_t]),Et=a((t=>{Tt((e=>{const o=e.filter((e=>e!==t));try{localStorage.setItem(_t,JSON.stringify(o))}catch{}return o}))}),[_t]),Nt=r((()=>P.debounce((t=>X(t)),500)),[X]);i((()=>()=>{Nt.cancel()}),[Nt]);const Rt=a((t=>{Q(t),Nt(t)}),[Nt]),Lt=a((t=>{Bt(t)}),[Bt]);i((()=>{if(!R.get("q")){const t=R.get(`filter_${F}`);if(t){let e=t;if("%USERNAME%"===e){const t=wt();if(!t)return;e=t}Q(e),X(e),ct.current=e}}}),[F,R,wt]),i((()=>{const t=R.get("q")||"";t!==ct.current&&t!==K&&(Q(t),X(t),ct.current=t,lt.current="")}),[R]),i((()=>(st.current&&clearTimeout(st.current),st.current=setTimeout((()=>{if(ct.current!==J){const t=new URLSearchParams(R);J?t.set("q",J):t.delete("q"),L(t,{replace:!0}),ct.current=J}}),500),()=>{st.current&&clearTimeout(st.current)})),[J,R,L]),i((()=>{if(!mt.current)return;const t=`${F}-${H}-${O}-${ut}-${K}`;if(t===lt.current)return;lt.current=t;const e=++dt.current,o=()=>e===dt.current,n=H?`${F}-${K}-${H}`:`${F}-${K||"all"}`,r=w.get(n);if(!r){if(rt(!1),ot(!0),H)mt.current.getMessagesFast([F],K).then((t=>{if(!o())return;const e=t.map((t=>{const e=new Date(t.createdAt).toLocaleString();return{id:t.id,data:{[H]:new S(t.content).content,createdAt:e},fileId:null,fileIds:[],tags:[F],mentions:[mt.current.codicent],createdAt:e}}));w.set(n,e),tt(e)})).catch(console.warn).finally((()=>{o()&&ot(!1)}));else{const t=K||"",e=O||t?void 0:T;mt.current.readDataMessages(F,t,void 0,0,e).then((e=>{o()&&(w.set(n,e),tt(e),rt(!O&&!t&&e.length===T))})).catch(console.warn).finally((()=>{o()&&ot(!1)}))}return()=>{lt.current=""}}if(tt(r),rt(!O&&!H&&r.length>=T),!H&&!K&&r.length>0){const t=r.reduce(((t,e)=>e.createdAt>t?e.createdAt:t),r[0].createdAt);mt.current.readDataMessages(F,"",void 0,void 0,void 0,t).then((t=>{o()&&tt((e=>{const o=new Set(e.map((t=>t.id))),r=t.filter((t=>!o.has(t.id)));if(0===r.length)return e;const i=[...r,...e];return w.set(n,i),i}))})).catch(console.warn)}}),[F,H,O,K,ut,pt]);const Ft=r((()=>{const t={};R.forEach(((e,o)=>{o.startsWith("filter_")&&(t[o.replace("filter_","")]=e)}));let e=Z.map(((t,e)=>({item:t,index:e})));for(const[o,n]of Object.entries(t)){const t=n.replace("%USERNAME%",wt()),r=s.context.nickname||"";e=e.filter((({item:e})=>{const n=e.data[o];return null!=n&&(String(n).includes(t)||String(n).includes(r))}))}if(N){const t=N(e.map((({item:t})=>t)),F),o=new Set(t.map((t=>t.id)));e=e.filter((({item:t})=>o.has(t.id)))}const o=e.map((({item:t,index:e})=>({...t.data,createdAt:t.createdAt,_id:t.id,originalMessageId:t.originalMessageId||t.id,_fileIds:t.fileIds??[],_index:e}))),n=E?.(F)?.deduplicateBy;if(n){const t=Array.isArray(n)?n:[n],e=e=>{for(const o of t){const t=e[o];if(null!=t&&""!==String(t).trim())return String(t)}},r=new Set;return o.filter((t=>{const o=e(t);return void 0===o||!r.has(o)&&(r.add(o),!0)}))}return o}),[Z,R,s.context.nickname,wt,N,F,E]),Ut=r((()=>{const t=(J||"").trim().toLowerCase();if(!t)return Ft;const e=St.filter((t=>!t.hidden)).map((t=>t.key));return Ft.filter((o=>{for(const n of e)if(Array.isArray(n))for(const e of n){const n=o[e];if(null!=n&&String(n).toLowerCase().includes(t))return!0}else{const e=o[n];if(null!=e&&String(e).toLowerCase().includes(t))return!0}return!1}))}),[Ft,J,St]),Ht=a((()=>{w.clearPattern(F),lt.current="",gt((t=>t+1))}),[F]),Ot=a((async()=>{at(!0);try{const t=await mt.current.readDataMessages(F,"",void 0,Z.length,T),e=[...Z,...t];tt(e),w.set(`${F}-all`,e),rt(t.length===T)}catch(t){console.warn(t)}finally{at(!1)}}),[Z,F]),Wt=a((t=>{ht(t)}),[]),qt=()=>{Dt(!1),Mt(null)},zt=a((async()=>{const t=`${F}_${(new Date).toISOString().split("T")[0]}`.replace(/[/\\:*?"<>|]/g,"_");const e=null!==ft?ft:Ut;await A(e,St,t,xt)||console.warn("No data to export")}),[ft,Ut,St,F,xt]),Vt=r((()=>null!==ft?ft.length:Ut.length),[ft,Ut]);return t(v,{hideHeader:!0,children:e("div",{className:jt.container,children:[t("div",{className:jt.backButton,children:t(c,{appearance:"subtle",icon:t(h,{}),onClick:()=>yt(-1),children:xt("Tillbaka")})}),e("div",{className:jt.headerRow,children:[t(f,{title:xt(U||"Loggbok")}),e("div",{className:jt.buttonGroup,children:[t(c,{appearance:"subtle",icon:t(j,{}),onClick:Ht,title:xt("Refresh"),children:xt("Refresh")}),z&&t(c,{appearance:"subtle",icon:t(x,{}),onClick:zt,title:xt("Export to Excel"),children:xt("Export to Excel")}),(W||V)&&t(c,{appearance:"primary",icon:t(y,{}),onClick:W?()=>{vt(null),Ct(!0)}:()=>window.open(V,"_blank"),className:jt.addButton,children:xt("Add Record")})]})]}),t("div",{className:jt.searchBox,children:t(I,{placeholder:xt("Sök")+"...",value:J,onChange:Rt,recentSearches:$t,onCommitSearch:Lt,onRemoveRecentSearch:Et})}),!et&&e("div",{className:jt.recordsCount,children:[xt("Total"),": ",Vt," ",xt("records"),Vt!==Z.length&&` (${xt("filtered from")} ${Z.length})`]}),e("div",{className:jt.listContainer,children:[et&&t("p",{children:xt("Söker")+"..."}),!et&&t(M,{data:Ut,columns:St,canEdit:q,onEdit:(t,e)=>{const o=Z[e];o&&(vt({data:{...o.data,_fileIds:o.fileIds??[]},index:e}),Ct(!0))},onDelete:(t,e)=>{const o=Z[e];o&&(Mt({index:e,id:o.id}),Dt(!0))},onToggleCheckbox:async(t,e,o,n)=>{const r=Z[n];if(r){const t=Array.isArray(e)?e[0]:e,i={...r.data,[t]:String(o)},a=await Y.updateDataMessage(r.id,i);tt((t=>{const e=[...t];return e[n]={...r,id:a,data:i},e})),w.clearPattern(F)}},onEnumChange:async(t,e,o,n)=>{const r=Z[n];if(r){const t=Array.isArray(e)?e[0]:e,i={...r.data,[t]:o},a=await Y.updateDataMessage(r.id,i);tt((t=>{const e=[...t];return e[n]={...r,id:a,data:i},e})),w.clearPattern(F)}},onFilteredDataChange:Wt})]}),nt&&!J.trim()&&t("div",{className:jt.loadMoreContainer,children:t(c,{appearance:"primary",size:"medium",onClick:Ot,disabled:it,children:it?xt("Laddar..."):xt("Visa fler")+` (${Z.length} ${xt("laddade")})`})}),t(_,{open:At,onClose:()=>Ct(!1),onSave:async t=>{const e=new Set(St.filter((t=>"thumbnail"===t.type)).map((t=>Array.isArray(t.key)?t.key[0]:t.key)));if(kt){const o=Z[kt.index];if(o){const n={};for(const o in t){if(e.has(o))continue;const r=t[o];n[o]=null==r?"":String(r)}const r=await Y.updateDataMessage(o.id,n);tt((t=>{const e=[...t];return e[kt.index]={...o,id:r,data:n},e})),w.clearPattern(F)}}else{const o={};for(const n in t){if(e.has(n))continue;const r=t[n];o[n]=null==r?"":String(r)}const n={id:await Y.createDataMessage(F,o),data:o,createdAt:(new Date).toISOString(),fileId:null,fileIds:[],tags:[F],mentions:[Y.codicent]};tt((t=>[n,...t])),w.clearPattern(F)}},columns:St,initialData:kt?.data}),t(l,{open:bt,onOpenChange:(t,e)=>!e.open&&qt(),children:t(d,{children:e(m,{children:[t(p,{children:xt("Confirm Delete")}),t(u,{children:xt("Are you sure you want to delete this record? This action cannot be undone.")}),e(g,{children:[t(c,{appearance:"secondary",onClick:qt,children:xt("Cancel")}),t(c,{appearance:"primary",onClick:async()=>{if(It)try{await Y.deleteDataMessage(It.id),tt((t=>t.filter(((t,e)=>e!==It.index)))),w.clearPattern(F)}catch(t){console.error("Failed to delete record:",t)}finally{Dt(!1),Mt(null)}},children:xt("Delete")})]})]})})})]})})};export{B as ListPage,B as default};
package/dist/index.d.ts CHANGED
@@ -2562,6 +2562,55 @@ interface ListPageProps {
2562
2562
  /** Optional data filter applied after fetch. Useful for app-specific filtering (e.g. empty notes). */
2563
2563
  filterData?: (data: DataMessage[], tag: string) => DataMessage[];
2564
2564
  }
2565
+ /**
2566
+ * ListPage — data fetch & cache behaviour
2567
+ *
2568
+ * ## Fetch key guard
2569
+ * A `fetchKey` string (`tag-contentTitle-loadAll-refreshKey-debouncedSearch`) is stored in
2570
+ * `lastFetchKey` (ref). The fetch effect bails out early if the key hasn't changed, preventing
2571
+ * duplicate calls when unrelated state updates re-render the component.
2572
+ * The ref is reset to `""` on effect cleanup (unmount / dep change) so that re-navigation
2573
+ * always triggers a fresh fetch.
2574
+ *
2575
+ * ## Race-condition protection
2576
+ * `activeFetchId` (ref) is incremented each time a new fetch starts. Each in-flight promise
2577
+ * captures its own `myFetchId` and checks `isActive()` before writing state. A superseded
2578
+ * fetch (e.g. from rapid search typing or quick navigation) silently discards its result
2579
+ * without calling `setSearching(false)`, so the spinner stays visible until the winning fetch
2580
+ * completes.
2581
+ *
2582
+ * ## Cache strategy
2583
+ * | Visit | Cache present? | Behaviour |
2584
+ * |-------|---------------|-----------|
2585
+ * | First | No | `searching=true` → API call → populate cache → show list |
2586
+ * | First | Yes | Instant render from cache (no spinner) → background delta-fetch for records newer than the latest cached timestamp → silently prepend new rows |
2587
+ * | Back-navigation | Yes | Unmount cleanup resets fetchKey → same as "First, cache present" |
2588
+ * | Back-navigation | Expired | Same as "First, no cache" |
2589
+ * | Refresh button | Any | Clears cache for tag + resets fetchKey + increments `refreshKey` → full API call |
2590
+ *
2591
+ * ## Search / filter flow
2592
+ * - Typing triggers a 500 ms debounce before `debouncedSearch` updates.
2593
+ * - `debouncedSearch` change → new fetchKey → new `activeFetchId` → previous in-flight
2594
+ * fetch is superseded and its result discarded.
2595
+ * - `filter_<field>=<value>` URL params are applied client-side after fetch (not sent to API)
2596
+ * except for `filter_<tag>=<value>` which seeds `debouncedSearch` for server-side filtering.
2597
+ * - `q=` URL param is kept in sync with the search box via a debounced `setSearchParams`
2598
+ * call that preserves all other existing params (`canAdd`, `canEdit`, `filter_*`, etc.).
2599
+ *
2600
+ * ## URL params (all optional)
2601
+ * | Param | Type | Default | Purpose |
2602
+ * |-------|------|---------|---------|
2603
+ * | `tag` | string | `"logbook"` | Data tag to query |
2604
+ * | `title` | string | `"Loggbok"` | Page heading |
2605
+ * | `contentTitle` | string | — | If set, fetches raw messages and maps this field as the display column |
2606
+ * | `loadAll` | `"true"` | — | Removes PAGE_SIZE cap; disables Load-more |
2607
+ * | `canAdd` | `"true"` | — | Shows Add Record button (opens RecordModal) |
2608
+ * | `canEdit` | `"true"` | — | Shows per-row Edit/Delete buttons |
2609
+ * | `canExport` | `"false"` to hide | visible | Shows Export to Excel button |
2610
+ * | `addUrl` | string | — | External URL opened on Add click (alternative to modal) |
2611
+ * | `q` | string | — | Initial search term; kept in sync with search box |
2612
+ * | `filter_<field>` | string | — | Client-side row filter; `%USERNAME%` resolves to current user |
2613
+ */
2565
2614
  declare const ListPage: ({ state, getColumnDefs, getListConfig, filterData }: ListPageProps) => react_jsx_runtime.JSX.Element;
2566
2615
 
2567
2616
  declare const CrmPage: React__default.FC<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codicent-app-sdk",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "SDK for building AI-powered applications with Codicent",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",