diff --git a/Signum.React/Scripts/Lines.tsx b/Signum.React/Scripts/Lines.tsx index 65064bb963..830d654b64 100644 --- a/Signum.React/Scripts/Lines.tsx +++ b/Signum.React/Scripts/Lines.tsx @@ -41,6 +41,8 @@ export { EntityList } from './Lines/EntityList' export { EntityRepeater } from './Lines/EntityRepeater' +export { EntityAccordion } from './Lines/EntityAccordion' + export { EntityTabRepeater } from './Lines/EntityTabRepeater' export { EntityStrip } from './Lines/EntityStrip' diff --git a/Signum.React/Scripts/Lines/EntityAccordion.tsx b/Signum.React/Scripts/Lines/EntityAccordion.tsx new file mode 100644 index 0000000000..dc05ca23d3 --- /dev/null +++ b/Signum.React/Scripts/Lines/EntityAccordion.tsx @@ -0,0 +1,211 @@ +import * as React from 'react' +import { classes } from '../Globals' +import * as Navigator from '../Navigator' +import { TypeContext } from '../TypeContext' +import { ModifiableEntity, Lite, Entity, EntityControlMessage, getToString, isLite } from '../Signum.Entities' +import { EntityBaseController } from './EntityBase' +import { EntityListBaseController, EntityListBaseProps, DragConfig, MoveConfig } from './EntityListBase' +import { RenderEntity } from './RenderEntity' +import { newMListElement } from '../Signum.Entities'; +import { tryGetTypeInfos, getTypeInfo } from '../Reflection'; +import { useController } from './LineBase' +import { TypeBadge } from './AutoCompleteConfig' +import { Accordion } from 'react-bootstrap' +import { useForceUpdate } from '../Hooks' +import { AccordionEventKey } from 'react-bootstrap/esm/AccordionContext' + +export interface EntityAccordionProps extends EntityListBaseProps { + createAsLink?: boolean | ((er: EntityAccordionController) => React.ReactElement); + avoidFieldSet?: boolean; + createMessage?: string; + getTitle?: (ctx: TypeContext) => React.ReactChild; + itemExtraButtons?: (er: EntityListBaseController, index: number) => React.ReactElement; + initialSelectedIndex?: number | null; + selectedIndex?: number | null; + onSelectTab?: (newIndex: number | null) => void; +} + +function isControlled(p: EntityAccordionProps) { + + if ((p.selectedIndex !== undefined) != (p.onSelectTab !== undefined)) + throw new Error("selectedIndex and onSelectTab should be set together"); + + return p.selectedIndex != null; +} + +export class EntityAccordionController extends EntityListBaseController { + + selectedIndex!: number | null; + setSelectedIndex!: (index: number | null) => void; + initialIsControlled!: boolean; + + init(p: EntityAccordionProps) { + super.init(p); + + this.initialIsControlled = React.useMemo(() => isControlled(p), []); + const currentIsControlled = isControlled(p); + if (currentIsControlled != this.initialIsControlled) + throw new Error(`selectedIndex was isControlled=${this.initialIsControlled} but now is ${currentIsControlled}`); + + if (!this.initialIsControlled) { + [this.selectedIndex, this.setSelectedIndex] = React.useState(p.initialSelectedIndex ?? null); + } else { + this.selectedIndex = p.selectedIndex!; + this.setSelectedIndex = p.onSelectTab!; + } + } + + getDefaultProps(p: EntityAccordionProps) { + super.getDefaultProps(p); + p.viewOnCreate = false; + p.createAsLink = true; + } + + addElement(entityOrLite: Lite | ModifiableEntity) { + + if (isLite(entityOrLite) != (this.props.type!.isLite || false)) + throw new Error("entityOrLite should be already converted"); + + const list = this.props.ctx.value!; + list.push(newMListElement(entityOrLite)); + this.setSelectedIndex(list.length - 1); + this.setValue(list); + } +} + + +export const EntityAccordion = React.forwardRef(function EntityAccordion(props: EntityAccordionProps, ref: React.Ref) { + var c = useController(EntityAccordionController, props, ref); + var p = c.props; + + if (c.isHidden) + return null; + + let ctx = p.ctx; + + if (p.avoidFieldSet == true) + return ( +
+ {renderButtons()} + {renderAccordion()} +
+ ); + + return ( +
+ +
+ {p.labelText} + {renderButtons()} +
+
+ {renderAccordion()} +
+ ); + + + function renderButtons() { + const buttons = ( + + {p.extraButtonsBefore && p.extraButtonsBefore(c)} + {p.createAsLink == false && c.renderCreateButton(false, p.createMessage)} + {c.renderFindButton(false)} + {p.extraButtonsAfter && p.extraButtonsAfter(c)} + + ); + + return EntityBaseController.hasChildrens(buttons) ? buttons : undefined; + } + + function handleSelectTab(eventKey: AccordionEventKey | null) { + var num = eventKey == null ? null: parseInt(eventKey as string); + c.setSelectedIndex(num); + } + + function renderAccordion() { + const readOnly = ctx.readOnly; + const showType = tryGetTypeInfos(ctx.propertyRoute!.typeReference().name).length > 1; + return ( + + { + c.getMListItemContext(ctx).map((mlec, i) => ( + c.handleRemoveElementClick(e, mlec.index!) : undefined} + ctx={mlec} + move={c.canMove(mlec.value) && p.moveMode == "MoveIcons" && !readOnly ? c.getMoveConfig(false, mlec.index!, "v") : undefined} + drag={c.canMove(mlec.value) && p.moveMode == "DragIcon" && !readOnly ? c.getDragConfig(mlec.index!, "v") : undefined} + itemExtraButtons={p.itemExtraButtons ? (() => p.itemExtraButtons!(c, mlec.index!)) : undefined} + getComponent={p.getComponent} + getViewPromise={p.getViewPromise} + getTitle={p.getTitle} + title={showType ? : undefined} />)) + } + { + p.createAsLink && p.create && !readOnly && + (typeof p.createAsLink == "function" ? p.createAsLink(c) : + + {EntityBaseController.createIcon} {p.createMessage ?? EntityControlMessage.Create.niceToString()} + ) + } + + ); + } +}); + + +export interface EntityAccordionElementProps { + ctx: TypeContext | ModifiableEntity>; + getComponent?: (ctx: TypeContext) => React.ReactElement; + getViewPromise?: (entity: ModifiableEntity) => undefined | string | Navigator.ViewPromise; + getTitle?: (ctx: TypeContext) => React.ReactChild; + onRemove?: (event: React.MouseEvent) => void; + move?: MoveConfig; + drag?: DragConfig; + title?: React.ReactElement; + itemExtraButtons?: () => React.ReactElement; +} + +export function EntityAccordionElement({ ctx, getComponent, getViewPromise, onRemove, move, drag, itemExtraButtons, title, getTitle }: EntityAccordionElementProps) +{ + + const forceUpdate = useForceUpdate(); + + return ( + + + +
+ {onRemove && + {EntityListBaseController.removeIcon} + } +   + {move?.renderMoveUp()} + {move?.renderMoveDown()} + {drag && + {EntityListBaseController.moveIcon} + } + {itemExtraButtons && itemExtraButtons()} + {'\xa0'} + {getTitle ? getTitle(ctx) : getToString(ctx.value)} +
+
+ + + +
+ ); +} diff --git a/Signum.React/Scripts/Lines/Lines.css b/Signum.React/Scripts/Lines/Lines.css index e899b08a87..dbce7bbefa 100644 --- a/Signum.React/Scripts/Lines/Lines.css +++ b/Signum.React/Scripts/Lines/Lines.css @@ -225,6 +225,24 @@ ul.nav-tabs { } +/*Accordion*/ + +.sf-accordion-element.drag-top >h2 >button { + box-shadow: inset #AAA 0px 3px 0px 0px !important; +} + +.sf-accordion-element.drag-bottom > h2 > button { + box-shadow: inset #AAA 0px -3px 0px 0px !important; +} + +/*.sf-accordion-element { + padding-bottom: 3px; +} + +.sf-accordion-element:first-child { + padding-top: 3px; +}*/ + /*buton colors*/ .sf-create:not(.disabled):hover { color: #22BA00;