From 85b2648205a6cd1e3a84ffdac955b74585cddfa2 Mon Sep 17 00:00:00 2001 From: Olmo del Corral Cano Date: Sat, 25 Dec 2021 22:39:23 +0100 Subject: [PATCH] more on Token Equivalences --- .../Dashboard/DashboardLogic.cs | 78 +++++++++++------ .../UserAssets/UserAssetsExporterImporter.cs | 2 +- .../Dashboard/DashboardEntity.cs | 74 +++++++++++++++- .../Translations/Signum.Entities.de.xml | 2 +- .../Dashboard/Admin/Dashboard.tsx | 11 +-- .../Dashboard/CachedQueryExecutor.ts | 2 +- .../Dashboard/DashboardServer.cs | 5 ++ .../Dashboard/Signum.Entities.Dashboard.ts | 4 +- .../View/DashboardFilterController.tsx | 85 +++++++++++++++++-- .../Dashboard/View/DashboardView.tsx | 2 +- Signum.React/Scripts/Lines/Lines.css | 3 - 11 files changed, 215 insertions(+), 53 deletions(-) diff --git a/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs b/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs index da3ece9f46..34a2b95587 100644 --- a/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs +++ b/Signum.Engine.Extensions/Dashboard/DashboardLogic.cs @@ -72,7 +72,7 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit OnGetCachedQueryDefinition.Register((LinkListPartEntity uqp, PanelPartEmbedded pp) => Array.Empty()); sb.Include() - .WithVirtualMList(a => a.TokenEquivalences, e => e.Dashboard) + .WithVirtualMList(a => a.TokenEquivalencesGroups, e => e.Dashboard) .WithQuery(() => cp => new { Entity = cp, @@ -83,6 +83,9 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit cp.DashboardPriority, }); + sb.Schema.EntityEvents().Retrieved += DashboardLogic_Retrieved; ; + + sb.Include() .WithExpressionFrom((DashboardEntity d) => d.CachedQueries()) .WithExpressionFrom((UserChartEntity d) => d.CachedQueries()) @@ -175,6 +178,18 @@ public static void Start(SchemaBuilder sb, IFileTypeAlgorithm cachedQueryAlgorit } } + private static void DashboardLogic_Retrieved(DashboardEntity db, PostRetrievingContext ctx) + { + db.ParseData(query => + { + object? queryName = query.ToQueryNameCatch(); + if (queryName == null) + return null; + + return QueryLogic.Queries.QueryDescription(queryName); + }); + } + class DashboardGraph : Graph { public static void Register() @@ -432,41 +447,40 @@ public static List GetCachedQueryDefinitions(DashboardEnt if (!writers.Any()) continue; - var equivalences = db.TokenEquivalences.Where(a => a.InteractionGroup == key || a.InteractionGroup == null); + var equivalences = db.TokenEquivalencesGroups.Where(a => a.InteractionGroup == key || a.InteractionGroup == null); foreach (var wr in writers) { - if (wr.QueryRequest.GroupResults) - { - var keyColumns = wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken); + var keyColumns = wr.QueryRequest.GroupResults ? + wr.QueryRequest.Columns.Where(c => c.Token is not AggregateToken) : + wr.QueryRequest.Columns; - var equivalencesDictionary = (from gr in equivalences - from t in gr.TokenEquivalences.Where(a => a.Query.ToQueryName() == wr.QueryRequest.QueryName) - select KeyValuePair.Create(t.QueryToken.Token, gr.TokenEquivalences.GroupToDictionary(a => a.Query.ToQueryName(), a => a.QueryToken.Token))) - .ToDictionaryEx(); + var equivalencesDictionary = (from gr in equivalences + from t in gr.TokenEquivalences.Where(a => a.Query.ToQueryName() == wr.QueryRequest.QueryName) + select KeyValuePair.Create(t.Token.Token, gr.TokenEquivalences.GroupToDictionary(a => a.Query.ToQueryName(), a => a.Token.Token))) + .ToDictionaryEx(); - foreach (var cqd in cqdefs.Where(e => e != wr)) + foreach (var cqd in cqdefs.Where(e => e != wr)) + { + var extraColumns = keyColumns.Select(k => { - var extraColumns = keyColumns.Select(k => - { - var translatedToken = TranslatedToken(k.Token, cqd.QueryRequest.QueryName, equivalencesDictionary); + var translatedToken = TranslatedToken(k.Token, cqd.QueryRequest.QueryName, equivalencesDictionary); - if (translatedToken == null) - return null; + if (translatedToken == null) + return null; - if (!cqd.QueryRequest.Columns.Any(c => translatedToken.Contains(c.Token))) - return translatedToken.FirstEx(); //Doesn't really matter if we add "Product" or "Entity.Product"; + if (!cqd.QueryRequest.Columns.Any(c => translatedToken.Contains(c.Token))) + return translatedToken.FirstEx(); //Doesn't really matter if we add "Product" or "Entity.Product"; return null; - }).NotNull().ToList(); + }).NotNull().ToList(); - if (extraColumns.Any()) - { - ExpandColumns(cqd, extraColumns); - } - - cqd.QueryRequest.Pagination = new Pagination.All(); + if (extraColumns.Any()) + { + ExpandColumns(cqd, extraColumns); } + + cqd.QueryRequest.Pagination = new Pagination.All(); } } } @@ -499,10 +513,20 @@ private static void ExpandColumns(CachedQueryDefinition cqd, List ex var toAppend = new List(); for (var t = original; t != null; t = t.Parent) { - if (equivalences.TryGetValue(t, out var dic) && dic.TryGetValue(targetQueryName, out var list)) - return list.Select(t => AppendTokens(t, toAppend)).ToList(); + { + if (equivalences.TryGetValue(t, out var dic) && dic.TryGetValue(targetQueryName, out var list)) + return list.Select(t => AppendTokens(t, toAppend)).ToList(); + } toAppend.Insert(0, t); + + if(t.Parent == null) + { + var entityToken = QueryUtils.Parse("Entity", QueryLogic.Queries.QueryDescription(original.QueryName), 0); + + if(equivalences.TryGetValue(entityToken, out var dic) && dic.TryGetValue(targetQueryName, out var list)) + return list.Select(t => AppendTokens(t, toAppend)).ToList(); + } } if (original.QueryName == targetQueryName) @@ -522,6 +546,8 @@ private static QueryToken AppendTokens(QueryToken t, List toAppend) if (newToken == null) throw new FormatException("Token with key '{0}' not found on {1} of query {2}".FormatWith(nt.Key, t, QueryUtils.GetKey(qd.QueryName))); + + t = newToken; } return t; diff --git a/Signum.Engine.Extensions/UserAssets/UserAssetsExporterImporter.cs b/Signum.Engine.Extensions/UserAssets/UserAssetsExporterImporter.cs index a20c5d7ccc..0dac034cae 100644 --- a/Signum.Engine.Extensions/UserAssets/UserAssetsExporterImporter.cs +++ b/Signum.Engine.Extensions/UserAssets/UserAssetsExporterImporter.cs @@ -145,7 +145,7 @@ public IUserAssetEntity GetEntity(Guid guid) Guid = guid, Action = entity.IsNew ? EntityAction.New : customResolutionModel.ContainsKey(entity.Guid) ? EntityAction.Different : - GraphExplorer.FromRoot((Entity)entity).Any(a => a.Modified != ModifiedState.Clean) ? EntityAction.Different : + GraphExplorer.FromRootVirtual((Entity)entity).Any(a => a.Modified != ModifiedState.Clean) ? EntityAction.Different : EntityAction.Identical, CustomResolution = customResolutionModel.TryGetCN(entity.Guid), }); diff --git a/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs b/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs index f9c658fda1..d9b0b131b2 100644 --- a/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs +++ b/Signum.Entities.Extensions/Dashboard/DashboardEntity.cs @@ -51,7 +51,7 @@ public Lite? EntityType public MList Parts { get; set; } = new MList(); [Ignore, QueryableProperty] - public MList TokenEquivalences { get; set; } = new MList(); + public MList TokenEquivalencesGroups { get; set; } = new MList(); [UniqueIndex] public Guid Guid { get; set; } = Guid.NewGuid(); @@ -105,6 +105,19 @@ protected override void ChildCollectionChanged(object? sender, NotifyCollectionC } + internal void ParseData(Func getDescription) + { + foreach (var f in TokenEquivalencesGroups) + { + foreach (var t in f.TokenEquivalences) + { + var description = getDescription(t.Query); + if (description != null) + t.Token.ParseData(this, description, SubTokensOptions.CanElement); + } + } + } + [Ignore] bool invalidating = false; protected override void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -151,7 +164,9 @@ public XElement ToXml(IToXmlContext ctx) EmbeddedInEntity == null ? null! : new XAttribute("EmbeddedInEntity", EmbeddedInEntity.Value.ToString()), new XAttribute("CombineSimilarRows", CombineSimilarRows), CacheQueryConfiguration?.ToXml(ctx), - new XElement("Parts", Parts.Select(p => p.ToXml(ctx)))); + new XElement("Parts", Parts.Select(p => p.ToXml(ctx))), + new XElement(nameof(TokenEquivalencesGroups), TokenEquivalencesGroups.Select(teg => teg.ToXml(ctx))) + ); } @@ -165,6 +180,8 @@ public void FromXml(XElement element, IFromXmlContext ctx) CombineSimilarRows = element.Attribute("CombineSimilarRows")?.Let(a => bool.Parse(a.Value)) ?? false; CacheQueryConfiguration = CacheQueryConfiguration.CreateOrAssignEmbedded(element.Element(nameof(CacheQueryConfiguration)), (cqc, elem) => cqc.FromXml(elem)); Parts.Synchronize(element.Element("Parts")!.Elements().ToList(), (pp, x) => pp.FromXml(x, ctx)); + TokenEquivalencesGroups.Synchronize(element.Element(nameof(TokenEquivalencesGroups))?.Elements().ToList() ?? new List(), (teg, x) => teg.FromXml(x, ctx)); + ParseData(q => ctx.GetQueryDescription(q)); } protected override string? PropertyValidation(PropertyInfo pi) @@ -183,6 +200,16 @@ public void FromXml(XElement element, IFromXmlContext ctx) return ValidationMessage._0ShouldBeNullWhen1IsSet.NiceToString(pi.NiceName(), NicePropertyName(() => EntityType)); } + if(pi.Name == nameof(TokenEquivalencesGroups)) + { + var dups = TokenEquivalencesGroups + .SelectMany(a => a.TokenEquivalences).Select(a => a.Token.Token).NotNull() + .GroupCount(a => a).Where(gr => gr.Value > 1).ToString(a => a.Value + " x " + a.Key.FullKey(), "\n"); + + if (dups.HasText()) + return "Duplicated tokens: " + dups; + } + return base.PropertyValidation(pi); } } @@ -270,18 +297,57 @@ public enum DashboardEmbedededInEntity [EntityKind(EntityKind.Part, EntityData.Master)] public class TokenEquivalenceGroupEntity : Entity { - [NotNullValidator(DisabledInModelBinder = true)] + [NotNullValidator(Disabled = true)] public Lite Dashboard { get; set; } public InteractionGroup? InteractionGroup { get; set; } [PreserveOrder, NoRepeatValidator, CountIsValidator(ComparisonType.GreaterThan, 1)] public MList TokenEquivalences { get; set; } = new MList(); + + internal void FromXml(XElement x, IFromXmlContext ctx) + { + InteractionGroup = x.Attribute("InteractionGroup")?.Value.ToEnum(); + TokenEquivalences.Synchronize(x.Elements("TokenEquivalence").ToList(), (teg, x) => teg.FromXml(x, ctx)); + } + + internal XElement ToXml(IToXmlContext ctx) + { + return new XElement("TokenEquivalenceGroup", + InteractionGroup == null ? null : new XAttribute(nameof(InteractionGroup), InteractionGroup.Value.ToString()), + TokenEquivalences.Select(te => te.ToXml(ctx))); + } + + protected override string? PropertyValidation(PropertyInfo pi) + { + if(pi.Name == nameof(TokenEquivalences)) + { + var list = TokenEquivalences.Select(a => a.Token.Token.Type.UnNullify().CleanType()).Distinct().ToList(); + if(list.Count > 1) + { + if (!list.Any(t => list.All(t2 => t.IsAssignableFrom(t2)))) + return "Types " + list.CommaAnd(t => t.TypeName()) + " are not compatible"; + } + } + + return base.PropertyValidation(pi); + } } public class TokenEquivalenceEmbedded : EmbeddedEntity { public QueryEntity Query { get; set; } - public QueryTokenEmbedded QueryToken { get; set; } + public QueryTokenEmbedded Token { get; set; } + + internal void FromXml(XElement element, IFromXmlContext ctx) + { + Query = ctx.GetQuery(element.Attribute("Query")!.Value); + Token = new QueryTokenEmbedded(element.Attribute("Token")!.Value); + } + + internal XElement ToXml(IToXmlContext ctx) => new XElement("TokenEquivalence", + new XAttribute("Query", Query.Key), + new XAttribute("Token", Token.Token.FullKey()) + ); } diff --git a/Signum.Entities/Translations/Signum.Entities.de.xml b/Signum.Entities/Translations/Signum.Entities.de.xml index 1aeb45ac01..449700753e 100644 --- a/Signum.Entities/Translations/Signum.Entities.de.xml +++ b/Signum.Entities/Translations/Signum.Entities.de.xml @@ -595,4 +595,4 @@ - \ No newline at end of file + diff --git a/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx b/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx index 77284ac90e..d568f9a6d1 100644 --- a/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx +++ b/Signum.React.Extensions/Dashboard/Admin/Dashboard.tsx @@ -168,10 +168,11 @@ export default function Dashboard(p: { ctx: TypeContext }) { cp.parts)} getComponent={renderPart} onCreate={handleOnCreate} /> - a.tokenEquivalences)} eventKey="equivalences"> - a.tokenEquivalences)} avoidFieldSet getComponent={(ctxGr: TypeContext) => + a.tokenEquivalencesGroups)} eventKey="equivalences"> + a.tokenEquivalencesGroups, { formSize: "ExtraSmall" })} avoidFieldSet getComponent={(ctxGr: TypeContext) =>
- cp.interactionGroup)} inlineCheckbox={true} /> + pp.interactionGroup)} + onRenderDropDownListItem={(io) => {io.label}} /> p.tokenEquivalences)} avoidFieldSet columns={EntityTable.typedColumns([ { property: p => p.query, @@ -179,8 +180,8 @@ export default function Dashboard(p: { ctx: TypeContext }) { headerHtmlAttributes: { style: { width: "30%" } }, }, { - property: p => p.query, - template: (ectx) => ectx.value.query && p.queryToken)} + property: p => p.token, + template: (ectx) => ectx.value.query && p.token)} queryKey={ectx.value.query.key} subTokenOptions={SubTokensOptions.CanAggregate | SubTokensOptions.CanElement | SubTokensOptions.CanAnyAll} />, headerHtmlAttributes: { style: { width: "100%" } }, }, diff --git a/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts b/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts index 8432b79218..e510d03d9a 100644 --- a/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts +++ b/Signum.React.Extensions/Dashboard/CachedQueryExecutor.ts @@ -577,7 +577,7 @@ function paginateRows(rt: ResultTable, reqPag: Pagination): ResultTable{ { switch (reqPag.mode) { case "All": return rt; - case "Firsts": return { ...rt, rows: rt.rows.slice(0, rt.pagination.elementsPerPage), pagination: reqPag }; + case "Firsts": return { ...rt, rows: rt.rows.slice(0, reqPag.elementsPerPage), pagination: reqPag }; case "Paginate": var startIndex = reqPag.elementsPerPage! * (reqPag.currentPage! - 1); return { ...rt, rows: rt.rows.slice(startIndex, startIndex + reqPag.elementsPerPage!), pagination: reqPag }; diff --git a/Signum.React.Extensions/Dashboard/DashboardServer.cs b/Signum.React.Extensions/Dashboard/DashboardServer.cs index 3753d60a16..7c8c985907 100644 --- a/Signum.React.Extensions/Dashboard/DashboardServer.cs +++ b/Signum.React.Extensions/Dashboard/DashboardServer.cs @@ -28,5 +28,10 @@ public static void Start(IApplicationBuilder app) if (result != null) ep.extension.Add("embeddedDashboards", result); }; + + SignumServer.WebEntityJsonConverterFactory.AfterDeserilization.Register((DashboardEntity uq) => + { + uq.ParseData(q => QueryLogic.Queries.QueryDescription(q.ToQueryName())); + }); } } diff --git a/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts b/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts index 15934d78df..156a9afc36 100644 --- a/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts +++ b/Signum.React.Extensions/Dashboard/Signum.Entities.Dashboard.ts @@ -76,7 +76,7 @@ export interface DashboardEntity extends Entities.Entity, UserAssets.IUserAssetE combineSimilarRows: boolean; cacheQueryConfiguration: CacheQueryConfigurationEmbedded | null; parts: Entities.MList; - tokenEquivalences: Entities.MList; + tokenEquivalencesGroups: Entities.MList; guid: string /*Guid*/; key: string | null; } @@ -151,7 +151,7 @@ export const TokenEquivalenceEmbedded = new Type("Toke export interface TokenEquivalenceEmbedded extends Entities.EmbeddedEntity { Type: "TokenEquivalenceEmbedded"; query: Basics.QueryEntity; - queryToken: UserAssets.QueryTokenEmbedded; + token: UserAssets.QueryTokenEmbedded; } export const TokenEquivalenceGroupEntity = new Type("TokenEquivalenceGroup"); diff --git a/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx b/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx index 1e7ba244d5..4548f9c73c 100644 --- a/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx +++ b/Signum.React.Extensions/Dashboard/View/DashboardFilterController.tsx @@ -1,4 +1,4 @@ -import { PanelPartEmbedded } from '../Signum.Entities.Dashboard'; +import { DashboardEntity, PanelPartEmbedded } from '../Signum.Entities.Dashboard'; import { FilterConditionOptionParsed, FilterGroupOptionParsed, FilterOptionParsed, FindOptions, QueryToken } from '@framework/FindOptions'; import { FilterGroupOperation } from '@framework/Signum.Entities.DynamicQuery'; import { ChartRequestModel } from '../../Chart/Signum.Entities.Chart'; @@ -6,6 +6,7 @@ import { ChartRow } from '../../Chart/ChartClient'; import { Entity, is, Lite } from '@framework/Signum.Entities'; import * as Finder from '../../../Signum.React/Scripts/Finder'; import { getQueryKey } from '@framework/Reflection'; +import { Dic, softCast } from '../../../Signum.React/Scripts/Globals'; export class DashboardFilterController { @@ -14,9 +15,11 @@ export class DashboardFilterController { filters: Map = new Map(); lastChange: Map = new Map(); + dashboard: DashboardEntity; - constructor(forceUpdate: () => void) { + constructor(forceUpdate: () => void, dashboard: DashboardEntity) { this.forceUpdate = forceUpdate; + this.dashboard = dashboard; } setFilter(filter: DashboardFilter) { @@ -41,17 +44,48 @@ export class DashboardFilterController { var otherFilters = Array.from(this.filters.values()).filter(f => f.partEmbedded != partEmbedded && f.partEmbedded.interactionGroup == partEmbedded.interactionGroup && f.rows?.length); - var result = otherFilters.filter(a => a.queryKey == queryKey).map( - df => groupFilter("Or", df.rows.map( - r => groupFilter("And", r.filters.map( - f => ({ token: f.token, operation: "EqualTo", value: f.value, frozen: false }) as FilterConditionOptionParsed - )) - ).notNull()) - ).notNull(); + debugger; + + var equivalences = this.dashboard.tokenEquivalencesGroups + .filter(a => a.element.interactionGroup == partEmbedded.interactionGroup || a.element.interactionGroup == null) + .flatMap(gr => { + var target = gr.element.tokenEquivalences.filter(a => a.element.query.key == queryKey) + + return gr.element.tokenEquivalences.flatMap(f => target.filter(t => t != f).map(t => softCast({ + fromQueryKey: f.element.query.key, + fromToken: f.element.token.token!, + toQuery: t.element.query.key, + toToken: t.element.token.token! + }))); + }).groupToObject(a => a.fromQueryKey) + + var result = otherFilters.map( + df => { + + + var tokenEquivalences = equivalences[df.queryKey]?.groupToObject(a => a.fromToken!.fullKey); + + if (df.queryKey != queryKey && tokenEquivalences == undefined) + return null; + + return groupFilter("Or", df.rows.map( + r => groupFilter("And", r.filters.map( + f => { + var token = df.queryKey == queryKey ? f.token : translateToken(f.token, tokenEquivalences); + if (token == null) + return undefined; + + return ({ token: token, operation: "EqualTo", value: f.value, frozen: false }) as FilterConditionOptionParsed; + } + ).notNull()) + ).notNull()) + }).notNull(); return result; } + + applyToFindOptions(partEmbedded: PanelPartEmbedded, fo: FindOptions): FindOptions { var fops = this.getFilterOptions(partEmbedded, getQueryKey(fo.queryName)); @@ -69,6 +103,33 @@ export class DashboardFilterController { } } +function translateToken(token: QueryToken, tokenEquivalences: { [token: string]: TokenEquivalenceTuple[] }) { + + var toAdd: QueryToken[] = []; + + for (var t = token; t != null; t = t.parent!) { + + var equivalence = tokenEquivalences[t.fullKey]; + + if (equivalence != null) { + return toAdd.reduce((t, nt) => ({ ...nt, parent: t, fullKey: t.fullKey + "." + nt.key }) as QueryToken, equivalence.first().toToken) + } + + toAdd.insertAt(0, t); + + if (t.parent == null) {//Is a Column, like 'Supplier', but maybe 'Entity' is mapped to we can interpret it as 'Entity.Supplier' + debugger; + equivalence = tokenEquivalences["Entity"]; + + if (equivalence != null) { + return toAdd.reduce((t, nt) => ({ ...nt, parent: t, fullKey: t.fullKey + "." + nt.key }) as QueryToken, equivalence.first().toToken) + } + } + } + + return null; +} + export function groupFilter(groupOperation: FilterGroupOperation, filters: FilterOptionParsed[]): FilterOptionParsed | undefined { @@ -84,6 +145,12 @@ export function groupFilter(groupOperation: FilterGroupOperation, filters: Filte }) as FilterGroupOptionParsed; } +interface TokenEquivalenceTuple { + fromQueryKey: string; + fromToken: QueryToken; + toQuery: string; + toToken: QueryToken; +} export class DashboardFilter { partEmbedded: PanelPartEmbedded; diff --git a/Signum.React.Extensions/Dashboard/View/DashboardView.tsx b/Signum.React.Extensions/Dashboard/View/DashboardView.tsx index c1c6c075b1..936e110196 100644 --- a/Signum.React.Extensions/Dashboard/View/DashboardView.tsx +++ b/Signum.React.Extensions/Dashboard/View/DashboardView.tsx @@ -19,7 +19,7 @@ import { CachedQueryJS } from '../CachedQueryExecutor' export default function DashboardView(p: { dashboard: DashboardEntity, cachedQueries: { [userAssetKey: string]: Promise }, entity?: Entity, deps?: React.DependencyList; reload: () => void; }) { const forceUpdate = useForceUpdate(); - var filterController = React.useMemo(() => new DashboardFilterController(forceUpdate), [p.dashboard]); + var filterController = React.useMemo(() => new DashboardFilterController(forceUpdate, p.dashboard), [p.dashboard]); function renderBasic() { diff --git a/Signum.React/Scripts/Lines/Lines.css b/Signum.React/Scripts/Lines/Lines.css index 11c4c7f6d3..578fd48f26 100644 --- a/Signum.React/Scripts/Lines/Lines.css +++ b/Signum.React/Scripts/Lines/Lines.css @@ -159,9 +159,6 @@ fieldset { /*EntityRepeater*/ fieldset.sf-repeater-element { - margin: 0; - padding-top: 6px; - padding-bottom: 3px; } fieldset.sf-repeater-element legend {