chore: use monorepo organization

This commit is contained in:
Rafael Taranto 2025-05-12 10:52:54 +01:00
parent deaf7d6ecc
commit a687827f7e
1099 changed files with 8184 additions and 11535 deletions

View file

@ -0,0 +1,3 @@
import React from 'react'
export default React.createContext()

View file

@ -0,0 +1,129 @@
import classnames from 'classnames'
import * as R from 'ramda'
import React, { useContext } from 'react'
import {
Td,
THead,
TDoubleLevelHead,
ThDoubleLevel
} from 'src/components/fake-table/Table'
import { sentenceCase } from 'src/utils/string'
import TableCtx from './Context'
const groupSecondHeader = elements => {
const doubleHeader = R.prop('doubleHeader')
const sameDoubleHeader = (a, b) => doubleHeader(a) === doubleHeader(b)
const group = R.pipe(
R.groupWith(sameDoubleHeader),
R.map(group =>
R.isNil(doubleHeader(group[0])) // No doubleHeader
? group
: [
{
width: R.sum(R.map(R.prop('width'), group)),
elements: group,
name: doubleHeader(group[0])
}
]
),
R.reduce(R.concat, [])
)
return R.all(R.pipe(doubleHeader, R.isNil), elements)
? [elements, THead]
: [group(elements), TDoubleLevelHead]
}
const Header = () => {
const {
elements,
enableEdit,
enableEditText,
editWidth,
enableDelete,
deleteWidth,
enableToggle,
toggleWidth,
orderedBy,
DEFAULT_COL_SIZE
} = useContext(TableCtx)
const mapElement2 = (it, idx) => {
const { width, elements, name } = it
if (elements && elements.length) {
return (
<ThDoubleLevel key={idx} width={width} title={name}>
{elements.map(mapElement)}
</ThDoubleLevel>
)
}
return mapElement(it, idx)
}
const mapElement = (
{ name, display, width = DEFAULT_COL_SIZE, header, textAlign },
idx
) => {
const orderClasses = classnames({
'whitespace-nowrap':
R.isNil(header) && !R.isNil(orderedBy) && R.equals(name, orderedBy.code)
})
const attachOrderedByToComplexHeader = header => {
if (!R.isNil(orderedBy) && R.equals(name, orderedBy.code)) {
try {
const cloneHeader = R.clone(header)
const children = R.path(['props', 'children'], cloneHeader)
const spanChild = R.find(it => R.equals(it.type, 'span'), children)
spanChild.props.children = R.append(' -', spanChild.props.children)
return cloneHeader
} catch (e) {
return header
}
}
return header
}
return (
<Td header key={idx} width={width} textAlign={textAlign}>
{!R.isNil(header) ? (
<>{attachOrderedByToComplexHeader(header) ?? header}</>
) : (
<span className={orderClasses}>
{!R.isNil(display) ? display : sentenceCase(name)}{' '}
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
</span>
)}
</Td>
)
}
const [innerElements, HeaderElement] = groupSecondHeader(elements)
return (
<HeaderElement>
{innerElements.map(mapElement2)}
{enableEdit && (
<Td header width={editWidth} textAlign="center">
{enableEditText ?? `Edit`}
</Td>
)}
{enableDelete && (
<Td header width={deleteWidth} textAlign="center">
Delete
</Td>
)}
{enableToggle && (
<Td header width={toggleWidth} textAlign="center">
Enable
</Td>
)}
</HeaderElement>
)
}
export default Header

View file

@ -0,0 +1,29 @@
import * as R from 'ramda'
import React from 'react'
import { fromNamespace, toNamespace } from 'src/utils/config'
import EditableTable from './Table'
const NamespacedTable = ({
name,
save,
data = {},
namespaces = [],
...props
}) => {
const innerSave = (...[, it]) => {
return save(toNamespace(it.id)(R.omit(['id2'], it)))
}
const innerData = R.map(it => ({
id: it,
...fromNamespace(it)(data)
}))(namespaces)
return (
<EditableTable name={name} data={innerData} save={innerSave} {...props} />
)
}
export default NamespacedTable

View file

