-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(dashboard): Digest liquid helper and popover handler #7439
Changes from all commits
997c1f8
feaf04c
163ad78
b2111ec
65733c8
624383a
75d2c7d
faa410b
96acb70
80ed1f3
6ec81ab
25141c8
c32cd7e
2c3e6a9
bf9324a
fefc2cc
96ffc1b
ed3eaae
c6be4ab
ec924ed
e717bc8
e56a5c6
c83af5c
93c1d46
50a25c9
74b7ba4
e78ee36
b378aac
c0f0c3e
05842f0
48b448c
62c48c2
ecc8a8c
fb0901f
95f249b
92f915f
f970981
f97c433
44c678a
2bd08b0
a20ee80
7209172
64564ef
d00d441
36da57b
c10c935
d355467
a148965
71d6d08
d1d512a
17164ed
bda722a
cc3b33b
a8c1816
967be19
dc7d907
3601b0d
b76fbfa
a88ba03
076d365
43bbbaa
eea4a0e
a0b3f91
81d94b4
a2b8f7a
e656ad4
4fdab39
1045e1e
650a5ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { useMemo } from 'react'; | ||
import { FILTERS } from '../constants'; | ||
import { Filters, FilterWithParam } from '../types'; | ||
|
||
type SuggestionGroup = { | ||
label: string; | ||
filters: Filters[]; | ||
}; | ||
|
||
export function useSuggestedFilters(variableName: string, currentFilters: FilterWithParam[]): SuggestionGroup[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These provide filter suggestion based on common variable names, patterns of data |
||
return useMemo(() => { | ||
const currentFilterValues = new Set(currentFilters.map((f) => f.value)); | ||
const suggestedFilters: Filters[] = []; | ||
|
||
const addSuggestions = (filterValues: string[]) => { | ||
const newFilters = FILTERS.filter((f) => filterValues.includes(f.value) && !currentFilterValues.has(f.value)); | ||
|
||
suggestedFilters.push(...newFilters); | ||
}; | ||
|
||
if (isStepsEventsPattern(variableName)) { | ||
addSuggestions(['digest']); | ||
} | ||
|
||
if (isDateVariable(variableName)) { | ||
addSuggestions(['date']); | ||
} | ||
|
||
if (isNumberVariable(variableName)) { | ||
addSuggestions(['round', 'floor', 'ceil', 'abs', 'plus', 'minus', 'times', 'divided_by']); | ||
} | ||
|
||
if (isArrayVariable(variableName)) { | ||
addSuggestions(['first', 'last', 'join', 'map', 'where', 'size']); | ||
} | ||
|
||
if (isTextVariable(variableName)) { | ||
addSuggestions(['upcase', 'downcase', 'capitalize', 'truncate', 'truncatewords']); | ||
} | ||
|
||
return suggestedFilters.length > 0 ? [{ label: 'Suggested', filters: suggestedFilters }] : []; | ||
}, [variableName, currentFilters]); | ||
} | ||
|
||
function isDateVariable(name: string): boolean { | ||
const datePatterns = ['date', 'time', 'created', 'updated', 'timestamp', 'scheduled']; | ||
|
||
return datePatterns.some((pattern) => name.toLowerCase().includes(pattern)); | ||
} | ||
|
||
function isNumberVariable(name: string): boolean { | ||
const numberPatterns = ['count', 'amount', 'total', 'price', 'quantity', 'number', 'sum', 'age']; | ||
|
||
return numberPatterns.some((pattern) => name.toLowerCase().includes(pattern)); | ||
} | ||
|
||
function isArrayVariable(name: string): boolean { | ||
const arrayPatterns = ['list', 'array', 'items', 'collection', 'set', 'group', 'events']; | ||
|
||
return arrayPatterns.some((pattern) => name.toLowerCase().includes(pattern)); | ||
} | ||
|
||
function isTextVariable(name: string): boolean { | ||
const textPatterns = ['name', 'title', 'description', 'text', 'message', 'content', 'label']; | ||
|
||
return textPatterns.some((pattern) => name.toLowerCase().includes(pattern)); | ||
} | ||
|
||
function isStepsEventsPattern(name: string): boolean { | ||
return /^steps\..*\.events$/.test(name); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
import { Liquid } from 'liquidjs'; | ||
import { digest } from './filters/digest'; | ||
|
||
import { ChannelStepEnum, PostActionEnum } from './constants'; | ||
import { | ||
|
@@ -76,9 +77,11 @@ export class Client { | |
this.apiUrl = builtOpts.apiUrl; | ||
this.secretKey = builtOpts.secretKey; | ||
this.strictAuthentication = builtOpts.strictAuthentication; | ||
|
||
this.templateEngine.registerFilter('json', (value, spaces) => | ||
stringifyDataStructureWithSingleQuotes(value, spaces) | ||
); | ||
this.templateEngine.registerFilter('digest', digest); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @SokratisVidros any name suggestions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Digest is fine, as it points to the step. |
||
} | ||
|
||
private buildOptions(providedOptions?: ClientOptions) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
type NestedObject = Record<string, unknown>; | ||
|
||
function getNestedValue(obj: NestedObject, path: string): string { | ||
const value = path.split('.').reduce((current: unknown, key) => { | ||
if (current && typeof current === 'object') { | ||
return (current as Record<string, unknown>)[key]; | ||
} | ||
|
||
return undefined; | ||
}, obj); | ||
|
||
if (value === null || value === undefined) return ''; | ||
if (typeof value === 'string') return value; | ||
if (typeof value === 'number' || typeof value === 'boolean') return String(value); | ||
if (typeof value === 'object') { | ||
const stringified = JSON.stringify(value); | ||
|
||
return stringified === '{}' ? '' : stringified; | ||
} | ||
|
||
return ''; | ||
} | ||
|
||
/** | ||
* Format a list of items for digest notifications with configurable behavior | ||
* Default formatting: | ||
* - 1 item: "John" | ||
* - 2 items: "John and Josh" | ||
* - 3 items: "John, Josh and Sarah" | ||
* - 4+ items: "John, Josh and 2 others" | ||
* | ||
* @param array The array of items to format | ||
* @param maxNames Maximum names to show before using "others" | ||
* @param keyPath Path to extract from objects (e.g., "name" or "profile.name") | ||
* @param separator Custom separator between names (default: ", ") | ||
* @returns Formatted string | ||
* | ||
* Examples: | ||
* {{ actors | digest }} => "John, Josh and 2 others" | ||
* {{ actors | digest: 2 }} => "John, Josh and 3 others" | ||
* {{ users | digest: 2, "name" }} => For array of {name: string} | ||
* {{ users | digest: 2, "profile.name", "•" }} => "John • Josh and 3 others" | ||
*/ | ||
export function digest(array: unknown, maxNames = 2, keyPath?: string, separator = ', '): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if that's the best name 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about summarize? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, I think it would be better to have they keyPath required? As it might be a common way to receive here an array of objects vs array of strings |
||
if (!Array.isArray(array) || array.length === 0) return ''; | ||
|
||
const values = keyPath | ||
? array.map((item) => { | ||
if (typeof item !== 'object' || !item) return ''; | ||
|
||
return getNestedValue(item as NestedObject, keyPath); | ||
}) | ||
: array; | ||
|
||
if (values.length === 1) return values[0]; | ||
if (values.length === 2) return `${values[0]} and ${values[1]}`; | ||
|
||
if (values.length === 3 && maxNames >= 3) { | ||
return `${values[0]}, ${separator}${values[1]} and ${values[2]}`; | ||
} | ||
|
||
// Use "others" format for 4+ items or when maxNames is less than array length | ||
const shownItems = values.slice(0, maxNames); | ||
const othersCount = values.length - maxNames; | ||
|
||
return `${shownItems.join(separator)} and ${othersCount} ${othersCount === 1 ? 'other' : 'others'}`; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently we have 2 version of liquid one in the framework and one in the API, didn't do it in this PR but worth considering consolidating them somehow or exporting from the framework the parser