@ynput/ayon-frontend-shared 0.3.49 → 0.3.51

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.
Files changed (108) hide show
  1. package/dist/Slicer.cjs.js +1 -1
  2. package/dist/Slicer.es.js +13 -11
  3. package/dist/Slicer.es.js.map +1 -1
  4. package/dist/_virtual/index.cjs10.js +1 -1
  5. package/dist/_virtual/index.cjs11.js +1 -1
  6. package/dist/_virtual/index.cjs12.js +1 -1
  7. package/dist/_virtual/index.cjs5.js +1 -1
  8. package/dist/_virtual/index.cjs6.js +1 -1
  9. package/dist/_virtual/index.es10.js +3 -3
  10. package/dist/_virtual/index.es11.js +5 -5
  11. package/dist/_virtual/index.es12.js +5 -5
  12. package/dist/_virtual/index.es5.js +5 -2
  13. package/dist/_virtual/index.es5.js.map +1 -1
  14. package/dist/_virtual/index.es6.js +2 -5
  15. package/dist/_virtual/index.es6.js.map +1 -1
  16. package/dist/components.cjs.js +1 -1
  17. package/dist/components.es.js +112 -109
  18. package/dist/components.es.js.map +1 -1
  19. package/dist/node_modules/rehype/node_modules/unified/lib/index.cjs.js +1 -1
  20. package/dist/node_modules/rehype/node_modules/unified/lib/index.es.js +2 -2
  21. package/dist/node_modules/rehype/node_modules/vfile/lib/index.cjs.js +1 -1
  22. package/dist/node_modules/rehype/node_modules/vfile/lib/index.es.js +1 -1
  23. package/dist/node_modules/rehype-parse/lib/index.cjs.js +1 -1
  24. package/dist/node_modules/rehype-parse/lib/index.es.js +1 -1
  25. package/dist/node_modules/remove-accents/index.cjs.js +1 -1
  26. package/dist/node_modules/remove-accents/index.es.js +1 -1
  27. package/dist/shared/node_modules/react-transition-group/esm/CSSTransition.cjs.js +1 -1
  28. package/dist/shared/node_modules/react-transition-group/esm/CSSTransition.es.js +1 -1
  29. package/dist/shared/node_modules/react-transition-group/esm/Transition.cjs.js +1 -1
  30. package/dist/shared/node_modules/react-transition-group/esm/Transition.es.js +1 -1
  31. package/dist/shared/node_modules/react-transition-group/esm/utils/PropTypes.cjs.js +1 -1
  32. package/dist/shared/node_modules/react-transition-group/esm/utils/PropTypes.es.js +1 -1
  33. package/dist/shared/src/api/generated/graphql.cjs.js +1 -1
  34. package/dist/shared/src/api/generated/graphql.cjs.js.map +1 -1
  35. package/dist/shared/src/api/generated/graphql.es.js +1 -1
  36. package/dist/shared/src/api/generated/graphql.es.js.map +1 -1
  37. package/dist/shared/src/api/queries/columnStats/columnStats.cjs.js +1 -1
  38. package/dist/shared/src/api/queries/columnStats/columnStats.cjs.js.map +1 -1
  39. package/dist/shared/src/api/queries/columnStats/columnStats.es.js +56 -44
  40. package/dist/shared/src/api/queries/columnStats/columnStats.es.js.map +1 -1
  41. package/dist/shared/src/api/queries/columnStats/groupCounts.cjs.js +1 -1
  42. package/dist/shared/src/api/queries/columnStats/groupCounts.cjs.js.map +1 -1
  43. package/dist/shared/src/api/queries/columnStats/groupCounts.es.js +10 -8
  44. package/dist/shared/src/api/queries/columnStats/groupCounts.es.js.map +1 -1
  45. package/dist/shared/src/components/LinksManager/LinksManager.cjs.js.map +1 -1
  46. package/dist/shared/src/components/LinksManager/LinksManager.es.js.map +1 -1
  47. package/dist/shared/src/containers/DetailsPanel/DetailsPanel.cjs.js +1 -1
  48. package/dist/shared/src/containers/DetailsPanel/DetailsPanel.cjs.js.map +1 -1
  49. package/dist/shared/src/containers/DetailsPanel/DetailsPanel.es.js +220 -216
  50. package/dist/shared/src/containers/DetailsPanel/DetailsPanel.es.js.map +1 -1
  51. package/dist/shared/src/containers/DetailsPanel/components/DetailsPanelMoreMenu/hooks/useMenuOptions.cjs.js +1 -1
  52. package/dist/shared/src/containers/DetailsPanel/components/DetailsPanelMoreMenu/hooks/useMenuOptions.cjs.js.map +1 -1
  53. package/dist/shared/src/containers/DetailsPanel/components/DetailsPanelMoreMenu/hooks/useMenuOptions.es.js +1 -1
  54. package/dist/shared/src/containers/DetailsPanel/components/DetailsPanelMoreMenu/hooks/useMenuOptions.es.js.map +1 -1
  55. package/dist/shared/src/containers/Feed/components/FeedSearchFilter.cjs.js +2 -2
  56. package/dist/shared/src/containers/Feed/components/FeedSearchFilter.cjs.js.map +1 -1
  57. package/dist/shared/src/containers/Feed/components/FeedSearchFilter.es.js +138 -127
  58. package/dist/shared/src/containers/Feed/components/FeedSearchFilter.es.js.map +1 -1
  59. package/dist/shared/src/containers/Feed/context/FeedContext.cjs.js.map +1 -1
  60. package/dist/shared/src/containers/Feed/context/FeedContext.es.js.map +1 -1
  61. package/dist/shared/src/containers/ProjectTreeTable/ProjectTreeTable.cjs.js +1 -1
  62. package/dist/shared/src/containers/ProjectTreeTable/ProjectTreeTable.cjs.js.map +1 -1
  63. package/dist/shared/src/containers/ProjectTreeTable/ProjectTreeTable.es.js +56 -55
  64. package/dist/shared/src/containers/ProjectTreeTable/ProjectTreeTable.es.js.map +1 -1
  65. package/dist/shared/src/containers/ProjectTreeTable/hooks/useFetchOverviewData.cjs.js +1 -1
  66. package/dist/shared/src/containers/ProjectTreeTable/hooks/useFetchOverviewData.cjs.js.map +1 -1
  67. package/dist/shared/src/containers/ProjectTreeTable/hooks/useFetchOverviewData.es.js +249 -254
  68. package/dist/shared/src/containers/ProjectTreeTable/hooks/useFetchOverviewData.es.js.map +1 -1
  69. package/dist/shared/src/containers/ProjectTreeTable/hooks/useGroupCounts.cjs.js.map +1 -1
  70. package/dist/shared/src/containers/ProjectTreeTable/hooks/useGroupCounts.es.js.map +1 -1
  71. package/dist/shared/src/containers/SimpleTable/SimpleTable.cjs.js +1 -1
  72. package/dist/shared/src/containers/SimpleTable/SimpleTable.cjs.js.map +1 -1
  73. package/dist/shared/src/containers/SimpleTable/SimpleTable.es.js +41 -40
  74. package/dist/shared/src/containers/SimpleTable/SimpleTable.es.js.map +1 -1
  75. package/dist/shared/src/containers/Slicer/components/Slicer.cjs.js +2 -2
  76. package/dist/shared/src/containers/Slicer/components/Slicer.cjs.js.map +1 -1
  77. package/dist/shared/src/containers/Slicer/components/Slicer.es.js +55 -47
  78. package/dist/shared/src/containers/Slicer/components/Slicer.es.js.map +1 -1
  79. package/dist/shared/src/containers/Slicer/hooks/useSlicerCounts.cjs.js +2 -0
  80. package/dist/shared/src/containers/Slicer/hooks/useSlicerCounts.cjs.js.map +1 -0
  81. package/dist/shared/src/containers/Slicer/hooks/useSlicerCounts.es.js +319 -0
  82. package/dist/shared/src/containers/Slicer/hooks/useSlicerCounts.es.js.map +1 -0
  83. package/dist/shared/src/containers/Slicer/hooks/useTableDataBySlice.cjs.js +1 -1
  84. package/dist/shared/src/containers/Slicer/hooks/useTableDataBySlice.cjs.js.map +1 -1
  85. package/dist/shared/src/containers/Slicer/hooks/useTableDataBySlice.es.js +118 -104
  86. package/dist/shared/src/containers/Slicer/hooks/useTableDataBySlice.es.js.map +1 -1
  87. package/dist/shared/src/context/DetailsPanelContext.cjs.js +1 -1
  88. package/dist/shared/src/context/DetailsPanelContext.cjs.js.map +1 -1
  89. package/dist/shared/src/context/DetailsPanelContext.es.js +57 -55
  90. package/dist/shared/src/context/DetailsPanelContext.es.js.map +1 -1
  91. package/dist/shared/src/util/confirmDelete.cjs.js +1 -1
  92. package/dist/shared/src/util/confirmDelete.cjs.js.map +1 -1
  93. package/dist/shared/src/util/confirmDelete.es.js +28 -24
  94. package/dist/shared/src/util/confirmDelete.es.js.map +1 -1
  95. package/dist/types/components/index.d.ts +1 -0
  96. package/dist/types/containers/Feed/components/FeedSearchFilter.d.ts +2 -2
  97. package/dist/types/containers/ProjectTreeTable/hooks/useGroupCounts.d.ts +2 -3
  98. package/dist/types/containers/ProjectTreeTable/types/overviewContext.d.ts +8 -0
  99. package/dist/types/containers/ProjectTreeTable/types/table.d.ts +0 -1
  100. package/dist/types/containers/SimpleTable/SimpleTable.types.d.ts +1 -0
  101. package/dist/types/containers/Slicer/components/Slicer.d.ts +2 -0
  102. package/dist/types/containers/Slicer/hooks/index.d.ts +1 -0
  103. package/dist/types/containers/Slicer/hooks/useSlicerCounts.d.ts +17 -0
  104. package/dist/types/containers/Slicer/hooks/useTableDataBySlice.d.ts +5 -1
  105. package/dist/types/util/confirmDelete.d.ts +3 -1
  106. package/dist/util.cjs.js +1 -1
  107. package/dist/util.es.js +77 -76
  108. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=t=>t.replace(/_([a-z0-9])/g,(e,o)=>o.toUpperCase()),f={task_type:"subType",folder_type:"subType",product_type:"subType"},a=t=>t in f?f[t]:t.startsWith("project_attributes_")?"attrib_"+t.slice(19):t.startsWith("inherited_attributes_")?"attrib_"+t.slice(21):t.startsWith("attrib_")?t:g(t),C=t=>{if(t==null)return;let e=t;if(typeof e=="string")try{e=JSON.parse(e)}catch{return}if(Array.isArray(e)){const o=new Map;for(const n of e){if(!n||n.value==null)continue;const l=Array.isArray(n.value)?n.value:[n.value];for(const r of l){const u=String(r),i=o.get(u);o.set(u,{...n,value:u,count:(i?.count??0)+(Number(n.count)||0)})}}return[...o.values()]}if(typeof e=="object")return Object.entries(e).map(([o,n])=>({value:o,count:Number(n)||0}))},d=(t,e)=>t==null&&e==null?void 0:(t??0)+(e??0),p=(t,e)=>t==null?e??void 0:e==null||t<e?t:e,h=(t,e)=>t==null?e??void 0:e==null||t>e?t:e,m=(t,e)=>t!=null&&e!=null&&t+e>0?Math.round(t/(t+e)*1e4)/100:void 0,y=(t,e)=>{const o=d(t.valueFilledCount,e.valueFilledCount),n=d(t.valueNotFilledCount,e.valueNotFilledCount),l=d(t.checkedCount,e.checkedCount),r=d(t.notCheckedCount,e.notCheckedCount);let u;if(t.avg!=null&&e.avg!=null){const c=t.valueFilledCount,s=e.valueFilledCount;u=c!=null&&s!=null&&c+s>0?(t.avg*c+e.avg*s)/(c+s):(t.avg+e.avg)/2,u=Math.round(u*100)/100}else u=t.avg??e.avg??void 0;let i;if(t.distribution?.length||e.distribution?.length){const c=new Map;for(const s of[...t.distribution??[],...e.distribution??[]]){const v=c.get(s.value);c.set(s.value,v?{...v,count:v.count+s.count}:{...s})}i=[...c.values()]}return{columnName:t.columnName,min:p(t.min,e.min),max:h(t.max,e.max),avg:u,sum:d(t.sum,e.sum),valueFilledCount:o,valueNotFilledCount:n,percentageFilled:m(o,n),percentageNotFilled:m(n,o),checkedCount:l,notCheckedCount:r,checkedPercentage:m(l,r),notCheckedPercentage:m(r,l),distribution:i,primaryCount:t.primaryCount??e.primaryCount,secondaryCount:t.secondaryCount??e.secondaryCount}},F=t=>t.map(e=>({...e,columnName:a(e.columnName),distribution:C(e.distribution)})),N=(t=[],e=[])=>{const o=new Map;for(const n of e)o.set(a(n.columnName),{...n});for(const n of t){const l=a(n.columnName),r={...o.get(l)||{columnName:n.columnName}};for(const[u,i]of Object.entries(n))i!=null&&(r[u]=i);o.set(l,r)}return[...o.values()]},_=t=>t.reduce((e,o)=>Math.max(e,(o.valueFilledCount??0)+(o.valueNotFilledCount??0)),0),S=(t,e=[])=>{const o=new Map;for(const l of t)o.set(a(l.columnName),l);const n=new Map(o);for(const l of e){const r=a(l.columnName),u=o.get(r);n.set(r,u?y(u,l):l)}return[...n.values()]},k=(t,e)=>{const o=t?.message??"Unknown stats error";return console.error(`Error fetching ${e} stats:`,t),o};exports.canonicalColumnId=a;exports.combineFieldStats=S;exports.mergeFieldStats=N;exports.normalizeFieldStats=F;exports.totalRowsFromStats=_;exports.transformStatsError=k;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=t=>t.replace(/_([a-z0-9])/g,(e,o)=>o.toUpperCase()),v={task_type:"subType",folder_type:"subType",product_type:"subType"},a=t=>t in v?v[t]:t.startsWith("project_attributes_")?"attrib_"+t.slice(19):t.startsWith("inherited_attributes_")?"attrib_"+t.slice(21):t.startsWith("attrib_")?t:g(t),p=t=>{if(Array.isArray(t))return t;if(typeof t=="string"){const e=t.trim();if(e.startsWith("[")&&e.endsWith("]"))try{const o=JSON.parse(e);if(Array.isArray(o))return o}catch{}}return[t]},C=t=>{if(t==null)return;let e=t;if(typeof e=="string")try{e=JSON.parse(e)}catch{return}if(Array.isArray(e)){const o=new Map;for(const n of e){if(!n||n.value==null)continue;const r=p(n.value);for(const u of r){const s=String(u),i=o.get(s);o.set(s,{...n,value:s,count:(i?.count??0)+(Number(n.count)||0)})}}return[...o.values()]}if(typeof e=="object")return Object.entries(e).map(([o,n])=>({value:o,count:Number(n)||0}))},d=(t,e)=>t==null&&e==null?void 0:(t??0)+(e??0),h=(t,e)=>t==null?e??void 0:e==null||t<e?t:e,y=(t,e)=>t==null?e??void 0:e==null||t>e?t:e,m=(t,e)=>t!=null&&e!=null&&t+e>0?Math.round(t/(t+e)*1e4)/100:void 0,F=(t,e)=>{const o=d(t.valueFilledCount,e.valueFilledCount),n=d(t.valueNotFilledCount,e.valueNotFilledCount),r=d(t.checkedCount,e.checkedCount),u=d(t.notCheckedCount,e.notCheckedCount);let s;if(t.avg!=null&&e.avg!=null){const c=t.valueFilledCount,l=e.valueFilledCount;s=c!=null&&l!=null&&c+l>0?(t.avg*c+e.avg*l)/(c+l):(t.avg+e.avg)/2,s=Math.round(s*100)/100}else s=t.avg??e.avg??void 0;let i;if(t.distribution?.length||e.distribution?.length){const c=new Map;for(const l of[...t.distribution??[],...e.distribution??[]]){const f=c.get(l.value);c.set(l.value,f?{...f,count:f.count+l.count}:{...l})}i=[...c.values()]}return{columnName:t.columnName,min:h(t.min,e.min),max:y(t.max,e.max),avg:s,sum:d(t.sum,e.sum),valueFilledCount:o,valueNotFilledCount:n,percentageFilled:m(o,n),percentageNotFilled:m(n,o),checkedCount:r,notCheckedCount:u,checkedPercentage:m(r,u),notCheckedPercentage:m(u,r),distribution:i,primaryCount:t.primaryCount??e.primaryCount,secondaryCount:t.secondaryCount??e.secondaryCount}},N=t=>t.map(e=>({...e,columnName:a(e.columnName),distribution:C(e.distribution)})),S=(t=[],e=[])=>{const o=new Map;for(const n of e)o.set(a(n.columnName),{...n});for(const n of t){const r=a(n.columnName),u={...o.get(r)||{columnName:n.columnName}};for(const[s,i]of Object.entries(n))i!=null&&(u[s]=i);o.set(r,u)}return[...o.values()]},_=t=>t.reduce((e,o)=>Math.max(e,(o.valueFilledCount??0)+(o.valueNotFilledCount??0)),0),k=(t,e=[])=>{const o=new Map;for(const r of t)o.set(a(r.columnName),r);const n=new Map(o);for(const r of e){const u=a(r.columnName),s=o.get(u);n.set(u,s?F(s,r):r)}return[...n.values()]},b=(t,e)=>{const o=t?.message??"Unknown stats error";return console.error(`Error fetching ${e} stats:`,t),o};exports.canonicalColumnId=a;exports.combineFieldStats=k;exports.mergeFieldStats=S;exports.normalizeFieldStats=N;exports.totalRowsFromStats=_;exports.transformStatsError=b;
