feat(in-app-analytics)/filters-rework - #1166
Conversation
89e4184 to
69cba3b
Compare
ChibiBlasphem
left a comment
There was a problem hiding this comment.
Overall there's a lot of comments/logs left in the code. Some parts are complex to understand due to naming and/or not being extracted in their own function.
| getDecisionOutcomesPerDay: async ( | ||
| args: AnalyticsQuery, | ||
| ): Promise<DecisionOutcomesPerPeriod | null> => { | ||
| const parsed = transformAnalyticsQuery.parse(args); | ||
| if (!parsed.length) throw new Error('No date range provided'); | ||
|
|
||
| try { | ||
| const [raw, rawCompare] = await Promise.all([ | ||
| client.getDecisionOutcomesPerDay(parsed[0]!), | ||
| ...(parsed[1] ? [client.getDecisionOutcomesPerDay(parsed[1])] : []), | ||
| ]); | ||
|
|
||
| const merged = mergeDateRanges([raw, ...(rawCompare ? [rawCompare] : [])]); | ||
|
|
||
| const start = args.compareDateRange | ||
| ? [args.dateRange.start, args.compareDateRange.start].sort(compareAsc)[0]! | ||
| : args.dateRange.start; | ||
| const end = args.compareDateRange | ||
| ? [args.dateRange.end, args.compareDateRange.end].sort(compareDesc)[0]! | ||
| : args.dateRange.end; | ||
| const start = | ||
| parsed.length === 2 | ||
| ? [parsed[0]!.start, parsed[1]!.start].sort(compareAsc)[0]! | ||
| : parsed[0]!.start; | ||
| const end = | ||
| parsed.length === 2 | ||
| ? [parsed[0]!.end, parsed[1]!.end].sort(compareDesc)[0]! | ||
| : parsed[0]!.end; | ||
|
|
||
| const startDate: LimitDate = { | ||
| date: start, | ||
| rangeId: start === args.dateRange.start ? 'base' : 'compare', | ||
| rangeId: start === parsed[0]!.start ? 'base' : 'compare', | ||
| }; | ||
| const endDate: LimitDate = { | ||
| date: end, | ||
| rangeId: end === args.dateRange.end ? 'base' : 'compare', | ||
| rangeId: end === parsed[0]!.end ? 'base' : 'compare', | ||
| }; | ||
|
|
||
| if (!merged.length) { | ||
| merged.push({ | ||
| ...startDate, | ||
| approve: 0, | ||
| block_and_review: 0, | ||
| decline: 0, | ||
| review: 0, | ||
| }); | ||
| merged.push({ | ||
| ...endDate, | ||
| approve: 0, | ||
| block_and_review: 0, | ||
| decline: 0, | ||
| review: 0, | ||
| }); | ||
| } | ||
| const rangeSize = differenceInDays(end, start); | ||
|
|
||
| return adaptDecisionOutcomesPerDay( | ||
| rangeSize === merged.length ? merged : fillMissingDays(merged, startDate, endDate), | ||
| ); | ||
| } catch (error) { | ||
| console.error(error); | ||
| console.error('error in getDecisionOutcomesPerDay', error); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
That's quite complex to understand what's happening with the naming. Do you think you can rename those vars and maybe create an util function (IMO input refining is not the main point of the repository function)? And why is there a need to try/catch all that code?
There was a problem hiding this comment.
You are right. I moved the date ranges computation appart.
The try/catch block was useless
| try { | ||
| const urlParams = urlParamsSchema.parse(params); | ||
| const queryParams = queryParamsSchema.parse(await request.json()); | ||
| const query = await analytics.getAvailableFilters({ | ||
| scenarioId: urlParams.scenarioId, | ||
| ranges: queryParams.ranges, | ||
| }); | ||
| return Response.json(query); | ||
| } catch (error) { | ||
| console.error('error in available_filters', error); | ||
| return Response.json({ error: 'Invalid request' }, { status: 400 }); | ||
| } |
There was a problem hiding this comment.
No success true or false?
| return Response.json({ | ||
| decisionOutcomesPerDay, | ||
| ruleHitTable, | ||
| }); | ||
| } catch (error) { | ||
| console.error('error in analytics query', error); | ||
| return Response.json({ error: 'Internal server error' }, { status: 500 }); | ||
| } |
There was a problem hiding this comment.
Same here, no success field ?
| const toTextArray = (selected: unknown): string[] => { | ||
| const arr = selected as any[]; | ||
| if (!Array.isArray(arr)) return []; | ||
| return arr.flatMap((item) => { | ||
| if (typeof item === 'string') return item; | ||
| const val = (item as any)?.value; | ||
| return Array.isArray(val) ? val : val != null ? [val] : []; | ||
| }); | ||
| }; |
There was a problem hiding this comment.
Seems like this function could be created outside the component
There was a problem hiding this comment.
This was to keep the Text filter resilient to different formats.
Let's discuss this later
There was a problem hiding this comment.
I understand but the function is not using any component variable, hence it should be outside. I kinda want to know what you me to "keep it resilient to different formats" .
| const isActive = (name: string) => active.includes(name); | ||
| return { emitSet, emitRemove, emitToggleActive, getValue, isActive }; | ||
| }, [value, active, onChange]); | ||
| console.log(value); |
| ...(additionalFilters.length > 0 ? [['additional', additionalFilters] as const] : []), | ||
| ]); | ||
| }, [mainFilters, additionalFilters]); | ||
| console.log(allDescriptors); |
…comparison filter types
889037a to
872212d
Compare
…ng a map for display values
…for better readability
…ith clear and apply buttons
…onents for improved UI consistency
ChibiBlasphem
left a comment
There was a problem hiding this comment.
I think those are the last ones, sorry for having miss those.
| const [localText, setLocalText] = useState<string[]>( | ||
| filter.selectedValue?.map((item) => item.value) ?? [], | ||
| ); |
There was a problem hiding this comment.
Do you really need a state for that? localText seems to be a computed value from filter.selectedValue.
| const [localText, setLocalText] = useState<string[]>( | |
| filter.selectedValue?.map((item) => item.value) ?? [], | |
| ); | |
| const localText = filter.selectedValue?.map((item) => item.value) ?? []; |
| useEffect(() => { | ||
| if (isOpen) setLocalText(toTextArray(filter.selectedValue)); | ||
| if (isOpen) setLocalText(filter.selectedValue?.map((item) => item.value) ?? []); | ||
| }, [isOpen, filter.selectedValue]); |
There was a problem hiding this comment.
You don't need the effect with my suggestion above
…nents for consistency
…ed layout and accessibility
ChibiBlasphem
left a comment
There was a problem hiding this comment.
Overall it's good but I still have a big issue with naming (sv instead of selectedValue or op instead of operator). Think about new developers or developers who didn't code the feature as it can be a headache trying to understand the global flow of the feature while also trying to understand the meaning of some variables
| const exportedFields = | ||
| await context.authInfo.dataModelRepository.updateDataModelTableExportedFields(tableId, { | ||
| triggerObjectFields: current.triggerObjectFields, | ||
| ingestedDataFields: [...(current.ingestedDataFields ?? undefined), field], |
There was a problem hiding this comment.
🆙 this. I think it's better to do ...(current.ingestedDataFields ?? [])
| (() => { | ||
| const sv = filter.selectedValue ?? { operator: 'eq', value: 0 }; | ||
| const raw = (sv as any).value as unknown; | ||
| const sv = filter.selectedValue ?? { op: '=', value: 0 }; |
There was a problem hiding this comment.
Some variable naming makes it hard to read, sv could easily be named selectedValue
… prevent potential errors
…lter for clarity and consistency
…date actions for exported fields
… improved data handling
…ter for enhanced UI presentation
6cae943 to
891489a
Compare
| } catch { | ||
| setToastMessage(toastSession, { | ||
| type: 'success', | ||
| type: 'error', |
There was a problem hiding this comment.
Bug: Guard Clause Absence Causes Undefined Iteration Error
The update.ts route attempts to spread undefined into an array literal when current.ingestedDataFields is null or undefined. This causes a runtime TypeError: undefined is not iterable. The corresponding delete.ts route correctly uses an empty array fallback.
This PR refactors and enhances the filter logic used in the in-app analytics module to make it more robust, flexible, and user friendly. Key changes include UI improvements, logic simplification, and bug fixes.
🛠️ What’s Changed
• Refactor core filter logic to simplify and clarify flow, improving maintainability and reducing complexity.
• Add “Apply” button to explicitly trigger filter execution rather than applying changes immediately on interaction.
• Introduce “loading” + “touched” states to better communicate form interactivity and pending updates.
• Make compareRange optional, allowing filters to operate when comparison ranges are not defined.
• Move filter buttons to the second line for cleaner layout and better visual grouping.
• Support filter “unavailability” (i.e., marking filters as unavailable/inactive) to improve the UX when certain filters don’t apply.
• Fix scenario filter naming, correcting inconsistencies in how scenario filters are labeled.
• Fix select display issue, resolving UI glitches in how dropdown selections were rendered.
• Fix edit modal behavior, improving stability and addressing edge cases when editing existing filters.
• Prevent render warnings, eliminating React warnings triggered during certain render paths.
✅ Benefits & Impact
• Better UX clarity — users now see an explicit “Apply” action, and UI states (loading, touched) show better feedback.
• More robust filtering — optional comparison ranges and unavailable states allow filters to adapt to more data conditions.
• Cleaner, maintainable code — the refactor cleans up branching and makes the filter logic easier to follow and extend.
• Fewer UI glitches — thanks to fixes around select/label rendering and modal handling.
Note
Revamps in-app analytics filtering with an Apply-based FiltersBar, dynamic/static date ranges, backend field filters, and updated API/UX across app and UI kit.
Analytics UI/UX (app + UI kit):
{op, value}for text/number; supports boolean/select and date-range popover (dynamic/static via Temporal).routes/_analytics/analytics.$scenarioId.tsx): parsesq(base64 JSON) viaanalyticsFiltersQuery; volatile updates without navigation; builds trigger payloads from filter values; fetches available filters for selected ranges.Decisions.tsx): sanitize data, stabilizeResponsiveBarvia key, minor tick/format tweaks.Models & Queries:
dateRangeFilterSchema,getIsoBoundsFromDateRanges, dynamic/static transforms.name/op/valueand map to backendFieldFilterDto.ranges(computed to start/end); adapter updated.useGetAnalytics/useGetAvailableFiltersconsumeq, send new payloads, and expect{ success, data }envelopes with error handling.Server & Repository:
createServerFnwith auth/redirect middleware; validate via zod; return{ success, data }.API/OpenAPI:
TriggerFilterDtowithFieldFilterDto;AnalyticsQueryDtonow carriesfields.Localization & deps:
en/fr/ar.remeda,temporal-polyfill,ts-patternto app and UI kit.Misc fixes:
useDateRangeSearchParamshook.Written by Cursor Bugbot for commit 891489a. This will update automatically on new commits. Configure here.