Skip to content

Commit

Permalink
combine with commonOnClick
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Jul 29, 2022
1 parent 0722d95 commit 65054d7
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 62 deletions.
5 changes: 2 additions & 3 deletions Signum.React.Extensions/Alerts/AlertsClient.tsx
Expand Up @@ -52,10 +52,9 @@ export function start(options: { routes: JSX.Element[], showAlerts?: (typeName:
}));

Operations.addSettings(new EntityOperationSettings(AlertOperation.Delay, {
onClick: (eoc) => chooseDate().then(d => d && eoc.defaultClick(d.toISO())),
commonOnClick: (eoc) => chooseDate().then(d => d && eoc.defaultClick(d.toISO())),
hideOnCanExecute: true,
contextual: { onClick: (coc) => chooseDate().then(d => d && coc.defaultContextualClick(d.toISO())) },
contextualFromMany: { onClick: (coc) => chooseDate().then(d => d && coc.defaultContextualClick(d.toISO())) },
contextualFromMany: { onClick: (coc) => chooseDate().then(d => d && coc.defaultClick(d.toISO())) },
}));

var cellFormatter = new Finder.CellFormatter((cell, ctx) => {
Expand Down
16 changes: 4 additions & 12 deletions Signum.React.Extensions/Dynamic/DynamicViewClient.tsx
Expand Up @@ -69,12 +69,8 @@ export function start(options: { routes: JSX.Element[] }) {
}));

Operations.addSettings(new EntityOperationSettings(DynamicViewOperation.Delete, {
onClick: ctx => {
cleanCaches();
return ctx.defaultClick();
},
contextual: { onClick: ctx => { cleanCaches(); return ctx.defaultContextualClick(); } },
contextualFromMany: { onClick: ctx => { cleanCaches(); return ctx.defaultContextualClick(); } },
commonOnClick: oc => { cleanCaches(); return oc.defaultClick(); },
contextualFromMany: { onClick: ctx => { cleanCaches(); return ctx.defaultClick(); } },
}));