2
2
  //# sourceMappingURL=columnStats.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"columnStats.cjs.js","sources":["../../../../../../src/api/queries/columnStats/columnStats.ts"],"sourcesContent":["import { ColumnStats } from '@shared/api/generated'\n// Shape of the backend GraphQL `ColumnStats` (connection.fieldStats).\n// `sum` and `distribution` are typed ahead of backend support so wiring them is a no-op.\nexport interface FieldStats extends ColumnStats {\n primaryCount?: number | null\n secondaryCount?: number | null\n}\n\nconst snakeToCamel = (s: string): string => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())\n\n// Per-entity \"type\" columns all map to the unified `subType` column in the table.\n// (`product_base_type` is a separate column — productBaseType — so not aliased.)\nconst COLUMN_ALIASES: Record<string, string> = {\n task_type: 'subType',\n folder_type: 'subType',\n product_type: 'subType',\n}\n\n// Canonical frontend column id for a backend column name. Attribute columns keep\n// their `attrib_` form; everything else is snake_case on the backend but camelCase\n// in the table (product_base_type → productBaseType, created_at → createdAt).\nexport const canonicalColumnId = (name: string): string => {\n if (name in COLUMN_ALIASES) return COLUMN_ALIASES[name]\n if (name.startsWith('project_attributes_')) {\n return 'attrib_' + name.slice('project_attributes_'.length)\n }\n if (name.startsWith('inherited_attributes_')) {\n return 'attrib_' + name.slice('inherited_attributes_'.length)\n }\n if (name.startsWith('attrib_')) return name\n return snakeToCamel(name)\n}\n\nconst normalizeDistribution = (raw: unknown): FieldStats['distribution'] => {\n if (raw == null) return undefined\n let value = raw\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value)\n } catch {\n return undefined\n }\n }\n if (Array.isArray(value)) {\n // dedupe by value; array buckets (tags/assignees combos) unnest per value\n const byValue = new Map<string, { value: string; count: number }>()\n for (const d of value) {\n if (!d || d.value == null) continue\n const vals = Array.isArray(d.value) ? d.value : [d.value]\n for (const raw of vals) {\n const v = String(raw)\n const prev = byValue.get(v)\n byValue.set(v, { ...d, value: v, count: (prev?.count ?? 0) + (Number(d.count) || 0) })\n }\n }\n return [...byValue.values()]\n }\n if (typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>).map(([v, count]) => ({\n value: v,\n count: Number(count) || 0,\n }))\n }\n return undefined\n}\n\nconst sumN = (a?: number | null, b?: number | null): number | undefined =>\n a == null && b == null ? undefined : (a ?? 0) + (b ?? 0)\nconst lowest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a < b ? a : b\nconst highest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a > b ? a : b\nconst pct = (part?: number, rest?: number): number | undefined =>\n part != null && rest != null && part + rest > 0\n ? Math.round((part / (part + rest)) * 10000) / 100\n : undefined\n\n// Additive combine of one column's stats from two entity sets (e.g. folder +\n// task). Counts sum, min/max widen, percentages recompute, avg is weighted by\n// filled counts when available.\nconst combineTwo = (a: FieldStats, b: FieldStats): FieldStats => {\n const filled = sumN(a.valueFilledCount, b.valueFilledCount)\n const notFilled = sumN(a.valueNotFilledCount, b.valueNotFilledCount)\n const checked = sumN(a.checkedCount, b.checkedCount)\n const notChecked = sumN(a.notCheckedCount, b.notCheckedCount)\n\n let avg: number | undefined\n if (a.avg != null && b.avg != null) {\n const wa = a.valueFilledCount\n const wb = b.valueFilledCount\n avg =\n wa != null && wb != null && wa + wb > 0\n ? (a.avg * wa + b.avg * wb) / (wa + wb)\n : (a.avg + b.avg) / 2\n avg = Math.round(avg * 100) / 100\n } else {\n avg = a.avg ?? b.avg ?? undefined\n }\n\n let distribution: FieldStats['distribution']\n if (a.distribution?.length || b.distribution?.length) {\n const byValue = new Map<string, NonNullable<FieldStats['distribution']>[number]>()\n for (const d of [...(a.distribution ?? []), ...(b.distribution ?? [])]) {\n const prev = byValue.get(d.value)\n byValue.set(d.value, prev ? { ...prev, count: prev.count + d.count } : { ...d })\n }\n distribution = [...byValue.values()]\n }\n\n return {\n columnName: a.columnName,\n min: lowest(a.min, b.min),\n max: highest(a.max, b.max),\n avg,\n sum: sumN(a.sum, b.sum),\n valueFilledCount: filled,\n valueNotFilledCount: notFilled,\n percentageFilled: pct(filled, notFilled),\n percentageNotFilled: pct(notFilled, filled),\n checkedCount: checked,\n notCheckedCount: notChecked,\n checkedPercentage: pct(checked, notChecked),\n notCheckedPercentage: pct(notChecked, checked),\n distribution,\n primaryCount: a.primaryCount ?? b.primaryCount,\n secondaryCount: a.secondaryCount ?? b.secondaryCount,\n }\n}\n\n// Normalize a raw fieldStats response (apply in transformResponse).\nexport const normalizeFieldStats = (stats: FieldStats[]): FieldStats[] =>\n stats.map((s) => ({\n ...s,\n columnName: canonicalColumnId(s.columnName),\n distribution: normalizeDistribution(s.distribution),\n }))\n\n// Merge stats lists by canonical column id, field-wise: overlay's non-null values\n// win, base fills the gaps. A single list also dedupes its own repeated columns.\nexport const mergeFieldStats = (\n overlay: FieldStats[] = [],\n base: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of base) byId.set(canonicalColumnId(s.columnName), { ...s })\n for (const s of overlay) {\n const id = canonicalColumnId(s.columnName)\n const merged: FieldStats = { ...(byId.get(id) || { columnName: s.columnName }) }\n for (const [k, v] of Object.entries(s)) {\n if (v !== null && v !== undefined) (merged as any)[k] = v\n }\n byId.set(id, merged)\n }\n return [...byId.values()]\n}\n\n// Total row count for an entity set = max(filled + not-filled) across its\n// stat columns. Feeds the main folders/tasks count cell.\nexport const totalRowsFromStats = (stats: FieldStats[]): number =>\n stats.reduce(\n (max, s) => Math.max(max, (s.valueFilledCount ?? 0) + (s.valueNotFilledCount ?? 0)),\n 0,\n )\n\n// Combine primary-entity stats (tasks/versions) with group-entity stats\n// (folders/products) for the \"include groups & folders\" row scope.\nexport const combineFieldStats = (\n primary: FieldStats[],\n group: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of primary) byId.set(canonicalColumnId(s.columnName), s)\n const out = new Map(byId)\n for (const g of group) {\n const id = canonicalColumnId(g.columnName)\n const p = byId.get(id)\n out.set(id, p ? combineTwo(p, g) : g)\n }\n return [...out.values()]\n}\n\nexport const transformStatsError = (error: any, entityType: string): string => {\n const message = error?.message ?? 'Unknown stats error'\n console.error(`Error fetching ${entityType} stats:`, error)\n return message\n}\n"],"names":["snakeToCamel","s","_","c","COLUMN_ALIASES","canonicalColumnId","name","normalizeDistribution","raw","value","byValue","d","vals","v","prev","count","sumN","a","b","lowest","highest","pct","part","rest","combineTwo","filled","notFilled","checked","notChecked","avg","wa","wb","distribution","normalizeFieldStats","stats","mergeFieldStats","overlay","base","byId","id","merged","k","totalRowsFromStats","max","combineFieldStats","primary","group","out","g","p","transformStatsError","error","entityType","message"],"mappings":"gFAQA,MAAMA,EAAgBC,GAAsBA,EAAE,QAAQ,eAAgB,CAACC,EAAGC,IAAMA,EAAE,aAAa,EAIzFC,EAAyC,CAC7C,UAAW,UACX,YAAa,UACb,aAAc,SAChB,EAKaC,EAAqBC,GAC5BA,KAAQF,EAAuBA,EAAeE,CAAI,EAClDA,EAAK,WAAW,qBAAqB,EAChC,UAAYA,EAAK,MAAM,EAA4B,EAExDA,EAAK,WAAW,uBAAuB,EAClC,UAAYA,EAAK,MAAM,EAA8B,EAE1DA,EAAK,WAAW,SAAS,EAAUA,EAChCN,EAAaM,CAAI,EAGpBC,EAAyBC,GAA6C,CAC1E,GAAIA,GAAO,KAAM,OACjB,IAAIC,EAAQD,EACZ,GAAI,OAAOC,GAAU,SACnB,GAAI,CACFA,EAAQ,KAAK,MAAMA,CAAK,CAC1B,MAAQ,CACN,MACF,CAEF,GAAI,MAAM,QAAQA,CAAK,EAAG,CAExB,MAAMC,MAAc,IACpB,UAAWC,KAAKF,EAAO,CACrB,GAAI,CAACE,GAAKA,EAAE,OAAS,KAAM,SAC3B,MAAMC,EAAO,MAAM,QAAQD,EAAE,KAAK,EAAIA,EAAE,MAAQ,CAACA,EAAE,KAAK,EACxD,UAAWH,KAAOI,EAAM,CACtB,MAAMC,EAAI,OAAOL,CAAG,EACdM,EAAOJ,EAAQ,IAAIG,CAAC,EAC1BH,EAAQ,IAAIG,EAAG,CAAE,GAAGF,EAAG,MAAOE,EAAG,OAAQC,GAAM,OAAS,IAAM,OAAOH,EAAE,KAAK,GAAK,GAAI,CACvF,CACF,CACA,MAAO,CAAC,GAAGD,EAAQ,QAAQ,CAC7B,CACA,GAAI,OAAOD,GAAU,SACnB,OAAO,OAAO,QAAQA,CAAgC,EAAE,IAAI,CAAC,CAACI,EAAGE,CAAK,KAAO,CAC3E,MAAOF,EACP,MAAO,OAAOE,CAAK,GAAK,CAAA,EACxB,CAGN,EAEMC,EAAO,CAACC,EAAmBC,IAC/BD,GAAK,MAAQC,GAAK,KAAO,QAAaD,GAAK,IAAMC,GAAK,GAClDC,EAAS,CAAIF,EAAcC,IAC/BD,GAAK,KAAOC,GAAK,OAAYA,GAAK,MAAWD,EAAIC,EAARD,EAAgBC,EACrDE,EAAU,CAAIH,EAAcC,IAChCD,GAAK,KAAOC,GAAK,OAAYA,GAAK,MAAWD,EAAIC,EAARD,EAAgBC,EACrDG,EAAM,CAACC,EAAeC,IAC1BD,GAAQ,MAAQC,GAAQ,MAAQD,EAAOC,EAAO,EAC1C,KAAK,MAAOD,GAAQA,EAAOC,GAAS,GAAK,EAAI,IAC7C,OAKAC,EAAa,CAACP,EAAeC,IAA8B,CAC/D,MAAMO,EAAST,EAAKC,EAAE,iBAAkBC,EAAE,gBAAgB,EACpDQ,EAAYV,EAAKC,EAAE,oBAAqBC,EAAE,mBAAmB,EAC7DS,EAAUX,EAAKC,EAAE,aAAcC,EAAE,YAAY,EAC7CU,EAAaZ,EAAKC,EAAE,gBAAiBC,EAAE,eAAe,EAE5D,IAAIW,EACJ,GAAIZ,EAAE,KAAO,MAAQC,EAAE,KAAO,KAAM,CAClC,MAAMY,EAAKb,EAAE,iBACPc,EAAKb,EAAE,iBACbW,EACEC,GAAM,MAAQC,GAAM,MAAQD,EAAKC,EAAK,GACjCd,EAAE,IAAMa,EAAKZ,EAAE,IAAMa,IAAOD,EAAKC,IACjCd,EAAE,IAAMC,EAAE,KAAO,EACxBW,EAAM,KAAK,MAAMA,EAAM,GAAG,EAAI,GAChC,MACEA,EAAMZ,EAAE,KAAOC,EAAE,KAAO,OAG1B,IAAIc,EACJ,GAAIf,EAAE,cAAc,QAAUC,EAAE,cAAc,OAAQ,CACpD,MAAMR,MAAc,IACpB,UAAWC,IAAK,CAAC,GAAIM,EAAE,cAAgB,CAAA,EAAK,GAAIC,EAAE,cAAgB,CAAA,CAAG,EAAG,CACtE,MAAMJ,EAAOJ,EAAQ,IAAIC,EAAE,KAAK,EAChCD,EAAQ,IAAIC,EAAE,MAAOG,EAAO,CAAE,GAAGA,EAAM,MAAOA,EAAK,MAAQH,EAAE,KAAA,EAAU,CAAE,GAAGA,EAAG,CACjF,CACAqB,EAAe,CAAC,GAAGtB,EAAQ,QAAQ,CACrC,CAEA,MAAO,CACL,WAAYO,EAAE,WACd,IAAKE,EAAOF,EAAE,IAAKC,EAAE,GAAG,EACxB,IAAKE,EAAQH,EAAE,IAAKC,EAAE,GAAG,EACzB,IAAAW,EACA,IAAKb,EAAKC,EAAE,IAAKC,EAAE,GAAG,EACtB,iBAAkBO,EAClB,oBAAqBC,EACrB,iBAAkBL,EAAII,EAAQC,CAAS,EACvC,oBAAqBL,EAAIK,EAAWD,CAAM,EAC1C,aAAcE,EACd,gBAAiBC,EACjB,kBAAmBP,EAAIM,EAASC,CAAU,EAC1C,qBAAsBP,EAAIO,EAAYD,CAAO,EAC7C,aAAAK,EACA,aAAcf,EAAE,cAAgBC,EAAE,aAClC,eAAgBD,EAAE,gBAAkBC,EAAE,cAAA,CAE1C,EAGae,EAAuBC,GAClCA,EAAM,IAAKjC,IAAO,CAChB,GAAGA,EACH,WAAYI,EAAkBJ,EAAE,UAAU,EAC1C,aAAcM,EAAsBN,EAAE,YAAY,CACpD,EAAE,EAISkC,EAAkB,CAC7BC,EAAwB,GACxBC,EAAqB,CAAA,IACJ,CACjB,MAAMC,MAAW,IACjB,UAAWrC,KAAKoC,EAAMC,EAAK,IAAIjC,EAAkBJ,EAAE,UAAU,EAAG,CAAE,GAAGA,CAAA,CAAG,EACxE,UAAWA,KAAKmC,EAAS,CACvB,MAAMG,EAAKlC,EAAkBJ,EAAE,UAAU,EACnCuC,EAAqB,CAAE,GAAIF,EAAK,IAAIC,CAAE,GAAK,CAAE,WAAYtC,EAAE,WAAW,EAC5E,SAAW,CAACwC,EAAG5B,CAAC,IAAK,OAAO,QAAQZ,CAAC,EAC/BY,GAAM,OAA0B2B,EAAeC,CAAC,EAAI5B,GAE1DyB,EAAK,IAAIC,EAAIC,CAAM,CACrB,CACA,MAAO,CAAC,GAAGF,EAAK,QAAQ,CAC1B,EAIaI,EAAsBR,GACjCA,EAAM,OACJ,CAACS,EAAK1C,IAAM,KAAK,IAAI0C,GAAM1C,EAAE,kBAAoB,IAAMA,EAAE,qBAAuB,EAAE,EAClF,CACF,EAIW2C,EAAoB,CAC/BC,EACAC,EAAsB,KACL,CACjB,MAAMR,MAAW,IACjB,UAAWrC,KAAK4C,EAASP,EAAK,IAAIjC,EAAkBJ,EAAE,UAAU,EAAGA,CAAC,EACpE,MAAM8C,EAAM,IAAI,IAAIT,CAAI,EACxB,UAAWU,KAAKF,EAAO,CACrB,MAAMP,EAAKlC,EAAkB2C,EAAE,UAAU,EACnCC,EAAIX,EAAK,IAAIC,CAAE,EACrBQ,EAAI,IAAIR,EAAIU,EAAIzB,EAAWyB,EAAGD,CAAC,EAAIA,CAAC,CACtC,CACA,MAAO,CAAC,GAAGD,EAAI,QAAQ,CACzB,EAEaG,EAAsB,CAACC,EAAYC,IAA+B,CAC7E,MAAMC,EAAUF,GAAO,SAAW,sBAClC,eAAQ,MAAM,kBAAkBC,CAAU,UAAWD,CAAK,EACnDE,CACT"}
