Remove sentiment, emotion, and the TopicsUnnested join from the Cube
schema, web registry, AI prompt, audit-allowlist, and i18n keys — these
required metadata enrichment that is no longer planned. Tests swap the
removed string dimensions for sourceType to keep coverage intact.
Add csatDissatisfiedCount and csatNeutralCount so dashboards can render
the standard satisfied/neutral/dissatisfied distribution alongside the
existing top-2-box metric.
Expose field_label, field_group_label, language, value_boolean,
value_date, created_at, and updated_at as Cube dimensions. fieldLabel
and fieldGroupLabel unlock "group by question" and the matrix/ranking
aggregations enabled by the recent composite-question PR; the others
round out coverage of the underlying feedback_records columns. Extend
FieldDefinition with a boolean type and matching filter operators.
- Drop the prisma.$transaction wrapper; find + delete is two sequential
steps, doesn't need a transaction.
- Drop the redundant ResourceNotFoundError catch branch; the trailing
`throw error` already lets it bubble.
- Let action-client infer ctx / parsedInput types.
Tests: cover the two catch branches (Prisma -> DatabaseError, unknown
rethrow) so the new function is fully line-covered.
Adds CSAT and CES measures, plus a couple of universal cross-type measures,
to the FeedbackRecords Cube. Also fixes correctness bugs in the existing NPS
measures and constrains the AI chart-query output to known measure / dimension
ids.
Measures added
- csatScore: % of answered CSAT responses rated 4 or 5 (top-2-box on 1-5).
- csatAverage: AVG of answered CSAT ratings.
- csatSatisfiedCount: count where field_type='csat' AND value_number >= 4.
- csatCount: count of answered CSAT responses.
- cesAverage: AVG of answered CES ratings (1-5 or 1-7 depending on the question).
- cesCount: count of answered CES responses.
- uniqueRespondents: countDistinct(user_id).
- uniqueResponses: countDistinct(submission_id).
- npsAverage: AVG of answered NPS ratings (replaces the old `averageScore`
measure, which silently averaged every numeric value across all field types).
Bug fixes (existing NPS measures)
- promoterCount / passiveCount / detractorCount / npsScore now filter
field_type = 'nps'. Before, any numeric value in the band was counted
(so a CSAT 5/5 was lumped in as a detractor).
- npsScore now divides by *answered* NPS responses only (value_number IS NOT
NULL); dismissed/abandoned NPS records no longer drag the score toward 0.
- npsScore returns NULL when no answered NPS responses exist instead of the
literal 0, so empty-data days render as gaps rather than a misleading
flat-zero line.
- Same NULL-safe denominator fix applied to csatScore.
- csatCount / cesCount now exclude dismissed (value_number IS NULL) responses
so they match the score denominators.
Dimensions
- Renamed `npsValue` -> `valueNumber` (the underlying value_number column
stores values for NPS, CSAT, CES, rating, and number field types).
- New `valueText` dimension for grouping by categorical / open-text answers.
AI chart generation
- ZGenerateAIQueryResponse now enum-constrains `measures`, `dimensions`,
`timeDimensions.dimension`, and `filters.member` against the known ids
derived from FEEDBACK_FIELDS. Vercel AI SDK + Gemini structured outputs
enforce these at decoding time, so the model can no longer return invalid
or hallucinated measure names.
- generateText now pins temperature: 0. Same prompt produces the same query.
Breaking changes
- Charts that referenced `FeedbackRecords.averageScore` need to switch to
`FeedbackRecords.npsAverage`.
- Charts that referenced `FeedbackRecords.npsValue` need to switch to
`FeedbackRecords.valueNumber`.
Pre-GA on epic/v5; no production charts are expected to be impacted yet.
- Updated `sanitizeCsvFieldMappings` to utilize constants for protected target IDs.
- Refactored connector actions to streamline field mapping processing.
- Added a new test case for sanitization to ensure correct handling of all-protected mappings.
- Improved localization for CSV row count and field types in the UI components.
- Introduced `sanitizeCsvFieldMappings` function to filter out user-controlled mappings for `tenant_id` and `source_type`.
- Updated connector actions to utilize the new sanitization logic when processing CSV field mappings.
- Added unit tests for `sanitizeCsvFieldMappings` to ensure correct functionality.
- Adjusted CSV transformation logic to consistently set `source_type` to "csv".
ENG-901: clicking Remove on a chart widget no longer enters dashboard edit mode
(which surfaced the rename input) and saving with the last widget removed no longer
surfaces a raw "widgets: Too small" zod error. Out of edit mode, Remove now goes
through a DeleteDialog -> dedicated removeWidgetFromDashboardAction. The batched
update path also allows an empty widgets array now that the lib already supports
deleting all widgets correctly.
ENG-903: add p-1 to AddExistingChartsDialog DialogBody so the focus ring on the
inner search input is no longer clipped by the body's overflow boundary.
Same pattern as CartesianChart's ChartTooltipRow — a module-level
component that calls useTranslation internally, plus a thin formatter
function passed to ChartTooltipContent. Resolves the nested-component
warning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch from the native browser tooltip to the in-app inline error pattern:
red Label, isInvalid Input border, and helper text below the field. The
onInvalid handler suppresses the default tooltip and scrolls + focuses the
field via event.currentTarget — no ref needed. Typing clears the error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Browsers default to "Please fill in this field"; use the existing
workspace.analysis.charts.please_enter_chart_name key so the message
matches the toast we surface elsewhere. Clear custom validity on change
so the form unblocks once the user types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the imperative scrollIntoView + focus on Save click with a tiny
<form> wrapping the required Input. The Save button becomes type=submit
with form="create-chart-form", so the browser handles scroll, focus, and
the "Please fill in this field" tooltip natively when the name is empty.
ChartDialogFooter now accepts either onSaveClick (button mode) or formId
(submit-button mode). All sibling buttons in the footer are explicitly
type="button" so they don't accidentally submit the form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Clicking Save Chart with the name field empty fired a toast but left the
modal scrolled wherever the user was (e.g. on the chart preview at the
bottom), so they couldn't see what was wrong. Scroll the name field into
view and focus it before delegating to the existing save handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fractional-tick bar/line/area scenario isn't the bug we're shipping in
this PR. Keep only the pie tooltip spacing/formatting change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Returning [value, name] from the tooltip formatter rendered as two adjacent text nodes ("3Nps"). Return a fragment with an explicit space so it reads "3 Nps".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>