Operations.addSettings(new EntityOperationSettings(DynamicViewSelectorOperation.Save, {
Expand All @@ -85,12 +81,8 @@ export function start(options: { routes: JSX.Element[] }) {
}));

Operations.addSettings(new EntityOperationSettings(DynamicViewSelectorOperation.Delete, {
onClick: ctx => {
cleanCaches();
return ctx.defaultClick();
},
contextual: { onClick: ctx => { cleanCaches(); return ctx.defaultContextualClick(); } },
contextualFromMany: { onClick: ctx => { cleanCaches(); return ctx.defaultContextualClick(); } },
commonOnClick: ctx => { cleanCaches(); return ctx.defaultClick(); },
contextualFromMany: { onClick: ctx => { cleanCaches(); return ctx.defaultClick(); } },
}));

Navigator.setViewDispatcher(new DynamicViewViewDispatcher());
Expand Down
17 changes: 4 additions & 13 deletions Signum.React.Extensions/MachineLearning/PredictorClient.tsx
Expand Up @@ -68,19 +68,10 @@ export function start(options: { routes: JSX.Element[] }) {

Operations.addSettings(new EntityOperationSettings(PredictorOperation.Publish, {
hideOnCanExecute: true,
onClick: eoc =>
API.publications(eoc.entity.mainQuery.query!.key)
.then(pubs => SelectorModal.chooseElement(pubs, { buttonDisplay: a => symbolNiceName(a), buttonName: a => a.key }))
.then(pps => pps && eoc.defaultClick(pps))
,
contextual: {
onClick: coc =>
Navigator.API.fetch(coc.context.lites[0])
.then(p => API.publications(p.mainQuery.query!.key))
.then(pubs => SelectorModal.chooseElement(pubs, { buttonDisplay: a => symbolNiceName(a), buttonName: a => a.key }))
.then(pps => pps && coc.defaultContextualClick(pps))

}
commonOnClick: oc => oc.getEntity()
.then(p => API.publications(p.mainQuery.query!.key))
.then(pubs => SelectorModal.chooseElement(pubs, { buttonDisplay: a => symbolNiceName(a), buttonName: a => a.key }))
.then(pps => pps && oc.defaultClick(pps)),
}));

Operations.addSettings(new EntityOperationSettings(PredictorOperation.AfterPublishProcess, {
Expand Down
4 changes: 2 additions & 2 deletions Signum.React.Extensions/Tree/TreeClient.tsx
Expand Up @@ -31,7 +31,7 @@ export function start(options: { routes: JSX.Element[] }) {
new EntityOperationSettings(TreeOperation.CreateNextSibling, { contextual: { isVisible: ctx => ctx.context.container instanceof SearchControlLoaded } }),
new EntityOperationSettings(TreeOperation.Move, {
onClick: ctx => moveModal(toLite(ctx.entity)).then(m => m && ctx.defaultClick(m)),
contextual: { onClick: ctx => moveModal(ctx.context.lites[0]).then(m => m && ctx.defaultContextualClick(m)) }
contextual: { onClick: ctx => moveModal(ctx.context.lites[0]).then(m => m && ctx.defaultClick(m)) }
}),
new EntityOperationSettings(TreeOperation.Copy, {
onClick: ctx => copyModal(toLite(ctx.entity)).then(m => {
Expand All @@ -44,7 +44,7 @@ export function start(options: { routes: JSX.Element[] }) {
onClick: ctx => copyModal(ctx.context.lites[0]).then(m => {
if (m) {
ctx.onConstructFromSuccess = pack => Operations.notifySuccess();
ctx.defaultContextualClick(m);
ctx.defaultClick(m);
}
})
}
Expand Down
58 changes: 26 additions & 32 deletions Signum.React.Extensions/Workflow/WorkflowClient.tsx
Expand Up @@ -208,15 +208,11 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
}));

Operations.addSettings(new EntityOperationSettings(CaseOperation.Delete, {
onClick: eoc => askDeleteMainEntity(eoc.entity.mainEntity)
.then(u => u == undefined ? undefined : eoc.defaultClick(u)),
contextual: {
onClick: coc => askDeleteMainEntity(coc.pack!.entity.mainEntity)
.then(u => u == undefined ? undefined : coc.defaultContextualClick(u))
},
commonOnClick: oc => oc.getEntity().then(e=> askDeleteMainEntity(e.mainEntity))
.then(u => u == undefined ? undefined : oc.defaultClick(u)),
contextualFromMany: {
onClick: coc => askDeleteMainEntity()
.then(u => u == undefined ? undefined : coc.defaultContextualClick(u))
.then(u => u == undefined ? undefined : coc.defaultClick(u))
},
}));

Expand All @@ -233,15 +229,11 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
Operations.addSettings(new EntityOperationSettings(CaseActivityOperation.Delete, {
hideOnCanExecute: true,
isVisible: ctx => false,
onClick: eoc => askDeleteMainEntity(eoc.entity.case.mainEntity)
.then(u => u == undefined ? undefined : eoc.defaultClick(u)),
contextual: {
onClick: coc => askDeleteMainEntity(coc.pack!.entity.case.mainEntity)
.then(u => u == undefined ? undefined : coc.defaultContextualClick(u))
},
commonOnClick: oc => oc.getEntity().then(e => askDeleteMainEntity(e.case.mainEntity))
.then(u => u == undefined ? undefined : oc.defaultClick(u)),
contextualFromMany: {
onClick: coc => askDeleteMainEntity()
.then(u => u == undefined ? undefined : coc.defaultContextualClick(u))
.then(u => u == undefined ? undefined : coc.defaultClick(u))
},
}));

Expand All @@ -267,7 +259,7 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
onClick: coc =>
Navigator.API.fetch(coc.context.lites[0])
.then(ca => getWorkflowJumpSelector(toLite(ca.workflowActivity as WorkflowActivityEntity)))
.then(dest => dest && coc.defaultContextualClick(dest))
.then(dest => dest && coc.defaultClick(dest))

}
}));
Expand All @@ -289,7 +281,7 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
onClick: coc =>
Navigator.API.fetch(coc.context.lites[0])
.then(ca => getWorkflowFreeJump(ca.case.workflow))
.then(dest => dest && coc.defaultContextualClick(dest))
.then(dest => dest && coc.defaultClick(dest))
}
}));
Operations.addSettings(new EntityOperationSettings(CaseActivityOperation.Timer, { isVisible: ctx => false }));
Expand Down Expand Up @@ -341,7 +333,7 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
: <OperationMenuItem coc={coc} color={wa.customNextButton.style.toLowerCase() as BsColor}>{wa.customNextButton.name}</OperationMenuItem>];

} else if (wa.type == "Decision") {
return wa.decisionOptions.map(mle => <OperationMenuItem coc={coc} onOperationClick={() => coc.defaultContextualClick(mle.element.name)} color={mle.element.style.toLowerCase() as BsColor}>{mle.element.name}</OperationMenuItem>);
return wa.decisionOptions.map(mle => <OperationMenuItem coc={coc} onOperationClick={() => coc.defaultClick(mle.element.name)} color={mle.element.style.toLowerCase() as BsColor}>{mle.element.name}</OperationMenuItem>);
}
else
return [];
Expand Down Expand Up @@ -379,16 +371,31 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
Operations.addSettings(new EntityOperationSettings(WorkflowOperation.Deactivate, {
onClick: eoc => chooseWorkflowExpirationDate([toLite(eoc.entity)]).then(val => !val ? undefined : eoc.defaultClick(val)),
contextual: {
onClick: coc => chooseWorkflowExpirationDate(coc.context.lites).then(val => !val ? undefined : coc.defaultContextualClick(val)),
onClick: coc => chooseWorkflowExpirationDate(coc.context.lites).then(val => !val ? undefined : coc.defaultClick(val)),
icon: ["far", "heart"],
iconColor: "gray"
},
contextualFromMany: {
onClick: coc => chooseWorkflowExpirationDate(coc.context.lites).then(val => !val ? undefined : coc.defaultContextualClick(val)),
onClick: coc => chooseWorkflowExpirationDate(coc.context.lites).then(val => !val ? undefined : coc.defaultClick(val)),
icon: ["far", "heart"],
iconColor: "gray"
},
}));

