-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e6de729
commit 3ba48fa
Showing
3 changed files
with
231 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<any>); | ||
avoidFieldSet?: boolean; | ||
createMessage?: string; | ||
getTitle?: (ctx: TypeContext<any /*T*/>) => React.ReactChild; | ||
itemExtraButtons?: (er: EntityListBaseController<EntityListBaseProps>, index: number) => React.ReactElement<any>; | ||
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<EntityAccordionProps> { | ||
|
||
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<number | null>(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<Entity> | 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<EntityAccordionController>) { | ||
var c = useController(EntityAccordionController, props, ref); | ||
var p = c.props; | ||
|
||
if (c.isHidden) | ||
return null; | ||
|
||
let ctx = p.ctx; | ||
|
||
if (p.avoidFieldSet == true) | ||
return ( | ||
<div className={classes("sf-accordion-field sf-control-container", ctx.errorClassBorder)} | ||
{...{ ...c.baseHtmlAttributes(), ...p.formGroupHtmlAttributes, ...ctx.errorAttributes() }}> | ||
{renderButtons()} | ||
{renderAccordion()} | ||
</div> | ||
); | ||
|
||
return ( | ||
<fieldset className={classes("sf-accordion-field sf-control-container", ctx.errorClass)} | ||
{...{ ...c.baseHtmlAttributes(), ...c.props.formGroupHtmlAttributes, ...ctx.errorAttributes() }}> | ||
<legend> | ||
<div> | ||
<span>{p.labelText}</span> | ||
{renderButtons()} | ||
</div> | ||
</legend> | ||
{renderAccordion()} | ||
</fieldset> | ||
); | ||
|
||
|
||
function renderButtons() { | ||
const buttons = ( | ||
<span className="float-end"> | ||
{p.extraButtonsBefore && p.extraButtonsBefore(c)} | ||
{p.createAsLink == false && c.renderCreateButton(false, p.createMessage)} | ||
{c.renderFindButton(false)} | ||
{p.extraButtonsAfter && p.extraButtonsAfter(c)} | ||
</span> | ||
); | ||
|
||
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 ( | ||
<Accordion className="sf-accordion-elements" activeKey={c.selectedIndex?.toString()} onSelect={handleSelectTab}> | ||
{ | ||
c.getMListItemContext(ctx).map((mlec, i) => ( | ||
<EntityAccordionElement key={c.keyGenerator.getKey(mlec.value)} | ||
onRemove={c.canRemove(mlec.value) && !readOnly ? e => 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 ? <TypeBadge entity={mlec.value} /> : undefined} />)) | ||
} | ||
{ | ||
p.createAsLink && p.create && !readOnly && | ||
(typeof p.createAsLink == "function" ? p.createAsLink(c) : | ||
<a href="#" title={ctx.titleLabels ? EntityControlMessage.Create.niceToString() : undefined} | ||
className="sf-line-button sf-create" | ||
onClick={c.handleCreateClick}> | ||
{EntityBaseController.createIcon} {p.createMessage ?? EntityControlMessage.Create.niceToString()} | ||
</a>) | ||
} | ||
</Accordion> | ||
); | ||
} | ||
}); | ||
|
||
|
||
export interface EntityAccordionElementProps { | ||
ctx: TypeContext<Lite<Entity> | ModifiableEntity>; | ||
getComponent?: (ctx: TypeContext<ModifiableEntity>) => React.ReactElement<any>; | ||
getViewPromise?: (entity: ModifiableEntity) => undefined | string | Navigator.ViewPromise<ModifiableEntity>; | ||
getTitle?: (ctx: TypeContext<any /*T*/>) => React.ReactChild; | ||
onRemove?: (event: React.MouseEvent<any>) => void; | ||
move?: MoveConfig; | ||
drag?: DragConfig; | ||
title?: React.ReactElement<any>; | ||
itemExtraButtons?: () => React.ReactElement<any>; | ||
} | ||
|
||
export function EntityAccordionElement({ ctx, getComponent, getViewPromise, onRemove, move, drag, itemExtraButtons, title, getTitle }: EntityAccordionElementProps) | ||
{ | ||
|
||
const forceUpdate = useForceUpdate(); | ||
|
||
return ( | ||
<Accordion.Item className={classes(drag?.dropClass, "sf-accordion-element")} eventKey={ctx.index!.toString()} | ||
onDragEnter={drag?.onDragOver} | ||
onDragOver={drag?.onDragOver} | ||
onDrop={drag?.onDrop}> | ||
|
||
<Accordion.Header {...EntityListBaseController.entityHtmlAttributes(ctx.value)}> | ||
<div className="d-flex align-items-center flex-grow-1"> | ||
{onRemove && <a href="#" className={classes("sf-line-button", "sf-remove")} | ||
onClick={onRemove} | ||
title={ctx.titleLabels ? EntityControlMessage.Remove.niceToString() : undefined}> | ||
{EntityListBaseController.removeIcon} | ||
</a>} | ||
| ||
{move?.renderMoveUp()} | ||
{move?.renderMoveDown()} | ||
{drag && <a href="#" className={classes("sf-line-button", "sf-move")} | ||
draggable={true} | ||
onDragStart={drag.onDragStart} | ||
onDragEnd={drag.onDragEnd} | ||
onKeyDown={drag.onKeyDown} | ||
title={drag.title}> | ||
{EntityListBaseController.moveIcon} | ||
</a>} | ||
{itemExtraButtons && itemExtraButtons()} | ||
{'\xa0'} | ||
{getTitle ? getTitle(ctx) : getToString(ctx.value)} | ||
</div> | ||
</Accordion.Header> | ||
<Accordion.Body> | ||
<RenderEntity ctx={ctx} getComponent={getComponent} getViewPromise={getViewPromise} onRefresh={forceUpdate} /> | ||
</Accordion.Body> | ||
</Accordion.Item> | ||
); | ||
} |
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
3ba48fa
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.
Presenting
EntityAccordion
There is a new component in
Signum.React
:EntityAccordion
.Creating this component is as simple as:
It's good option for a
MList
ofEmbeddedEntity
/Entity
withEntityKind.Part
where you want a good one-line overview, but you need a block to actually modify / see details of the entity.It's almost a replacement for an
EntityRepeater
, that typically takes too much vertical space.It also shares some characteristics with
EntityTabRepeater
(both have agetTitle
), butEntityAccordion
is optimized for more place for the title and less place for the expanded content.If you want that customize the title you can do it like this:
And to refresh the title after changes in the container components (without needing to Save):
Like all the other components inheriting from
EntityListBase
, it has support for moving with drag-and-drop.Now that
EntityAccordion
tries to replaceEntityRepeater
.... do we need aEntityCollapsableCard
trying to replaceEntityDetails
?3ba48fa
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.
Great and very useful component, let`s use it 🥇