1
+ {"version":3,"file":"columnStats.cjs.js","sources":["../../../../../../src/api/queries/columnStats/columnStats.ts"],"sourcesContent":["import { ColumnStats } from '@shared/api/generated'\n// Shape of the backend GraphQL `ColumnStats` (connection.fieldStats).\n// `sum` and `distribution` are typed ahead of backend support so wiring them is a no-op.\nexport interface FieldStats extends ColumnStats {\n primaryCount?: number | null\n secondaryCount?: number | null\n}\n\nconst snakeToCamel = (s: string): string => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())\n\n// Per-entity \"type\" columns all map to the unified `subType` column in the table.\n// (`product_base_type` is a separate column — productBaseType — so not aliased.)\nconst COLUMN_ALIASES: Record<string, string> = {\n task_type: 'subType',\n folder_type: 'subType',\n product_type: 'subType',\n}\n\n// Canonical frontend column id for a backend column name. Attribute columns keep\n// their `attrib_` form; everything else is snake_case on the backend but camelCase\n// in the table (product_base_type → productBaseType, created_at → createdAt).\nexport const canonicalColumnId = (name: string): string => {\n if (name in COLUMN_ALIASES) return COLUMN_ALIASES[name]\n if (name.startsWith('project_attributes_')) {\n return 'attrib_' + name.slice('project_attributes_'.length)\n }\n if (name.startsWith('inherited_attributes_')) {\n return 'attrib_' + name.slice('inherited_attributes_'.length)\n }\n if (name.startsWith('attrib_')) return name\n return snakeToCamel(name)\n}\n\n// list_of_strings attrs arrive as one bucket keyed by the whole array serialized\n// to text (backend GROUP BY on attrib->>'name'); split it so each element counts.\nconst toDistributionValues = (v: unknown): unknown[] => {\n if (Array.isArray(v)) return v\n if (typeof v === 'string') {\n const t = v.trim()\n if (t.startsWith('[') && t.endsWith(']')) {\n try {\n const parsed = JSON.parse(t)\n if (Array.isArray(parsed)) return parsed\n } catch {}\n }\n }\n return [v]\n}\n\nconst normalizeDistribution = (raw: unknown): FieldStats['distribution'] => {\n if (raw == null) return undefined\n let value = raw\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value)\n } catch {\n return undefined\n }\n }\n if (Array.isArray(value)) {\n // dedupe by value; array buckets (tags/assignees combos) unnest per value\n const byValue = new Map<string, { value: string; count: number }>()\n for (const d of value) {\n if (!d || d.value == null) continue\n const vals = toDistributionValues(d.value)\n for (const raw of vals) {\n const v = String(raw)\n const prev = byValue.get(v)\n byValue.set(v, { ...d, value: v, count: (prev?.count ?? 0) + (Number(d.count) || 0) })\n }\n }\n return [...byValue.values()]\n }\n if (typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>).map(([v, count]) => ({\n value: v,\n count: Number(count) || 0,\n }))\n }\n return undefined\n}\n\nconst sumN = (a?: number | null, b?: number | null): number | undefined =>\n a == null && b == null ? undefined : (a ?? 0) + (b ?? 0)\nconst lowest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a < b ? a : b\nconst highest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a > b ? a : b\nconst pct = (part?: number, rest?: number): number | undefined =>\n part != null && rest != null && part + rest > 0\n ? Math.round((part / (part + rest)) * 10000) / 100\n : undefined\n\n// Additive combine of one column's stats from two entity sets (e.g. folder +\n// task). Counts sum, min/max widen, percentages recompute, avg is weighted by\n// filled counts when available.\nconst combineTwo = (a: FieldStats, b: FieldStats): FieldStats => {\n const filled = sumN(a.valueFilledCount, b.valueFilledCount)\n const notFilled = sumN(a.valueNotFilledCount, b.valueNotFilledCount)\n const checked = sumN(a.checkedCount, b.checkedCount)\n const notChecked = sumN(a.notCheckedCount, b.notCheckedCount)\n\n let avg: number | undefined\n if (a.avg != null && b.avg != null) {\n const wa = a.valueFilledCount\n const wb = b.valueFilledCount\n avg =\n wa != null && wb != null && wa + wb > 0\n ? (a.avg * wa + b.avg * wb) / (wa + wb)\n : (a.avg + b.avg) / 2\n avg = Math.round(avg * 100) / 100\n } else {\n avg = a.avg ?? b.avg ?? undefined\n }\n\n let distribution: FieldStats['distribution']\n if (a.distribution?.length || b.distribution?.length) {\n const byValue = new Map<string, NonNullable<FieldStats['distribution']>[number]>()\n for (const d of [...(a.distribution ?? []), ...(b.distribution ?? [])]) {\n const prev = byValue.get(d.value)\n byValue.set(d.value, prev ? { ...prev, count: prev.count + d.count } : { ...d })\n }\n distribution = [...byValue.values()]\n }\n\n return {\n columnName: a.columnName,\n min: lowest(a.min, b.min),\n max: highest(a.max, b.max),\n avg,\n sum: sumN(a.sum, b.sum),\n valueFilledCount: filled,\n valueNotFilledCount: notFilled,\n percentageFilled: pct(filled, notFilled),\n percentageNotFilled: pct(notFilled, filled),\n checkedCount: checked,\n notCheckedCount: notChecked,\n checkedPercentage: pct(checked, notChecked),\n notCheckedPercentage: pct(notChecked, checked),\n distribution,\n primaryCount: a.primaryCount ?? b.primaryCount,\n secondaryCount: a.secondaryCount ?? b.secondaryCount,\n }\n}\n\n// Normalize a raw fieldStats response (apply in transformResponse).\nexport const normalizeFieldStats = (stats: FieldStats[]): FieldStats[] =>\n stats.map((s) => ({\n ...s,\n columnName: canonicalColumnId(s.columnName),\n distribution: normalizeDistribution(s.distribution),\n }))\n\n// Merge stats lists by canonical column id, field-wise: overlay's non-null values\n// win, base fills the gaps. A single list also dedupes its own repeated columns.\nexport const mergeFieldStats = (\n overlay: FieldStats[] = [],\n base: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of base) byId.set(canonicalColumnId(s.columnName), { ...s })\n for (const s of overlay) {\n const id = canonicalColumnId(s.columnName)\n const merged: FieldStats = { ...(byId.get(id) || { columnName: s.columnName }) }\n for (const [k, v] of Object.entries(s)) {\n if (v !== null && v !== undefined) (merged as any)[k] = v\n }\n byId.set(id, merged)\n }\n return [...byId.values()]\n}\n\n// Total row count for an entity set = max(filled + not-filled) across its\n// stat columns. Feeds the main folders/tasks count cell.\nexport const totalRowsFromStats = (stats: FieldStats[]): number =>\n stats.reduce(\n (max, s) => Math.max(max, (s.valueFilledCount ?? 0) + (s.valueNotFilledCount ?? 0)),\n 0,\n )\n\n// Combine primary-entity stats (tasks/versions) with group-entity stats\n// (folders/products) for the \"include groups & folders\" row scope.\nexport const combineFieldStats = (\n primary: FieldStats[],\n group: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of primary) byId.set(canonicalColumnId(s.columnName), s)\n const out = new Map(byId)\n for (const g of group) {\n const id = canonicalColumnId(g.columnName)\n const p = byId.get(id)\n out.set(id, p ? combineTwo(p, g) : g)\n }\n return [...out.values()]\n}\n\nexport const transformStatsError = (error: any, entityType: string): string => {\n const message = error?.message ?? 'Unknown stats error'\n console.error(`Error fetching ${entityType} stats:`, error)\n return message\n}\n"],"names":["snakeToCamel","s","_","c","COLUMN_ALIASES","canonicalColumnId","name","toDistributionValues","v","t","parsed","normalizeDistribution","raw","value","byValue","d","vals","prev","count","sumN","a","b","lowest","highest","pct","part","rest","combineTwo","filled","notFilled","checked","notChecked","avg","wa","wb","distribution","normalizeFieldStats","stats","mergeFieldStats","overlay","base","byId","id","merged","k","totalRowsFromStats","max","combineFieldStats","primary","group","out","g","p","transformStatsError","error","entityType","message"],"mappings":"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"}
@@ -2,7 +2,19 @@ const g = (t) => t.replace(/_([a-z0-9])/g, (e, n) => n.toUpperCase()), f = {
2
2
  task_type: "subType",
3
3
  folder_type: "subType",
4
4
  product_type: "subType"
5
- }, d = (t) => t in f ? f[t] : t.startsWith("project_attributes_") ? "attrib_" + t.slice(19) : t.startsWith("inherited_attributes_") ? "attrib_" + t.slice(21) : t.startsWith("attrib_") ? t : g(t), C = (t) => {
5
+ }, d = (t) => t in f ? f[t] : t.startsWith("project_attributes_") ? "attrib_" + t.slice(19) : t.startsWith("inherited_attributes_") ? "attrib_" + t.slice(21) : t.startsWith("attrib_") ? t : g(t), p = (t) => {
6
+ if (Array.isArray(t)) return t;
7
+ if (typeof t == "string") {
8
+ const e = t.trim();
9
+ if (e.startsWith("[") && e.endsWith("]"))
10
+ try {
11
+ const n = JSON.parse(e);
12
+ if (Array.isArray(n)) return n;
13
+ } catch {
14
+ }
15
+ }
16
+ return [t];
17
+ }, C = (t) => {
6
18
  if (t == null) return;
7
19
  let e = t;
8
20
  if (typeof e == "string")
@@ -15,10 +27,10 @@ const g = (t) => t.replace(/_([a-z0-9])/g, (e, n) => n.toUpperCase()), f = {
15
27
  const n = /* @__PURE__ */ new Map();
16
28
  for (const o of e) {
17
29
  if (!o || o.value == null) continue;
18
- const u = Array.isArray(o.value) ? o.value : [o.value];
19
- for (const s of u) {
20
- const l = String(s), c = n.get(l);
21
- n.set(l, { ...o, value: l, count: (c?.count ?? 0) + (Number(o.count) || 0) });
30
+ const r = p(o.value);
31
+ for (const s of r) {
32
+ const u = String(s), i = n.get(u);
33
+ n.set(u, { ...o, value: u, count: (i?.count ?? 0) + (Number(o.count) || 0) });
22
34
  }
23
35
  }
24
36
  return [...n.values()];
@@ -28,77 +40,77 @@ const g = (t) => t.replace(/_([a-z0-9])/g, (e, n) => n.toUpperCase()), f = {
28
40
  value: n,
29
41
  count: Number(o) || 0
30
42
  }));
31
- }, a = (t, e) => t == null && e == null ? void 0 : (t ?? 0) + (e ?? 0), p = (t, e) => t == null ? e ?? void 0 : e == null || t < e ? t : e, h = (t, e) => t == null ? e ?? void 0 : e == null || t > e ? t : e, v = (t, e) => t != null && e != null && t + e > 0 ? Math.round(t / (t + e) * 1e4) / 100 : void 0, y = (t, e) => {
32
- const n = a(t.valueFilledCount, e.valueFilledCount), o = a(t.valueNotFilledCount, e.valueNotFilledCount), u = a(t.checkedCount, e.checkedCount), s = a(t.notCheckedCount, e.notCheckedCount);
33
- let l;
43
+ }, a = (t, e) => t == null && e == null ? void 0 : (t ?? 0) + (e ?? 0), h = (t, e) => t == null ? e ?? void 0 : e == null || t < e ? t : e, y = (t, e) => t == null ? e ?? void 0 : e == null || t > e ? t : e, m = (t, e) => t != null && e != null && t + e > 0 ? Math.round(t / (t + e) * 1e4) / 100 : void 0, N = (t, e) => {
44
+ const n = a(t.valueFilledCount, e.valueFilledCount), o = a(t.valueNotFilledCount, e.valueNotFilledCount), r = a(t.checkedCount, e.checkedCount), s = a(t.notCheckedCount, e.notCheckedCount);
45
+ let u;
34
46
  if (t.avg != null && e.avg != null) {
35
- const i = t.valueFilledCount, r = e.valueFilledCount;
36
- l = i != null && r != null && i + r > 0 ? (t.avg * i + e.avg * r) / (i + r) : (t.avg + e.avg) / 2, l = Math.round(l * 100) / 100;
47
+ const c = t.valueFilledCount, l = e.valueFilledCount;
48
+ u = c != null && l != null && c + l > 0 ? (t.avg * c + e.avg * l) / (c + l) : (t.avg + e.avg) / 2, u = Math.round(u * 100) / 100;
37
49
  } else
38
- l = t.avg ?? e.avg ?? void 0;
39
- let c;
50
+ u = t.avg ?? e.avg ?? void 0;
51
+ let i;
40
52
  if (t.distribution?.length || e.distribution?.length) {
41
- const i = /* @__PURE__ */ new Map();
42
- for (const r of [...t.distribution ?? [], ...e.distribution ?? []]) {
43
- const m = i.get(r.value);
44
- i.set(r.value, m ? { ...m, count: m.count + r.count } : { ...r });
53
+ const c = /* @__PURE__ */ new Map();
54
+ for (const l of [...t.distribution ?? [], ...e.distribution ?? []]) {
55
+ const v = c.get(l.value);
56
+ c.set(l.value, v ? { ...v, count: v.count + l.count } : { ...l });
45
57
  }
46
- c = [...i.values()];
58
+ i = [...c.values()];
47
59
  }
48
60
  return {
49
61
  columnName: t.columnName,
50
- min: p(t.min, e.min),
51
- max: h(t.max, e.max),
52
- avg: l,
62
+ min: h(t.min, e.min),
63
+ max: y(t.max, e.max),
64
+ avg: u,
53
65
  sum: a(t.sum, e.sum),
54
66
  valueFilledCount: n,
55
67
  valueNotFilledCount: o,
56
- percentageFilled: v(n, o),
57
- percentageNotFilled: v(o, n),
58
- checkedCount: u,
68
+ percentageFilled: m(n, o),
69
+ percentageNotFilled: m(o, n),
70
+ checkedCount: r,
59
71
  notCheckedCount: s,
60
- checkedPercentage: v(u, s),
61
- notCheckedPercentage: v(s, u),
62
- distribution: c,
72
+ checkedPercentage: m(r, s),
73
+ notCheckedPercentage: m(s, r),
74
+ distribution: i,
63
75
  primaryCount: t.primaryCount ?? e.primaryCount,
64
76
  secondaryCount: t.secondaryCount ?? e.secondaryCount
65
77
  };
66
- }, N = (t) => t.map((e) => ({
78
+ }, F = (t) => t.map((e) => ({
67
79
  ...e,
68
80
  columnName: d(e.columnName),
69
81
  distribution: C(e.distribution)
70
- })), F = (t = [], e = []) => {
82
+ })), _ = (t = [], e = []) => {
71
83
  const n = /* @__PURE__ */ new Map();
72
84
  for (const o of e) n.set(d(o.columnName), { ...o });
73
85
  for (const o of t) {
74
- const u = d(o.columnName), s = { ...n.get(u) || { columnName: o.columnName } };
75
- for (const [l, c] of Object.entries(o))
76
- c != null && (s[l] = c);
77
- n.set(u, s);
86
+ const r = d(o.columnName), s = { ...n.get(r) || { columnName: o.columnName } };
87
+ for (const [u, i] of Object.entries(o))
88
+ i != null && (s[u] = i);
89
+ n.set(r, s);
78
90
  }
79
91
  return [...n.values()];
80
- }, _ = (t) => t.reduce(
92
+ }, k = (t) => t.reduce(
81
93
  (e, n) => Math.max(e, (n.valueFilledCount ?? 0) + (n.valueNotFilledCount ?? 0)),
82
94
  0
83
- ), k = (t, e = []) => {
95
+ ), w = (t, e = []) => {
84
96
  const n = /* @__PURE__ */ new Map();
85
- for (const u of t) n.set(d(u.columnName), u);
97
+ for (const r of t) n.set(d(r.columnName), r);
86
98
  const o = new Map(n);
87
- for (const u of e) {
88
- const s = d(u.columnName), l = n.get(s);
89
- o.set(s, l ? y(l, u) : u);
99
+ for (const r of e) {
100
+ const s = d(r.columnName), u = n.get(s);
101
+ o.set(s, u ? N(u, r) : r);
90
102
  }
91
103
  return [...o.values()];
92
- }, w = (t, e) => {
104
+ }, S = (t, e) => {
93
105
  const n = t?.message ?? "Unknown stats error";
94
106
  return console.error(`Error fetching ${e} stats:`, t), n;
95
107
  };
96
108
  export {
97
109
  d as canonicalColumnId,
98
- k as combineFieldStats,
99
- F as mergeFieldStats,
100
- N as normalizeFieldStats,
101
- _ as totalRowsFromStats,
102
- w as transformStatsError
110
+ w as combineFieldStats,
111
+ _ as mergeFieldStats,
112
+ F as normalizeFieldStats,
113
+ k as totalRowsFromStats,
114
+ S as transformStatsError
103
115
  };
104
116
  //# sourceMappingURL=columnStats.es.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"columnStats.es.js","sources":["../../../../../../src/api/queries/columnStats/columnStats.ts"],"sourcesContent":["import { ColumnStats } from '@shared/api/generated'\n// Shape of the backend GraphQL `ColumnStats` (connection.fieldStats).\n// `sum` and `distribution` are typed ahead of backend support so wiring them is a no-op.\nexport interface FieldStats extends ColumnStats {\n primaryCount?: number | null\n secondaryCount?: number | null\n}\n\nconst snakeToCamel = (s: string): string => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())\n\n// Per-entity \"type\" columns all map to the unified `subType` column in the table.\n// (`product_base_type` is a separate column — productBaseType — so not aliased.)\nconst COLUMN_ALIASES: Record<string, string> = {\n task_type: 'subType',\n folder_type: 'subType',\n product_type: 'subType',\n}\n\n// Canonical frontend column id for a backend column name. Attribute columns keep\n// their `attrib_` form; everything else is snake_case on the backend but camelCase\n// in the table (product_base_type → productBaseType, created_at → createdAt).\nexport const canonicalColumnId = (name: string): string => {\n if (name in COLUMN_ALIASES) return COLUMN_ALIASES[name]\n if (name.startsWith('project_attributes_')) {\n return 'attrib_' + name.slice('project_attributes_'.length)\n }\n if (name.startsWith('inherited_attributes_')) {\n return 'attrib_' + name.slice('inherited_attributes_'.length)\n }\n if (name.startsWith('attrib_')) return name\n return snakeToCamel(name)\n}\n\nconst normalizeDistribution = (raw: unknown): FieldStats['distribution'] => {\n if (raw == null) return undefined\n let value = raw\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value)\n } catch {\n return undefined\n }\n }\n if (Array.isArray(value)) {\n // dedupe by value; array buckets (tags/assignees combos) unnest per value\n const byValue = new Map<string, { value: string; count: number }>()\n for (const d of value) {\n if (!d || d.value == null) continue\n const vals = Array.isArray(d.value) ? d.value : [d.value]\n for (const raw of vals) {\n const v = String(raw)\n const prev = byValue.get(v)\n byValue.set(v, { ...d, value: v, count: (prev?.count ?? 0) + (Number(d.count) || 0) })\n }\n }\n return [...byValue.values()]\n }\n if (typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>).map(([v, count]) => ({\n value: v,\n count: Number(count) || 0,\n }))\n }\n return undefined\n}\n\nconst sumN = (a?: number | null, b?: number | null): number | undefined =>\n a == null && b == null ? undefined : (a ?? 0) + (b ?? 0)\nconst lowest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a < b ? a : b\nconst highest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a > b ? a : b\nconst pct = (part?: number, rest?: number): number | undefined =>\n part != null && rest != null && part + rest > 0\n ? Math.round((part / (part + rest)) * 10000) / 100\n : undefined\n\n// Additive combine of one column's stats from two entity sets (e.g. folder +\n// task). Counts sum, min/max widen, percentages recompute, avg is weighted by\n// filled counts when available.\nconst combineTwo = (a: FieldStats, b: FieldStats): FieldStats => {\n const filled = sumN(a.valueFilledCount, b.valueFilledCount)\n const notFilled = sumN(a.valueNotFilledCount, b.valueNotFilledCount)\n const checked = sumN(a.checkedCount, b.checkedCount)\n const notChecked = sumN(a.notCheckedCount, b.notCheckedCount)\n\n let avg: number | undefined\n if (a.avg != null && b.avg != null) {\n const wa = a.valueFilledCount\n const wb = b.valueFilledCount\n avg =\n wa != null && wb != null && wa + wb > 0\n ? (a.avg * wa + b.avg * wb) / (wa + wb)\n : (a.avg + b.avg) / 2\n avg = Math.round(avg * 100) / 100\n } else {\n avg = a.avg ?? b.avg ?? undefined\n }\n\n let distribution: FieldStats['distribution']\n if (a.distribution?.length || b.distribution?.length) {\n const byValue = new Map<string, NonNullable<FieldStats['distribution']>[number]>()\n for (const d of [...(a.distribution ?? []), ...(b.distribution ?? [])]) {\n const prev = byValue.get(d.value)\n byValue.set(d.value, prev ? { ...prev, count: prev.count + d.count } : { ...d })\n }\n distribution = [...byValue.values()]\n }\n\n return {\n columnName: a.columnName,\n min: lowest(a.min, b.min),\n max: highest(a.max, b.max),\n avg,\n sum: sumN(a.sum, b.sum),\n valueFilledCount: filled,\n valueNotFilledCount: notFilled,\n percentageFilled: pct(filled, notFilled),\n percentageNotFilled: pct(notFilled, filled),\n checkedCount: checked,\n notCheckedCount: notChecked,\n checkedPercentage: pct(checked, notChecked),\n notCheckedPercentage: pct(notChecked, checked),\n distribution,\n primaryCount: a.primaryCount ?? b.primaryCount,\n secondaryCount: a.secondaryCount ?? b.secondaryCount,\n }\n}\n\n// Normalize a raw fieldStats response (apply in transformResponse).\nexport const normalizeFieldStats = (stats: FieldStats[]): FieldStats[] =>\n stats.map((s) => ({\n ...s,\n columnName: canonicalColumnId(s.columnName),\n distribution: normalizeDistribution(s.distribution),\n }))\n\n// Merge stats lists by canonical column id, field-wise: overlay's non-null values\n// win, base fills the gaps. A single list also dedupes its own repeated columns.\nexport const mergeFieldStats = (\n overlay: FieldStats[] = [],\n base: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of base) byId.set(canonicalColumnId(s.columnName), { ...s })\n for (const s of overlay) {\n const id = canonicalColumnId(s.columnName)\n const merged: FieldStats = { ...(byId.get(id) || { columnName: s.columnName }) }\n for (const [k, v] of Object.entries(s)) {\n if (v !== null && v !== undefined) (merged as any)[k] = v\n }\n byId.set(id, merged)\n }\n return [...byId.values()]\n}\n\n// Total row count for an entity set = max(filled + not-filled) across its\n// stat columns. Feeds the main folders/tasks count cell.\nexport const totalRowsFromStats = (stats: FieldStats[]): number =>\n stats.reduce(\n (max, s) => Math.max(max, (s.valueFilledCount ?? 0) + (s.valueNotFilledCount ?? 0)),\n 0,\n )\n\n// Combine primary-entity stats (tasks/versions) with group-entity stats\n// (folders/products) for the \"include groups & folders\" row scope.\nexport const combineFieldStats = (\n primary: FieldStats[],\n group: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of primary) byId.set(canonicalColumnId(s.columnName), s)\n const out = new Map(byId)\n for (const g of group) {\n const id = canonicalColumnId(g.columnName)\n const p = byId.get(id)\n out.set(id, p ? combineTwo(p, g) : g)\n }\n return [...out.values()]\n}\n\nexport const transformStatsError = (error: any, entityType: string): string => {\n const message = error?.message ?? 'Unknown stats error'\n console.error(`Error fetching ${entityType} stats:`, error)\n return message\n}\n"],"names":["snakeToCamel","s","_","c","COLUMN_ALIASES","canonicalColumnId","name","normalizeDistribution","raw","value","byValue","d","vals","v","prev","count","sumN","a","b","lowest","highest","pct","part","rest","combineTwo","filled","notFilled","checked","notChecked","avg","wa","wb","distribution","normalizeFieldStats","stats","mergeFieldStats","overlay","base","byId","id","merged","k","totalRowsFromStats","max","combineFieldStats","primary","group","out","g","p","transformStatsError","error","entityType","message"],"mappings":"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"}
1
+ {"version":3,"file":"columnStats.es.js","sources":["../../../../../../src/api/queries/columnStats/columnStats.ts"],"sourcesContent":["import { ColumnStats } from '@shared/api/generated'\n// Shape of the backend GraphQL `ColumnStats` (connection.fieldStats).\n// `sum` and `distribution` are typed ahead of backend support so wiring them is a no-op.\nexport interface FieldStats extends ColumnStats {\n primaryCount?: number | null\n secondaryCount?: number | null\n}\n\nconst snakeToCamel = (s: string): string => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase())\n\n// Per-entity \"type\" columns all map to the unified `subType` column in the table.\n// (`product_base_type` is a separate column — productBaseType — so not aliased.)\nconst COLUMN_ALIASES: Record<string, string> = {\n task_type: 'subType',\n folder_type: 'subType',\n product_type: 'subType',\n}\n\n// Canonical frontend column id for a backend column name. Attribute columns keep\n// their `attrib_` form; everything else is snake_case on the backend but camelCase\n// in the table (product_base_type → productBaseType, created_at → createdAt).\nexport const canonicalColumnId = (name: string): string => {\n if (name in COLUMN_ALIASES) return COLUMN_ALIASES[name]\n if (name.startsWith('project_attributes_')) {\n return 'attrib_' + name.slice('project_attributes_'.length)\n }\n if (name.startsWith('inherited_attributes_')) {\n return 'attrib_' + name.slice('inherited_attributes_'.length)\n }\n if (name.startsWith('attrib_')) return name\n return snakeToCamel(name)\n}\n\n// list_of_strings attrs arrive as one bucket keyed by the whole array serialized\n// to text (backend GROUP BY on attrib->>'name'); split it so each element counts.\nconst toDistributionValues = (v: unknown): unknown[] => {\n if (Array.isArray(v)) return v\n if (typeof v === 'string') {\n const t = v.trim()\n if (t.startsWith('[') && t.endsWith(']')) {\n try {\n const parsed = JSON.parse(t)\n if (Array.isArray(parsed)) return parsed\n } catch {}\n }\n }\n return [v]\n}\n\nconst normalizeDistribution = (raw: unknown): FieldStats['distribution'] => {\n if (raw == null) return undefined\n let value = raw\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value)\n } catch {\n return undefined\n }\n }\n if (Array.isArray(value)) {\n // dedupe by value; array buckets (tags/assignees combos) unnest per value\n const byValue = new Map<string, { value: string; count: number }>()\n for (const d of value) {\n if (!d || d.value == null) continue\n const vals = toDistributionValues(d.value)\n for (const raw of vals) {\n const v = String(raw)\n const prev = byValue.get(v)\n byValue.set(v, { ...d, value: v, count: (prev?.count ?? 0) + (Number(d.count) || 0) })\n }\n }\n return [...byValue.values()]\n }\n if (typeof value === 'object') {\n return Object.entries(value as Record<string, unknown>).map(([v, count]) => ({\n value: v,\n count: Number(count) || 0,\n }))\n }\n return undefined\n}\n\nconst sumN = (a?: number | null, b?: number | null): number | undefined =>\n a == null && b == null ? undefined : (a ?? 0) + (b ?? 0)\nconst lowest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a < b ? a : b\nconst highest = <T>(a?: T | null, b?: T | null): T | undefined =>\n a == null ? b ?? undefined : b == null ? a : a > b ? a : b\nconst pct = (part?: number, rest?: number): number | undefined =>\n part != null && rest != null && part + rest > 0\n ? Math.round((part / (part + rest)) * 10000) / 100\n : undefined\n\n// Additive combine of one column's stats from two entity sets (e.g. folder +\n// task). Counts sum, min/max widen, percentages recompute, avg is weighted by\n// filled counts when available.\nconst combineTwo = (a: FieldStats, b: FieldStats): FieldStats => {\n const filled = sumN(a.valueFilledCount, b.valueFilledCount)\n const notFilled = sumN(a.valueNotFilledCount, b.valueNotFilledCount)\n const checked = sumN(a.checkedCount, b.checkedCount)\n const notChecked = sumN(a.notCheckedCount, b.notCheckedCount)\n\n let avg: number | undefined\n if (a.avg != null && b.avg != null) {\n const wa = a.valueFilledCount\n const wb = b.valueFilledCount\n avg =\n wa != null && wb != null && wa + wb > 0\n ? (a.avg * wa + b.avg * wb) / (wa + wb)\n : (a.avg + b.avg) / 2\n avg = Math.round(avg * 100) / 100\n } else {\n avg = a.avg ?? b.avg ?? undefined\n }\n\n let distribution: FieldStats['distribution']\n if (a.distribution?.length || b.distribution?.length) {\n const byValue = new Map<string, NonNullable<FieldStats['distribution']>[number]>()\n for (const d of [...(a.distribution ?? []), ...(b.distribution ?? [])]) {\n const prev = byValue.get(d.value)\n byValue.set(d.value, prev ? { ...prev, count: prev.count + d.count } : { ...d })\n }\n distribution = [...byValue.values()]\n }\n\n return {\n columnName: a.columnName,\n min: lowest(a.min, b.min),\n max: highest(a.max, b.max),\n avg,\n sum: sumN(a.sum, b.sum),\n valueFilledCount: filled,\n valueNotFilledCount: notFilled,\n percentageFilled: pct(filled, notFilled),\n percentageNotFilled: pct(notFilled, filled),\n checkedCount: checked,\n notCheckedCount: notChecked,\n checkedPercentage: pct(checked, notChecked),\n notCheckedPercentage: pct(notChecked, checked),\n distribution,\n primaryCount: a.primaryCount ?? b.primaryCount,\n secondaryCount: a.secondaryCount ?? b.secondaryCount,\n }\n}\n\n// Normalize a raw fieldStats response (apply in transformResponse).\nexport const normalizeFieldStats = (stats: FieldStats[]): FieldStats[] =>\n stats.map((s) => ({\n ...s,\n columnName: canonicalColumnId(s.columnName),\n distribution: normalizeDistribution(s.distribution),\n }))\n\n// Merge stats lists by canonical column id, field-wise: overlay's non-null values\n// win, base fills the gaps. A single list also dedupes its own repeated columns.\nexport const mergeFieldStats = (\n overlay: FieldStats[] = [],\n base: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of base) byId.set(canonicalColumnId(s.columnName), { ...s })\n for (const s of overlay) {\n const id = canonicalColumnId(s.columnName)\n const merged: FieldStats = { ...(byId.get(id) || { columnName: s.columnName }) }\n for (const [k, v] of Object.entries(s)) {\n if (v !== null && v !== undefined) (merged as any)[k] = v\n }\n byId.set(id, merged)\n }\n return [...byId.values()]\n}\n\n// Total row count for an entity set = max(filled + not-filled) across its\n// stat columns. Feeds the main folders/tasks count cell.\nexport const totalRowsFromStats = (stats: FieldStats[]): number =>\n stats.reduce(\n (max, s) => Math.max(max, (s.valueFilledCount ?? 0) + (s.valueNotFilledCount ?? 0)),\n 0,\n )\n\n// Combine primary-entity stats (tasks/versions) with group-entity stats\n// (folders/products) for the \"include groups & folders\" row scope.\nexport const combineFieldStats = (\n primary: FieldStats[],\n group: FieldStats[] = [],\n): FieldStats[] => {\n const byId = new Map<string, FieldStats>()\n for (const s of primary) byId.set(canonicalColumnId(s.columnName), s)\n const out = new Map(byId)\n for (const g of group) {\n const id = canonicalColumnId(g.columnName)\n const p = byId.get(id)\n out.set(id, p ? combineTwo(p, g) : g)\n }\n return [...out.values()]\n}\n\nexport const transformStatsError = (error: any, entityType: string): string => {\n const message = error?.message ?? 'Unknown stats error'\n console.error(`Error fetching ${entityType} stats:`, error)\n return message\n}\n"],"names":["snakeToCamel","s","_","c","COLUMN_ALIASES","canonicalColumnId","name","toDistributionValues","v","t","parsed","normalizeDistribution","raw","value","byValue","d","vals","prev","count","sumN","a","b","lowest","highest","pct","part","rest","combineTwo","filled","notFilled","checked","notChecked","avg","wa","wb","distribution","normalizeFieldStats","stats","mergeFieldStats","overlay","base","byId","id","merged","k","totalRowsFromStats","max","combineFieldStats","primary","group","out","g","p","transformStatsError","error","entityType","message"],"mappings":"AAQA,MAAMA,IAAe,CAACC,MAAsBA,EAAE,QAAQ,gBAAgB,CAACC,GAAGC,MAAMA,EAAE,aAAa,GAIzFC,IAAyC;AAAA,EAC7C,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAChB,GAKaC,IAAoB,CAACC,MAC5BA,KAAQF,IAAuBA,EAAeE,CAAI,IAClDA,EAAK,WAAW,qBAAqB,IAChC,YAAYA,EAAK,MAAM,EAA4B,IAExDA,EAAK,WAAW,uBAAuB,IAClC,YAAYA,EAAK,MAAM,EAA8B,IAE1DA,EAAK,WAAW,SAAS,IAAUA,IAChCN,EAAaM,CAAI,GAKpBC,IAAuB,CAACC,MAA0B;AACtD,MAAI,MAAM,QAAQA,CAAC,EAAG,QAAOA;AAC7B,MAAI,OAAOA,KAAM,UAAU;AACzB,UAAMC,IAAID,EAAE,KAAA;AACZ,QAAIC,EAAE,WAAW,GAAG,KAAKA,EAAE,SAAS,GAAG;AACrC,UAAI;AACF,cAAMC,IAAS,KAAK,MAAMD,CAAC;AAC3B,YAAI,MAAM,QAAQC,CAAM,EAAG,QAAOA;AAAA,MACpC,QAAQ;AAAA,MAAC;AAAA,EAEb;AACA,SAAO,CAACF,CAAC;AACX,GAEMG,IAAwB,CAACC,MAA6C;AAC1E,MAAIA,KAAO,KAAM;AACjB,MAAIC,IAAQD;AACZ,MAAI,OAAOC,KAAU;AACnB,QAAI;AACF,MAAAA,IAAQ,KAAK,MAAMA,CAAK;AAAA,IAC1B,QAAQ;AACN;AAAA,IACF;AAEF,MAAI,MAAM,QAAQA,CAAK,GAAG;AAExB,UAAMC,wBAAc,IAAA;AACpB,eAAWC,KAAKF,GAAO;AACrB,UAAI,CAACE,KAAKA,EAAE,SAAS,KAAM;AAC3B,YAAMC,IAAOT,EAAqBQ,EAAE,KAAK;AACzC,iBAAWH,KAAOI,GAAM;AACtB,cAAMR,IAAI,OAAOI,CAAG,GACdK,IAAOH,EAAQ,IAAIN,CAAC;AAC1B,QAAAM,EAAQ,IAAIN,GAAG,EAAE,GAAGO,GAAG,OAAOP,GAAG,QAAQS,GAAM,SAAS,MAAM,OAAOF,EAAE,KAAK,KAAK,IAAI;AAAA,MACvF;AAAA,IACF;AACA,WAAO,CAAC,GAAGD,EAAQ,QAAQ;AAAA,EAC7B;AACA,MAAI,OAAOD,KAAU;AACnB,WAAO,OAAO,QAAQA,CAAgC,EAAE,IAAI,CAAC,CAACL,GAAGU,CAAK,OAAO;AAAA,MAC3E,OAAOV;AAAA,MACP,OAAO,OAAOU,CAAK,KAAK;AAAA,IAAA,EACxB;AAGN,GAEMC,IAAO,CAACC,GAAmBC,MAC/BD,KAAK,QAAQC,KAAK,OAAO,UAAaD,KAAK,MAAMC,KAAK,IAClDC,IAAS,CAAIF,GAAcC,MAC/BD,KAAK,OAAOC,KAAK,SAAYA,KAAK,QAAWD,IAAIC,IAARD,IAAgBC,GACrDE,IAAU,CAAIH,GAAcC,MAChCD,KAAK,OAAOC,KAAK,SAAYA,KAAK,QAAWD,IAAIC,IAARD,IAAgBC,GACrDG,IAAM,CAACC,GAAeC,MAC1BD,KAAQ,QAAQC,KAAQ,QAAQD,IAAOC,IAAO,IAC1C,KAAK,MAAOD,KAAQA,IAAOC,KAAS,GAAK,IAAI,MAC7C,QAKAC,IAAa,CAACP,GAAeC,MAA8B;AAC/D,QAAMO,IAAST,EAAKC,EAAE,kBAAkBC,EAAE,gBAAgB,GACpDQ,IAAYV,EAAKC,EAAE,qBAAqBC,EAAE,mBAAmB,GAC7DS,IAAUX,EAAKC,EAAE,cAAcC,EAAE,YAAY,GAC7CU,IAAaZ,EAAKC,EAAE,iBAAiBC,EAAE,eAAe;AAE5D,MAAIW;AACJ,MAAIZ,EAAE,OAAO,QAAQC,EAAE,OAAO,MAAM;AAClC,UAAMY,IAAKb,EAAE,kBACPc,IAAKb,EAAE;AACb,IAAAW,IACEC,KAAM,QAAQC,KAAM,QAAQD,IAAKC,IAAK,KACjCd,EAAE,MAAMa,IAAKZ,EAAE,MAAMa,MAAOD,IAAKC,MACjCd,EAAE,MAAMC,EAAE,OAAO,GACxBW,IAAM,KAAK,MAAMA,IAAM,GAAG,IAAI;AAAA,EAChC;AACE,IAAAA,IAAMZ,EAAE,OAAOC,EAAE,OAAO;AAG1B,MAAIc;AACJ,MAAIf,EAAE,cAAc,UAAUC,EAAE,cAAc,QAAQ;AACpD,UAAMP,wBAAc,IAAA;AACpB,eAAWC,KAAK,CAAC,GAAIK,EAAE,gBAAgB,CAAA,GAAK,GAAIC,EAAE,gBAAgB,CAAA,CAAG,GAAG;AACtE,YAAMJ,IAAOH,EAAQ,IAAIC,EAAE,KAAK;AAChC,MAAAD,EAAQ,IAAIC,EAAE,OAAOE,IAAO,EAAE,GAAGA,GAAM,OAAOA,EAAK,QAAQF,EAAE,MAAA,IAAU,EAAE,GAAGA,GAAG;AAAA,IACjF;AACA,IAAAoB,IAAe,CAAC,GAAGrB,EAAQ,QAAQ;AAAA,EACrC;AAEA,SAAO;AAAA,IACL,YAAYM,EAAE;AAAA,IACd,KAAKE,EAAOF,EAAE,KAAKC,EAAE,GAAG;AAAA,IACxB,KAAKE,EAAQH,EAAE,KAAKC,EAAE,GAAG;AAAA,IACzB,KAAAW;AAAA,IACA,KAAKb,EAAKC,EAAE,KAAKC,EAAE,GAAG;AAAA,IACtB,kBAAkBO;AAAA,IAClB,qBAAqBC;AAAA,IACrB,kBAAkBL,EAAII,GAAQC,CAAS;AAAA,IACvC,qBAAqBL,EAAIK,GAAWD,CAAM;AAAA,IAC1C,cAAcE;AAAA,IACd,iBAAiBC;AAAA,IACjB,mBAAmBP,EAAIM,GAASC,CAAU;AAAA,IAC1C,sBAAsBP,EAAIO,GAAYD,CAAO;AAAA,IAC7C,cAAAK;AAAA,IACA,cAAcf,EAAE,gBAAgBC,EAAE;AAAA,IAClC,gBAAgBD,EAAE,kBAAkBC,EAAE;AAAA,EAAA;AAE1C,GAGae,IAAsB,CAACC,MAClCA,EAAM,IAAI,CAACpC,OAAO;AAAA,EAChB,GAAGA;AAAA,EACH,YAAYI,EAAkBJ,EAAE,UAAU;AAAA,EAC1C,cAAcU,EAAsBV,EAAE,YAAY;AACpD,EAAE,GAISqC,IAAkB,CAC7BC,IAAwB,IACxBC,IAAqB,CAAA,MACJ;AACjB,QAAMC,wBAAW,IAAA;AACjB,aAAWxC,KAAKuC,EAAM,CAAAC,EAAK,IAAIpC,EAAkBJ,EAAE,UAAU,GAAG,EAAE,GAAGA,EAAA,CAAG;AACxE,aAAWA,KAAKsC,GAAS;AACvB,UAAMG,IAAKrC,EAAkBJ,EAAE,UAAU,GACnC0C,IAAqB,EAAE,GAAIF,EAAK,IAAIC,CAAE,KAAK,EAAE,YAAYzC,EAAE,aAAW;AAC5E,eAAW,CAAC2C,GAAGpC,CAAC,KAAK,OAAO,QAAQP,CAAC;AACnC,MAAIO,KAAM,SAA0BmC,EAAeC,CAAC,IAAIpC;AAE1D,IAAAiC,EAAK,IAAIC,GAAIC,CAAM;AAAA,EACrB;AACA,SAAO,CAAC,GAAGF,EAAK,QAAQ;AAC1B,GAIaI,IAAqB,CAACR,MACjCA,EAAM;AAAA,EACJ,CAACS,GAAK7C,MAAM,KAAK,IAAI6C,IAAM7C,EAAE,oBAAoB,MAAMA,EAAE,uBAAuB,EAAE;AAAA,EAClF;AACF,GAIW8C,IAAoB,CAC/BC,GACAC,IAAsB,OACL;AACjB,QAAMR,wBAAW,IAAA;AACjB,aAAWxC,KAAK+C,EAAS,CAAAP,EAAK,IAAIpC,EAAkBJ,EAAE,UAAU,GAAGA,CAAC;AACpE,QAAMiD,IAAM,IAAI,IAAIT,CAAI;AACxB,aAAWU,KAAKF,GAAO;AACrB,UAAMP,IAAKrC,EAAkB8C,EAAE,UAAU,GACnCC,IAAIX,EAAK,IAAIC,CAAE;AACrB,IAAAQ,EAAI,IAAIR,GAAIU,IAAIzB,EAAWyB,GAAGD,CAAC,IAAIA,CAAC;AAAA,EACtC;AACA,SAAO,CAAC,GAAGD,EAAI,QAAQ;AACzB,GAEaG,IAAsB,CAACC,GAAYC,MAA+B;AAC7E,QAAMC,IAAUF,GAAO,WAAW;AAClC,iBAAQ,MAAM,kBAAkBC,CAAU,WAAWD,CAAK,GACnDE;AACT;"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});require("../../base/client.cjs.js");const d=require("../../generated/graphql.cjs.js");require("../../generated/graphqlLinks.cjs.js");require("../../generated/access.cjs.js");require("../../generated/actions.cjs.js");require("../../generated/activityFeed.cjs.js");require("../../generated/addons.cjs.js");require("../../generated/anatomy.cjs.js");require("../../generated/attributes.cjs.js");require("../../generated/authentication.cjs.js");require("../../generated/bundles.cjs.js");require("../../generated/configuration.cjs.js");require("../../generated/dataImport.cjs.js");require("../../generated/desktop.cjs.js");require("../../generated/entityLists.cjs.js");require("../../generated/events.cjs.js");require("../../generated/files.cjs.js");require("../../generated/folders.cjs.js");require("../../generated/inbox.cjs.js");require("../../generated/links.cjs.js");require("../../generated/market.cjs.js");require("../../generated/onboarding.cjs.js");require("../../generated/operations.cjs.js");require("../../generated/products.cjs.js");require("../../generated/projectDashboard.cjs.js");require("../../generated/projects.cjs.js");require("../../generated/projectFolders.cjs.js");require("../../generated/reviewables.cjs.js");require("../../generated/services.cjs.js");require("../../generated/system.cjs.js");require("../../generated/tasks.cjs.js");require("../../generated/teams.cjs.js");require("../../generated/thumbnails.cjs.js");require("../../generated/uRIs.cjs.js");require("../../generated/users.cjs.js");require("../../generated/versions.cjs.js");require("../../generated/workfiles.cjs.js");require("../../generated/ynputCloud.cjs.js");require("../../generated/grouping.cjs.js");require("../../generated/views.cjs.js");const m=require("./columnStats.cjs.js"),p=[d.StatsOperation.Distribution,d.StatsOperation.Filled,d.StatsOperation.NotFilled],T={status:"status",assignees:"assignees",tags:"tags",taskType:"task_type"},F={status:"status",tags:"tags"},I=(r,e)=>{const o=(e==="version"||e==="product"?F:T)[r.id];if(o)return{field:o,aggregations:p};if(r.id.startsWith("attrib.")){const s=r.id.slice(7);return{field:e==="task"||e==="folder"?`inherited_attributes.${s}`:`attrib.${s}`,aggregations:p}}return null},h=r=>{const e=r.indexOf(".");return e!==-1?"attrib_"+r.slice(e+1):m.canonicalColumnId(r)},O=(r,e)=>{const n=new Map,o={count:0,percentage:0};if(!e)return{counts:n,total:0,ungrouped:o,complete:!1};const s=h(e.field),i=r.find(t=>t.columnName===s),l=i?.distribution||[],u=i?.valueNotFilledCount??0,q=i?.valueFilledCount??0,S=!!i&&(l.length>0||q===0),g=l.reduce((t,c)=>t+(c.count||0),0),a=q?q+u:g+u;for(const t of l){const c=t.count||0,b=a>0?Math.round(c/a*100):0;n.set(String(t.value),{count:c,percentage:b})}const f={count:u,percentage:g>0&&u>0?Math.round(u/a*100):void 0};return{counts:n,total:a,ungrouped:f,complete:S}};exports.groupByToStatsTarget=I;exports.selectGroupCounts=O;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});require("../../base/client.cjs.js");const d=require("../../generated/graphql.cjs.js");require("../../generated/graphqlLinks.cjs.js");require("../../generated/access.cjs.js");require("../../generated/actions.cjs.js");require("../../generated/activityFeed.cjs.js");require("../../generated/addons.cjs.js");require("../../generated/anatomy.cjs.js");require("../../generated/attributes.cjs.js");require("../../generated/authentication.cjs.js");require("../../generated/bundles.cjs.js");require("../../generated/configuration.cjs.js");require("../../generated/dataImport.cjs.js");require("../../generated/desktop.cjs.js");require("../../generated/entityLists.cjs.js");require("../../generated/events.cjs.js");require("../../generated/files.cjs.js");require("../../generated/folders.cjs.js");require("../../generated/inbox.cjs.js");require("../../generated/links.cjs.js");require("../../generated/market.cjs.js");require("../../generated/onboarding.cjs.js");require("../../generated/operations.cjs.js");require("../../generated/products.cjs.js");require("../../generated/projectDashboard.cjs.js");require("../../generated/projects.cjs.js");require("../../generated/projectFolders.cjs.js");require("../../generated/reviewables.cjs.js");require("../../generated/services.cjs.js");require("../../generated/system.cjs.js");require("../../generated/tasks.cjs.js");require("../../generated/teams.cjs.js");require("../../generated/thumbnails.cjs.js");require("../../generated/uRIs.cjs.js");require("../../generated/users.cjs.js");require("../../generated/versions.cjs.js");require("../../generated/workfiles.cjs.js");require("../../generated/ynputCloud.cjs.js");require("../../generated/grouping.cjs.js");require("../../generated/views.cjs.js");const m=require("./columnStats.cjs.js"),p=[d.StatsOperation.Distribution,d.StatsOperation.Filled,d.StatsOperation.NotFilled],T={status:"status",assignees:"assignees",tags:"tags",taskType:"task_type"},h={status:"status",tags:"tags",author:"author",productType:"product_type"},F=(r,e)=>{const n=(e==="version"||e==="product"?h:T)[r.id];if(n)return{field:n,aggregations:p};if(r.id.startsWith("attrib.")){const s=r.id.slice(7);return{field:e==="task"||e==="folder"?`inherited_attributes.${s}`:`attrib.${s}`,aggregations:p}}return null},I=r=>{const e=r.indexOf(".");return e!==-1?"attrib_"+r.slice(e+1):m.canonicalColumnId(r)},O=(r,e)=>{const o=new Map,n={count:0,percentage:0};if(!e)return{counts:o,total:0,ungrouped:n,complete:!1};const s=I(e.field),i=r.find(t=>t.columnName===s),l=i?.distribution||[],u=i?.valueNotFilledCount??0,q=i?.valueFilledCount??0,S=!!i&&(l.length>0||q===0),g=l.reduce((t,c)=>t+(c.count||0),0),a=q?q+u:g+u;for(const t of l){const c=t.count||0,b=a>0?Math.round(c/a*100):0;o.set(String(t.value),{count:c,percentage:b})}const f={count:u,percentage:g>0&&u>0?Math.round(u/a*100):void 0};return{counts:o,total:a,ungrouped:f,complete:S}};exports.groupByToStatsTarget=F;exports.selectGroupCounts=O;
2
2
  //# sourceMappingURL=groupCounts.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"groupCounts.cjs.js","sources":["../../../../../../src/api/queries/columnStats/groupCounts.ts"],"sourcesContent":["// Pure helpers for per-group counts on grouped table rows. Reuse the existing\n// filter-aware column-stats distribution; no new endpoint. Kept free of query\n// hooks so it stays unit-testable and outside the api<->container import edge.\nimport { StatsOperation } from '@shared/api/generated'\nimport { canonicalColumnId, type FieldStats } from './columnStats'\nimport type { MetricTarget, StatsEntity } from './metricTargets'\n\nexport type GroupCount = { count: number; percentage?: number }\nexport type GroupCountsMap = Map<string, GroupCount>\nexport type GroupCountsSelection = {\n counts: GroupCountsMap\n total: number\n // the null/empty bucket (no value for the grouped field) -> Ungrouped row\n ungrouped: GroupCount\n complete: boolean\n}\n\n// Distribution = per-group counts; Filled/NotFilled = entity totals for the\n// percentage denominator (an entity with N values counts once, not N times).\nconst DISTRIBUTION_AGGS = [\n StatsOperation.Distribution,\n StatsOperation.Filled,\n StatsOperation.NotFilled,\n]\n\n// groupBy.id -> backend stats field, per entity. Only the field-value groupings\n// the distribution endpoint covers; folder/folderType/hierarchy return null.\nconst TASK_FIELDS: Record<string, string> = {\n status: 'status',\n assignees: 'assignees',\n tags: 'tags',\n taskType: 'task_type',\n}\nconst VERSION_FIELDS: Record<string, string> = {\n status: 'status',\n tags: 'tags',\n}\n\nexport const groupByToStatsTarget = (\n groupBy: { id: string },\n entity: StatsEntity,\n): MetricTarget | null => {\n const fields = entity === 'version' || entity === 'product' ? VERSION_FIELDS : TASK_FIELDS\n const field = fields[groupBy.id]\n if (field) return { field, aggregations: DISTRIBUTION_AGGS }\n\n if (groupBy.id.startsWith('attrib.')) {\n const name = groupBy.id.slice('attrib.'.length)\n // tasks/folders display inherited attrib values; versions/products use own\n const attribField =\n entity === 'task' || entity === 'folder' ? `inherited_attributes.${name}` : `attrib.${name}`\n return { field: attribField, aggregations: DISTRIBUTION_AGGS }\n }\n\n return null\n}\n\n// Map a stats target field to the canonical fieldStats columnName. Attribute\n// targets are dot-paths (inherited_attributes.x / attrib.x) that canonicalColumnId\n// (underscore-based) won't normalize, so resolve those to attrib_<name> here.\nconst targetToColumnId = (field: string): string => {\n const dot = field.indexOf('.')\n if (dot !== -1) return 'attrib_' + field.slice(dot + 1)\n return canonicalColumnId(field)\n}\n\nexport const selectGroupCounts = (\n fieldStats: FieldStats[],\n target: MetricTarget | null,\n): GroupCountsSelection => {\n const counts: GroupCountsMap = new Map()\n const none: GroupCount = { count: 0, percentage: 0 }\n if (!target) return { counts, total: 0, ungrouped: none, complete: false }\n\n const columnId = targetToColumnId(target.field)\n const stat = fieldStats.find((s) => s.columnName === columnId)\n const distribution = (stat?.distribution || []) as { value: string; count?: number | null }[]\n const notFilled = stat?.valueNotFilledCount ?? 0\n const filled = stat?.valueFilledCount ?? 0\n const complete = !!stat && (distribution.length > 0 || filled === 0)\n\n const distSum = distribution.reduce((sum, d) => sum + (d.count || 0), 0)\n // entity total when valueFilledCount is available (an entity with N values\n // counts once); else fall back to the distribution sum (may over-count lists).\n const total = filled ? filled + notFilled : distSum + notFilled\n\n for (const d of distribution) {\n const count = d.count || 0\n const percentage = total > 0 ? Math.round((count / total) * 100) : 0\n counts.set(String(d.value), { count, percentage })\n }\n\n // No distribution (e.g. free-text fields like description) -> named groups carry\n // no %, so the Ungrouped remainder would be a meaningless 100%. Show count only.\n const ungrouped: GroupCount = {\n count: notFilled,\n percentage: distSum > 0 && notFilled > 0 ? Math.round((notFilled / total) * 100) : undefined,\n }\n return { counts, total, ungrouped, complete }\n}\n"],"names":["DISTRIBUTION_AGGS","StatsOperation","TASK_FIELDS","VERSION_FIELDS","groupByToStatsTarget","groupBy","entity","field","name","targetToColumnId","dot","canonicalColumnId","selectGroupCounts","fieldStats","target","counts","none","columnId","stat","s","distribution","notFilled","filled","complete","distSum","sum","d","total","count","percentage","ungrouped"],"mappings":"4zDAmBMA,EAAoB,CACxBC,EAAAA,eAAe,aACfA,EAAAA,eAAe,OACfA,iBAAe,SACjB,EAIMC,EAAsC,CAC1C,OAAQ,SACR,UAAW,YACX,KAAM,OACN,SAAU,WACZ,EACMC,EAAyC,CAC7C,OAAQ,SACR,KAAM,MACR,EAEaC,EAAuB,CAClCC,EACAC,IACwB,CAExB,MAAMC,GADSD,IAAW,WAAaA,IAAW,UAAYH,EAAiBD,GAC1DG,EAAQ,EAAE,EAC/B,GAAIE,EAAO,MAAO,CAAE,MAAAA,EAAO,aAAcP,CAAA,EAEzC,GAAIK,EAAQ,GAAG,WAAW,SAAS,EAAG,CACpC,MAAMG,EAAOH,EAAQ,GAAG,MAAM,CAAgB,EAI9C,MAAO,CAAE,MADPC,IAAW,QAAUA,IAAW,SAAW,wBAAwBE,CAAI,GAAK,UAAUA,CAAI,GAC/D,aAAcR,CAAA,CAC7C,CAEA,OAAO,IACT,EAKMS,EAAoBF,GAA0B,CAClD,MAAMG,EAAMH,EAAM,QAAQ,GAAG,EAC7B,OAAIG,IAAQ,GAAW,UAAYH,EAAM,MAAMG,EAAM,CAAC,EAC/CC,EAAAA,kBAAkBJ,CAAK,CAChC,EAEaK,EAAoB,CAC/BC,EACAC,IACyB,CACzB,MAAMC,MAA6B,IAC7BC,EAAmB,CAAE,MAAO,EAAG,WAAY,CAAA,EACjD,GAAI,CAACF,EAAQ,MAAO,CAAE,OAAAC,EAAQ,MAAO,EAAG,UAAWC,EAAM,SAAU,EAAA,EAEnE,MAAMC,EAAWR,EAAiBK,EAAO,KAAK,EACxCI,EAAOL,EAAW,KAAMM,GAAMA,EAAE,aAAeF,CAAQ,EACvDG,EAAgBF,GAAM,cAAgB,CAAA,EACtCG,EAAYH,GAAM,qBAAuB,EACzCI,EAASJ,GAAM,kBAAoB,EACnCK,EAAW,CAAC,CAACL,IAASE,EAAa,OAAS,GAAKE,IAAW,GAE5DE,EAAUJ,EAAa,OAAO,CAACK,EAAKC,IAAMD,GAAOC,EAAE,OAAS,GAAI,CAAC,EAGjEC,EAAQL,EAASA,EAASD,EAAYG,EAAUH,EAEtD,UAAWK,KAAKN,EAAc,CAC5B,MAAMQ,EAAQF,EAAE,OAAS,EACnBG,EAAaF,EAAQ,EAAI,KAAK,MAAOC,EAAQD,EAAS,GAAG,EAAI,EACnEZ,EAAO,IAAI,OAAOW,EAAE,KAAK,EAAG,CAAE,MAAAE,EAAO,WAAAC,EAAY,CACnD,CAIA,MAAMC,EAAwB,CAC5B,MAAOT,EACP,WAAYG,EAAU,GAAKH,EAAY,EAAI,KAAK,MAAOA,EAAYM,EAAS,GAAG,EAAI,MAAA,EAErF,MAAO,CAAE,OAAAZ,EAAQ,MAAAY,EAAO,UAAAG,EAAW,SAAAP,CAAA,CACrC"}