function chooseWorkflowExpirationDate(workflows: Lite<WorkflowEntity>[]): Promise<string | undefined> {
return ValueLineModal.show({
type: { name: "string" },
valueLineType: "DateTime",
modalSize: "md",
title: WorkflowMessage.DeactivateWorkflow.niceToString(),
message:
<div>
<strong>{WorkflowMessage.PleaseChooseExpirationDate.niceToString()}</strong>
<ul>{workflows.map((w, i) => <li key={i}>{getToString(w)}</li>)}</ul>
</div>
});
}

Navigator.addSettings(new EntitySettings(WorkflowEntity, w => import('./Workflow/Workflow'), { avoidPopup: true }));

hide(WorkflowPoolEntity);
Expand Down Expand Up @@ -443,19 +450,6 @@ export function start(options: { routes: JSX.Element[], overrideCaseActivityMixi
}
}

function chooseWorkflowExpirationDate(workflows: Lite<WorkflowEntity>[]): Promise<string | undefined> {
return ValueLineModal.show({
type: { name: "string" },
valueLineType: "DateTime",
modalSize: "md",
title: WorkflowMessage.DeactivateWorkflow.niceToString(),
message:
<div>
<strong>{WorkflowMessage.PleaseChooseExpirationDate.niceToString()}</strong>
<ul>{workflows.map((w, i) => <li key={i}>{getToString(w)}</li>)}</ul>
</div>
});
}

