Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/components/RLCrud/RLCrud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,15 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
: rowsPerPageOptions[0]
)
const [totalRows, setTotalRows] = useState(0)
const [filtersApplied, setFiltersApplied] = useState<Record<string, unknown>>({})
const [filtersApplied, setFiltersApplied] = useState<Record<string, unknown>>(() =>
filtersConfig.reduce(
(acc, filter) => ({
...acc,
[filter.value]: filter.default_value
}),
{}
)
)
const [showDialog, setShowDialog] = useState(false)
const [dialog, setDialog] = useState<string | null>(null)
const [dialogProps, setDialogProps] = useState<Record<string, unknown>>({})
Expand Down Expand Up @@ -169,7 +177,7 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(

const onFiltersApplied = useCallback((appliedFilters: Record<string, unknown>) => {
setFiltersApplied(appliedFilters)
setCurrentPage(1)
setCurrentPage(0)
}, [])

const onConfirm = useCallback(async () => {
Expand Down Expand Up @@ -207,7 +215,7 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
setFiltersApplied({ [primary_key]: newId as RLCrudInputValueType })
filtersRef.current?.setFilterModel({ [primary_key]: newId as RLCrudInputValueType })
filtersRef.current?.setOpen(true)
setCurrentPage(1)
setCurrentPage(0)
await Promise.resolve() // Allow state to update
skipWatchersRef.current = false
}
Expand Down Expand Up @@ -318,8 +326,8 @@ export const RLCrud = forwardRef<RLCrudRef, RLCrudProps>(
{bottomContainerSlot}
</div>
<RLPaginator
page={currentPage}
onPageChange={setCurrentPage}
page={currentPage + 1}
onPageChange={(page) => setCurrentPage(page - 1)}
rowsPerPage={rowsPerPage}
onRowsPerPageChange={setRowsPerPage}
totalRows={totalRows}
Expand Down
3 changes: 1 addition & 2 deletions src/components/RLCrudFilters/RLCrudFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ export const RLCrudFilters = forwardRef<RLCrudFiltersRef, RLCrudFiltersProps>(

// Initialize on mount
useEffect(() => {
const initialFilters = resetFields()
onFiltersApplied?.(initialFilters)
resetFields()
}, []) // eslint-disable-line react-hooks/exhaustive-deps

const handleApply = useCallback(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/RLDialog/RLDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const RLDialog = forwardRef<RLDialogRef, RLDialogProps>(
<SlDialog
ref={dialogRef}
className={className ?? 'dialog'}
label={label}
label={label ?? ''}
open={open}
noHeader={noHeader}
onSlShow={handleShow}
Expand Down
227 changes: 227 additions & 0 deletions src/components/examples/UsersCrudExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import type { ComponentType } from 'react'
import { RLCrud } from '../RLCrud'
import type { RLCrudProps } from '../RLCrud/types'
import { addIcon } from '../../icons'
import { ActiveCell } from './cells/ActiveCell'
import { DateCell } from './cells/DateCell'
import { DeleteDialog } from './dialogs/DeleteDialog'
import { usersStore } from './stores/usersStore'

import ghost from '@mdi/svg/svg/ghost.svg'
import deleteIcon from '@mdi/svg/svg/delete.svg'

addIcon('ghost', ghost)
addIcon('delete', deleteIcon)

const users_crud: Omit<RLCrudProps, 'getItems'> = {
id: 'users',
singular_label: 'user',
primary_key: 'id',
filters_title: 'filters',
headers: [
{
i18n_key: 'Username',
sortable: false,
value: 'username',
columnProps: {
className: 'w-1/4'
}
},
{
i18n_key: 'First name',
sortable: false,
value: 'firstName'
},
{
i18n_key: 'Last name',
sortable: false,
value: 'lastName'
},
{
i18n_key: 'Role',
sortable: false,
value: 'role'
},
{
i18n_key: 'Age',
sortable: false,
value: 'age'
},
{
i18n_key: 'Active',
sortable: false,
value: 'active',
type: 'boolean',
componentProps: {
trueColor: 'text-success-500'
}
},
{
i18n_key: 'Activation date',
value: 'activation_date',
sortable: false,
type: 'date'
},
{
i18n_key: 'Expiration date',
value: 'expiration_date',
sortable: false,
type: 'date'
},
{
i18n_key: 'Description',
value: 'description'
}
],
filters: [
{
i18n_key: 'Username',
value: 'username',
input_type: 'text'
},
{
i18n_key: 'First name',
value: 'firstName',
input_type: 'text'
},
{
i18n_key: 'Last name',
value: 'lastName',
input_type: 'text'
},
{
i18n_key: 'Role',
value: 'role',
input_type: 'select',
options: [
{ value: '', text: '' },
{ value: 'admin', text: 'admin' },
{ value: 'user', text: 'user' },
{ value: 'guest', text: 'guest', icon: 'ghost' }
],
default_value: ''
},
{
i18n_key: 'Activation date',
value: 'activation_date',
input_type: 'date'
}
],
form_fields: [
{
i18n_key: 'Username',
value: 'username',
placeholder: 'Enter username',
required: true,
rules: [
{ validateFn: (v: unknown) => !!(v as string), message: 'Username is required' },
{
validateFn: (v: unknown) => (v as string).length > 3,
message: 'Username must be at least 4 characters long'
}
],
side_effect: (model, fields) => {
const { username } = model as { username: string }
if (username === 'admin') {
fields.role.options = [{ value: 'admin', text: 'admin' }]
;(model as { role: string }).role = 'admin'
} else {
fields.role.options = [
{ value: '', text: '' },
{ value: 'admin', text: 'admin' },
{ value: 'user', text: 'user' },
{ value: 'guest', text: 'guest' }
]
}
},
input_type: 'text'
},
{
i18n_key: 'First name',
value: 'firstName',
input_type: 'text'
},
{
i18n_key: 'Last name',
value: 'lastName',
input_type: 'text'
},
{
i18n_key: 'Active',
value: 'active',
input_type: 'checkbox',
default_value: true
},
{
i18n_key: 'Role',
value: 'role',
input_type: 'select',
options: [
{ value: '', text: '' },
{ value: 'admin', text: 'admin' },
{ value: 'user', text: 'user' },
{ value: 'guest', text: 'guest' }
]
},
{
i18n_key: 'Age',
value: 'age',
input_type: 'number'
},
{
i18n_key: 'Activation date',
value: 'activation_date',
input_type: 'date'
},
{
i18n_key: 'Expiration date',
value: 'expiration_date',
input_type: 'date'
},
{
i18n_key: 'Description',
value: 'description',
input_type: 'textarea'
}
],
actions: [
{
name: 'Delete',
i18n_key: 'Delete',
icon_name: 'delete',
onClick: (data: unknown) => {
console.log('Delete side effect', { ...data as object })
},
component: DeleteDialog,
dialogProperties: {
noCloseOnOutsideClick: false
}
}
]
}

export const UsersCrudExample = () => {
return (
<RLCrud
{...users_crud}
getItems={usersStore.getUsers}
addItem={usersStore.createUser}
editItem={usersStore.updateUser}
components={{
boolean: ActiveCell as ComponentType<unknown>,
date: DateCell as ComponentType<unknown>
}}
actionHeaderI18nKey="Actions"
addI18nKey="Add"
applyI18nKey="Apply"
resetI18nKey="Reset"
cancelI18nKey="Cancel"
addButtonI18nKey="Add user"
addTitleI18nKey="Add user"
editTitleI18nKey="Edit user"
editTooltipI18nKey="Edit"
/>
)
}

UsersCrudExample.displayName = 'UsersCrudExample'
21 changes: 21 additions & 0 deletions src/components/examples/actions/ActionDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { RLIcon } from '../../RLIcon'
import { addIcon } from '../../../icons'

import deleteCircle from '@mdi/svg/svg/delete-circle.svg'

addIcon('deleteCircle', deleteCircle)

interface ActionDeleteProps {
data: unknown
}

export const ActionDelete = (_props: ActionDeleteProps) => {
return (
<RLIcon
className="text-3xl text-red-500 cursor-pointer hover:opacity-40"
name="deleteCircle"
/>
)
}

ActionDelete.displayName = 'ActionDelete'
21 changes: 21 additions & 0 deletions src/components/examples/actions/ActionEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { RLIcon } from '../../RLIcon'
import { addIcon } from '../../../icons'

import pencilCircle from '@mdi/svg/svg/pencil-circle.svg'

addIcon('pencilCircle', pencilCircle)

interface ActionEditProps {
data: unknown
}

export const ActionEdit = (_props: ActionEditProps) => {
return (
<RLIcon
className="text-3xl text-red-500 cursor-pointer hover:opacity-40"
name="pencilCircle"
/>
)
}

ActionEdit.displayName = 'ActionEdit'
27 changes: 27 additions & 0 deletions src/components/examples/cells/ActiveCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { RLIcon } from '../../RLIcon'
import { addIcon } from '../../../icons'

import checkCircle from '@mdi/svg/svg/check-circle.svg'
import closeCircle from '@mdi/svg/svg/close-circle.svg'

addIcon('checkCircle', checkCircle)
addIcon('closeCircle', closeCircle)

interface ActiveCellProps {
data?: unknown
trueColor?: string
}

export const ActiveCell = ({ data, trueColor = 'text-green-500' }: ActiveCellProps) => {
const typedData = data as { active?: boolean } | undefined
const isActive = typedData?.active

return (
<RLIcon
className={`text-2xl ${isActive ? trueColor : 'text-red-500'}`}
name={isActive ? 'checkCircle' : 'closeCircle'}
/>
)
}

ActiveCell.displayName = 'ActiveCell'
23 changes: 23 additions & 0 deletions src/components/examples/cells/DateCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
interface DateCellProps {
data?: unknown
field?: string
}

export const DateCell = ({ data, field }: DateCellProps) => {
if (!data || !field) {
return <div />
}

const typedData = data as { [key: string]: unknown }
const dateValue = typedData[field] as Date | undefined

if (!dateValue || !(dateValue instanceof Date)) {
return <div />
}

const formattedDate = `${dateValue.getFullYear()}-${('0' + (dateValue.getMonth() + 1)).slice(-2)}-${('0' + dateValue.getDate()).slice(-2)}`

return <div>{formattedDate}</div>
}

DateCell.displayName = 'DateCell'
Loading