-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(*) added a cell that can render and edit context dynamically
- Loading branch information
Showing
27 changed files
with
2,897 additions
and
216 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
101 changes: 101 additions & 0 deletions
101
apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/EditableDetailsV2.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { Button, TextWithNAFallback } from '@ballerine/ui'; | ||
|
||
import { FormField } from '../Form/Form.Field'; | ||
import { titleCase } from 'string-ts'; | ||
import { Form } from '../Form/Form'; | ||
import { FunctionComponent } from 'react'; | ||
import { FormItem } from '../Form/Form.Item'; | ||
import { FormLabel } from '../Form/Form.Label'; | ||
import { FormMessage } from '../Form/Form.Message'; | ||
import { TEditableDetailsV2Props } from './types'; | ||
import { useNewEditableDetailsLogic } from './hooks/useEditableDetailsV2Logic/useEditableDetailsV2Logic'; | ||
import { EditableDetailsV2Options } from './components/EditableDetailsV2Options'; | ||
import { EditableDetailV2 } from './components/EditableDetailV2'; | ||
|
||
export const EditableDetailsV2: FunctionComponent<TEditableDetailsV2Props> = ({ | ||
title, | ||
fields, | ||
onSubmit, | ||
onEnableIsEditable, | ||
onCancel, | ||
blacklist, | ||
whitelist, | ||
isEditable, | ||
isSaveDisabled, | ||
parse, | ||
}) => { | ||
if (blacklist && whitelist) { | ||
throw new Error('Cannot provide both blacklist and whitelist'); | ||
} | ||
|
||
const { form, handleSubmit, filteredFields } = useNewEditableDetailsLogic({ | ||
fields, | ||
blacklist, | ||
whitelist, | ||
onSubmit, | ||
}); | ||
|
||
return ( | ||
<div className={'px-3.5'}> | ||
<div className={'my-4 flex justify-between'}> | ||
<h2 className={'text-xl font-bold'}>{title}</h2> | ||
<EditableDetailsV2Options onEnableIsEditable={onEnableIsEditable} /> | ||
</div> | ||
<Form {...form}> | ||
<form onSubmit={form.handleSubmit(handleSubmit)}> | ||
<div className={'grid grid-cols-3 gap-x-4 gap-y-6'}> | ||
<legend className={'sr-only'}>{title}</legend> | ||
{filteredFields.map(({ title, path, props }) => { | ||
const originalValue = form.watch(path); | ||
|
||
return ( | ||
<FormField | ||
key={path} | ||
control={form.control} | ||
name={path} | ||
render={({ field }) => ( | ||
<FormItem> | ||
<TextWithNAFallback as={FormLabel} className={`block`}> | ||
{titleCase(title ?? '')} | ||
</TextWithNAFallback> | ||
<EditableDetailV2 | ||
type={props.type} | ||
format={props.format} | ||
minimum={props.minimum} | ||
maximum={props.maximum} | ||
pattern={props.pattern} | ||
options={props.options} | ||
isEditable={isEditable && props.isEditable} | ||
originalValue={originalValue} | ||
form={form} | ||
field={field} | ||
parse={parse} | ||
/> | ||
<FormMessage /> | ||
</FormItem> | ||
)} | ||
/> | ||
); | ||
})} | ||
</div> | ||
<div className={'min-h-12 mt-3 flex justify-end gap-x-3'}> | ||
{isEditable && filteredFields?.some(({ props }) => props.isEditable) && ( | ||
<Button type="button" onClick={onCancel}> | ||
Cancel | ||
</Button> | ||
)} | ||
{isEditable && filteredFields?.some(({ props }) => props.isEditable) && ( | ||
<Button | ||
type="submit" | ||
className={`aria-disabled:pointer-events-none aria-disabled:opacity-50`} | ||
aria-disabled={isSaveDisabled} | ||
> | ||
Save | ||
</Button> | ||
)} | ||
</div> | ||
</form> | ||
</Form> | ||
</div> | ||
); | ||
}; |
186 changes: 186 additions & 0 deletions
186
...fice-v2/src/common/components/organisms/EditableDetailsV2/components/EditableDetailV2.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
import { FunctionComponent, ComponentProps, useCallback, ChangeEvent } from 'react'; | ||
import { FieldValues, UseFormReturn } from 'react-hook-form'; | ||
import { ExtendedJson } from '@/common/types'; | ||
import { isValidDatetime } from '@/common/utils/is-valid-datetime'; | ||
import { FileJson2 } from 'lucide-react'; | ||
import { JsonDialog, ctw, BallerineLink, checkIsDate } from '@ballerine/ui'; | ||
import { isObject, isNullish, checkIsIsoDate, checkIsUrl } from '@ballerine/common'; | ||
import { Input } from '@ballerine/ui'; | ||
import { Select } from '../../../atoms/Select/Select'; | ||
import { SelectTrigger } from '../../../atoms/Select/Select.Trigger'; | ||
import { SelectValue } from '../../../atoms/Select/Select.Value'; | ||
import { SelectContent } from '../../../atoms/Select/Select.Content'; | ||
import { SelectItem } from '../../../atoms/Select/Select.Item'; | ||
import { keyFactory } from '@/common/utils/key-factory/key-factory'; | ||
import { Checkbox_ } from '../../../atoms/Checkbox_/Checkbox_'; | ||
import dayjs from 'dayjs'; | ||
import { ReadOnlyDetailV2 } from './ReadOnlyDetailV2'; | ||
import { getDisplayValue } from '../utils/get-display-value'; | ||
import { FormField } from '../../Form/Form.Field'; | ||
import { FormControl } from '../../Form/Form.Control'; | ||
import { getInputType } from '../utils/get-input-type'; | ||
|
||
export const EditableDetailV2: FunctionComponent<{ | ||
isEditable: boolean; | ||
className?: string; | ||
options?: Array<{ | ||
label: string; | ||
value: string; | ||
}>; | ||
form: UseFormReturn<FieldValues>; | ||
field: Parameters<ComponentProps<typeof FormField>['render']>[0]['field']; | ||
originalValue: ExtendedJson; | ||
type: string | undefined; | ||
format: string | undefined; | ||
minimum?: number; | ||
maximum?: number; | ||
pattern?: string; | ||
parse?: { | ||
date?: boolean; | ||
isoDate?: boolean; | ||
datetime?: boolean; | ||
boolean?: boolean; | ||
url?: boolean; | ||
nullish?: boolean; | ||
}; | ||
}> = ({ | ||
isEditable, | ||
className, | ||
options, | ||
originalValue, | ||
form, | ||
field, | ||
type, | ||
format, | ||
minimum, | ||
maximum, | ||
pattern, | ||
parse, | ||
}) => { | ||
const displayValue = getDisplayValue({ value: field.value, originalValue, isEditable }); | ||
const onInputChange = useCallback( | ||
(event: ChangeEvent<HTMLInputElement>) => { | ||
const value = event.target.value === 'N/A' ? '' : event.target.value; | ||
|
||
form.setValue(field.name, value); | ||
}, | ||
[field.name, form], | ||
); | ||
|
||
if (Array.isArray(field.value) || isObject(field.value)) { | ||
return ( | ||
<div className={ctw(`flex items-end justify-start`, className)}> | ||
<JsonDialog | ||
buttonProps={{ | ||
variant: 'link', | ||
className: 'p-0 text-blue-500', | ||
}} | ||
rightIcon={<FileJson2 size={`16`} />} | ||
dialogButtonText={`View Information`} | ||
json={JSON.stringify(field.value)} | ||
/> | ||
</div> | ||
); | ||
} | ||
|
||
if (isEditable && options) { | ||
return ( | ||
<Select disabled={!isEditable} onValueChange={field.onChange} defaultValue={field.value}> | ||
<FormControl> | ||
<SelectTrigger className="h-9 w-full border-input p-1 shadow-sm"> | ||
<SelectValue /> | ||
</SelectTrigger> | ||
</FormControl> | ||
<SelectContent> | ||
{options?.map(({ label, value }, index) => { | ||
return ( | ||
<SelectItem key={keyFactory(label, index?.toString(), `select-item`)} value={value}> | ||
{label} | ||
</SelectItem> | ||
); | ||
})} | ||
</SelectContent> | ||
</Select> | ||
); | ||
} | ||
|
||
if (parse?.boolean && (typeof field.value === 'boolean' || type === 'boolean')) { | ||
return ( | ||
<FormControl> | ||
<Checkbox_ | ||
disabled={!isEditable} | ||
checked={field.value} | ||
onCheckedChange={field.onChange} | ||
className={ctw('border-[#E5E7EB]', className)} | ||
/> | ||
</FormControl> | ||
); | ||
} | ||
|
||
if (isEditable) { | ||
const inputType = getInputType({ format, type, value: originalValue }); | ||
|
||
return ( | ||
<FormControl> | ||
<Input | ||
{...field} | ||
{...(typeof minimum === 'number' && { min: minimum })} | ||
{...(typeof maximum === 'number' && { max: maximum })} | ||
{...(pattern && { pattern })} | ||
{...(inputType === 'datetime-local' && { step: '1' })} | ||
type={inputType} | ||
value={displayValue} | ||
onChange={onInputChange} | ||
autoComplete={'off'} | ||
className={ctw(`p-1`, { | ||
'text-slate-400': isNullish(field.value) || field.value === '', | ||
})} | ||
/> | ||
</FormControl> | ||
); | ||
} | ||
|
||
if (typeof field.value === 'boolean' || type === 'boolean') { | ||
return <ReadOnlyDetailV2 className={className}>{`${field.value}`}</ReadOnlyDetailV2>; | ||
} | ||
|
||
if (parse?.url && checkIsUrl(field.value)) { | ||
return ( | ||
<BallerineLink href={field.value} className={className}> | ||
{field.value} | ||
</BallerineLink> | ||
); | ||
} | ||
|
||
if (parse?.datetime && (isValidDatetime(field.value) || type === 'date-time')) { | ||
const value = field.value.endsWith(':00') ? field.value : `${field.value}:00`; | ||
|
||
return ( | ||
<ReadOnlyDetailV2 className={className}> | ||
{dayjs(value).utc().format('DD/MM/YYYY HH:mm')} | ||
</ReadOnlyDetailV2> | ||
); | ||
} | ||
|
||
if ( | ||
(parse?.date && checkIsDate(field.value, { isStrict: false })) || | ||
(parse?.isoDate && checkIsIsoDate(field.value)) || | ||
(type === 'date' && (parse?.date || parse?.isoDate)) | ||
) { | ||
return ( | ||
<ReadOnlyDetailV2 className={className}> | ||
{dayjs(field.value).format('DD/MM/YYYY')} | ||
</ReadOnlyDetailV2> | ||
); | ||
} | ||
|
||
if (parse?.nullish && isNullish(field.value)) { | ||
return <ReadOnlyDetailV2 className={className}>{field.value}</ReadOnlyDetailV2>; | ||
} | ||
|
||
if (isNullish(field.value)) { | ||
return <ReadOnlyDetailV2 className={className}>{`${field.value}`}</ReadOnlyDetailV2>; | ||
} | ||
|
||
return <ReadOnlyDetailV2 className={className}>{field.value}</ReadOnlyDetailV2>; | ||
}; |
34 changes: 34 additions & 0 deletions
34
...src/common/components/organisms/EditableDetailsV2/components/EditableDetailsV2Options.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { | ||
DropdownMenuContent, | ||
Button, | ||
DropdownMenu, | ||
DropdownMenuTrigger, | ||
DropdownMenuItem, | ||
} from '@ballerine/ui'; | ||
import { Edit } from 'lucide-react'; | ||
import { FunctionComponent } from 'react'; | ||
|
||
export const EditableDetailsV2Options: FunctionComponent<{ | ||
onEnableIsEditable: () => void; | ||
}> = ({ onEnableIsEditable }) => { | ||
return ( | ||
<DropdownMenu> | ||
<DropdownMenuTrigger asChild> | ||
<Button variant="outline" className={'px-2 py-0 text-xs'}> | ||
Options | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent align="end"> | ||
<DropdownMenuItem className={`h-6 w-full`} asChild> | ||
<Button | ||
variant={'ghost'} | ||
className="justify-start text-xs leading-tight" | ||
onClick={onEnableIsEditable} | ||
> | ||
<Edit size={16} className="me-2" /> Edit | ||
</Button> | ||
</DropdownMenuItem> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
); | ||
}; |
24 changes: 24 additions & 0 deletions
24
...fice-v2/src/common/components/organisms/EditableDetailsV2/components/ReadOnlyDetailV2.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { TextWithNAFallback, ctw } from '@ballerine/ui'; | ||
import { FunctionComponent, ComponentProps } from 'react'; | ||
|
||
export const ReadOnlyDetailV2: FunctionComponent<ComponentProps<typeof TextWithNAFallback>> = ({ | ||
children, | ||
className, | ||
...props | ||
}) => { | ||
return ( | ||
<TextWithNAFallback | ||
as={'div'} | ||
tabIndex={0} | ||
role={'textbox'} | ||
aria-readonly | ||
{...props} | ||
className={ctw( | ||
'flex h-9 w-full max-w-[30ch] items-center break-all rounded-md border border-transparent p-1 pt-1.5 text-sm', | ||
className, | ||
)} | ||
> | ||
{children} | ||
</TextWithNAFallback> | ||
); | ||
}; |
1 change: 1 addition & 0 deletions
1
apps/backoffice-v2/src/common/components/organisms/EditableDetailsV2/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const __ROOT__ = '__ROOT__'; |
Oops, something went wrong.