export function workflowActivityMonitorUrl
(workflow: Lite<WorkflowEntity>) {
Expand Down

4 comments on commit 65054d7

@olmobrutall
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CellOperationButton and commonOnClick

Thanks to the effort of @rezanos this commit finished a very welcome feature to the SearchControl: Adding simple operation buttons inside of a column's cell!

image

How to use it?

The way it works is by having a two new types of QueryToken that can only be added as columns (not available for Filters or Orders):

  • OperationsToken: Every Entity has a new token named [Operations] that serves as a container, to keep all the available operations together (explained below).
  • OperationToken: Represents one operation that can be added as column. This operation has to be elegible for CellOperation, this means:

image

What makes an operation elegible for CellOperation?

For performance reasons, not every operation can be shown in a cell:

  • The operation should be allowed for the user.
  • The operation should not have CanExecute (or CanDelete / CanConstruct) defined.
  • Or, it also defines a CanExecuteExpresion (or CanDeleteExpression / CanConstructExpression respectively).

This new CanExecuteExpression variants are required to make the query efficient, avoiding retrieving every entity in the result just to evaluate CanExecute.

So in most of the cases just replace CanExecute by CanExecuteExpression to make it appear.

            new Execute(OrderOperation.Ship)
            {
                // CanExecute = o => o.Details.IsEmpty() ? ValidationMessage._0IsEmpty.NiceToString(Entity.NicePropertyName(() => o.Details)) : null,
                CanExecuteExpression = o => o.Details.IsEmpty() ? ValidationMessage._0IsEmpty.NiceToString(Entity.NicePropertyName(() => o.Details)) : null,
                FromStates = { OrderState.Ordered },
                ToStates = { OrderState.Shipped },
                CanBeModified = true,
                Execute = (o, args) =>
                {
                    o.ShippedDate = args.TryGetArgS<DateTime>() ?? DateTime.Now;
                    o.State = OrderState.Shipped;
                }
            }.Register();

image

Just select Ship and voilà! An operation button directly in the column:

image

Not that the button is automatically disabled depending on the state or the CanExecuteExpression, showing the reasons in a Tooltip.

How to Customize the button.

There is a new cell block of type CellOperationSettings in EntityOperationSettings to customize an operation when shown in a cell:

  Operations.addSettings(new EntityOperationSettings(OrderOperation.Ship, {
    cell: {
      color: "info",
      icon: "truck",
    }
  }));//Ship

Similarly to contextual, if you override values only in the EntityOperationSettings they will be used by default for CellOperationSettings too. So this code has identical results.

Operations.addSettings(new EntityOperationSettings(OrderOperation.Ship, {
    color: "info",
    icon: "truck",
  }));//Ship

image

For isVisible / onClick, the same function can not be used so easily, because one takes an EntityOperationContext<T>, while insice cell it takes a CellOperationContext<T>.

This means that, on top of overriding contextual.click, now you need to override cell.click?

  const selectShippedDate = () => ValueLineModal.show({
    type: { name: "datetime" },
    initialValue: DateTime.local().toISO(),
    labelText: OrderEntity.nicePropertyName(a => a.shippedDate)
  });

  Operations.addSettings(new EntityOperationSettings(OrderOperation.Ship, {
    onClick: (eoc) => selectShippedDate().then(date => eoc.defaultClick(date)), //Overrides main button, but disables contextual and cell
    contextual: {  onClick: coc => selectShippedDate().then(date => coc.defaultContextualClick(date)) }, //Restores contextual behaviour 
    cell: { onClick: coc => selectShippedDate().then(date => coc.defaultClick(date)) }, // Restores cell behavour
  }));//Ship

image

Presenting commonOnClick

To solve this duplication we have added a a new field to EntityOperationSettings:

export interface EntityOperationOptions<T extends Entity> {
  contextual?: ContextualOperationOptions<T>;
  contextualFromMany?: ContextualOperationOptions<T>;
  cell?: CellOperationOptions<T>;

  text?: (coc: EntityOperationContext<T>) => string;
  isVisible?: (eoc: EntityOperationContext<T>) => boolean;
  overrideCanExecute?: (eoc: EntityOperationContext<T>) => string | undefined | null;
  confirmMessage?: (eoc: EntityOperationContext<T>) => string | undefined | null;
  onClick?: (eoc: EntityOperationContext<T>) => Promise<void>;

   //NEW!
  commonOnClick?: (oc: EntityOperationContext<T> | ContextualOperationContext<T> | CellOperationContext<T>) => Promise<void>;

This field commonOnClick can be used to override all three onClicks at the same time and accepts the union of EntityOperationContext<T> | ContextualOperationContext<T> | CellOperationContext<T> as an argument. Bold move @rezanos!!.. and with some small touches is actually enought in many cases.

  const selectShippedDate = () => ValueLineModal.show({
    type: { name: "datetime" },
    initialValue: DateTime.local().toISO(),
    labelText: OrderEntity.nicePropertyName(a => a.shippedDate)
  });

  Operations.addSettings(new EntityOperationSettings(OrderOperation.Ship, {
    commonOnClick: oc => selectShippedDate().then(date => oc.defaultClick(date)),
  }));//Ship

image

And what if we need the entity?

Easy!, just use getEntity method.

  const selectShippedDate = (e: OrderEntity) => ValueLineModal.show({
    type: { name: "datetime" },
    initialValue: e.requiredDate,
    labelText: OrderEntity.nicePropertyName(a => a.shippedDate)
  });

  Operations.addSettings(new EntityOperationSettings(OrderOperation.Ship, {
    commonOnClick: oc => oc.getEntity().then(e=>selectShippedDate(e)).then(date => oc.defaultClick(date)),
  }));//Ship

The getEntity method only realy retrieves the entity for the CellOperationContext case, the other ones (EntityOperationContext and ContextualOperationContext) just returns the already-retrieved one.

Finally, what about contextualFromMany?

Since the framework can not know if you are using getEntity in your commonOnClick, is risky to reuse if for contextualFromMany too, so you will need to make this duplication

  const selectShippedDate = (e?: OrderEntity) => ValueLineModal.show({
    type: { name: "datetime" },
    initialValue: e?.requiredDate ?? DateTime.local().toISO(),
    labelText: OrderEntity.nicePropertyName(a => a.shippedDate)
  });


  Operations.addSettings(new EntityOperationSettings(OrderOperation.Ship, {
    commonOnClick: oc => oc.getEntity().then(e => selectShippedDate(e)).then(date => oc.defaultClick(date)),
    contextualFromMany: {
      onClick: coc => selectShippedDate().then(date => coc.defaultClick(date))
    }
  }));//Ship

image

How to update?

There are two small breaking changes to make this feature work:

  1. In TypeScript, defaultContextualClick has been renamed to defaultClick in ContextualOperationSettings, to allow for the union neede in commonOnClick.
  • You can kust a replace should fix the code 😈
  • ...but it's a perfect moment to consider removing the duplication and using commonOnClick instead 😇.

In this message and this commit you have many examples of this refactoring.

2 . In C# the following extension methods have been removed:

    public static PropertyInfo PropertyInfo<T>(this T entity, Expression<Func<T, object?>> property) where T : ModifiableEntity
    {
        return ReflectionTools.GetPropertyInfo(property);
    }

    public static string NicePropertyName<T>(this T entity, Expression<Func<T, object?>> property) where T : ModifiableEntity
    {
        return ReflectionTools.GetPropertyInfo(property).NiceName();
    }

Because they are often used in CanExecute CanExecuteExpression but could end-up retrieving the full entity. Instead use the static method defined in ModifiableEntity (or Entity for short):

    [MethodExpander(typeof(NicePropertyNameExpander))]
    public static string NicePropertyName<R>(Expression<Func<R>> property)
    {
        return ReflectionTools.GetPropertyInfo(property).NiceName();
    }

This means that if you have code like this:

e.NicePropertyName(_ => _.Details)         //Old
Entity.NicePropertyName(()=> e.Details)  //New

Enjoy!

@doganc
Copy link

@doganc doganc commented on 65054d7 Jul 30, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect,

@rezanos
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great document, Comprehensive and attractive 😆
Thanks @olmobrutall, 🙏

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 65054d7 Jul 31, 2022 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.