@ -0,0 +1,299 @@
import Switch from '@mui/material/Switch'
import IconButton from '@mui/material/IconButton'
import SvgIcon from '@mui/material/SvgIcon'
import classnames from 'classnames'
import { Field, useFormikContext } from 'formik'
import * as R from 'ramda'
import React, { useContext, useState } from 'react'
import { DeleteDialog } from 'src/components/DeleteDialog'
import { Td, Tr } from 'src/components/fake-table/Table'
import { Label2 } from 'src/components/typography'
import DisabledDeleteIcon from 'src/styling/icons/action/delete/disabled.svg?react'
import DeleteIcon from 'src/styling/icons/action/delete/enabled.svg?react'
import DisabledEditIcon from 'src/styling/icons/action/edit/disabled.svg?react'
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
import StripesSvg from 'src/styling/icons/stripes.svg?react'
import { Link } from 'src/components/buttons'
import TableCtx from './Context'
import moduleStyles from './Row.module.css'
const ActionCol = ({ disabled, editing }) => {
const { values, submitForm, resetForm } = useFormikContext()
const {
editWidth,
onEdit,
enableEdit,
enableDelete,
disableRowEdit,
onDelete,
deleteWidth,
enableToggle,
onToggle,
toggleWidth,
forceAdd,
clearError,
actionColSize,
error
} = useContext(TableCtx)
const disableEdit = disabled || (disableRowEdit && disableRowEdit(values))
const cancel = () => {
clearError()
resetForm()
}
const [deleteDialog, setDeleteDialog] = useState(false)
const onConfirmed = () => {
onDelete(values.id).then(res => {
if (!R.isNil(res)) setDeleteDialog(false)
})
}
return (
<>
{editing && (
<Td textAlign="center" width={actionColSize}>
<Link
className={moduleStyles.saveButton}
type="submit"
color="primary"
onClick={submitForm}>
Save
</Link>
{!forceAdd && (
<Link color="secondary" onClick={cancel}>
Cancel
</Link>
)}
</Td>
)}
{!editing && enableEdit && (
<Td textAlign="center" width={editWidth}>
<IconButton
disabled={disableEdit}
onClick={() => onEdit && onEdit(values.id)}
size="small">
<SvgIcon>
{disableEdit ? <DisabledEditIcon /> : <EditIcon />}
</SvgIcon>
</IconButton>
</Td>
)}
{!editing && enableDelete && (
<Td textAlign="center" width={deleteWidth}>
<IconButton
disabled={disabled}
onClick={() => {
setDeleteDialog(true)
}}
size="small">
<SvgIcon>
{disabled ? <DisabledDeleteIcon /> : <DeleteIcon />}
</SvgIcon>
</IconButton>
<DeleteDialog
open={deleteDialog}
setDeleteDialog={setDeleteDialog}
onConfirmed={onConfirmed}
onDismissed={() => {
setDeleteDialog(false)
clearError()
}}
errorMessage={error}
/>
</Td>
)}
{!editing && enableToggle && (
<Td textAlign="center" width={toggleWidth}>
<Switch
checked={!!values.active}
value={!!values.active}
disabled={disabled}
onChange={() => onToggle(values.id)}
/>
</Td>
)}
</>
)
}
const ECol = ({ editing, focus, config, extraPaddingRight, extraPadding }) => {
const {
name,
names,
bypassField,
input,
editable = true,
size,
bold,
width,
textAlign,
editingAlign = textAlign,
prefix,
PrefixComponent = Label2,
suffix,
SuffixComponent = Label2,
textStyle = it => {},
isHidden = it => false,
view = it => it?.toString(),
inputProps = {}
} = config
const fields = names ?? [name]
const { values } = useFormikContext()
const isEditable = editable => {
if (typeof editable === 'function') return editable(values)
return editable
}
const isEditing = editing && isEditable(editable)
const isField = !bypassField
const innerProps = {
fullWidth: true,
autoFocus: focus,
size,
bold,
textAlign: isEditing ? editingAlign : textAlign,
...inputProps
}
const newAlign = isEditing ? editingAlign : textAlign
const justifyContent = newAlign === 'right' ? 'flex-end' : newAlign
const style = suffix || prefix ? { justifyContent } : {}
return (
<div className={moduleStyles.fields}>
{fields.map((f, idx) => (
<Td
style={style}
key={idx}
className={{
[moduleStyles.extraPaddingRight]: extraPaddingRight,
[moduleStyles.extraPadding]: extraPadding,
'flex items-center': suffix || prefix
}}
width={width}
size={size}
bold={bold}
textAlign={textAlign}>
{prefix && !isHidden(values) && (
<PrefixComponent
className={moduleStyles.prefix}
style={isEditing ? {} : textStyle(values, isEditing)}>
{typeof prefix === 'function' ? prefix(f) : prefix}
</PrefixComponent>
)}
{isEditing && isField && !isHidden(values) && (
<Field name={f} component={input} {...innerProps} />
)}
{isEditing && !isField && !isHidden(values) && (
<config.input name={f} />
)}
{!isEditing && values && !isHidden(values) && (
<div style={textStyle(values, isEditing)}>
{view(values[f], values)}
</div>
)}
{suffix && !isHidden(values) && (
<SuffixComponent
className={moduleStyles.suffix}
style={isEditing ? {} : textStyle(values, isEditing)}>
{suffix}
</SuffixComponent>
)}
{isHidden(values) && <StripesSvg />}
</Td>
))}
</div>
)
}
const groupStriped = elements => {
const [toStripe, noStripe] = R.partition(R.propEq('stripe', true))(elements)
if (!toStripe.length) {
return elements
}
const index = R.indexOf(toStripe[0], elements)
const width = R.compose(R.sum, R.map(R.path(['width'])))(toStripe)
return R.insert(
index,
{ width, editable: false, view: () => <StripesSvg /> },
noStripe
)
}
const ERow = ({ editing, disabled, lastOfGroup, newRow }) => {
const { touched, errors, values } = useFormikContext()
const {
elements,
enableEdit,
enableDelete,
error,
enableToggle,
rowSize,
stripeWhen
} = useContext(TableCtx)
const shouldStripe = !editing && stripeWhen && stripeWhen(values)
const innerElements = shouldStripe ? groupStriped(elements) : elements
const [toSHeader] = R.partition(R.has('doubleHeader'))(elements)
const extraPaddingIndex = toSHeader?.length
? R.indexOf(toSHeader[0], elements)
: -1
const extraPaddingRightIndex = toSHeader?.length
? R.indexOf(toSHeader[toSHeader.length - 1], elements)
: -1
const elementToFocusIndex = innerElements.findIndex(
it => it.editable === undefined || it.editable
)
const classNames = {
[moduleStyles.lastOfGroup]: lastOfGroup
}
const touchedErrors = R.pick(R.keys(touched), errors)
const hasTouchedErrors = touchedErrors && R.keys(touchedErrors).length > 0
const hasErrors = hasTouchedErrors || !!error
const errorMessage =
error || (touchedErrors && R.values(touchedErrors).join(', '))
return (
<Tr
className={classnames(classNames)}
size={rowSize}
error={editing && hasErrors}
newRow={newRow && !hasErrors}
shouldShowError
errorMessage={errorMessage}>
{innerElements.map((it, idx) => {
return (
<ECol
key={idx}
config={it}
editing={editing}
focus={idx === elementToFocusIndex && editing}
extraPaddingRight={extraPaddingRightIndex === idx}
extraPadding={extraPaddingIndex === idx}
/>
)
})}
{(enableEdit || enableDelete || enableToggle) && (
<ActionCol disabled={disabled} editing={editing} />
)}
</Tr>
)
}
export default ERow

