-
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): scheduled digest #7314
base: next
Are you sure you want to change the base?
Changes from 1 commit
3529190
162e086
49ef9d5
a384a7e
e7ce5fd
3b2b19f
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 |
---|---|---|
|
@@ -60,6 +60,7 @@ | |
"@sentry/react": "^8.35.0", | ||
"@tanstack/react-query": "^5.59.6", | ||
"@types/js-cookie": "^3.0.6", | ||
"@types/lodash.isequal": "^4.5.8", | ||
"@uiw/codemirror-extensions-langs": "^4.23.6", | ||
"@uiw/codemirror-theme-material": "^4.23.6", | ||
"@uiw/codemirror-theme-white": "^4.23.6", | ||
|
@@ -69,11 +70,13 @@ | |
"class-variance-authority": "^0.7.0", | ||
"clsx": "^2.1.1", | ||
"cmdk": "1.0.0", | ||
"cron-parser": "^4.9.0", | ||
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. the library used to parse and generate cron expression |
||
"date-fns": "^4.1.0", | ||
"flat": "^6.0.1", | ||
"js-cookie": "^3.0.5", | ||
"launchdarkly-react-client-sdk": "^3.3.2", | ||
"lodash.debounce": "^4.0.8", | ||
"lodash.isequal": "^4.5.0", | ||
"lodash.merge": "^4.6.2", | ||
"lucide-react": "^0.439.0", | ||
"mixpanel-browser": "^2.52.0", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import { FocusEventHandler } from 'react'; | ||
import { useFormContext } from 'react-hook-form'; | ||
|
||
import { cn } from '@/utils/ui'; | ||
import { FormControl, FormField, FormItem, FormMessagePure } from '@/components/primitives/form/form'; | ||
import { Input } from '@/components/primitives/input'; | ||
import { InputFieldPure } from '@/components/primitives/input'; | ||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/primitives/select'; | ||
import { useFormContext } from 'react-hook-form'; | ||
import { AUTOCOMPLETE_PASSWORD_MANAGERS_OFF } from '@/utils/constants'; | ||
|
||
const HEIGHT = { | ||
|
@@ -22,18 +24,124 @@ type InputWithSelectProps = { | |
inputKey: string; | ||
selectKey: string; | ||
}; | ||
options: string[]; | ||
options: Array<{ label: string; value: string }>; | ||
defaultOption?: string; | ||
className?: string; | ||
placeholder?: string; | ||
isReadOnly?: boolean; | ||
onValueChange?: (value: string) => void; | ||
onValueChange?: () => void; | ||
size?: 'sm' | 'md'; | ||
min?: number; | ||
showError?: boolean; | ||
shouldUnregister?: boolean; | ||
}; | ||
|
||
const AmountInputContainer = ({ | ||
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. All the changes I made here are: split components into smaller independent pieces so they can be reused without being dependent on the form |
||
children, | ||
className, | ||
size = 'sm', | ||
hasError, | ||
}: { | ||
children?: React.ReactNode | React.ReactNode[]; | ||
className?: string; | ||
size?: 'sm' | 'md'; | ||
hasError?: boolean; | ||
}) => { | ||
return ( | ||
<InputFieldPure | ||
className={cn(HEIGHT[size].base, 'rounded-lg border pr-0', className)} | ||
state={hasError ? 'error' : 'default'} | ||
> | ||
{children} | ||
</InputFieldPure> | ||
); | ||
}; | ||
|
||
export const AmountInput = ({ | ||
const AmountInputField = ({ | ||
value, | ||
min, | ||
placeholder, | ||
disabled, | ||
onChange, | ||
onBlur, | ||
}: { | ||
value?: string | number; | ||
placeholder?: string; | ||
disabled?: boolean; | ||
min?: number; | ||
onChange: (arg: string | number) => void; | ||
onBlur?: FocusEventHandler<HTMLInputElement>; | ||
}) => { | ||
return ( | ||
<Input | ||
type="number" | ||
className="font-code min-w-[20ch] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" | ||
placeholder={placeholder} | ||
disabled={disabled} | ||
value={value} | ||
onKeyDown={(e) => { | ||
if (e.key === 'e' || e.key === '-' || e.key === '+' || e.key === '.' || e.key === ',') { | ||
e.preventDefault(); | ||
} | ||
}} | ||
onChange={(e) => { | ||
if (e.target.value === '') { | ||
onChange(''); | ||
return; | ||
} | ||
|
||
const numberValue = Number(e.target.value); | ||
onChange(numberValue); | ||
}} | ||
min={min} | ||
onBlur={onBlur} | ||
{...AUTOCOMPLETE_PASSWORD_MANAGERS_OFF} | ||
/> | ||
); | ||
}; | ||
|
||
const AmountUnitSelect = ({ | ||
value, | ||
defaultOption, | ||
options, | ||
size = 'sm', | ||
disabled, | ||
onValueChange, | ||
}: { | ||
value?: string; | ||
defaultOption?: string; | ||
options: Array<{ label: string; value: string }>; | ||
size?: 'sm' | 'md'; | ||
disabled?: boolean; | ||
onValueChange?: (val: string) => void; | ||
}) => { | ||
return ( | ||
<Select onValueChange={onValueChange} defaultValue={defaultOption} disabled={disabled} value={value}> | ||
<SelectTrigger | ||
className={cn( | ||
HEIGHT[size].trigger, | ||
'w-auto gap-1 rounded-l-none border-x-0 border-y-0 border-l bg-neutral-50 p-2 text-xs' | ||
)} | ||
> | ||
<SelectValue /> | ||
</SelectTrigger> | ||
<SelectContent | ||
onBlur={(e) => { | ||
e.preventDefault(); | ||
e.stopPropagation(); | ||
}} | ||
> | ||
{options.map(({ label, value }) => ( | ||
<SelectItem key={value} value={value}> | ||
{label} | ||
</SelectItem> | ||
))} | ||
</SelectContent> | ||
</Select> | ||
); | ||
}; | ||
|
||
const AmountInput = ({ | ||
fields, | ||
options, | ||
defaultOption, | ||
|
@@ -44,6 +152,7 @@ export const AmountInput = ({ | |
size = 'sm', | ||
min, | ||
showError = true, | ||
shouldUnregister = false, | ||
}: InputWithSelectProps) => { | ||
const { getFieldState, setValue, control } = useFormContext(); | ||
|
||
|
@@ -53,38 +162,23 @@ export const AmountInput = ({ | |
|
||
return ( | ||
<> | ||
<InputFieldPure | ||
className={cn(HEIGHT[size].base, 'rounded-lg border pr-0', className)} | ||
state={input.error ? 'error' : 'default'} | ||
> | ||
<AmountInputContainer className={className} hasError={!!input.error}> | ||
<FormField | ||
control={control} | ||
name={fields.inputKey} | ||
shouldUnregister={shouldUnregister} | ||
render={({ field }) => ( | ||
<FormItem className="w-full overflow-hidden"> | ||
<FormControl> | ||
<Input | ||
type="number" | ||
className="min-w-[20ch] [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" | ||
<AmountInputField | ||
placeholder={placeholder} | ||
disabled={isReadOnly} | ||
value={field.value} | ||
onKeyDown={(e) => { | ||
if (e.key === 'e' || e.key === '-' || e.key === '+' || e.key === '.' || e.key === ',') { | ||
e.preventDefault(); | ||
} | ||
}} | ||
onChange={(e) => { | ||
if (e.target.value === '') { | ||
field.onChange(''); | ||
return; | ||
} | ||
|
||
const numberValue = Number(e.target.value); | ||
field.onChange(numberValue); | ||
onChange={field.onChange} | ||
onBlur={() => { | ||
onValueChange?.(); | ||
}} | ||
min={min} | ||
{...AUTOCOMPLETE_PASSWORD_MANAGERS_OFF} | ||
/> | ||
</FormControl> | ||
</FormItem> | ||
|
@@ -93,40 +187,29 @@ export const AmountInput = ({ | |
<FormField | ||
control={control} | ||
name={fields.selectKey} | ||
shouldUnregister={shouldUnregister} | ||
render={({ field }) => ( | ||
<FormItem> | ||
<FormControl> | ||
<Select | ||
<AmountUnitSelect | ||
value={field.value} | ||
defaultOption={defaultOption} | ||
options={options} | ||
size={size} | ||
disabled={isReadOnly} | ||
onValueChange={(value) => { | ||
setValue(fields.selectKey, value, { shouldDirty: true }); | ||
onValueChange?.(value); | ||
onValueChange?.(); | ||
}} | ||
defaultValue={defaultOption} | ||
disabled={isReadOnly} | ||
value={field.value} | ||
> | ||
<SelectTrigger | ||
className={cn( | ||
HEIGHT[size].trigger, | ||
'w-auto gap-1 rounded-l-none border-x-0 border-y-0 border-l bg-neutral-50 p-2 text-xs' | ||
)} | ||
> | ||
<SelectValue /> | ||
</SelectTrigger> | ||
<SelectContent> | ||
{options.map((option) => ( | ||
<SelectItem key={option} value={option}> | ||
{option} | ||
</SelectItem> | ||
))} | ||
</SelectContent> | ||
</Select> | ||
/> | ||
</FormControl> | ||
</FormItem> | ||
)} | ||
/> | ||
</InputFieldPure> | ||
</AmountInputContainer> | ||
{showError && <FormMessagePure error={error ? String(error.message) : undefined} />} | ||
</> | ||
); | ||
}; | ||
|
||
export { AmountInput, AmountInputContainer, AmountInputField, AmountUnitSelect }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,7 +61,7 @@ const FormLabel = React.forwardRef< | |
> | ||
<BsFillInfoCircleFill className="text-foreground-300 -mt-0.5 inline size-3" /> | ||
</TooltipTrigger> | ||
<TooltipContent>{tooltip}</TooltipContent> | ||
<TooltipContent className="max-w-56">{tooltip}</TooltipContent> | ||
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. Adjust the max width of the tooltip content so it doesn't look weird |
||
</Tooltip> | ||
)} | ||
|
||
|
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.
the font used for the
amount
input