Skip to content

Commit

Permalink
more on MultiPropertySetter
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Sep 15, 2020
1 parent 70b9dde commit e11a04d
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 50 deletions.
12 changes: 11 additions & 1 deletion Signum.Entities/DynamicQuery/QueryUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,14 +500,24 @@ internal static Type BuildLite(this Type type)
static readonly MethodInfo miLike = ReflectionTools.GetMethodInfo((string s) => s.Like(s));
static readonly MethodInfo miDistinctNullable = ReflectionTools.GetMethodInfo((string s) => LinqHints.DistinctNull<int>(null, null)).GetGenericMethodDefinition();
static readonly MethodInfo miDistinct = ReflectionTools.GetMethodInfo((string s) => LinqHints.DistinctNull<string>(null, null)).GetGenericMethodDefinition();
static readonly MethodInfo miEquals = ReflectionTools.GetMethodInfo(() => object.Equals(null, null));

public static Expression GetCompareExpression(FilterOperation operation, Expression left, Expression right, bool inMemory = false)
{
switch (operation)
{
case FilterOperation.EqualTo: return Expression.Equal(left, right);
case FilterOperation.EqualTo:
{
if (inMemory)
return Expression.Call(null, miEquals, left, right);

return Expression.Equal(left, right);
}
case FilterOperation.DistinctTo:
{
if (inMemory)
return Expression.Not(Expression.Call(null, miEquals, left, right));

var t = left.Type.UnNullify();
var mi = t.IsValueType ? miDistinctNullable : miDistinct;
return Expression.Call(mi.MakeGenericMethod(t), left.Nullify(), right.Nullify());
Expand Down
2 changes: 2 additions & 0 deletions Signum.Entities/EnumMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public enum OperationMessage

Predictate,
Setters,
[Description("Add setter")]
AddSetter,
}

public enum SynchronizerMessage
Expand Down
13 changes: 7 additions & 6 deletions Signum.React/ApiControllers/OperationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Signum.Engine.Maps;
using Signum.Engine.Operations;
using Signum.Entities;
using Signum.Entities.DynamicQuery;
using Signum.Entities.Reflection;
using Signum.React.Facades;
using Signum.React.Filters;
Expand Down Expand Up @@ -258,7 +259,8 @@ public class MultiOperationRequest : BaseOperationRequest
public class PropertySetter
{
public string Property;
public PropertyOperation Operation;
public PropertyOperation? Operation;
public FilterOperation? FilterOperation;
public object? Value;
public string? EntityType;
public List<PropertySetter>? Predicate;
Expand Down Expand Up @@ -338,8 +340,8 @@ public static void SetSetters(ModifiableEntity entity, List<PropertySetter> sett
{
case PropertyOperation.AddElement:
{
var item = (ModifiableEntity)Activator.CreateInstance(pr.Type.ElementType()!)!;
SetSetters(item, setter.Setters!, pr);
var item = (ModifiableEntity)Activator.CreateInstance(elementPr.Type)!;
SetSetters(item, setter.Setters!, elementPr);
((IList)mlist).Add(item);
}
break;
Expand All @@ -359,7 +361,7 @@ public static void SetSetters(ModifiableEntity entity, List<PropertySetter> sett
var toRemove = ((IEnumerable<object>)mlist).Where(predicate.Compile()).ToList();
foreach (var item in toRemove)
{
((IList)mlist).Add(item);
((IList)mlist).Remove(item);
}
}
break;
Expand Down Expand Up @@ -407,7 +409,6 @@ private static void SetProperty(ModifiableEntity entity, PropertyRoute pr, Prope
return pr.PropertyInfo!.GetValue(subEntity);
}

static MethodInfo miEquals = ReflectionTools.GetMethodInfo(() => object.Equals(null, null));

static Expression<Func<object, bool>> GetPredicate(List<PropertySetter> predicate, PropertyRoute mainRoute, JsonSerializer serializer)
{
Expand All @@ -422,7 +423,7 @@ static Expression<Func<object, bool>> GetPredicate(List<PropertySetter> predicat
var left = Expression.Invoke(lambda, param);
object? objClean = ConvertObject(p.Value, pr, serializer);

return (Expression)Expression.Call(null, miEquals, left, Expression.Constant(objClean));
return (Expression)QueryUtils.GetCompareExpression(p.FilterOperation!.Value, left, Expression.Constant(objClean), inMemory: true);

}).Aggregate((a, b) => Expression.AndAlso(a, b));

Expand Down
11 changes: 11 additions & 0 deletions Signum.React/JsonConverters/EntityJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,17 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer
WriteJsonProperty(writer, serializer, mod, kvp.Key, kvp.Value, tup.pr);
}

var readonlyProps = PropertyConverter.GetPropertyConverters(value!.GetType())
.Where(kvp => kvp.Value.PropertyValidator?.IsPropertyReadonly(mod) == true)
.Select(a => a.Key)
.ToList();

if (readonlyProps.Any())
{
writer.WritePropertyName("readonlyProperties");
serializer.Serialize(writer, readonlyProps);
}

if (mod.Mixins.Any())
{
writer.WritePropertyName("mixins");
Expand Down
30 changes: 29 additions & 1 deletion Signum.React/Scripts/FindOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypeReference, PseudoType, QueryKey, getLambdaMembers, QueryTokenString } from './Reflection';
import { TypeReference, PseudoType, QueryKey, getLambdaMembers, QueryTokenString, tryGetTypeInfos } from './Reflection';
import { Lite, Entity } from './Signum.Entities';
import { PaginationMode, OrderType, FilterOperation, FilterType, ColumnOptionsMode, UniqueType, SystemTimeMode, FilterGroupOperation, PinnedFilterActive } from './Signum.Entities.DynamicQuery';
import { SearchControlProps, SearchControlLoaded } from "./Search";
Expand Down Expand Up @@ -421,6 +421,34 @@ export function isList(fo: FilterOperation) {
}


export function getFilterType(tr: TypeReference): FilterType | null {
if (tr.name == "number")
return "Integer";

if (tr.name == "decmial")
return "Decimal";

if (tr.name == "boolean")
return "Boolean";

if (tr.name == "string")
return "String";

if (tr.name == "dateTime")
return "DateTime";

if (tr.name == "Guid")
return "Guid";

if (tr.isEmbedded)
return "Embedded";

if (tr.isLite || tryGetTypeInfos(tr)[0]?.name)
return "Lite";

return null;
}

export const filterOperations: { [a: string /*FilterType*/]: FilterOperation[] } = {};
filterOperations["String"] = [
"Contains",
Expand Down
12 changes: 10 additions & 2 deletions Signum.React/Scripts/Lines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ export function taskSetFormat(lineBase: LineBaseController<any>, state: LineBase
}
}

tasks.push(taskSetReadOnly);
export function taskSetReadOnly(lineBase: LineBaseController<any>, state: LineBaseProps) {
tasks.push(taskSetReadOnlyProperty);
export function taskSetReadOnlyProperty(lineBase: LineBaseController<any>, state: LineBaseProps) {
if (!state.ctx.readOnly &&
state.ctx.propertyRoute &&
state.ctx.propertyRoute.propertyRouteType == "Field" &&
Expand All @@ -108,6 +108,14 @@ export function taskSetReadOnly(lineBase: LineBaseController<any>, state: LineBa
}
}

tasks.push(taskSetReadOnly);
export function taskSetReadOnly(lineBase: LineBaseController<any>, state: LineBaseProps) {
if (!state.ctx.readOnly &&
state.ctx.binding.getIsReadonly()) {
state.ctx.readOnly = true;
}
}

tasks.push(taskSetMandatory);
export function taskSetMandatory(lineBase: LineBaseController<any>, state: LineBaseProps) {
if (state.ctx.propertyRoute && state.mandatory == undefined &&
Expand Down
13 changes: 9 additions & 4 deletions Signum.React/Scripts/Operations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ContextualItemsContext } from './SearchControl/ContextualItems';
import { BsColor, KeyCodes } from "./Components/Basic";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import Notify from './Frames/Notify';
import { FilterOperation } from "./Signum.Entities.DynamicQuery";

export namespace Options {
export function maybeReadonly(ti: TypeInfo) {
Expand Down Expand Up @@ -454,11 +455,14 @@ export namespace Defaults {
return oi.key.endsWith(".Save");
}

export function defaultSetterConfig(oi: OperationInfo): SettersConfig {
if (!oi.canBeModified)
export function defaultSetterConfig(coc: ContextualOperationContext<Entity>): SettersConfig {
if (!coc.operationInfo.canBeModified)
return "No";

return isSave(oi) ? "Mandatory" : "Optional";
if (coc.context.lites.length == 1) //Will create too much noise
return "No";

return isSave(coc.operationInfo) ? "Mandatory" : "Optional";
}

export function getColor(oi: OperationInfo): BsColor {
Expand Down Expand Up @@ -580,7 +584,8 @@ export namespace API {

export interface PropertySetter {
property: string;
operation: PropertyOperation;
operation?: PropertyOperation;
filterOperation?: FilterOperation;
value?: any;
entityType?: string;
predicate?: PropertySetter[];
Expand Down
6 changes: 3 additions & 3 deletions Signum.React/Scripts/Operations/ContextualOperations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function getEntityOperationsContextualItems(ctx: ContextualItemsContext<E
coc.entityOperationSettings = eos;

const visibleByDefault =
(!oi.canBeModified || (coc.settings?.settersConfig ?? Defaults.defaultSetterConfig(oi)) != "No") &&
(!oi.canBeModified || (coc.settings?.settersConfig ?? Defaults.defaultSetterConfig(coc)) != "No") &&
(oi.operationType != OperationType.ConstructorFrom || ctx.lites.length == 1);

if (eos == undefined ? visibleByDefault :
Expand Down Expand Up @@ -335,7 +335,7 @@ export function defaultContextualClick(coc: ContextualOperationContext<any>, ...
if (!coc.operationInfo.canBeModified)
return Promise.resolve([]);

var settersConfig = coc.settings?.settersConfig ?? Defaults.defaultSetterConfig(coc.operationInfo);
var settersConfig = coc.settings?.settersConfig ?? Defaults.defaultSetterConfig(coc);

if (settersConfig == "No")
return Promise.resolve([]);
Expand All @@ -345,7 +345,7 @@ export function defaultContextualClick(coc: ContextualOperationContext<any>, ...
if (!onlyType)
return Promise.resolve([]);

return MultiPropertySetterModal.show(getTypeInfo(onlyType), coc.context.lites, coc.operationInfo);
return MultiPropertySetterModal.show(getTypeInfo(onlyType), coc.context.lites, coc.operationInfo, settersConfig == "Mandatory");
}
}

Expand Down
Loading

3 comments on commit e11a04d

@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.

Introducing MultiPropertySetter

This commit finishes the new MultiPropertySetter feature, a solution to be able to apply similar changes in many entities without having to open-change-save each one of them.

How it works?

By default if you have an operation with CanBeModified=true and you select more than one entity in the search control, you can now execute this operation (before was typically disabled).

image

Simple Set Property

When you select it, you get windows where you can select the modifications you want to make:

image

In most of the cases, this means selecting the property and the value you want to set, but things can get more complicated...

Changing Embedded entities

If you have and embedded entity, like Address, maybe you want to modify some sub-properties:

image

If you want to modify more than one sub-property, you can also group them together:

image

Or use the group to create a new embedded entity:

image

Changing Collections

You can also change the operation to add elements to an MList:
image

Remove elements using filters:
image

Or change elements finding then by filter first:
image

NOTE: Even if the UI drop-downs look very similar, here we are using PropertyRoutes and not QueryTokens (like in the SearchControl). Expressions won't be shown and users can not go through Lites transparently.

Security concerns

This new feature allows users to change properties skipping the standad entity UI. Of course you need to have access to modify the entity and have the operation allowed, but maybe your data consitency depends on some readonly attributes in the UI in some points.

Relying on the front-end for security was never a bullet-proof solution (user could just modify the JSON with chrome dev tools), but I can imagine this makes the insecurity much easier to exploit by an standard user.

In order to solve it, entities have a new IsPropertyReadonly virtual method, very similar to PropertyValidation but returning bool.

This method is used in three places:

  • Is used by the the new MultiPropertySetter, fixin this security problem.
  • When deserializing the entity from JSON, avoids a property to be set, improving the security on the stadart entity UI (like if the user modifies the JSON using Chrome dev tools).
  • Additionaly, when serializing the entity, a new readonlyProperties property is filled. This property is taking into account by LineBaseController (affecting ValueLine, EntitityLine, EntityDetail, EntityStrip, etc..) so the UI will be automatically readonly as well.

This change plays very well with the general philosophy of the framework, makes it more secure and gives more control to the automatic UI (DynamicComponent).

NOTE: The readonlyProperties is evaluated on the server when serializing, and won't be automatically changed on the client side. For changing readonly-ness without sending the entity back-and-forth to the server you still need to change the readonly property in the .tsx file.

Here is an example in Southwind moving readonly-ness from client to server: signumsoftware/southwind@01b8371

Conclusion

I hope your users enjoy this new feature by saving some repetitive open-change-save cycles.

Please tell me if you find some problems using it.

Enjoy!

@rezanos
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow! Great useful feature!

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on e11a04d Sep 16, 2020 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.