1
+ {"version":3,"file":"groupCounts.cjs.js","sources":["../../../../../../src/api/queries/columnStats/groupCounts.ts"],"sourcesContent":["// Pure helpers for per-group counts on grouped table rows. Reuse the existing\n// filter-aware column-stats distribution; no new endpoint. Kept free of query\n// hooks so it stays unit-testable and outside the api<->container import edge.\nimport { StatsOperation } from '@shared/api/generated'\nimport { canonicalColumnId, type FieldStats } from './columnStats'\nimport type { MetricTarget, StatsEntity } from './metricTargets'\n\nexport type GroupCount = { count: number; percentage?: number }\nexport type GroupCountsMap = Map<string, GroupCount>\nexport type GroupCountsSelection = {\n counts: GroupCountsMap\n total: number\n // the null/empty bucket (no value for the grouped field) -> Ungrouped row\n ungrouped: GroupCount\n complete: boolean\n}\n\n// Distribution = per-group counts; Filled/NotFilled = entity totals for the\n// percentage denominator (an entity with N values counts once, not N times).\nconst DISTRIBUTION_AGGS = [\n StatsOperation.Distribution,\n StatsOperation.Filled,\n StatsOperation.NotFilled,\n]\n\n// groupBy.id -> backend stats field, per entity. Only the field-value groupings\n// the distribution endpoint covers; folder/folderType/hierarchy return null.\nconst TASK_FIELDS: Record<string, string> = {\n status: 'status',\n assignees: 'assignees',\n tags: 'tags',\n taskType: 'task_type',\n}\n// Versions distribute over columns projected into their stats raw_data CTE: native\n// versions.* columns (status, tags, author, own attrib.*) plus the joined subtype\n// columns the backend now projects (product_type/task_type/folder_type).\n// Only ONE subtype field can back version counts at a time: product_type and task_type\n// both canonicalize to the `subType` columnName (COLUMN_ALIASES) and would clobber each\n// other in the merged stats cache. productType is the version slicer's primary\n// dimension, so it wins; taskType stays unmapped.\nconst VERSION_FIELDS: Record<string, string> = {\n status: 'status',\n tags: 'tags',\n author: 'author',\n productType: 'product_type',\n}\n\nexport const groupByToStatsTarget = (\n groupBy: { id: string },\n entity: StatsEntity,\n): MetricTarget | null => {\n const fields = entity === 'version' || entity === 'product' ? VERSION_FIELDS : TASK_FIELDS\n const field = fields[groupBy.id]\n if (field) return { field, aggregations: DISTRIBUTION_AGGS }\n\n if (groupBy.id.startsWith('attrib.')) {\n const name = groupBy.id.slice('attrib.'.length)\n // tasks/folders display inherited attrib values; versions/products use own\n const attribField =\n entity === 'task' || entity === 'folder' ? `inherited_attributes.${name}` : `attrib.${name}`\n return { field: attribField, aggregations: DISTRIBUTION_AGGS }\n }\n\n return null\n}\n\n// Map a stats target field to the canonical fieldStats columnName. Attribute\n// targets are dot-paths (inherited_attributes.x / attrib.x) that canonicalColumnId\n// (underscore-based) won't normalize, so resolve those to attrib_<name> here.\nconst targetToColumnId = (field: string): string => {\n const dot = field.indexOf('.')\n if (dot !== -1) return 'attrib_' + field.slice(dot + 1)\n return canonicalColumnId(field)\n}\n\nexport const selectGroupCounts = (\n fieldStats: FieldStats[],\n target: MetricTarget | null,\n): GroupCountsSelection => {\n const counts: GroupCountsMap = new Map()\n const none: GroupCount = { count: 0, percentage: 0 }\n if (!target) return { counts, total: 0, ungrouped: none, complete: false }\n\n const columnId = targetToColumnId(target.field)\n const stat = fieldStats.find((s) => s.columnName === columnId)\n const distribution = (stat?.distribution || []) as { value: string; count?: number | null }[]\n const notFilled = stat?.valueNotFilledCount ?? 0\n const filled = stat?.valueFilledCount ?? 0\n const complete = !!stat && (distribution.length > 0 || filled === 0)\n\n const distSum = distribution.reduce((sum, d) => sum + (d.count || 0), 0)\n // entity total when valueFilledCount is available (an entity with N values\n // counts once); else fall back to the distribution sum (may over-count lists).\n const total = filled ? filled + notFilled : distSum + notFilled\n\n for (const d of distribution) {\n const count = d.count || 0\n const percentage = total > 0 ? Math.round((count / total) * 100) : 0\n counts.set(String(d.value), { count, percentage })\n }\n\n // No distribution (e.g. free-text fields like description) -> named groups carry\n // no %, so the Ungrouped remainder would be a meaningless 100%. Show count only.\n const ungrouped: GroupCount = {\n count: notFilled,\n percentage: distSum > 0 && notFilled > 0 ? Math.round((notFilled / total) * 100) : undefined,\n }\n return { counts, total, ungrouped, complete }\n}\n"],"names":["DISTRIBUTION_AGGS","StatsOperation","TASK_FIELDS","VERSION_FIELDS","groupByToStatsTarget","groupBy","entity","field","name","targetToColumnId","dot","canonicalColumnId","selectGroupCounts","fieldStats","target","counts","none","columnId","stat","s","distribution","notFilled","filled","complete","distSum","sum","d","total","count","percentage","ungrouped"],"mappings":"4zDAmBMA,EAAoB,CACxBC,EAAAA,eAAe,aACfA,EAAAA,eAAe,OACfA,iBAAe,SACjB,EAIMC,EAAsC,CAC1C,OAAQ,SACR,UAAW,YACX,KAAM,OACN,SAAU,WACZ,EAQMC,EAAyC,CAC7C,OAAQ,SACR,KAAM,OACN,OAAQ,SACR,YAAa,cACf,EAEaC,EAAuB,CAClCC,EACAC,IACwB,CAExB,MAAMC,GADSD,IAAW,WAAaA,IAAW,UAAYH,EAAiBD,GAC1DG,EAAQ,EAAE,EAC/B,GAAIE,EAAO,MAAO,CAAE,MAAAA,EAAO,aAAcP,CAAA,EAEzC,GAAIK,EAAQ,GAAG,WAAW,SAAS,EAAG,CACpC,MAAMG,EAAOH,EAAQ,GAAG,MAAM,CAAgB,EAI9C,MAAO,CAAE,MADPC,IAAW,QAAUA,IAAW,SAAW,wBAAwBE,CAAI,GAAK,UAAUA,CAAI,GAC/D,aAAcR,CAAA,CAC7C,CAEA,OAAO,IACT,EAKMS,EAAoBF,GAA0B,CAClD,MAAMG,EAAMH,EAAM,QAAQ,GAAG,EAC7B,OAAIG,IAAQ,GAAW,UAAYH,EAAM,MAAMG,EAAM,CAAC,EAC/CC,EAAAA,kBAAkBJ,CAAK,CAChC,EAEaK,EAAoB,CAC/BC,EACAC,IACyB,CACzB,MAAMC,MAA6B,IAC7BC,EAAmB,CAAE,MAAO,EAAG,WAAY,CAAA,EACjD,GAAI,CAACF,EAAQ,MAAO,CAAE,OAAAC,EAAQ,MAAO,EAAG,UAAWC,EAAM,SAAU,EAAA,EAEnE,MAAMC,EAAWR,EAAiBK,EAAO,KAAK,EACxCI,EAAOL,EAAW,KAAMM,GAAMA,EAAE,aAAeF,CAAQ,EACvDG,EAAgBF,GAAM,cAAgB,CAAA,EACtCG,EAAYH,GAAM,qBAAuB,EACzCI,EAASJ,GAAM,kBAAoB,EACnCK,EAAW,CAAC,CAACL,IAASE,EAAa,OAAS,GAAKE,IAAW,GAE5DE,EAAUJ,EAAa,OAAO,CAACK,EAAKC,IAAMD,GAAOC,EAAE,OAAS,GAAI,CAAC,EAGjEC,EAAQL,EAASA,EAASD,EAAYG,EAAUH,EAEtD,UAAWK,KAAKN,EAAc,CAC5B,MAAMQ,EAAQF,EAAE,OAAS,EACnBG,EAAaF,EAAQ,EAAI,KAAK,MAAOC,EAAQD,EAAS,GAAG,EAAI,EACnEZ,EAAO,IAAI,OAAOW,EAAE,KAAK,EAAG,CAAE,MAAAE,EAAO,WAAAC,EAAY,CACnD,CAIA,MAAMC,EAAwB,CAC5B,MAAOT,EACP,WAAYG,EAAU,GAAKH,EAAY,EAAI,KAAK,MAAOA,EAAYM,EAAS,GAAG,EAAI,MAAA,EAErF,MAAO,CAAE,OAAAZ,EAAQ,MAAAY,EAAO,UAAAG,EAAW,SAAAP,CAAA,CACrC"}
@@ -48,27 +48,29 @@ const g = [
48
48
  assignees: "assignees",
49
49
  tags: "tags",
50
50
  taskType: "task_type"
51
- }, T = {
51
+ }, h = {
52
52
  status: "status",
53
- tags: "tags"
53
+ tags: "tags",
54
+ author: "author",
55
+ productType: "product_type"
54
56
  }, at = (o, t) => {
55
- const e = (t === "version" || t === "product" ? T : I)[o.id];
57
+ const e = (t === "version" || t === "product" ? h : I)[o.id];
56
58
  if (e) return { field: e, aggregations: g };
57
59
  if (o.id.startsWith("attrib.")) {
58
60
  const p = o.id.slice(7);
59
61
  return { field: t === "task" || t === "folder" ? `inherited_attributes.${p}` : `attrib.${p}`, aggregations: g };
60
62
  }
61
63
  return null;
62
- }, h = (o) => {
64
+ }, T = (o) => {
63
65
  const t = o.indexOf(".");
64
66
  return t !== -1 ? "attrib_" + o.slice(t + 1) : F(o);
65
67
  }, ct = (o, t) => {
66
68
  const s = /* @__PURE__ */ new Map(), e = { count: 0, percentage: 0 };
67
69
  if (!t) return { counts: s, total: 0, ungrouped: e, complete: !1 };
68
- const p = h(t.field), r = o.find((i) => i.columnName === p), c = r?.distribution || [], n = r?.valueNotFilledCount ?? 0, u = r?.valueFilledCount ?? 0, f = !!r && (c.length > 0 || u === 0), d = c.reduce((i, a) => i + (a.count || 0), 0), m = u ? u + n : d + n;
69
- for (const i of c) {
70
- const a = i.count || 0, S = m > 0 ? Math.round(a / m * 100) : 0;
71
- s.set(String(i.value), { count: a, percentage: S });
70
+ const p = T(t.field), i = o.find((r) => r.columnName === p), c = i?.distribution || [], n = i?.valueNotFilledCount ?? 0, u = i?.valueFilledCount ?? 0, f = !!i && (c.length > 0 || u === 0), d = c.reduce((r, a) => r + (a.count || 0), 0), m = u ? u + n : d + n;
71
+ for (const r of c) {
72
+ const a = r.count || 0, S = m > 0 ? Math.round(a / m * 100) : 0;
73
+ s.set(String(r.value), { count: a, percentage: S });
72
74
  }
73
75
  const b = {
74
76
  count: n,
@@ -1 +1 @@
1
- {"version":3,"file":"groupCounts.es.js","sources":["../../../../../../src/api/queries/columnStats/groupCounts.ts"],"sourcesContent":["// Pure helpers for per-group counts on grouped table rows. Reuse the existing\n// filter-aware column-stats distribution; no new endpoint. Kept free of query\n// hooks so it stays unit-testable and outside the api<->container import edge.\nimport { StatsOperation } from '@shared/api/generated'\nimport { canonicalColumnId, type FieldStats } from './columnStats'\nimport type { MetricTarget, StatsEntity } from './metricTargets'\n\nexport type GroupCount = { count: number; percentage?: number }\nexport type GroupCountsMap = Map<string, GroupCount>\nexport type GroupCountsSelection = {\n counts: GroupCountsMap\n total: number\n // the null/empty bucket (no value for the grouped field) -> Ungrouped row\n ungrouped: GroupCount\n complete: boolean\n}\n\n// Distribution = per-group counts; Filled/NotFilled = entity totals for the\n// percentage denominator (an entity with N values counts once, not N times).\nconst DISTRIBUTION_AGGS = [\n StatsOperation.Distribution,\n StatsOperation.Filled,\n StatsOperation.NotFilled,\n]\n\n// groupBy.id -> backend stats field, per entity. Only the field-value groupings\n// the distribution endpoint covers; folder/folderType/hierarchy return null.\nconst TASK_FIELDS: Record<string, string> = {\n status: 'status',\n assignees: 'assignees',\n tags: 'tags',\n taskType: 'task_type',\n}\nconst VERSION_FIELDS: Record<string, string> = {\n status: 'status',\n tags: 'tags',\n}\n\nexport const groupByToStatsTarget = (\n groupBy: { id: string },\n entity: StatsEntity,\n): MetricTarget | null => {\n const fields = entity === 'version' || entity === 'product' ? VERSION_FIELDS : TASK_FIELDS\n const field = fields[groupBy.id]\n if (field) return { field, aggregations: DISTRIBUTION_AGGS }\n\n if (groupBy.id.startsWith('attrib.')) {\n const name = groupBy.id.slice('attrib.'.length)\n // tasks/folders display inherited attrib values; versions/products use own\n const attribField =\n entity === 'task' || entity === 'folder' ? `inherited_attributes.${name}` : `attrib.${name}`\n return { field: attribField, aggregations: DISTRIBUTION_AGGS }\n }\n\n return null\n}\n\n// Map a stats target field to the canonical fieldStats columnName. Attribute\n// targets are dot-paths (inherited_attributes.x / attrib.x) that canonicalColumnId\n// (underscore-based) won't normalize, so resolve those to attrib_<name> here.\nconst targetToColumnId = (field: string): string => {\n const dot = field.indexOf('.')\n if (dot !== -1) return 'attrib_' + field.slice(dot + 1)\n return canonicalColumnId(field)\n}\n\nexport const selectGroupCounts = (\n fieldStats: FieldStats[],\n target: MetricTarget | null,\n): GroupCountsSelection => {\n const counts: GroupCountsMap = new Map()\n const none: GroupCount = { count: 0, percentage: 0 }\n if (!target) return { counts, total: 0, ungrouped: none, complete: false }\n\n const columnId = targetToColumnId(target.field)\n const stat = fieldStats.find((s) => s.columnName === columnId)\n const distribution = (stat?.distribution || []) as { value: string; count?: number | null }[]\n const notFilled = stat?.valueNotFilledCount ?? 0\n const filled = stat?.valueFilledCount ?? 0\n const complete = !!stat && (distribution.length > 0 || filled === 0)\n\n const distSum = distribution.reduce((sum, d) => sum + (d.count || 0), 0)\n // entity total when valueFilledCount is available (an entity with N values\n // counts once); else fall back to the distribution sum (may over-count lists).\n const total = filled ? filled + notFilled : distSum + notFilled\n\n for (const d of distribution) {\n const count = d.count || 0\n const percentage = total > 0 ? Math.round((count / total) * 100) : 0\n counts.set(String(d.value), { count, percentage })\n }\n\n // No distribution (e.g. free-text fields like description) -> named groups carry\n // no %, so the Ungrouped remainder would be a meaningless 100%. Show count only.\n const ungrouped: GroupCount = {\n count: notFilled,\n percentage: distSum > 0 && notFilled > 0 ? Math.round((notFilled / total) * 100) : undefined,\n }\n return { counts, total, ungrouped, complete }\n}\n"],"names":["DISTRIBUTION_AGGS","StatsOperation","TASK_FIELDS","VERSION_FIELDS","groupByToStatsTarget","groupBy","entity","field","name","targetToColumnId","dot","canonicalColumnId","selectGroupCounts","fieldStats","target","counts","none","columnId","stat","s","distribution","notFilled","filled","complete","distSum","sum","d","total","count","percentage","ungrouped"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,MAAMA,IAAoB;AAAA,EACxBC,EAAe;AAAA,EACfA,EAAe;AAAA,EACfA,EAAe;AACjB,GAIMC,IAAsC;AAAA,EAC1C,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,MAAM;AAAA,EACN,UAAU;AACZ,GACMC,IAAyC;AAAA,EAC7C,QAAQ;AAAA,EACR,MAAM;AACR,GAEaC,KAAuB,CAClCC,GACAC,MACwB;AAExB,QAAMC,KADSD,MAAW,aAAaA,MAAW,YAAYH,IAAiBD,GAC1DG,EAAQ,EAAE;AAC/B,MAAIE,EAAO,QAAO,EAAE,OAAAA,GAAO,cAAcP,EAAA;AAEzC,MAAIK,EAAQ,GAAG,WAAW,SAAS,GAAG;AACpC,UAAMG,IAAOH,EAAQ,GAAG,MAAM,CAAgB;AAI9C,WAAO,EAAE,OADPC,MAAW,UAAUA,MAAW,WAAW,wBAAwBE,CAAI,KAAK,UAAUA,CAAI,IAC/D,cAAcR,EAAA;AAAA,EAC7C;AAEA,SAAO;AACT,GAKMS,IAAmB,CAACF,MAA0B;AAClD,QAAMG,IAAMH,EAAM,QAAQ,GAAG;AAC7B,SAAIG,MAAQ,KAAW,YAAYH,EAAM,MAAMG,IAAM,CAAC,IAC/CC,EAAkBJ,CAAK;AAChC,GAEaK,KAAoB,CAC/BC,GACAC,MACyB;AACzB,QAAMC,wBAA6B,IAAA,GAC7BC,IAAmB,EAAE,OAAO,GAAG,YAAY,EAAA;AACjD,MAAI,CAACF,EAAQ,QAAO,EAAE,QAAAC,GAAQ,OAAO,GAAG,WAAWC,GAAM,UAAU,GAAA;AAEnE,QAAMC,IAAWR,EAAiBK,EAAO,KAAK,GACxCI,IAAOL,EAAW,KAAK,CAACM,MAAMA,EAAE,eAAeF,CAAQ,GACvDG,IAAgBF,GAAM,gBAAgB,CAAA,GACtCG,IAAYH,GAAM,uBAAuB,GACzCI,IAASJ,GAAM,oBAAoB,GACnCK,IAAW,CAAC,CAACL,MAASE,EAAa,SAAS,KAAKE,MAAW,IAE5DE,IAAUJ,EAAa,OAAO,CAACK,GAAKC,MAAMD,KAAOC,EAAE,SAAS,IAAI,CAAC,GAGjEC,IAAQL,IAASA,IAASD,IAAYG,IAAUH;AAEtD,aAAWK,KAAKN,GAAc;AAC5B,UAAMQ,IAAQF,EAAE,SAAS,GACnBG,IAAaF,IAAQ,IAAI,KAAK,MAAOC,IAAQD,IAAS,GAAG,IAAI;AACnE,IAAAZ,EAAO,IAAI,OAAOW,EAAE,KAAK,GAAG,EAAE,OAAAE,GAAO,YAAAC,GAAY;AAAA,EACnD;AAIA,QAAMC,IAAwB;AAAA,IAC5B,OAAOT;AAAA,IACP,YAAYG,IAAU,KAAKH,IAAY,IAAI,KAAK,MAAOA,IAAYM,IAAS,GAAG,IAAI;AAAA,EAAA;AAErF,SAAO,EAAE,QAAAZ,GAAQ,OAAAY,GAAO,WAAAG,GAAW,UAAAP,EAAA;AACrC;"}
1
+ {"version":3,"file":"groupCounts.es.js","sources":["../../../../../../src/api/queries/columnStats/groupCounts.ts"],"sourcesContent":["// Pure helpers for per-group counts on grouped table rows. Reuse the existing\n// filter-aware column-stats distribution; no new endpoint. Kept free of query\n// hooks so it stays unit-testable and outside the api<->container import edge.\nimport { StatsOperation } from '@shared/api/generated'\nimport { canonicalColumnId, type FieldStats } from './columnStats'\nimport type { MetricTarget, StatsEntity } from './metricTargets'\n\nexport type GroupCount = { count: number; percentage?: number }\nexport type GroupCountsMap = Map<string, GroupCount>\nexport type GroupCountsSelection = {\n counts: GroupCountsMap\n total: number\n // the null/empty bucket (no value for the grouped field) -> Ungrouped row\n ungrouped: GroupCount\n complete: boolean\n}\n\n// Distribution = per-group counts; Filled/NotFilled = entity totals for the\n// percentage denominator (an entity with N values counts once, not N times).\nconst DISTRIBUTION_AGGS = [\n StatsOperation.Distribution,\n StatsOperation.Filled,\n StatsOperation.NotFilled,\n]\n\n// groupBy.id -> backend stats field, per entity. Only the field-value groupings\n// the distribution endpoint covers; folder/folderType/hierarchy return null.\nconst TASK_FIELDS: Record<string, string> = {\n status: 'status',\n assignees: 'assignees',\n tags: 'tags',\n taskType: 'task_type',\n}\n// Versions distribute over columns projected into their stats raw_data CTE: native\n// versions.* columns (status, tags, author, own attrib.*) plus the joined subtype\n// columns the backend now projects (product_type/task_type/folder_type).\n// Only ONE subtype field can back version counts at a time: product_type and task_type\n// both canonicalize to the `subType` columnName (COLUMN_ALIASES) and would clobber each\n// other in the merged stats cache. productType is the version slicer's primary\n// dimension, so it wins; taskType stays unmapped.\nconst VERSION_FIELDS: Record<string, string> = {\n status: 'status',\n tags: 'tags',\n author: 'author',\n productType: 'product_type',\n}\n\nexport const groupByToStatsTarget = (\n groupBy: { id: string },\n entity: StatsEntity,\n): MetricTarget | null => {\n const fields = entity === 'version' || entity === 'product' ? VERSION_FIELDS : TASK_FIELDS\n const field = fields[groupBy.id]\n if (field) return { field, aggregations: DISTRIBUTION_AGGS }\n\n if (groupBy.id.startsWith('attrib.')) {\n const name = groupBy.id.slice('attrib.'.length)\n // tasks/folders display inherited attrib values; versions/products use own\n const attribField =\n entity === 'task' || entity === 'folder' ? `inherited_attributes.${name}` : `attrib.${name}`\n return { field: attribField, aggregations: DISTRIBUTION_AGGS }\n }\n\n return null\n}\n\n// Map a stats target field to the canonical fieldStats columnName. Attribute\n// targets are dot-paths (inherited_attributes.x / attrib.x) that canonicalColumnId\n// (underscore-based) won't normalize, so resolve those to attrib_<name> here.\nconst targetToColumnId = (field: string): string => {\n const dot = field.indexOf('.')\n if (dot !== -1) return 'attrib_' + field.slice(dot + 1)\n return canonicalColumnId(field)\n}\n\nexport const selectGroupCounts = (\n fieldStats: FieldStats[],\n target: MetricTarget | null,\n): GroupCountsSelection => {\n const counts: GroupCountsMap = new Map()\n const none: GroupCount = { count: 0, percentage: 0 }\n if (!target) return { counts, total: 0, ungrouped: none, complete: false }\n\n const columnId = targetToColumnId(target.field)\n const stat = fieldStats.find((s) => s.columnName === columnId)\n const distribution = (stat?.distribution || []) as { value: string; count?: number | null }[]\n const notFilled = stat?.valueNotFilledCount ?? 0\n const filled = stat?.valueFilledCount ?? 0\n const complete = !!stat && (distribution.length > 0 || filled === 0)\n\n const distSum = distribution.reduce((sum, d) => sum + (d.count || 0), 0)\n // entity total when valueFilledCount is available (an entity with N values\n // counts once); else fall back to the distribution sum (may over-count lists).\n const total = filled ? filled + notFilled : distSum + notFilled\n\n for (const d of distribution) {\n const count = d.count || 0\n const percentage = total > 0 ? Math.round((count / total) * 100) : 0\n counts.set(String(d.value), { count, percentage })\n }\n\n // No distribution (e.g. free-text fields like description) -> named groups carry\n // no %, so the Ungrouped remainder would be a meaningless 100%. Show count only.\n const ungrouped: GroupCount = {\n count: notFilled,\n percentage: distSum > 0 && notFilled > 0 ? Math.round((notFilled / total) * 100) : undefined,\n }\n return { counts, total, ungrouped, complete }\n}\n"],"names":["DISTRIBUTION_AGGS","StatsOperation","TASK_FIELDS","VERSION_FIELDS","groupByToStatsTarget","groupBy","entity","field","name","targetToColumnId","dot","canonicalColumnId","selectGroupCounts","fieldStats","target","counts","none","columnId","stat","s","distribution","notFilled","filled","complete","distSum","sum","d","total","count","percentage","ungrouped"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,MAAMA,IAAoB;AAAA,EACxBC,EAAe;AAAA,EACfA,EAAe;AAAA,EACfA,EAAe;AACjB,GAIMC,IAAsC;AAAA,EAC1C,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,MAAM;AAAA,EACN,UAAU;AACZ,GAQMC,IAAyC;AAAA,EAC7C,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,aAAa;AACf,GAEaC,KAAuB,CAClCC,GACAC,MACwB;AAExB,QAAMC,KADSD,MAAW,aAAaA,MAAW,YAAYH,IAAiBD,GAC1DG,EAAQ,EAAE;AAC/B,MAAIE,EAAO,QAAO,EAAE,OAAAA,GAAO,cAAcP,EAAA;AAEzC,MAAIK,EAAQ,GAAG,WAAW,SAAS,GAAG;AACpC,UAAMG,IAAOH,EAAQ,GAAG,MAAM,CAAgB;AAI9C,WAAO,EAAE,OADPC,MAAW,UAAUA,MAAW,WAAW,wBAAwBE,CAAI,KAAK,UAAUA,CAAI,IAC/D,cAAcR,EAAA;AAAA,EAC7C;AAEA,SAAO;AACT,GAKMS,IAAmB,CAACF,MAA0B;AAClD,QAAMG,IAAMH,EAAM,QAAQ,GAAG;AAC7B,SAAIG,MAAQ,KAAW,YAAYH,EAAM,MAAMG,IAAM,CAAC,IAC/CC,EAAkBJ,CAAK;AAChC,GAEaK,KAAoB,CAC/BC,GACAC,MACyB;AACzB,QAAMC,wBAA6B,IAAA,GAC7BC,IAAmB,EAAE,OAAO,GAAG,YAAY,EAAA;AACjD,MAAI,CAACF,EAAQ,QAAO,EAAE,QAAAC,GAAQ,OAAO,GAAG,WAAWC,GAAM,UAAU,GAAA;AAEnE,QAAMC,IAAWR,EAAiBK,EAAO,KAAK,GACxCI,IAAOL,EAAW,KAAK,CAACM,MAAMA,EAAE,eAAeF,CAAQ,GACvDG,IAAgBF,GAAM,gBAAgB,CAAA,GACtCG,IAAYH,GAAM,uBAAuB,GACzCI,IAASJ,GAAM,oBAAoB,GACnCK,IAAW,CAAC,CAACL,MAASE,EAAa,SAAS,KAAKE,MAAW,IAE5DE,IAAUJ,EAAa,OAAO,CAACK,GAAKC,MAAMD,KAAOC,EAAE,SAAS,IAAI,CAAC,GAGjEC,IAAQL,IAASA,IAASD,IAAYG,IAAUH;AAEtD,aAAWK,KAAKN,GAAc;AAC5B,UAAMQ,IAAQF,EAAE,SAAS,GACnBG,IAAaF,IAAQ,IAAI,KAAK,MAAOC,IAAQD,IAAS,GAAG,IAAI;AACnE,IAAAZ,EAAO,IAAI,OAAOW,EAAE,KAAK,GAAG,EAAE,OAAAE,GAAO,YAAAC,GAAY;AAAA,EACnD;AAIA,QAAMC,IAAwB;AAAA,IAC5B,OAAOT;AAAA,IACP,YAAYG,IAAU,KAAKH,IAAY,IAAI,KAAK,MAAOA,IAAYM,IAAS,GAAG,IAAI;AAAA,EAAA;AAErF,SAAO,EAAE,QAAAZ,GAAQ,OAAAY,GAAO,WAAAG,GAAW,UAAAP,EAAA;AACrC;"}
@@ -1 +1 @@
1
- {"version":3,"file":"LinksManager.cjs.js","sources":["../../../../../src/components/LinksManager/LinksManager.tsx"],"sourcesContent":["import { FC, useState } from 'react'\nimport * as Styled from './LinksManager.styled'\nimport { getEntityId } from '@shared/util'\nimport useUpdateLinks from './hooks/useUpdateLinks'\nimport AddNewLinks, { LinkSearchType } from './AddNewLinks'\nimport { EntityPickerDialog, PickerEntityType } from '@shared/containers/EntityPickerDialog'\nimport { upperFirst } from 'lodash'\nimport { LinkManagerItem } from './LinkManagerItem'\nimport { Button } from '@ynput/ayon-react-components'\nimport { useGlobalContext } from '@shared/context'\nimport { groupLinksByEntity, GroupedLink } from './utils/groupLinks'\n\nexport type LinkEntity = {\n linkId: string\n entityId: string\n label: string\n parents: string[]\n entityType: string\n icon: string\n color?: string // color from folder/task type anatomy\n isRestricted?: boolean // flag to indicate if this link is restricted (node is null from API)\n}\n\nexport interface LinksManagerProps {\n linkTypeLabel: string\n direction?: 'in' | 'out'\n links: LinkEntity[] // used to display basic info about the links entity\n projectName: string\n entityId: string // the entity id of the entity that has these links\n linkType: string // full link type e.g. workflow|task|task\n entityType: string // the entity type of the entity that has these links\n targetEntityType: string // the entity type of the out links\n folderId?: string | null // the folder selected or the parent folder of the selected (used in EntityPickerDialog)\n selectedEntityIds?: string[] // list of selected entity IDs to highlight\n onClose?: () => void\n onEntityClick?: (entityId: string, entityType: string) => void // a click on an linked entity\n}\n\nexport const LinksManager: FC<LinksManagerProps> = ({\n linkTypeLabel,\n direction,\n links = [],\n projectName,\n entityId,\n entityType,\n linkType,\n targetEntityType,\n folderId,\n selectedEntityIds = [],\n onClose,\n onEntityClick,\n}) => {\n const { user } = useGlobalContext()\n const isManager = user?.data?.isAdmin || user?.data?.isManager\n\n const linksUpdater = useUpdateLinks({\n projectName,\n direction,\n entityId,\n entityType,\n targetEntityType,\n linkType,\n })\n\n const [searchType, setSearchType] = useState<LinkSearchType>(null)\n // Optimistic counts for immediate UI feedback (creates are not optimistic in the cache)\n const [optimisticCounts, setOptimisticCounts] = useState<Record<string, number>>({})\n\n const groupedLinks = groupLinksByEntity(links)\n\n // Get display count: use optimistic value until real data catches up\n const getDisplayCount = (group: GroupedLink) => {\n const optimistic = optimisticCounts[group.groupKey]\n if (optimistic !== undefined && optimistic !== group.count) {\n return optimistic\n }\n return group.count\n }\n\n const handleRemoveGroup = (e: React.MouseEvent<HTMLButtonElement>, group: GroupedLink) => {\n e.stopPropagation()\n\n linksUpdater.remove(\n group.linkIds.map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n })),\n )\n }\n\n const handleCountChange = (group: GroupedLink, newCount: number) => {\n const currentCount = getDisplayCount(group)\n // Set optimistic count immediately for instant UI feedback\n setOptimisticCounts((prev) => ({ ...prev, [group.groupKey]: newCount }))\n\n const diff = newCount - currentCount\n if (diff > 0) {\n // Add more links\n const newLinks = Array.from({ length: diff }, () => ({\n targetEntityId: group.entityId,\n linkId: getEntityId(),\n }))\n linksUpdater.add(newLinks)\n } else if (diff < 0) {\n // Remove links from the end (diff is negative, so slice(-3) takes last 3)\n const linksToRemove = group.linkIds.slice(diff).map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n }))\n linksUpdater.remove(linksToRemove)\n }\n }\n\n return (\n <>\n <Styled.Container\n onMouseDown={(e) => {\n // Blur active input when clicking anywhere in the dialog (so count input commits)\n if (e.target !== document.activeElement && document.activeElement instanceof HTMLInputElement) {\n document.activeElement.blur()\n }\n }}\n >\n <Styled.Header>\n {upperFirst(linkTypeLabel)} links ({direction})\n <Button\n icon={'close'}\n variant=\"text\"\n className=\"remove\"\n onClick={onClose}\n data-shortcut={'Escape'}\n data-tooltip-delay={500}\n />\n </Styled.Header>\n <Styled.LinksList>\n {groupedLinks.map((group) => (\n <LinkManagerItem\n key={group.representative.linkId}\n link={group.representative}\n count={getDisplayCount(group)}\n isSelected={selectedEntityIds.includes(group.entityId)}\n onEntityClick={onEntityClick}\n onRemove={(e) => handleRemoveGroup(e, group)}\n onCountChange={(newCount) => handleCountChange(group, newCount)}\n isManager={isManager}\n />\n ))}\n {links.length === 0 && <Styled.SubHeader>No links yet</Styled.SubHeader>}\n </Styled.LinksList>\n <AddNewLinks\n targetEntityType={targetEntityType}\n projectName={projectName}\n onClose={onClose}\n onAdd={(id) => linksUpdater.add([{ targetEntityId: id, linkId: getEntityId() }])}\n onSearchTypeChange={setSearchType}\n />\n </Styled.Container>\n {searchType === 'picker' && (\n <EntityPickerDialog\n onClose={() => setSearchType(null)}\n projectName={projectName}\n entityType={targetEntityType as PickerEntityType} // the type of entity to pick\n initialSelection={folderId ? { folder: { [folderId]: true } } : undefined} // preselect current folder\n onSubmit={(s) =>\n linksUpdater.add(s.map((id) => ({ targetEntityId: id, linkId: getEntityId() })))\n }\n isMultiSelect\n />\n )}\n </>\n )\n}\n"],"names":["LinksManager","linkTypeLabel","direction","links","projectName","entityId","entityType","linkType","targetEntityType","folderId","selectedEntityIds","onClose","onEntityClick","user","useGlobalContext","isManager","linksUpdater","useUpdateLinks","searchType","setSearchType","useState","optimisticCounts","setOptimisticCounts","groupedLinks","groupLinksByEntity","getDisplayCount","group","optimistic","handleRemoveGroup","id","handleCountChange","newCount","currentCount","prev","diff","newLinks","getEntityId","linksToRemove","jsxs","Fragment","Styled.Container","Styled.Header","upperFirst","jsx","Button","Styled.LinksList","LinkManagerItem","e","Styled.SubHeader","AddNewLinks","EntityPickerDialog","s"],"mappings":"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"}
1
+ {"version":3,"file":"LinksManager.cjs.js","sources":["../../../../../src/components/LinksManager/LinksManager.tsx"],"sourcesContent":["import { FC, useState } from 'react'\nimport * as Styled from './LinksManager.styled'\nimport { getEntityId } from '@shared/util'\nimport useUpdateLinks from './hooks/useUpdateLinks'\nimport AddNewLinks, { LinkSearchType } from './AddNewLinks'\nimport { EntityPickerDialog, PickerEntityType } from '@shared/containers/EntityPickerDialog'\nimport { upperFirst } from 'lodash'\nimport { LinkManagerItem } from './LinkManagerItem'\nimport { Button } from '@ynput/ayon-react-components'\nimport { useGlobalContext } from '@shared/context'\nimport { groupLinksByEntity, GroupedLink } from './utils/groupLinks'\n\nexport type LinkEntity = {\n linkId: string\n entityId: string\n label: string\n parents: string[]\n entityType: string\n icon: string\n color?: string // color from folder/task type anatomy\n isRestricted?: boolean // flag to indicate if this link is restricted (node is null from API)\n}\n\nexport interface LinksManagerProps {\n linkTypeLabel: string\n direction?: 'in' | 'out'\n links: LinkEntity[] // used to display basic info about the links entity\n projectName: string\n entityId: string // the entity id of the entity that has these links\n linkType: string // full link type e.g. workflow|task|task\n entityType: string // the entity type of the entity that has these links\n targetEntityType: string // the entity type of the out links\n folderId?: string | null // the folder selected or the parent folder of the selected (used in EntityPickerDialog)\n selectedEntityIds?: string[] // list of selected entity IDs to highlight\n onClose?: () => void\n onEntityClick?: (entityId: string, entityType: string) => void // a click on an linked entity\n}\n\nexport const LinksManager: FC<LinksManagerProps> = ({\n linkTypeLabel,\n direction,\n links = [],\n projectName,\n entityId,\n entityType,\n linkType,\n targetEntityType,\n folderId,\n selectedEntityIds = [],\n onClose,\n onEntityClick,\n}) => {\n const { user } = useGlobalContext()\n const isManager = user?.data?.isAdmin || user?.data?.isManager\n\n const linksUpdater = useUpdateLinks({\n projectName,\n direction,\n entityId,\n entityType,\n targetEntityType,\n linkType,\n })\n\n const [searchType, setSearchType] = useState<LinkSearchType>(null)\n // Optimistic counts for immediate UI feedback (creates are not optimistic in the cache)\n const [optimisticCounts, setOptimisticCounts] = useState<Record<string, number>>({})\n\n const groupedLinks = groupLinksByEntity(links)\n\n // Get display count: use optimistic value until real data catches up\n const getDisplayCount = (group: GroupedLink) => {\n const optimistic = optimisticCounts[group.groupKey]\n if (optimistic !== undefined && optimistic !== group.count) {\n return optimistic\n }\n return group.count\n }\n\n const handleRemoveGroup = (e: React.MouseEvent<HTMLButtonElement>, group: GroupedLink) => {\n e.stopPropagation()\n\n linksUpdater.remove(\n group.linkIds.map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n })),\n )\n }\n\n const handleCountChange = (group: GroupedLink, newCount: number) => {\n const currentCount = getDisplayCount(group)\n // Set optimistic count immediately for instant UI feedback\n setOptimisticCounts((prev) => ({ ...prev, [group.groupKey]: newCount }))\n\n const diff = newCount - currentCount\n if (diff > 0) {\n // Add more links\n const newLinks = Array.from({ length: diff }, () => ({\n targetEntityId: group.entityId,\n linkId: getEntityId(),\n }))\n linksUpdater.add(newLinks)\n } else if (diff < 0) {\n // Remove links from the end (diff is negative, so slice(-3) takes last 3)\n const linksToRemove = group.linkIds.slice(diff).map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n }))\n linksUpdater.remove(linksToRemove)\n }\n }\n\n return (\n <>\n <Styled.Container\n onMouseDown={(e) => {\n // Blur active input when clicking anywhere in the dialog (so count input commits)\n if (\n e.target !== document.activeElement &&\n document.activeElement instanceof HTMLInputElement\n ) {\n document.activeElement.blur()\n }\n }}\n >\n <Styled.Header>\n {upperFirst(linkTypeLabel)} links ({direction})\n <Button\n icon={'close'}\n variant=\"text\"\n className=\"remove\"\n onClick={onClose}\n data-shortcut={'Escape'}\n data-tooltip-delay={500}\n />\n </Styled.Header>\n <Styled.LinksList>\n {groupedLinks.map((group) => (\n <LinkManagerItem\n key={group.representative.linkId}\n link={group.representative}\n count={getDisplayCount(group)}\n isSelected={selectedEntityIds.includes(group.entityId)}\n onEntityClick={onEntityClick}\n onRemove={(e) => handleRemoveGroup(e, group)}\n onCountChange={(newCount) => handleCountChange(group, newCount)}\n isManager={isManager}\n />\n ))}\n {links.length === 0 && <Styled.SubHeader>No links yet</Styled.SubHeader>}\n </Styled.LinksList>\n <AddNewLinks\n targetEntityType={targetEntityType}\n projectName={projectName}\n onClose={onClose}\n onAdd={(id) => linksUpdater.add([{ targetEntityId: id, linkId: getEntityId() }])}\n onSearchTypeChange={setSearchType}\n />\n </Styled.Container>\n {searchType === 'picker' && (\n <EntityPickerDialog\n onClose={() => setSearchType(null)}\n projectName={projectName}\n entityType={targetEntityType as PickerEntityType} // the type of entity to pick\n initialSelection={folderId ? { folder: { [folderId]: true } } : undefined} // preselect current folder\n onSubmit={(s) =>\n linksUpdater.add(s.map((id) => ({ targetEntityId: id, linkId: getEntityId() })))\n }\n isMultiSelect\n />\n )}\n </>\n )\n}\n"],"names":["LinksManager","linkTypeLabel","direction","links","projectName","entityId","entityType","linkType","targetEntityType","folderId","selectedEntityIds","onClose","onEntityClick","user","useGlobalContext","isManager","linksUpdater","useUpdateLinks","searchType","setSearchType","useState","optimisticCounts","setOptimisticCounts","groupedLinks","groupLinksByEntity","getDisplayCount","group","optimistic","handleRemoveGroup","id","handleCountChange","newCount","currentCount","prev","diff","newLinks","getEntityId","linksToRemove","jsxs","Fragment","Styled.Container","Styled.Header","upperFirst","jsx","Button","Styled.LinksList","LinkManagerItem","e","Styled.SubHeader","AddNewLinks","EntityPickerDialog","s"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LinksManager.es.js","sources":["../../../../../src/components/LinksManager/LinksManager.tsx"],"sourcesContent":["import { FC, useState } from 'react'\nimport * as Styled from './LinksManager.styled'\nimport { getEntityId } from '@shared/util'\nimport useUpdateLinks from './hooks/useUpdateLinks'\nimport AddNewLinks, { LinkSearchType } from './AddNewLinks'\nimport { EntityPickerDialog, PickerEntityType } from '@shared/containers/EntityPickerDialog'\nimport { upperFirst } from 'lodash'\nimport { LinkManagerItem } from './LinkManagerItem'\nimport { Button } from '@ynput/ayon-react-components'\nimport { useGlobalContext } from '@shared/context'\nimport { groupLinksByEntity, GroupedLink } from './utils/groupLinks'\n\nexport type LinkEntity = {\n linkId: string\n entityId: string\n label: string\n parents: string[]\n entityType: string\n icon: string\n color?: string // color from folder/task type anatomy\n isRestricted?: boolean // flag to indicate if this link is restricted (node is null from API)\n}\n\nexport interface LinksManagerProps {\n linkTypeLabel: string\n direction?: 'in' | 'out'\n links: LinkEntity[] // used to display basic info about the links entity\n projectName: string\n entityId: string // the entity id of the entity that has these links\n linkType: string // full link type e.g. workflow|task|task\n entityType: string // the entity type of the entity that has these links\n targetEntityType: string // the entity type of the out links\n folderId?: string | null // the folder selected or the parent folder of the selected (used in EntityPickerDialog)\n selectedEntityIds?: string[] // list of selected entity IDs to highlight\n onClose?: () => void\n onEntityClick?: (entityId: string, entityType: string) => void // a click on an linked entity\n}\n\nexport const LinksManager: FC<LinksManagerProps> = ({\n linkTypeLabel,\n direction,\n links = [],\n projectName,\n entityId,\n entityType,\n linkType,\n targetEntityType,\n folderId,\n selectedEntityIds = [],\n onClose,\n onEntityClick,\n}) => {\n const { user } = useGlobalContext()\n const isManager = user?.data?.isAdmin || user?.data?.isManager\n\n const linksUpdater = useUpdateLinks({\n projectName,\n direction,\n entityId,\n entityType,\n targetEntityType,\n linkType,\n })\n\n const [searchType, setSearchType] = useState<LinkSearchType>(null)\n // Optimistic counts for immediate UI feedback (creates are not optimistic in the cache)\n const [optimisticCounts, setOptimisticCounts] = useState<Record<string, number>>({})\n\n const groupedLinks = groupLinksByEntity(links)\n\n // Get display count: use optimistic value until real data catches up\n const getDisplayCount = (group: GroupedLink) => {\n const optimistic = optimisticCounts[group.groupKey]\n if (optimistic !== undefined && optimistic !== group.count) {\n return optimistic\n }\n return group.count\n }\n\n const handleRemoveGroup = (e: React.MouseEvent<HTMLButtonElement>, group: GroupedLink) => {\n e.stopPropagation()\n\n linksUpdater.remove(\n group.linkIds.map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n })),\n )\n }\n\n const handleCountChange = (group: GroupedLink, newCount: number) => {\n const currentCount = getDisplayCount(group)\n // Set optimistic count immediately for instant UI feedback\n setOptimisticCounts((prev) => ({ ...prev, [group.groupKey]: newCount }))\n\n const diff = newCount - currentCount\n if (diff > 0) {\n // Add more links\n const newLinks = Array.from({ length: diff }, () => ({\n targetEntityId: group.entityId,\n linkId: getEntityId(),\n }))\n linksUpdater.add(newLinks)\n } else if (diff < 0) {\n // Remove links from the end (diff is negative, so slice(-3) takes last 3)\n const linksToRemove = group.linkIds.slice(diff).map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n }))\n linksUpdater.remove(linksToRemove)\n }\n }\n\n return (\n <>\n <Styled.Container\n onMouseDown={(e) => {\n // Blur active input when clicking anywhere in the dialog (so count input commits)\n if (e.target !== document.activeElement && document.activeElement instanceof HTMLInputElement) {\n document.activeElement.blur()\n }\n }}\n >\n <Styled.Header>\n {upperFirst(linkTypeLabel)} links ({direction})\n <Button\n icon={'close'}\n variant=\"text\"\n className=\"remove\"\n onClick={onClose}\n data-shortcut={'Escape'}\n data-tooltip-delay={500}\n />\n </Styled.Header>\n <Styled.LinksList>\n {groupedLinks.map((group) => (\n <LinkManagerItem\n key={group.representative.linkId}\n link={group.representative}\n count={getDisplayCount(group)}\n isSelected={selectedEntityIds.includes(group.entityId)}\n onEntityClick={onEntityClick}\n onRemove={(e) => handleRemoveGroup(e, group)}\n onCountChange={(newCount) => handleCountChange(group, newCount)}\n isManager={isManager}\n />\n ))}\n {links.length === 0 && <Styled.SubHeader>No links yet</Styled.SubHeader>}\n </Styled.LinksList>\n <AddNewLinks\n targetEntityType={targetEntityType}\n projectName={projectName}\n onClose={onClose}\n onAdd={(id) => linksUpdater.add([{ targetEntityId: id, linkId: getEntityId() }])}\n onSearchTypeChange={setSearchType}\n />\n </Styled.Container>\n {searchType === 'picker' && (\n <EntityPickerDialog\n onClose={() => setSearchType(null)}\n projectName={projectName}\n entityType={targetEntityType as PickerEntityType} // the type of entity to pick\n initialSelection={folderId ? { folder: { [folderId]: true } } : undefined} // preselect current folder\n onSubmit={(s) =>\n linksUpdater.add(s.map((id) => ({ targetEntityId: id, linkId: getEntityId() })))\n }\n isMultiSelect\n />\n )}\n </>\n )\n}\n"],"names":["LinksManager","linkTypeLabel","direction","links","projectName","entityId","entityType","linkType","targetEntityType","folderId","selectedEntityIds","onClose","onEntityClick","user","useGlobalContext","isManager","linksUpdater","useUpdateLinks","searchType","setSearchType","useState","optimisticCounts","setOptimisticCounts","groupedLinks","groupLinksByEntity","getDisplayCount","group","optimistic","handleRemoveGroup","e","id","handleCountChange","newCount","currentCount","prev","diff","newLinks","getEntityId","linksToRemove","jsxs","Fragment","Styled.Container","Styled.Header","upperFirst","jsx","Button","Styled.LinksList","LinkManagerItem","Styled.SubHeader","AddNewLinks","EntityPickerDialog","s"],"mappings":"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"}
1
+ {"version":3,"file":"LinksManager.es.js","sources":["../../../../../src/components/LinksManager/LinksManager.tsx"],"sourcesContent":["import { FC, useState } from 'react'\nimport * as Styled from './LinksManager.styled'\nimport { getEntityId } from '@shared/util'\nimport useUpdateLinks from './hooks/useUpdateLinks'\nimport AddNewLinks, { LinkSearchType } from './AddNewLinks'\nimport { EntityPickerDialog, PickerEntityType } from '@shared/containers/EntityPickerDialog'\nimport { upperFirst } from 'lodash'\nimport { LinkManagerItem } from './LinkManagerItem'\nimport { Button } from '@ynput/ayon-react-components'\nimport { useGlobalContext } from '@shared/context'\nimport { groupLinksByEntity, GroupedLink } from './utils/groupLinks'\n\nexport type LinkEntity = {\n linkId: string\n entityId: string\n label: string\n parents: string[]\n entityType: string\n icon: string\n color?: string // color from folder/task type anatomy\n isRestricted?: boolean // flag to indicate if this link is restricted (node is null from API)\n}\n\nexport interface LinksManagerProps {\n linkTypeLabel: string\n direction?: 'in' | 'out'\n links: LinkEntity[] // used to display basic info about the links entity\n projectName: string\n entityId: string // the entity id of the entity that has these links\n linkType: string // full link type e.g. workflow|task|task\n entityType: string // the entity type of the entity that has these links\n targetEntityType: string // the entity type of the out links\n folderId?: string | null // the folder selected or the parent folder of the selected (used in EntityPickerDialog)\n selectedEntityIds?: string[] // list of selected entity IDs to highlight\n onClose?: () => void\n onEntityClick?: (entityId: string, entityType: string) => void // a click on an linked entity\n}\n\nexport const LinksManager: FC<LinksManagerProps> = ({\n linkTypeLabel,\n direction,\n links = [],\n projectName,\n entityId,\n entityType,\n linkType,\n targetEntityType,\n folderId,\n selectedEntityIds = [],\n onClose,\n onEntityClick,\n}) => {\n const { user } = useGlobalContext()\n const isManager = user?.data?.isAdmin || user?.data?.isManager\n\n const linksUpdater = useUpdateLinks({\n projectName,\n direction,\n entityId,\n entityType,\n targetEntityType,\n linkType,\n })\n\n const [searchType, setSearchType] = useState<LinkSearchType>(null)\n // Optimistic counts for immediate UI feedback (creates are not optimistic in the cache)\n const [optimisticCounts, setOptimisticCounts] = useState<Record<string, number>>({})\n\n const groupedLinks = groupLinksByEntity(links)\n\n // Get display count: use optimistic value until real data catches up\n const getDisplayCount = (group: GroupedLink) => {\n const optimistic = optimisticCounts[group.groupKey]\n if (optimistic !== undefined && optimistic !== group.count) {\n return optimistic\n }\n return group.count\n }\n\n const handleRemoveGroup = (e: React.MouseEvent<HTMLButtonElement>, group: GroupedLink) => {\n e.stopPropagation()\n\n linksUpdater.remove(\n group.linkIds.map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n })),\n )\n }\n\n const handleCountChange = (group: GroupedLink, newCount: number) => {\n const currentCount = getDisplayCount(group)\n // Set optimistic count immediately for instant UI feedback\n setOptimisticCounts((prev) => ({ ...prev, [group.groupKey]: newCount }))\n\n const diff = newCount - currentCount\n if (diff > 0) {\n // Add more links\n const newLinks = Array.from({ length: diff }, () => ({\n targetEntityId: group.entityId,\n linkId: getEntityId(),\n }))\n linksUpdater.add(newLinks)\n } else if (diff < 0) {\n // Remove links from the end (diff is negative, so slice(-3) takes last 3)\n const linksToRemove = group.linkIds.slice(diff).map((id) => ({\n id,\n target: { entityId: group.entityId, entityType: group.representative.entityType },\n }))\n linksUpdater.remove(linksToRemove)\n }\n }\n\n return (\n <>\n <Styled.Container\n onMouseDown={(e) => {\n // Blur active input when clicking anywhere in the dialog (so count input commits)\n if (\n e.target !== document.activeElement &&\n document.activeElement instanceof HTMLInputElement\n ) {\n document.activeElement.blur()\n }\n }}\n >\n <Styled.Header>\n {upperFirst(linkTypeLabel)} links ({direction})\n <Button\n icon={'close'}\n variant=\"text\"\n className=\"remove\"\n onClick={onClose}\n data-shortcut={'Escape'}\n data-tooltip-delay={500}\n />\n </Styled.Header>\n <Styled.LinksList>\n {groupedLinks.map((group) => (\n <LinkManagerItem\n key={group.representative.linkId}\n link={group.representative}\n count={getDisplayCount(group)}\n isSelected={selectedEntityIds.includes(group.entityId)}\n onEntityClick={onEntityClick}\n onRemove={(e) => handleRemoveGroup(e, group)}\n onCountChange={(newCount) => handleCountChange(group, newCount)}\n isManager={isManager}\n />\n ))}\n {links.length === 0 && <Styled.SubHeader>No links yet</Styled.SubHeader>}\n </Styled.LinksList>\n <AddNewLinks\n targetEntityType={targetEntityType}\n projectName={projectName}\n onClose={onClose}\n onAdd={(id) => linksUpdater.add([{ targetEntityId: id, linkId: getEntityId() }])}\n onSearchTypeChange={setSearchType}\n />\n </Styled.Container>\n {searchType === 'picker' && (\n <EntityPickerDialog\n onClose={() => setSearchType(null)}\n projectName={projectName}\n entityType={targetEntityType as PickerEntityType} // the type of entity to pick\n initialSelection={folderId ? { folder: { [folderId]: true } } : undefined} // preselect current folder\n onSubmit={(s) =>\n linksUpdater.add(s.map((id) => ({ targetEntityId: id, linkId: getEntityId() })))\n }\n isMultiSelect\n />\n )}\n </>\n )\n}\n"],"names":["LinksManager","linkTypeLabel","direction","links","projectName","entityId","entityType","linkType","targetEntityType","folderId","selectedEntityIds","onClose","onEntityClick","user","useGlobalContext","isManager","linksUpdater","useUpdateLinks","searchType","setSearchType","useState","optimisticCounts","setOptimisticCounts","groupedLinks","groupLinksByEntity","getDisplayCount","group","optimistic","handleRemoveGroup","e","id","handleCountChange","newCount","currentCount","prev","diff","newLinks","getEntityId","linksToRemove","jsxs","Fragment","Styled.Container","Styled.Header","upperFirst","jsx","Button","Styled.LinksList","LinkManagerItem","Styled.SubHeader","AddNewLinks","EntityPickerDialog","s"],"mappings":"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"}