View file

@ -0,0 +1,29 @@
.saveButton {
margin-right: 20px;
}
.lastOfGroup {
margin-bottom: 24px;
}
.extraPadding {
padding-left: 35px;
padding-right: 30px;
}
.extraPaddingRight {
padding-right: 39px;
}
.suffix {
margin: 0 0 0 7px;
}
.prefix {
margin: 0 7px 0 0;
}
.fields {
display: flex;
flex-direction: column;
}

View file

@ -0,0 +1,249 @@
import { Form, Formik } from 'formik'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import Link from 'src/components/buttons/Link'
import { TBody, Table } from 'src/components/fake-table/Table'
import { Info2, TL1 } from 'src/components/typography'
import { v4 as uuidv4 } from 'uuid'
import { AddButton } from 'src/components/buttons/index'
import TableCtx from './Context'
import Header from './Header'
import ERow from './Row'
import classes from './Table.module.css'
const ACTION_COL_SIZE = 87
const DEFAULT_COL_SIZE = 100
const getWidth = R.compose(
R.reduce(R.add)(0),
R.map(it => it.width ?? DEFAULT_COL_SIZE)
)
const ETable = ({
name,
title,
titleLg,
elements = [],
data = [],
save,
error: externalError,
rowSize = 'md',
validationSchema,
enableCreate,
enableEdit,
enableEditText,
editWidth: outerEditWidth,
enableDelete,
deleteWidth = ACTION_COL_SIZE,
enableToggle,
toggleWidth = ACTION_COL_SIZE,
onToggle,
forceDisable,
disableAdd,
initialValues,
setEditing,
shouldOverrideEdit,
editOverride,
stripeWhen,
disableRowEdit,
groupBy,
sortBy,
createText = 'Add override',
forceAdd = false,
tbodyWrapperClass,
orderedBy = null
}) => {
const [editingId, setEditingId] = useState(null)
const [adding, setAdding] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
useEffect(() => setError(externalError), [externalError])
useEffect(() => {
setError(null)
setAdding(forceAdd)
}, [forceAdd])
const innerSave = async value => {
if (saving) return
setSaving(true)
const it = validationSchema.cast(value, { assert: 'ignore-optionality' })
const index = R.findIndex(R.propEq('id', it.id))(data)
const list = index !== -1 ? R.update(index, it, data) : R.prepend(it, data)
if (!R.equals(data[index], it)) {
try {
await save({ [name]: list }, it)
} catch (err) {
setSaving(false)
return
}
}
setAdding(false)
setEditing && setEditing(false)
setSaving(false)
}
const onDelete = id => {
const list = R.reject(it => it.id === id, data)
return save({ [name]: list })
}
const onReset = () => {
setAdding(false)
setEditingId(null)
setEditing && setEditing(false)
}
const onEdit = it => {
if (shouldOverrideEdit && shouldOverrideEdit(it)) return editOverride(it)
setEditingId(it)
setError(null)
setEditing && setEditing(it, true)
}
const addField = () => {
setAdding(true)
setError(null)
setEditing && setEditing(true, true)
}
const widthIfEditNull =
enableDelete || enableToggle ? ACTION_COL_SIZE : ACTION_COL_SIZE * 2
const editWidth = R.defaultTo(widthIfEditNull)(outerEditWidth)
const actionColSize =
((enableDelete && deleteWidth) ?? 0) +
((enableEdit && editWidth) ?? 0) +
((enableToggle && toggleWidth) ?? 0)
const width = getWidth(elements) + actionColSize
const showButtonOnEmpty = !data.length && enableCreate && !adding
const canAdd = !forceDisable && !editingId && !disableAdd && !adding
const showTable = adding || data.length !== 0
const innerData = sortBy ? R.sortWith(sortBy)(data) : data
const ctxValue = {
elements,
enableEdit,
enableEditText,
onEdit,
clearError: () => setError(null),
error: error,
disableRowEdit,
editWidth,
enableDelete,
onDelete,
deleteWidth,
enableToggle,
rowSize,
onToggle,
toggleWidth,
actionColSize,
stripeWhen,
forceAdd,
orderedBy,
DEFAULT_COL_SIZE
}
return (
<TableCtx.Provider value={ctxValue}>
<div style={{ width }}>
{showButtonOnEmpty && canAdd && (
<AddButton onClick={addField}>{createText}</AddButton>
)}
{showTable && (
<>
{(title || enableCreate) && (
<div className={classes.outerHeader}>
{title && titleLg && (
<TL1 className={classes.title}>{title}</TL1>
)}
{title && !titleLg && (
<Info2 className={classes.title}>{title}</Info2>
)}
{enableCreate && canAdd && (
<Link className={classes.addLink} onClick={addField}>
{createText}
</Link>
)}
</div>
)}
<Table>
<Header />
<div className={tbodyWrapperClass}>
<TBody>
{adding && (
<Formik
validateOnBlur={false}
validateOnChange={false}
initialValues={{ id: uuidv4(), ...initialValues }}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<PromptWhenDirty />
<ERow
editing={true}
disabled={forceDisable}
newRow={true}
/>
</Form>
</Formik>
)}
{innerData.map((it, idx) => {
const nextElement = innerData[idx + 1]
const canGroup = !!groupBy && nextElement
const isFunction = R.type(groupBy) === 'Function'
const groupFunction = isFunction ? groupBy : R.prop(groupBy)
const isLastOfGroup =
canGroup &&
groupFunction(it) !== groupFunction(nextElement)
return (
<Formik
validateOnBlur={false}
validateOnChange={false}
key={it.id ?? idx}
enableReinitialize
initialValues={it}
onReset={onReset}
validationSchema={validationSchema}
onSubmit={innerSave}>
<Form>
<PromptWhenDirty />
<ERow
lastOfGroup={isLastOfGroup}
editing={editingId === it.id}
disabled={
forceDisable ||
(editingId && editingId !== it.id) ||
adding
}
/>
</Form>
</Formik>
)
})}
</TBody>
</div>
</Table>
</>
)}
</div>
</TableCtx.Provider>
)
}
export default ETable

View file

@ -0,0 +1,16 @@
.addLink {
margin-left: auto;
}
.title {
margin: 0;
color: var(--comet);
}
.outerHeader {
min-height: 16px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -0,0 +1,4 @@
import NamespacedTable from './NamespacedTable'
import Table from './Table'
export { Table, NamespacedTable }