diff --git a/.vscode/settings.json b/.vscode/settings.json index 484dcdcc..a72a9aa0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,22 @@ { "cSpell.words": [ "Dataverse", + "msapp", "PPUX", - "msapp" + "RGBA" ], "[yaml]": {}, "yaml.schemaStore.enable": true, "yaml.validate": true, - "yaml.schemas": { - "https://raw.githubusercontent.com/microsoft/PowerApps-Tooling/master/docs/pa.yaml-schema.json": "*.pa.yaml" - }, + "yaml.schemas": { + "http://json-schema.org/draft-07/schema#": "*.schema.yaml", + "src/schemas/pa-yaml/v3.0/pa.schema.yaml": [ + //"*.pa.yaml", + "src/schemas-tests/pa-yaml/v3.0/**/*.pa.yaml" + ], + "https://raw.githubusercontent.com/microsoft/PowerApps-Tooling/master/schemas/pa-yaml/v3.0/pa.schema.yaml": [ + "src/schemas-tests/**/*.pa.yaml" + ], + "https://raw.githubusercontent.com/microsoft/PowerApps-Tooling/master/docs/pa.yaml-schema.json": "src/Persistence.Tests/**/*.pa.yaml" + } } diff --git a/schemas/pa-yaml/v2.2/pa.schema.yaml b/schemas/pa-yaml/v3.0/pa.schema.yaml similarity index 63% rename from schemas/pa-yaml/v2.2/pa.schema.yaml rename to schemas/pa-yaml/v3.0/pa.schema.yaml index 31c8f036..2be01fc7 100644 --- a/schemas/pa-yaml/v2.2/pa.schema.yaml +++ b/schemas/pa-yaml/v3.0/pa.schema.yaml @@ -1,9 +1,6 @@ -# Unfortunately, it also seems to make it so that schema cache doesn't get updated if the schema file updates. -# WORKAROUND: Close the VSCode window and reopen. - $schema: http://json-schema.org/draft-07/schema# -$id: http://powerapps.com/schemas/pa-yaml/v2.2/pa.schema -title: Microsoft Power Apps schema for app source yaml files (v2.2). +$id: http://powerapps.com/schemas/pa-yaml/v3.0/pa.schema +title: Microsoft Power Apps schema for app source yaml files (v3.0). description: >- The schema for all *.pa.yaml files which are used to describe a Power Apps canvas app. All *.pa.yaml files in an *.msapp are logically combined into a single *.pa.yaml file. @@ -17,6 +14,18 @@ properties: $ref: "#/definitions/Screens-name-instance-map" ComponentDefinitions: $ref: "#/definitions/ComponentDefinitions-name-instance-map" +defaultSnippets: + - label: App + body: + App: + Properties: + StartScreen: =${1:Screen1} + - label: Screens + body: + Screens: + ${1:Screen1}: + Children: + - $0 definitions: App-instance: @@ -26,35 +35,31 @@ definitions: properties: Properties: { $ref: "#/definitions/Properties-formula-map" } - # Note: App children is fixed. - Children: - description: The App currently only supports the 'Host' child. - type: object - additionalProperties: false - properties: - Host: - type: object - additionalProperties: false - properties: - # Currently, the Control identifier is static, but may need to be exposed in order to support variants - #Control: { $ref: "#/definitions/Control-type-identifier" } - Properties: { $ref: "#/definitions/Properties-formula-map" } - Screens-name-instance-map: description: |- Unordered map where keys are the names of each screen. type: object propertyNames: { $ref: "#/definitions/Screen-name" } additionalProperties: - type: object - additionalProperties: false - properties: - Properties: { $ref: "#/definitions/Properties-formula-map" } - Children: { $ref: "#/definitions/Children-Control-instance-sequence" } + $ref: "#/definitions/Screen-instance" + defaultSnippets: + - label: Add a Screen + body: + ${1:Screen1}: + Children: + - $0 Screen-name: $ref: "#/definitions/entity-name" + Screen-instance: + type: object + additionalProperties: false + properties: + Properties: { $ref: "#/definitions/Properties-formula-map" } + Groups: { $ref: "#/definitions/Groups-of-controls" } + Children: { $ref: "#/definitions/Children-Control-instance-sequence" } + Children-Control-instance-sequence: description: >- A sequence of control instances, where each item is a control's name with a control instance. @@ -67,73 +72,179 @@ definitions: propertyNames: { $ref: "#/definitions/Control-instance-name" } additionalProperties: $ref: "#/definitions/Control-instance" + defaultSnippets: + - label: Add Control + body: + '${2:Control1}': + Control: ${1:Label} + Properties: + X: =10 + Y: =10$0 + - label: Add custom `Component` instance + body: + '${2:${1}1}': + Control: Component + ComponentName: ${1:MyComponent} + Properties: + X: =10 + Y: =10$0 + - label: Add `CodeComponent` instance (aka PCF control) + body: + '${2:${1}1}': + Control: CodeComponent + ComponentName: ${1:MyComponent} + Properties: + X: =10 + Y: =10$0 Control-instance-name: $ref: "#/definitions/entity-name" + ControlTypeId: + description: The invariant identifier for the type of control being instantiated. + allOf: + - $ref: "#/definitions/ControlTypeId-pattern" + not: + anyOf: + - $ref: "#/definitions/ControlTypeId-disallowed-types" + - $ref: "#/definitions/ControlTypeId-not-yet-supported" + oneOf: + - $ref: "#/definitions/ControlTypeId-oneOf-3P-types" + - $ref: "#/definitions/ControlTypeId-1P-controls" + + ControlTypeId-pattern: + $comment: Defines reusable schema for validating the pattern allowed for control type identifiers. + type: string + pattern: |- + ^([A-Z][a-zA-Z0-9]*/)?[A-Z][a-zA-Z0-9]*$ + + ControlTypeId-disallowed-types: + enum: + - AppInfo + - HostControl + - Screen + - AppTest + - TestCase + - TestSuite + + ControlTypeId-not-yet-supported: + enum: + - CommandComponent + - DataComponent + - FunctionComponent + + ControlTypeId-oneOf-3P-types: + $comment: The set of ControlTypeIds that represent third-party controls. + oneOf: + - $ref: "#/definitions/ControlTypeId-Component" + - $ref: "#/definitions/ControlTypeId-CodeComponent" + + + ControlTypeId-Component: + description: |- + Identifies a custom component instance. This control type requires additional properties to be specified. + type: string + const: Component + + ControlTypeId-CodeComponent: + description: |- + Identifies a custom code component (aka PCF control) instance. This control type requires additional properties to be specified. + type: string + const: CodeComponent + + ControlTypeId-1P-controls: + description: The invariant identifier of a first-party control published by Power Apps (aka the 'Control Library'). + allOf: + - $ref: "#/definitions/ControlTypeId-pattern" + - $ref: "#/definitions/ControlTypeId-1P-controls-enum" + not: + $comment: Exclude built-in control identifiers as these are not defined in the 'Control Library'. + $ref: "#/definitions/ControlTypeId-oneOf-3P-types" + + ControlTypeId-1P-controls-enum: + true + + Control-variant-name: + description: The variant of a control template being instantiated. + allOf: + - $ref: "#/definitions/entity-name" + Control-instance: type: object required: [Control] properties: - Control: { $ref: "#/definitions/Control-type-identifier" } - Variant: { $ref: "#/definitions/Control-variant-name" } + Control: { $ref: "#/definitions/ControlTypeId" } Properties: { $ref: "#/definitions/Properties-formula-map" } - Children: { $ref: "#/definitions/Children-Control-instance-sequence" } if: required: [Control] properties: - Control: { $ref: "#/definitions/Control-type-component" } + Control: { $ref: "#/definitions/ControlTypeId-oneOf-3P-types" } then: - required: [ComponentName] - properties: - Control: true - ComponentLibraryUniqueName: { $ref: "#/definitions/ComponentLibrary-unique-name" } - ComponentName: { $ref: "#/definitions/ComponentDefinition-name" } - Properties: true - # Note: Component instances do not support Variants or Children. - additionalProperties: false + allOf: + - if: + properties: + Control: { $ref: "#/definitions/ControlTypeId-Component" } + then: + required: [ComponentName] + additionalProperties: false + properties: + Control: true + ComponentLibraryUniqueName: { $ref: "#/definitions/ComponentLibrary-unique-name" } + ComponentName: { $ref: "#/definitions/ComponentDefinition-name" } + Properties: true + - if: + properties: + Control: { $ref: "#/definitions/ControlTypeId-CodeComponent" } + then: + required: [ComponentName] + additionalProperties: false + properties: + Control: true + ComponentName: { $ref: "#/definitions/CodeComponent-name" } + Properties: true else: - # Expected to be a built-in control library template + additionalProperties: false properties: Control: true - Variant: true + Variant: { $ref: "#/definitions/Control-variant-name" } Properties: true - Children: true - additionalProperties: false - - Control-type-identifier: - description: The invariant type of control being instantiated. - type: string - oneOf: - - $ref: "#/definitions/control-library-template-name" - - $ref: "#/definitions/Control-type-component" + Groups: { $ref: "#/definitions/Groups-of-controls" } + Children: { $ref: "#/definitions/Children-Control-instance-sequence" } - Control-type-component: + Groups-of-controls: description: |- - Identifies a custom component instance. This control type requires additional properties to be specified. - type: string - const: component + A mapping of groups of controls under this container. The keys of this object represent the name of the Group. + + Groups do not impact the behavior of an app, but are used in the Studio to organize controls when editing. + type: object + propertyNames: { $ref: "#/definitions/Control-instance-name" } + additionalProperties: + type: object + required: [ControlNames] + additionalProperties: false + properties: + ControlNames: + description: |- + An array of the names of controls that are part of this group. + A group must have at least two (2) controls in it. + type: array + minItems: 2 + items: { $ref: "#/definitions/Control-instance-name" } + defaultSnippets: + - label: Add Group + body: + ${1:Group1}: + ControlNames: + - ${2:ControlName1} + - ${3:ControlName2} - control-library-template-name: - description: The invariant name of a control template published by Power Apps (aka the 'Control Library'). + CodeComponent-name: + description: |- + The unique name of the Code Component (aka PCF control) as it occurs in Dataverse. + The format is: '_' '.' type: string - minimum: 1 - # NOTE: The pattern here is more restrictive than a DName, to represent the actual set of chars used. - # By doing this, we can catch invalid uses. We can always expand the char set in the future iif needed. pattern: |- - ^[a-zA-Z0-9][a-zA-Z0-9]*$ - not: - $comment: Exclude 1st class control type names which are not defined in the 'Control Library'. - enum: [component] - examples: - # TODO: Add additional well-known control types here for usability - - label - - gallery - - Control-variant-name: - description: The variant of a control template being instantiated. - allOf: - - $ref: "#/definitions/entity-name" + ^([a-z][a-z0-9]{1,7})_([a-zA-Z0-9]\.)+[a-zA-Z0-9]+*$ ComponentDefinitions-name-instance-map: type: object @@ -161,7 +272,6 @@ definitions: - $ref: "#/definitions/Properties-formula-map" - propertyNames: examples: - # These are the known properties for a component definition, but others may be allowed in the future, along with those defined by custom Output properties. - ContentLanguage - ChildTabPriority - EnableChildFocus @@ -169,6 +279,7 @@ definitions: - Height - Width - OnReset + Groups: { $ref: "#/definitions/Groups-of-controls" } Children: { $ref: "#/definitions/Children-Control-instance-sequence" } ComponentDefinition-name: @@ -198,7 +309,6 @@ definitions: - const: Action description: A property that represents an action. DisplayName: - # TODO: This property will get removed from the document description: DEPRECATED. This is not used anywhere and will be removed. type: string Description: @@ -291,17 +401,6 @@ definitions: The unique name of the component library within Dataverse. It has the form "{PublisherPrefix}_{ComponentLibraryName}". type: string - # Dataverse Publisher Prefix: - # - must be 2 to 8 characters long, can only consist of alpha-numerics, must start with a letter. - # AppName Specification (owned by PPUX service): - # - SaveAsDialog.tsx#onSave() : https://dev.azure.com/msazure/OneAgile/_git/PowerApps-Client?path=/src/AppMagic/powerapps-client/packages/powerapps-authoring/src/components/SaveDialog/SaveAsDialog.tsx&version=GBmaster&_a=contents - # - maxLength: 64 - # - isValidAppName : https://dev.azure.com/msazure/OneAgile/_git/PowerApps-Client?path=/src/AppMagic/powerapps-client/packages/powerapps-authoring/src/components/Backstage/AppSettings/BasicAppSettings/Common.ts - # - First char must be non-whitespace char. i.e. \S - # - Must contain at least one non-whitespace char. i.e. \S - # - StringUtility.ts#isValidFileName - # - Must contain at least one non-whitespace char. i.e. \S - # - Must not contain an invalid filename char. i.e. const invalidFileNameChars = '*".?:\\<>|/'; pattern: |- ^([a-z][a-z0-9]{1,7})_(\S.{0,63})$ not: @@ -322,7 +421,6 @@ definitions: $ref: "#/definitions/pfx-formula" entity-name: - # aka: DName description: The base requirements for a named entity in an app. type: string @@ -349,8 +447,6 @@ definitions: pfx-function-parameter-name: description: The name of a Power Fx function parameter. type: string - # TODO: Add `pattern` with correct char set - # pattern: "^[a-zA-Z0-9_]+$" pfx-function-return-type: oneOf: @@ -375,7 +471,6 @@ definitions: pfx-formula: oneOf: - # Note: The first item of a 'oneOf' will be used for error message when none match. So we make sure our default preferred normalization is first. - type: string pattern: ^=.* - type: 'null' diff --git a/src/Persistence.Tests/Extensions/PersistenceFluentExtensions.cs b/src/Persistence.Tests/Extensions/PersistenceFluentExtensions.cs index f6a0d813..47b04192 100644 --- a/src/Persistence.Tests/Extensions/PersistenceFluentExtensions.cs +++ b/src/Persistence.Tests/Extensions/PersistenceFluentExtensions.cs @@ -30,6 +30,28 @@ public static void ShouldNotBeNull([NotNull] this T? value) } } + public static AndConstraint NotDefineMember(this ObjectAssertions assertions, string memberName, string because = "", params object[] becauseArgs) + where TAssertions : ObjectAssertions + where TSubject : class + { + _ = assertions ?? throw new ArgumentNullException(nameof(assertions)); + + assertions.Subject.ShouldNotBeNull(); + if (assertions.Subject is not null) + { + var subjectType = assertions.Subject.GetType(); + var matchingMembers = subjectType.GetMembers().Where(m => m.Name == memberName).ToArray(); + + Execute.Assertion + .ForCondition(matchingMembers!.Length == 0) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:Type} with Type name {0} to not have a member with name {1}{reason}, but does.", + subjectType.Name, memberName); + } + + return new AndConstraint((TAssertions)assertions); + } + public static AndConstraint BeYamlEquivalentTo(this StringAssertions assertions, string expectedYaml, string because = "", params object[] becauseArgs) where TAssertions : StringAssertions { diff --git a/src/Persistence.Tests/Extensions/PersistenceExceptionAssertionsExtensions.cs b/src/Persistence.Tests/Extensions/PersistenceLibraryExceptionAssertionsExtensions.cs similarity index 94% rename from src/Persistence.Tests/Extensions/PersistenceExceptionAssertionsExtensions.cs rename to src/Persistence.Tests/Extensions/PersistenceLibraryExceptionAssertionsExtensions.cs index ef6323f1..c7bec3d1 100644 --- a/src/Persistence.Tests/Extensions/PersistenceExceptionAssertionsExtensions.cs +++ b/src/Persistence.Tests/Extensions/PersistenceLibraryExceptionAssertionsExtensions.cs @@ -8,14 +8,14 @@ namespace Persistence.Tests.Extensions; -public static class PersistenceExceptionAssertionsExtensions +public static class PersistenceLibraryExceptionAssertionsExtensions { public static ExceptionAssertions WithErrorCode( this ExceptionAssertions assertion, PersistenceErrorCode errorCode, string? because = null, params object[] becauseArgs) - where TException : PersistenceException + where TException : PersistenceLibraryException { _ = assertion ?? throw new ArgumentNullException(nameof(assertion)); @@ -36,7 +36,7 @@ public static ExceptionAssertions WithReason( string? wildcardPattern, string? because = null, params object[] becauseArgs) - where TException : PersistenceException + where TException : PersistenceLibraryException { _ = assertion ?? throw new ArgumentNullException(nameof(assertion)); diff --git a/src/Persistence.Tests/MsApp/MsappArchiveTests.cs b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs index 01a3c803..f648df87 100644 --- a/src/Persistence.Tests/MsApp/MsappArchiveTests.cs +++ b/src/Persistence.Tests/MsApp/MsappArchiveTests.cs @@ -73,7 +73,7 @@ public void AddEntryTests(string[] entries, string[] expectedEntries) } // Get the required entry should throw if it doesn't exist - msappArchive.Invoking(a => a.GetRequiredEntry("not-exist")).Should().Throw() + msappArchive.Invoking(a => a.GetRequiredEntry("not-exist")).Should().Throw() .WithErrorCode(PersistenceErrorCode.MsappArchiveError); } diff --git a/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs b/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs index 0fb9ffe7..78320783 100644 --- a/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs +++ b/src/Persistence.Tests/MsApp/RoundTripWriterTests.cs @@ -58,7 +58,7 @@ public void Write_Should_Fail(string input, string output, int line, int column) }; // Assert - var thrownEx = act.Should().ThrowExactly() + var thrownEx = act.Should().ThrowExactly() .WithErrorCode(PersistenceErrorCode.RoundTripValidationFailed) .Which; thrownEx.MsappEntryFullPath.Should().Be("test"); diff --git a/src/Persistence.Tests/PaYaml/Models/PaControlInstanceContainerTests.cs b/src/Persistence.Tests/PaYaml/Models/PaControlInstanceContainerTests.cs new file mode 100644 index 00000000..c2738580 --- /dev/null +++ b/src/Persistence.Tests/PaYaml/Models/PaControlInstanceContainerTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models; +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; + +namespace Persistence.Tests.PaYaml.Models; + +[TestClass] +public class PaControlInstanceContainerTests : TestBase +{ + [TestMethod] + public void DescendantControlInstancesSingleLevel() + { + var screen = new NamedObject("Screen1", new() + { + Children = + { + new("Ctrl0", new("GroupContainer")), + new("Ctrl1", new("GroupContainer")), + new("Ctrl2", new("GroupContainer")), + }, + }); + + screen.DescendantControlInstances().SelectNames().Should().Equal(new[] + { + "Ctrl0", + "Ctrl1", + "Ctrl2", + }, "items should be in document order"); + } + + [TestMethod] + public void DescendantControlInstances1AtEachLevel() + { + var screen = new NamedObject("Screen1", new() + { + Children = + { + new("Ctrl0", new("GroupContainer") + { + Children = + { + new("Ctrl0.0", new("GroupContainer") + { + Children = + { + new("Ctrl0.0.0", new("GroupContainer")), + } + }), + } + }), + }, + }); + + screen.DescendantControlInstances().SelectNames().Should().Equal(new[] + { + "Ctrl0", + "Ctrl0.0", + "Ctrl0.0.0", + }, "items should be in document order"); + } + + [TestMethod] + public void DescendantControlInstancesMultiLevelTest() + { + var screen = new NamedObject("Screen1", new() + { + Children = + { + new("Ctrl0", new("GroupContainer") + { + Children = + { + new("Ctrl0.0", new("Label")), + new("Ctrl0.1", new("Label")), + }, + }), + new("Ctrl1", new("GroupContainer") + { + Children = + { + new("Ctrl1.0", new("Label")), + new("Ctrl1.1", new("GroupContainer") + { + Children = + { + new("Ctrl1.1.0", new("Label")), + new("Ctrl1.1.1", new("Label")), + }, + }), + new("Ctrl1.2", new("Label")), + }, + }), + new("Ctrl2", new("GroupContainer") + { + Children = + { + new("Ctrl2.0", new("Label")), + new("Ctrl2.1", new("Label")), + }, + }), + }, + }); + + screen.DescendantControlInstances().SelectNames().Should().Equal(new[] + { + "Ctrl0", + "Ctrl0.0", + "Ctrl0.1", + "Ctrl1", + "Ctrl1.0", + "Ctrl1.1", + "Ctrl1.1.0", + "Ctrl1.1.1", + "Ctrl1.2", + "Ctrl2", + "Ctrl2.0", + "Ctrl2.1", + }, "items should be in document order"); + } +} diff --git a/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs b/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs index c8215f07..da40c3fc 100644 --- a/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs +++ b/src/Persistence.Tests/PaYaml/Serialization/PaYamlSerializerTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Serialization; namespace Persistence.Tests.PaYaml.Serialization; @@ -12,10 +12,10 @@ public class PaYamlSerializerTests : VSTestBase #region Deserialize Examples [TestMethod] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/App.pa.yaml", 5, 5)] - public void DeserializeExamplePaYamlApp(string path, int expectedAppPropertiesCount, int? expectedHostPropertiesCount) + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/App.pa.yaml", 5)] + public void DeserializeExamplePaYamlApp(string path, int expectedAppPropertiesCount) { - var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); + var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); paFileRoot.ShouldNotBeNull(); // Top level properties @@ -25,24 +25,16 @@ public void DeserializeExamplePaYamlApp(string path, int expectedAppPropertiesCo paFileRoot.App.Properties.Should().NotBeNull() .And.HaveCount(expectedAppPropertiesCount); - if (expectedHostPropertiesCount == null) - { - paFileRoot.App.Children?.Host.Should().BeNull(); - } - else - { - paFileRoot.App.Children?.Host.Should().NotBeNull(); - paFileRoot.App.Children?.Host?.Properties.Should().HaveCount(expectedHostPropertiesCount.Value); - } + paFileRoot.App.Should().NotDefineMember("Children", "App.Children is still under design review"); } [TestMethod] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Screens/Screen1.pa.yaml", 2, 6, 16)] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Screens/FormsScreen2.pa.yaml", 0, 1, 62)] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Screens/ComponentsScreen4.pa.yaml", 0, 6, 6)] - public void DeserializeExamplePaYamlScreen(string path, int expectedScreenPropertiesCount, int expectedScreenChildrenCount, int expectedDescendantsCount) + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/Screen1.pa.yaml", 2, 8, 14, 2, 3)] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/FormsScreen2.pa.yaml", 0, 1, 62, 0, 0)] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/ComponentsScreen4.pa.yaml", 0, 6, 6, 0, 0)] + public void DeserializeExamplePaYamlScreen(string path, int expectedScreenPropertiesCount, int expectedScreenChildrenCount, int expectedDescendantsCount, int expectedScreenGroupsCount, int expectedTotalGroupsCount) { - var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); + var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); paFileRoot.ShouldNotBeNull(); // Top level properties @@ -55,14 +47,17 @@ public void DeserializeExamplePaYamlScreen(string path, int expectedScreenProper var screen = paFileRoot.Screens.First().Value; screen.Properties.Should().HaveCount(expectedScreenPropertiesCount); screen.Children.Should().HaveCount(expectedScreenChildrenCount); - screen.GetDescendantsCount().Should().Be(expectedDescendantsCount); + screen.DescendantControlInstances().Should().HaveCount(expectedDescendantsCount); + + screen.Groups.Should().HaveCount(expectedScreenGroupsCount); + screen.DescendantControlInstances().SelectMany(nc => nc.Value.Groups).Should().HaveCount(expectedTotalGroupsCount - expectedScreenGroupsCount); } [TestMethod] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Components/MyHeaderComponent.pa.yaml", 9, 6, 1)] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyHeaderComponent.pa.yaml", 9, 6, 1)] public void DeserializeExamplePaYamlComponentDefinition(string path, int expectedCustomPropertiesCount, int expectedPropertiesCount, int expectedChildrenCount) { - var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); + var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); paFileRoot.ShouldNotBeNull(); // Top level properties @@ -81,8 +76,8 @@ public void DeserializeExamplePaYamlComponentDefinition(string path, int expecte [TestMethod] public void DeserializeExamplePaYamlSingleFileApp() { - var path = @"_TestData/SchemaV2_2/Examples/Single-File-App.pa.yaml"; - var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); + var path = @"_TestData/SchemaV3_0/Examples/Single-File-App.pa.yaml"; + var paFileRoot = PaYamlSerializer.Deserialize(File.ReadAllText(path)); paFileRoot.ShouldNotBeNull(); // Top level properties @@ -104,16 +99,20 @@ public void DeserializeExamplePaYamlSingleFileApp() #region RoundTrip from yaml [TestMethod] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/App.pa.yaml")] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Screens/Screen1.pa.yaml")] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Screens/FormsScreen2.pa.yaml")] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Screens/ComponentsScreen4.pa.yaml")] - [DataRow(@"_TestData/SchemaV2_2/Examples/Src/Components/MyHeaderComponent.pa.yaml")] - [DataRow(@"_TestData/SchemaV2_2/Examples/Single-File-App.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/App.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/Screen1.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/FormsScreen2.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Screens/ComponentsScreen4.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Src/Components/MyHeaderComponent.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/Single-File-App.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/Examples/AmbiguousComponentNames.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/FullSchemaUses/App.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/FullSchemaUses/Screens-general-controls.pa.yaml")] + [DataRow(@"_TestData/SchemaV3_0/FullSchemaUses/Screens-with-components.pa.yaml")] public void RoundTripFromYaml(string path) { var originalYaml = File.ReadAllText(path); - var paFileRoot = PaYamlSerializer.Deserialize(originalYaml); + var paFileRoot = PaYamlSerializer.Deserialize(originalYaml); paFileRoot.ShouldNotBeNull(); var roundTrippedYaml = PaYamlSerializer.Serialize(paFileRoot); diff --git a/src/Persistence.Tests/Persistence.Tests.csproj b/src/Persistence.Tests/Persistence.Tests.csproj index 7c4c3318..ec782fa1 100644 --- a/src/Persistence.Tests/Persistence.Tests.csproj +++ b/src/Persistence.Tests/Persistence.Tests.csproj @@ -18,7 +18,10 @@ PreserveNewest - + + PreserveNewest + + PreserveNewest diff --git a/src/Persistence.Tests/PersistenceExceptionTests.cs b/src/Persistence.Tests/PersistenceLibraryExceptionTests.cs similarity index 65% rename from src/Persistence.Tests/PersistenceExceptionTests.cs rename to src/Persistence.Tests/PersistenceLibraryExceptionTests.cs index bb6e9464..ed92607b 100644 --- a/src/Persistence.Tests/PersistenceExceptionTests.cs +++ b/src/Persistence.Tests/PersistenceLibraryExceptionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using FluentAssertions.Specialized; @@ -7,18 +7,18 @@ namespace Persistence.Tests; [TestClass] -public class PersistenceExceptionTests +public class PersistenceLibraryExceptionTests { [TestMethod] public void ConstructTest() { - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.DeserializationError)) + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.DeserializationError)) .WithErrorCode(PersistenceErrorCode.DeserializationError) .WithReason(null); - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.SerializationError, "A test reason.")) + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.SerializationError, "A test reason.")) .WithErrorCode(PersistenceErrorCode.SerializationError) .WithReason("A test reason."); - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.MsappArchiveError, "A test reason2.") + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.MsappArchiveError, "A test reason2.") { MsappEntryFullPath = "src/entry1.txt", LineNumber = 5, @@ -32,17 +32,17 @@ public void ConstructTest() [TestMethod] public void MessageTest() { - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.DeserializationError)) + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.DeserializationError)) .WithMessage("[3000:DeserializationError] An error occurred during deserialization."); - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.SerializationError, "A test reason.")) + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.SerializationError, "A test reason.")) .WithMessage("[2000:SerializationError] An error occurred during serialization. A test reason."); - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.YamlInvalidSyntax) + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.YamlInvalidSyntax) { LineNumber = 5, Column = 3, }) .WithMessage("[3001:YamlInvalidSyntax] Invalid YAML syntax was encountered during deserialization. Line: 5; Column: 3;"); - ThrowAndVerify(new PersistenceException(PersistenceErrorCode.MsappArchiveError, "A test reason2.") + ThrowAndVerify(new PersistenceLibraryException(PersistenceErrorCode.MsappArchiveError, "A test reason2.") { MsappEntryFullPath = "src/entry1.txt", LineNumber = 5, @@ -55,9 +55,9 @@ public void MessageTest() [TestMethod] public void IsSerializableTests() { - new PersistenceException(PersistenceErrorCode.DeserializationError).Should().BeBinarySerializable(); - new PersistenceException(PersistenceErrorCode.SerializationError, "A test reason.").Should().BeBinarySerializable(); - new PersistenceException(PersistenceErrorCode.MsappArchiveError, "A test reason2.") + new PersistenceLibraryException(PersistenceErrorCode.DeserializationError).Should().BeBinarySerializable(); + new PersistenceLibraryException(PersistenceErrorCode.SerializationError, "A test reason.").Should().BeBinarySerializable(); + new PersistenceLibraryException(PersistenceErrorCode.MsappArchiveError, "A test reason2.") { MsappEntryFullPath = "src/entry1.txt", LineNumber = 5, diff --git a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs index d52aac0a..b1657406 100644 --- a/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs +++ b/src/Persistence.Tests/Yaml/DeserializerInvalidTests.cs @@ -27,7 +27,7 @@ public void Deserialize_ShouldFailWhenYamlIsInvalid(bool isControlIdentifiers) var yaml = File.ReadAllText(filePath); using var yamlReader = new StringReader(yaml); var act = () => deserializer.Deserialize(yamlReader); - act.Should().ThrowExactly("deserializing file '{0}' is expected to be invalid", filePath) + act.Should().ThrowExactly("deserializing file '{0}' is expected to be invalid", filePath) .WithErrorCode(PersistenceErrorCode.DeserializationError) .WithInnerExceptionExactly(); } @@ -48,7 +48,7 @@ public void Deserialize_ShouldFailWhenExpectingDifferentType(bool isControlIdent Action act = () => { deserializer.Deserialize(yamlReader); }; // Assert - act.Should().Throw() + act.Should().Throw() .WithErrorCode(PersistenceErrorCode.DeserializationError) .WithInnerExceptionExactly() .WithInnerException() @@ -78,7 +78,7 @@ public void Deserialize_Screens_List(string path, bool isControlIdentifiers) }; // Assert - act.Should().Throw() + act.Should().Throw() .WithErrorCode(PersistenceErrorCode.DeserializationError) .WithReason("Duplicate control property*") .WithInnerExceptionExactly(); diff --git a/src/Persistence/GlobalSuppressions.cs b/src/Persistence/GlobalSuppressions.cs index 58767c9d..7396319c 100644 --- a/src/Persistence/GlobalSuppressions.cs +++ b/src/Persistence/GlobalSuppressions.cs @@ -5,4 +5,4 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Version number in namespace.", Scope = "namespace", Target = "~N:Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Version number in namespace.", Scope = "namespace", Target = "~N:Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0")] diff --git a/src/Persistence/MsApp/MsappArchive.cs b/src/Persistence/MsApp/MsappArchive.cs index babfb720..8c388a22 100644 --- a/src/Persistence/MsApp/MsappArchive.cs +++ b/src/Persistence/MsApp/MsappArchive.cs @@ -263,7 +263,7 @@ public DataSources? DataSources /// /// /// - /// + /// public T Deserialize(string entryName, bool ensureRoundTrip = true) where T : Control { if (string.IsNullOrWhiteSpace(entryName)) @@ -292,11 +292,11 @@ public T Deserialize(ZipArchiveEntry archiveEntry) where T : Control _ = archiveEntry ?? throw new ArgumentNullException(nameof(archiveEntry)); if (!archiveEntry.FullName.EndsWith(YamlFileExtension, StringComparison.OrdinalIgnoreCase)) - throw new PersistenceException(PersistenceErrorCode.MsappArchiveError, $"Entry {archiveEntry} is not a yaml file.") { MsappEntryFullPath = archiveEntry.FullName }; + throw new PersistenceLibraryException(PersistenceErrorCode.MsappArchiveError, $"Entry {archiveEntry} is not a yaml file.") { MsappEntryFullPath = archiveEntry.FullName }; using var textReader = new StreamReader(archiveEntry.Open()); return _yamlDeserializer.Deserialize(textReader) - ?? throw new PersistenceException(PersistenceErrorCode.EditorStateJsonEmptyOrNull, "Deserialization of file resulted in null object.") { MsappEntryFullPath = archiveEntry.FullName }; + ?? throw new PersistenceLibraryException(PersistenceErrorCode.EditorStateJsonEmptyOrNull, "Deserialization of file resulted in null object.") { MsappEntryFullPath = archiveEntry.FullName }; } /// @@ -346,11 +346,11 @@ public IEnumerable GetDirectoryEntries(string directoryName, st /// /// /// - /// + /// public ZipArchiveEntry GetRequiredEntry(string entryName) { return GetEntry(entryName) ?? - throw new PersistenceException(PersistenceErrorCode.MsappArchiveError, $"Entry with name '{entryName}' not found in msapp archive."); + throw new PersistenceLibraryException(PersistenceErrorCode.MsappArchiveError, $"Entry with name '{entryName}' not found in msapp archive."); } /// @@ -632,11 +632,11 @@ private static T DeserializeMsappJsonFile(ZipArchiveEntry entry) try { return JsonSerializer.Deserialize(entry.Open()) - ?? throw new PersistenceException(PersistenceErrorCode.EditorStateJsonEmptyOrNull, "Deserialization of json file resulted in null object.") { MsappEntryFullPath = entry.FullName }; + ?? throw new PersistenceLibraryException(PersistenceErrorCode.EditorStateJsonEmptyOrNull, "Deserialization of json file resulted in null object.") { MsappEntryFullPath = entry.FullName }; } catch (JsonException ex) { - throw new PersistenceException(PersistenceErrorCode.InvalidEditorStateJson, $"Failed to deserialize json file to an instance of {typeof(T).Name}.", ex) + throw new PersistenceLibraryException(PersistenceErrorCode.InvalidEditorStateJson, $"Failed to deserialize json file to an instance of {typeof(T).Name}.", ex) { MsappEntryFullPath = entry.FullName, LineNumber = ex.LineNumber, diff --git a/src/Persistence/MsApp/RoundTripWriter.cs b/src/Persistence/MsApp/RoundTripWriter.cs index 15ac6412..7b082222 100644 --- a/src/Persistence/MsApp/RoundTripWriter.cs +++ b/src/Persistence/MsApp/RoundTripWriter.cs @@ -47,7 +47,7 @@ public override void Write(char value) if (inputValue == -1 || inputValue != value) { _exThrown = true; - throw new PersistenceException(PersistenceErrorCode.RoundTripValidationFailed, $"Round trip serialization failed") + throw new PersistenceLibraryException(PersistenceErrorCode.RoundTripValidationFailed, $"Round trip serialization failed") { MsappEntryFullPath = _entryFullPath, LineNumber = _lineNumber, @@ -72,7 +72,7 @@ protected override void Dispose(bool disposing) var inputValue = _input.Read(); if (inputValue != -1) { - throw new PersistenceException(PersistenceErrorCode.RoundTripValidationFailed, $"Round trip serialization failed. Additional input not read when disposing.") + throw new PersistenceLibraryException(PersistenceErrorCode.RoundTripValidationFailed, $"Round trip serialization failed. Additional input not read when disposing.") { MsappEntryFullPath = _entryFullPath, LineNumber = _lineNumber, diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/AppInstance.cs b/src/Persistence/PaYaml/Models/SchemaV2_2/AppInstance.cs deleted file mode 100644 index eb5359d7..00000000 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/AppInstance.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; -using YamlDotNet.Serialization; - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; - -public record AppInstance -{ - public NamedObjectMapping Properties { get; init; } = new(); - - public AppInstanceChildren? Children { get; init; } -} - -public record AppInstanceChildren -{ - public HostControlInstance? Host { get; init; } - - [YamlIgnore] - public int Count => Host != null ? 1 : 0; -} - -public record HostControlInstance -{ - public NamedObjectMapping Properties { get; init; } = new(); -} diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/ModelsExtensions.cs b/src/Persistence/PaYaml/Models/SchemaV2_2/ModelsExtensions.cs deleted file mode 100644 index a356f1e4..00000000 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/ModelsExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; - -public static class ModelsExtensions -{ - public static int GetDescendantsCount(this ScreenInstance screen) - { - _ = screen ?? throw new ArgumentNullException(nameof(screen)); - - return screen.Children?.Count > 0 - ? screen.Children.Count + screen.Children.Sum(namedControl => namedControl.Value.GetDescendantsCount()) - : 0; - } - - public static int GetDescendantsCount(this ControlInstance control) - { - _ = control ?? throw new ArgumentNullException(nameof(control)); - - return control.Children?.Count > 0 - ? control.Children.Count + control.Children.Sum(namedControl => namedControl.Value.GetDescendantsCount()) - : 0; - } -} diff --git a/src/Persistence/PaYaml/Models/SchemaV3_0/AppInstance.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/AppInstance.cs new file mode 100644 index 00000000..8d989d31 --- /dev/null +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/AppInstance.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; + +public record AppInstance +{ + public NamedObjectMapping Properties { get; init; } = new(); + + // WorkItem 27966436: Support saving AppHost instances to top-level property 'App' +} diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/ComponentDefinition.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/ComponentDefinition.cs similarity index 90% rename from src/Persistence/PaYaml/Models/SchemaV2_2/ComponentDefinition.cs rename to src/Persistence/PaYaml/Models/SchemaV3_0/ComponentDefinition.cs index 05d95206..ba7f5b59 100644 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/ComponentDefinition.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/ComponentDefinition.cs @@ -4,7 +4,7 @@ using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; using YamlDotNet.Serialization; -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; public enum ComponentPropertyKind { @@ -16,7 +16,7 @@ public enum ComponentPropertyKind Action, } -public class ComponentDefinition +public class ComponentDefinition : IPaControlInstanceContainer { public string? Description { get; init; } @@ -26,6 +26,8 @@ public class ComponentDefinition public NamedObjectMapping Properties { get; init; } = new(); + public NamedObjectMapping Groups { get; init; } = new(); + public NamedObjectSequence Children { get; init; } = new(); } diff --git a/src/Persistence/PaYaml/Models/SchemaV3_0/ControlGroup.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/ControlGroup.cs new file mode 100644 index 00000000..adb25dea --- /dev/null +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/ControlGroup.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; + +/// +/// Represents a group of controls under the same parent. +/// +public record ControlGroup +{ + public string[] ControlNames { get; init; } = Array.Empty(); +} diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/ControlInstance.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/ControlInstance.cs similarity index 61% rename from src/Persistence/PaYaml/Models/SchemaV2_2/ControlInstance.cs rename to src/Persistence/PaYaml/Models/SchemaV3_0/ControlInstance.cs index d3c0e451..16f210ba 100644 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/ControlInstance.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/ControlInstance.cs @@ -1,15 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; using YamlDotNet.Serialization; -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; public record ControlInstance { + public ControlInstance() { } + + [SetsRequiredMembers] + public ControlInstance(string controlTypeId) + { + ControlTypeId = controlTypeId ?? throw new ArgumentNullException(nameof(controlTypeId)); + } + [property: YamlMember(Alias = "Control")] - public required string ControlType { get; init; } + public required string ControlTypeId { get; init; } public string? Variant { get; init; } @@ -19,5 +28,7 @@ public record ControlInstance public NamedObjectMapping Properties { get; init; } = new(); + public NamedObjectMapping Groups { get; init; } = new(); + public NamedObjectSequence Children { get; init; } = new(); } diff --git a/src/Persistence/PaYaml/Models/SchemaV3_0/IPaControlInstanceContainer.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/IPaControlInstanceContainer.cs new file mode 100644 index 00000000..5e4789f2 --- /dev/null +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/IPaControlInstanceContainer.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; + +public interface IPaControlInstanceContainer +{ + NamedObjectMapping Groups { get; } + NamedObjectSequence Children { get; } +} diff --git a/src/Persistence/PaYaml/Models/SchemaV3_0/ModelsExtensions.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/ModelsExtensions.cs new file mode 100644 index 00000000..cc52b624 --- /dev/null +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/ModelsExtensions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; + +public static class ModelsExtensions +{ + public static IEnumerable> DescendantControlInstances(this NamedObject namedContainer) + where TContainer : IPaControlInstanceContainer + { + _ = namedContainer ?? throw new ArgumentNullException(nameof(namedContainer)); + + return namedContainer.Value.DescendantControlInstances(); + } + + public static IEnumerable> DescendantControlInstances(this IPaControlInstanceContainer container) + { + _ = container ?? throw new ArgumentNullException(nameof(container)); + + // Preorder Traverse of tree using a loop + var stack = new Stack>(); + + // Load up with top-level children first + foreach (var child in container.Children.Reverse()) + { + stack.Push(child); + } + + while (stack.Count != 0) + { + var topNamedObject = stack.Pop(); + foreach (var child in topNamedObject.Value.Children.Reverse()) + { + stack.Push(child); + } + yield return topNamedObject; + } + } + + public static IEnumerable SelectNames(this IEnumerable> namedObjects) + where TValue : notnull + { + return namedObjects.Select(o => o.Name); + } +} diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/PaFileRoot.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/PaModule.cs similarity index 75% rename from src/Persistence/PaYaml/Models/SchemaV2_2/PaFileRoot.cs rename to src/Persistence/PaYaml/Models/SchemaV3_0/PaModule.cs index 6f19cfd0..3a5f3c0a 100644 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/PaFileRoot.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/PaModule.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; -public record PaFileRoot +/// +/// Represents a Power Apps Yaml module file. +/// +public record PaModule { public AppInstance? App { get; init; } - public NamedObjectMapping Screens { get; init; } = new(); public NamedObjectMapping ComponentDefinitions { get; init; } = new(); + public NamedObjectMapping Screens { get; init; } = new(); } diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/SchemaKeywords.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/SchemaKeywords.cs similarity index 53% rename from src/Persistence/PaYaml/Models/SchemaV2_2/SchemaKeywords.cs rename to src/Persistence/PaYaml/Models/SchemaV3_0/SchemaKeywords.cs index d074b4eb..94c12903 100644 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/SchemaKeywords.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/SchemaKeywords.cs @@ -1,18 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; public static class SchemaKeywords { // Top-level property names: public const string App = nameof(App); public const string Screens = nameof(Screens); + public const string ComponentDefinitions = nameof(ComponentDefinitions); // Common property names: + public const string Control = nameof(Control); + public const string ComponentName = nameof(ComponentName); public const string Properties = nameof(Properties); + public const string Groups = nameof(Groups); public const string Children = nameof(Children); - - // Other names: - public const string Host = nameof(Host); + public const string CustomProperties = nameof(CustomProperties); + public const string ControlNames = nameof(ControlNames); } diff --git a/src/Persistence/PaYaml/Models/SchemaV2_2/ScreenInstance.cs b/src/Persistence/PaYaml/Models/SchemaV3_0/ScreenInstance.cs similarity index 71% rename from src/Persistence/PaYaml/Models/SchemaV2_2/ScreenInstance.cs rename to src/Persistence/PaYaml/Models/SchemaV3_0/ScreenInstance.cs index b8b6aeaf..88e7cd34 100644 --- a/src/Persistence/PaYaml/Models/SchemaV2_2/ScreenInstance.cs +++ b/src/Persistence/PaYaml/Models/SchemaV3_0/ScreenInstance.cs @@ -3,11 +3,13 @@ using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; -namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +namespace Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; -public record ScreenInstance +public record ScreenInstance : IPaControlInstanceContainer { public NamedObjectMapping Properties { get; init; } = new(); + public NamedObjectMapping Groups { get; init; } = new(); + public NamedObjectSequence Children { get; init; } = new(); } diff --git a/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs b/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs index 7a04d465..c780fa68 100644 --- a/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs +++ b/src/Persistence/PaYaml/Serialization/PaYamlSerializer.cs @@ -60,7 +60,7 @@ private static void WriteTextWriter(TextWriter writer, in TValue? value, } catch (YamlException ex) { - throw PersistenceException.FromYamlException(ex, PersistenceErrorCode.SerializationError); + throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.SerializationError); } } #endregion @@ -117,7 +117,7 @@ private static void WriteTextWriter(TextWriter writer, in TValue? value, } catch (YamlException ex) { - throw PersistenceException.FromYamlException(ex, PersistenceErrorCode.YamlInvalidSyntax); + throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.YamlInvalidSyntax); } // TODO: Consider using FluentValidation nuget package to validate the deserialized object diff --git a/src/Persistence/PaYaml/Serialization/PaYamlSerializerOptions.cs b/src/Persistence/PaYaml/Serialization/PaYamlSerializerOptions.cs index 7872783f..7845446a 100644 --- a/src/Persistence/PaYaml/Serialization/PaYamlSerializerOptions.cs +++ b/src/Persistence/PaYaml/Serialization/PaYamlSerializerOptions.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.PowerFx; -using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV2_2; +using Microsoft.PowerPlatform.PowerApps.Persistence.PaYaml.Models.SchemaV3_0; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; diff --git a/src/Persistence/PersistenceException.cs b/src/Persistence/PersistenceLibraryException.cs similarity index 81% rename from src/Persistence/PersistenceException.cs rename to src/Persistence/PersistenceLibraryException.cs index bfe2b58f..24a4cfaa 100644 --- a/src/Persistence/PersistenceException.cs +++ b/src/Persistence/PersistenceLibraryException.cs @@ -9,30 +9,30 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence; [Serializable] -public class PersistenceException : Exception +public class PersistenceLibraryException : Exception { - public PersistenceException(PersistenceErrorCode errorCode) + public PersistenceLibraryException(PersistenceErrorCode errorCode) : this(errorCode, null, null) { } - public PersistenceException(PersistenceErrorCode errorCode, string reason) + public PersistenceLibraryException(PersistenceErrorCode errorCode, string reason) : this(errorCode, reason, null) { } - public PersistenceException(PersistenceErrorCode errorCode, Exception innerException) + public PersistenceLibraryException(PersistenceErrorCode errorCode, Exception innerException) : this(errorCode, null, innerException) { } - public PersistenceException(PersistenceErrorCode errorCode, string? reason, Exception? innerException) + public PersistenceLibraryException(PersistenceErrorCode errorCode, string? reason, Exception? innerException) : base(reason ?? string.Empty, innerException) // Convert reason to non-null so base.Message doesn't get set to the default ex message. { ErrorCode = errorCode.CheckArgumentInRange(); } - protected PersistenceException(SerializationInfo info, StreamingContext context) + protected PersistenceLibraryException(SerializationInfo info, StreamingContext context) : base(info, context) { ErrorCode = ((PersistenceErrorCode)info.GetInt32(nameof(ErrorCode))).CheckArgumentInRange(); @@ -114,11 +114,11 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); } - internal static PersistenceException FromYamlException(YamlException ex, PersistenceErrorCode errorCode) + internal static PersistenceLibraryException FromYamlException(YamlException ex, PersistenceErrorCode errorCode) { return ex.Start.Equals(Mark.Empty) - ? new PersistenceException(errorCode, ex.Message, ex) - : new PersistenceException(errorCode, ex.Message, ex) + ? new PersistenceLibraryException(errorCode, ex.Message, ex) + : new PersistenceLibraryException(errorCode, ex.Message, ex) { LineNumber = ex.Start.Line, Column = ex.Start.Column, diff --git a/src/Persistence/Yaml/IYamlDeserializer.cs b/src/Persistence/Yaml/IYamlDeserializer.cs index 902b41d3..c7744f18 100644 --- a/src/Persistence/Yaml/IYamlDeserializer.cs +++ b/src/Persistence/Yaml/IYamlDeserializer.cs @@ -5,9 +5,9 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; public interface IYamlDeserializer { - /// Thrown when an error occurs while deserializing. + /// Thrown when an error occurs while deserializing. public T? Deserialize(string yaml) where T : notnull; - /// Thrown when an error occurs while deserializing. + /// Thrown when an error occurs while deserializing. public T? Deserialize(TextReader reader) where T : notnull; } diff --git a/src/Persistence/Yaml/IYamlSerializer.cs b/src/Persistence/Yaml/IYamlSerializer.cs index 5a9c0cbd..88301d3b 100644 --- a/src/Persistence/Yaml/IYamlSerializer.cs +++ b/src/Persistence/Yaml/IYamlSerializer.cs @@ -7,12 +7,12 @@ namespace Microsoft.PowerPlatform.PowerApps.Persistence.Yaml; public interface IYamlSerializer { - /// Thrown when an error occurs while serializing. + /// Thrown when an error occurs while serializing. public string Serialize(object graph); - /// Thrown when an error occurs while serializing. + /// Thrown when an error occurs while serializing. public string SerializeControl(T graph) where T : Control; - /// Thrown when an error occurs while serializing. + /// Thrown when an error occurs while serializing. public void SerializeControl(TextWriter writer, T graph) where T : Control; } diff --git a/src/Persistence/Yaml/YamlDeserializer.cs b/src/Persistence/Yaml/YamlDeserializer.cs index 898724c3..79d056ca 100644 --- a/src/Persistence/Yaml/YamlDeserializer.cs +++ b/src/Persistence/Yaml/YamlDeserializer.cs @@ -38,7 +38,7 @@ internal YamlDeserializer(IDeserializer deserializer) } catch (YamlException ex) { - throw PersistenceException.FromYamlException(ex, PersistenceErrorCode.DeserializationError); + throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.DeserializationError); } } } diff --git a/src/Persistence/Yaml/YamlSerializer.cs b/src/Persistence/Yaml/YamlSerializer.cs index f459ff17..c5c56d66 100644 --- a/src/Persistence/Yaml/YamlSerializer.cs +++ b/src/Persistence/Yaml/YamlSerializer.cs @@ -46,7 +46,7 @@ private void SerializeCore(TextWriter writer, T graph) } catch (YamlException ex) { - throw PersistenceException.FromYamlException(ex, PersistenceErrorCode.SerializationError); + throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.SerializationError); } } @@ -58,7 +58,7 @@ private string SerializeCore(T graph) } catch (YamlException ex) { - throw PersistenceException.FromYamlException(ex, PersistenceErrorCode.SerializationError); + throw PersistenceLibraryException.FromYamlException(ex, PersistenceErrorCode.SerializationError); } } } diff --git a/src/schemas-tests/.vscode/settings.json b/src/schemas-tests/.vscode/settings.json index 170d832f..aad689d6 100644 --- a/src/schemas-tests/.vscode/settings.json +++ b/src/schemas-tests/.vscode/settings.json @@ -18,8 +18,8 @@ ], "yaml.schemas": { "http://json-schema.org/draft-07/schema#": "*.schema.yaml", - "../schemas/pa-yaml/v2.2/pa.schema.yaml": [ - "pa-yaml/v2.2/**/*.pa.yaml" + "../schemas/pa-yaml/v3.0/pa.schema.yaml": [ + "pa-yaml/v3.0/**/*.pa.yaml" ] } } diff --git a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/App.pa.yaml b/src/schemas-tests/pa-yaml/v2.2/Examples/Src/App.pa.yaml deleted file mode 100644 index 453c7f56..00000000 --- a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/App.pa.yaml +++ /dev/null @@ -1,16 +0,0 @@ -App: - Properties: - BackEnabled: =true - OnStart: =Set(var1, "hello world!") - StartScreen: =Screen1 - Theme: =PowerAppsTheme - Foo: =anything - - Children: - Host: - Properties: - OnCancel: =false - OnEdit: =false - OnNew: =false - OnSave: =false - OnView: =false diff --git a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/Screen1.pa.yaml b/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/Screen1.pa.yaml deleted file mode 100644 index df98b653..00000000 --- a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/Screen1.pa.yaml +++ /dev/null @@ -1,157 +0,0 @@ -Screens: - Screen1: - Properties: - Fill: =RGBA(200, 200, 200, 1) - OnVisible: |- - =Set(var1, "hello world!") - Children: - - Label1: - Control: label - Properties: - Text: ="A label" - X: =40 - Y: =40 - - Text Input 1: - Control: text - Properties: - Default: ="Default input" - X: =40 - Y: =80 - - "Control name with special '*&:/\\\" chars": - Control: button - Properties: - Text: ="A Button" - X: =40 - Y: =138 - - Gallery1: - Control: gallery - Variant: BrowseLayout_Vertical_TwoTextOneImageVariant_ver5.0 - Properties: - DelayItemLoading: =true - Height: =479 - Items: =CustomGallerySample - #... - Children: - - Image1: - Control: image - Properties: - Height: =61 - OnSelect: =Select(Parent) - - NextArrow1: - Control: icon - Variant: ChevronRight - Properties: - AccessibleLabel: =Self.Tooltip - Color: =RGBA(166, 166, 166, 1) - Height: =50 - Icon: =Icon.ChevronRight - OnSelect: =Select(Parent) - PaddingBottom: =16 - PaddingLeft: =16 - PaddingRight: =16 - PaddingTop: =16 - Tooltip: ="View item details" - Width: =50 - X: =Parent.TemplateWidth - Self.Width - 12 - Y: =(Parent.TemplateHeight / 2) - (Self.Height / 2) - - Separator1: - Control: rectangle - Properties: - Height: =8 - OnSelect: =Select(Parent) - Width: =Parent.TemplateWidth - Y: =Parent.TemplateHeight - Self.Height - - - Rectangle1: - Control: rectangle - Properties: - Height: =Parent.TemplateHeight - Separator1.Height - OnSelect: =Select(Parent) - Visible: =ThisItem.IsSelected - Width: =4 - - - TitleSubTitleGroup: - Control: group - Properties: - Height: =5 - Width: =5 - X: =40 - Y: =40 - Children: - - Title1: - Control: label - Properties: - FontWeight: =If(ThisItem.IsSelected, FontWeight.Semibold, FontWeight.Normal) - Height: =25 - OnSelect: =Select(Parent) - PaddingBottom: =0 - PaddingLeft: =0 - PaddingRight: =0 - PaddingTop: =0 - Text: =ThisItem.SampleHeading - VerticalAlign: =VerticalAlign.Top - Width: =345 - X: =103 - Y: =(Parent.TemplateHeight - (Self.Size * 1.8 + Subtitle1.Size * 1.8)) / 2 - - - Subtitle1: - Control: label - Properties: - FontWeight: =If(ThisItem.IsSelected, FontWeight.Semibold, FontWeight.Normal) - Height: =35 - OnSelect: =Select(Parent) - PaddingBottom: =0 - PaddingLeft: =0 - PaddingRight: =0 - PaddingTop: =0 - Text: =ThisItem.SampleText - VerticalAlign: =VerticalAlign.Top - Width: =Title1.Width - X: =Title1.X - Y: =Title1.Y + Title1.Height - - - GalleryTitleLabel: - Control: label - Properties: - Text: ="A gallery example" - Width: =322 - X: =726 - Y: =40 - - - BasicControlsCopyGroup: - Control: group - Properties: - Height: =5 - Width: =5 - X: =60 - Y: =60 - Children: - - Label1_2: - Control: label - Properties: - DisplayMode: =DisplayMode.View - Fill: =RGBA(232, 244, 217, 1) - Text: ="A label" - Tooltip: ="This is a copy of some other controls." - X: =40 - Y: =444 - - - TextInput1_2: - Control: text - Properties: - Default: ="Default input" - DisplayMode: =DisplayMode.View - Fill: =RGBA(232, 244, 217, 1) - Tooltip: ="This is a copy of some other controls." - X: =40 - Y: =484 - - - Button1_2: - Control: button - Properties: - DisplayMode: =DisplayMode.View - Fill: =RGBA(232, 244, 217, 1) - Text: ="A Button" - Tooltip: ="This is a copy of some other controls." - X: =40 - Y: =542 diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/AmbiguousComponentNames.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/AmbiguousComponentNames.pa.yaml new file mode 100644 index 00000000..82a84699 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/AmbiguousComponentNames.pa.yaml @@ -0,0 +1,51 @@ +# This file showcases how we resolve when 3rd party controls can cause invariant name conflicts. +# Namely, the 'Control' property on all control instances MUST be a 1P ControlTypeId, which are +# owned by Power Apps and use a strict naming convention. +# All 3P names required to disambiguate control type instances must be in a separate property, which +# in combination with the 'Control' property will identify a unique control instance. +App: + Properties: + BackEnabled: =true + Theme: =PowerAppsTheme + +ComponentDefinitions: + Slider: + Description: A local custom Component with the same name as a 1P ControlTypeId. + slicer: + Description: A local custom Component with the same name as a 1P ControlTypeId, that differs by case only + +Screens: + 1PControlsScreen: + Children: + - 1P-Slider: + Control: Slider + Properties: + X: =20 + + 3PComponentInstancesScreen: + Children: + - 3P-local-Slider1: + Control: Component + ComponentName: Slider + Properties: + X: =20 + - 3P-local-slider1: + Control: Component + ComponentName: slider + Properties: + X: =20 + - 3P-external-Slider1: + Control: Component + ComponentName: Slider + ComponentLibraryUniqueName: pubpref_orgcomponentslibrary_1e112 + Properties: + X: =20 + + # CodeComponents: aka 3P PCF controls + 3PCodeComponentsScreen: + Children: + - 3P-pcf-Slider1: + Control: CodeComponent + ComponentName: pubpref_Org.Namespace.Slider + Properties: + X: =20 diff --git a/src/schemas-tests/pa-yaml/v2.2/Examples/Single-File-App.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Single-File-App.pa.yaml similarity index 61% rename from src/schemas-tests/pa-yaml/v2.2/Examples/Single-File-App.pa.yaml rename to src/schemas-tests/pa-yaml/v3.0/Examples/Single-File-App.pa.yaml index 5804ec03..5913ee13 100644 --- a/src/schemas-tests/pa-yaml/v2.2/Examples/Single-File-App.pa.yaml +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Single-File-App.pa.yaml @@ -3,27 +3,18 @@ App: BackEnabled: =true Theme: =PowerAppsTheme - Children: - Host: - Properties: - OnCancel: =false - OnEdit: =false - OnNew: =false - OnSave: =false - OnView: =false - Screens: Screen1: Children: - Label1: - Control: label + Control: Label Properties: Text: ="A label" X: =40 Y: =40 - TextInput1: - Control: text + Control: TextInput Properties: Default: ="Default input" X: =40 diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/App.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/App.pa.yaml new file mode 100644 index 00000000..27ffc2a3 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/App.pa.yaml @@ -0,0 +1,7 @@ +App: + Properties: + BackEnabled: =true + OnStart: =Set(var1, "hello world!") + StartScreen: =Screen1 + Theme: =PowerAppsTheme + Foo: =anything diff --git a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Components/MyHeaderComponent.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml similarity index 99% rename from src/schemas-tests/pa-yaml/v2.2/Examples/Src/Components/MyHeaderComponent.pa.yaml rename to src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml index 4b9cdee1..c674155c 100644 --- a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Components/MyHeaderComponent.pa.yaml +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Components/MyHeaderComponent.pa.yaml @@ -88,7 +88,7 @@ ComponentDefinitions: Children: - Label4: - Control: label + Control: Label Properties: Align: =Align.Center BorderColor: =RGBA(0, 18, 107, 1) diff --git a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/ComponentsScreen4.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/ComponentsScreen4.pa.yaml similarity index 89% rename from src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/ComponentsScreen4.pa.yaml rename to src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/ComponentsScreen4.pa.yaml index 2489d6b3..cba13ff9 100644 --- a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/ComponentsScreen4.pa.yaml +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/ComponentsScreen4.pa.yaml @@ -2,7 +2,7 @@ Screens: ComponentsScreen4: Children: - MyHeaderComponent_2: - Control: component + Control: Component ComponentName: MyHeaderComponent Properties: Height: =77 @@ -10,21 +10,21 @@ Screens: Width: =1366 - MyHeaderComponent_3: - Control: component + Control: Component ComponentName: MyHeaderComponent Properties: ScreenTitle: ="Screen B" Y: =98 - MyHeaderComponent_4: - Control: component + Control: Component ComponentName: MyHeaderComponent Properties: ScreenTitle: ="Screen C" Y: =197 - CommonHeader_1: - Control: component + Control: Component ComponentName: CommonHeader ComponentLibraryUniqueName: joem_joemcomponentlibraryserialization_1e112 Properties: @@ -36,7 +36,7 @@ Screens: Y: =294 - MenuTemplate_1: - Control: component + Control: Component ComponentName: MenuTemplate ComponentLibraryUniqueName: joem_joemcomponentlibraryserialization_1e112 Properties: @@ -49,7 +49,7 @@ Screens: Y: =294 - selectedMenuItemLabel: - Control: label + Control: Label Properties: Text: =$"{MenuTemplate_1.Selected.Title} ({MenuTemplate_1.Selected.Tag})" Width: =218 diff --git a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/FormsScreen2.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/FormsScreen2.pa.yaml similarity index 93% rename from src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/FormsScreen2.pa.yaml rename to src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/FormsScreen2.pa.yaml index 7071618f..5313b8da 100644 --- a/src/schemas-tests/pa-yaml/v2.2/Examples/Src/Screens/FormsScreen2.pa.yaml +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/FormsScreen2.pa.yaml @@ -3,7 +3,7 @@ Screens: # Variant: "autoLayout_SplitScreen_ver1.0" Children: - ScreenContainer1: - Control: groupContainer + Control: GroupContainer Variant: horizontalAutoLayoutContainer Properties: Fill: =RGBA(245, 245, 245, 1) @@ -20,7 +20,7 @@ Screens: Children: - LeftContainer1: - Control: groupContainer + Control: GroupContainer Variant: verticalAutoLayoutContainer Properties: Fill: =RGBA(255, 255, 255, 1) @@ -35,7 +35,7 @@ Screens: Children: - Form1: - Control: form + Control: Form Properties: DataSource: =Contacts LayoutMinHeight: =250 @@ -44,7 +44,7 @@ Screens: Children: - "First Name_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -63,7 +63,7 @@ Screens: Children: - DataCardKey7: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -74,7 +74,7 @@ Screens: Y: =10 - DataCardValue7: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -92,7 +92,7 @@ Screens: Y: =DataCardKey7.Y + DataCardKey7.Height + 5 - ErrorMessage5: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -108,7 +108,7 @@ Screens: Y: =DataCardValue7.Y + DataCardValue7.Height - StarVisible5: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey7.Height @@ -119,7 +119,7 @@ Screens: Y: =DataCardKey7.Y - "Last Name_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -138,7 +138,7 @@ Screens: Children: - DataCardKey9: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -149,7 +149,7 @@ Screens: Y: =10 - DataCardValue9: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -167,7 +167,7 @@ Screens: Y: =DataCardKey9.Y + DataCardKey9.Height + 5 - ErrorMessage7: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -183,7 +183,7 @@ Screens: Y: =DataCardValue9.Y + DataCardValue9.Height - StarVisible7: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey9.Height @@ -194,7 +194,7 @@ Screens: Y: =DataCardKey9.Y - "Company Name_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: blankPolymorphicEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -210,7 +210,7 @@ Screens: Y: =2 - "Job Title_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -229,7 +229,7 @@ Screens: Children: - DataCardKey8: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -240,7 +240,7 @@ Screens: Y: =10 - DataCardValue8: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -258,7 +258,7 @@ Screens: Y: =DataCardKey8.Y + DataCardKey8.Height + 5 - ErrorMessage6: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -274,7 +274,7 @@ Screens: Y: =DataCardValue8.Y + DataCardValue8.Height - StarVisible6: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey8.Height @@ -285,7 +285,7 @@ Screens: Y: =DataCardKey8.Y - "Mobile Phone_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -304,7 +304,7 @@ Screens: Children: - DataCardKey10: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -315,7 +315,7 @@ Screens: Y: =10 - DataCardValue10: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -333,7 +333,7 @@ Screens: Y: =DataCardKey10.Y + DataCardKey10.Height + 5 - ErrorMessage8: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -349,7 +349,7 @@ Screens: Y: =DataCardValue10.Y + DataCardValue10.Height - StarVisible8: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey10.Height @@ -360,7 +360,7 @@ Screens: Y: =DataCardKey10.Y - "User Name_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -379,7 +379,7 @@ Screens: Children: - DataCardKey12: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -390,7 +390,7 @@ Screens: Y: =10 - DataCardValue12: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -408,7 +408,7 @@ Screens: Y: =DataCardKey12.Y + DataCardKey12.Height + 5 - ErrorMessage10: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -424,7 +424,7 @@ Screens: Y: =DataCardValue12.Y + DataCardValue12.Height - StarVisible10: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey12.Height @@ -435,7 +435,7 @@ Screens: Y: =DataCardKey12.Y - "Address 1_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualViewCard Properties: BorderStyle: =BorderStyle.Solid @@ -452,7 +452,7 @@ Screens: Children: - DataCardKey4: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -463,7 +463,7 @@ Screens: Y: =10 - DataCardValue4: - Control: label + Control: Label Properties: AutoHeight: =true DisplayMode: =Parent.DisplayMode @@ -477,7 +477,7 @@ Screens: Y: =DataCardKey4.Y + DataCardKey4.Height + 5 - Website_DataCard1: - Control: typedDataCard + Control: TypedDataCard Variant: urlEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -495,7 +495,7 @@ Screens: Children: - DataCardKey11: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -506,7 +506,7 @@ Screens: Y: =10 - DataCardValue11: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -523,7 +523,7 @@ Screens: Y: =DataCardKey11.Y + DataCardKey11.Height + 5 - ErrorMessage9: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -539,7 +539,7 @@ Screens: Y: =DataCardValue11.Y + DataCardValue11.Height - StarVisible9: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey11.Height @@ -550,7 +550,7 @@ Screens: Y: =DataCardKey11.Y - Birthday_DataCard1: - Control: typedDataCard + Control: TypedDataCard Variant: dateEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -568,7 +568,7 @@ Screens: Children: - DataCardKey5: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -579,7 +579,7 @@ Screens: Y: =10 - DataCardValue5: - Control: datepicker + Control: DatePicker Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) DefaultDate: =Parent.Default @@ -595,7 +595,7 @@ Screens: Y: =DataCardKey5.Y + DataCardKey5.Height + 5 - ErrorMessage3: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -611,7 +611,7 @@ Screens: Y: =DataCardValue5.Y + DataCardValue5.Height - StarVisible3: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey5.Height @@ -622,7 +622,7 @@ Screens: Y: =DataCardKey5.Y - Description_DataCard1: - Control: typedDataCard + Control: TypedDataCard Variant: textualMultiLineEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -640,7 +640,7 @@ Screens: Children: - DataCardKey6: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -651,7 +651,7 @@ Screens: Y: =10 - DataCardValue6: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -670,7 +670,7 @@ Screens: Y: =DataCardKey6.Y + DataCardKey6.Height + 5 - ErrorMessage4: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -686,7 +686,7 @@ Screens: Y: =DataCardValue6.Y + DataCardValue6.Height - StarVisible4: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey6.Height @@ -697,7 +697,7 @@ Screens: Y: =DataCardKey6.Y - "Full Name_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualViewCard Properties: BorderStyle: =BorderStyle.Solid @@ -714,7 +714,7 @@ Screens: Children: - DataCardKey1: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -725,7 +725,7 @@ Screens: Y: =10 - DataCardValue1: - Control: label + Control: Label Properties: AutoHeight: =true DisplayMode: =Parent.DisplayMode @@ -739,7 +739,7 @@ Screens: Y: =DataCardKey1.Y + DataCardKey1.Height + 5 - Email_DataCard1: - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -758,7 +758,7 @@ Screens: Children: - DataCardKey2: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -769,7 +769,7 @@ Screens: Y: =10 - DataCardValue2: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -787,7 +787,7 @@ Screens: Y: =DataCardKey2.Y + DataCardKey2.Height + 5 - ErrorMessage1: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -803,7 +803,7 @@ Screens: Y: =DataCardValue2.Y + DataCardValue2.Height - StarVisible1: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey2.Height @@ -814,7 +814,7 @@ Screens: Y: =DataCardKey2.Y - "Business Phone_DataCard1": - Control: typedDataCard + Control: TypedDataCard Variant: textualEditCard Properties: BorderStyle: =BorderStyle.Solid @@ -833,7 +833,7 @@ Screens: Children: - DataCardKey3: - Control: label + Control: Label Properties: AutoHeight: =true Height: =34 @@ -844,7 +844,7 @@ Screens: Y: =10 - DataCardValue3: - Control: text + Control: TextInput Properties: BorderColor: =If(IsBlank(Parent.Error), Parent.BorderColor, Color.Red) Default: =Parent.Default @@ -862,7 +862,7 @@ Screens: Y: =DataCardKey3.Y + DataCardKey3.Height + 5 - ErrorMessage2: - Control: label + Control: Label Properties: AutoHeight: =true Height: =10 @@ -878,7 +878,7 @@ Screens: Y: =DataCardValue3.Y + DataCardValue3.Height - StarVisible2: - Control: label + Control: Label Properties: Align: =Align.Center Height: =DataCardKey3.Height @@ -889,7 +889,7 @@ Screens: Y: =DataCardKey3.Y - RightContainer1: - Control: groupContainer + Control: GroupContainer Variant: verticalAutoLayoutContainer Properties: Fill: =RGBA(255, 255, 255, 1) @@ -904,7 +904,8 @@ Screens: Children: - MyHeaderComponent_1: - Control: MyHeaderComponent + Control: Component + ComponentName: MyHeaderComponent Properties: LayoutMinHeight: =640 Width: =Parent.Width diff --git a/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/Screen1.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/Screen1.pa.yaml new file mode 100644 index 00000000..f8d3cb26 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/Examples/Src/Screens/Screen1.pa.yaml @@ -0,0 +1,152 @@ +Screens: + Screen1: + Properties: + Fill: =RGBA(200, 200, 200, 1) + OnVisible: |- + =Set(var1, "hello world!") + Groups: + Group1: + ControlNames: + - Text Input 1 + - "Control name with special '*&:/\\\" chars" + BasicControlsCopyGroup: + ControlNames: + - Label1_2 + - TextInput1_2 + - Button1_2 + Children: + - Label1: + Control: Label + Properties: + Text: ="A label" + X: =40 + Y: =40 + - Text Input 1: + Control: TextInput + Properties: + Default: ="Default input" + X: =40 + Y: =80 + - "Control name with special '*&:/\\\" chars": + Control: Button + Properties: + Text: ="A Button" + X: =40 + Y: =138 + - Gallery1: + Control: Gallery + Variant: BrowseLayout_Vertical_TwoTextOneImageVariant_ver5.0 + Properties: + DelayItemLoading: =true + Height: =479 + Items: =CustomGallerySample + Groups: + TitleSubTitleGroup: + ControlNames: + - Title1 + - Subtitle1 + Children: + - Image1: + Control: Image + Properties: + Height: =61 + OnSelect: =Select(Parent) + - NextArrow1: + Control: Icon + Variant: ChevronRight + Properties: + AccessibleLabel: =Self.Tooltip + Color: =RGBA(166, 166, 166, 1) + Height: =50 + Icon: =Icon.ChevronRight + OnSelect: =Select(Parent) + PaddingBottom: =16 + PaddingLeft: =16 + PaddingRight: =16 + PaddingTop: =16 + Tooltip: ="View item details" + Width: =50 + X: =Parent.TemplateWidth - Self.Width - 12 + Y: =(Parent.TemplateHeight / 2) - (Self.Height / 2) + - Separator1: + Control: Rectangle + Properties: + Height: =8 + OnSelect: =Select(Parent) + Width: =Parent.TemplateWidth + Y: =Parent.TemplateHeight - Self.Height + - Rectangle1: + Control: Rectangle + Properties: + Height: =Parent.TemplateHeight - Separator1.Height + OnSelect: =Select(Parent) + Visible: =ThisItem.IsSelected + Width: =4 + - Title1: + Control: Label + Properties: + FontWeight: =If(ThisItem.IsSelected, FontWeight.Semibold, FontWeight.Normal) + Height: =25 + OnSelect: =Select(Parent) + PaddingBottom: =0 + PaddingLeft: =0 + PaddingRight: =0 + PaddingTop: =0 + Text: =ThisItem.SampleHeading + VerticalAlign: =VerticalAlign.Top + Width: =345 + X: =103 + Y: =(Parent.TemplateHeight - (Self.Size * 1.8 + Subtitle1.Size * 1.8)) / 2 + - Subtitle1: + Control: Label + Properties: + FontWeight: =If(ThisItem.IsSelected, FontWeight.Semibold, FontWeight.Normal) + Height: =35 + OnSelect: =Select(Parent) + PaddingBottom: =0 + PaddingLeft: =0 + PaddingRight: =0 + PaddingTop: =0 + Text: =ThisItem.SampleText + VerticalAlign: =VerticalAlign.Top + Width: =Title1.Width + X: =Title1.X + Y: =Title1.Y + Title1.Height + + - GalleryTitleLabel: + Control: Label + Properties: + Text: ="A gallery example" + Width: =322 + X: =726 + Y: =40 + + - Label1_2: + Control: Label + Properties: + DisplayMode: =DisplayMode.View + Fill: =RGBA(232, 244, 217, 1) + Text: ="A label" + Tooltip: ="This is a copy of some other controls." + X: =40 + Y: =444 + + - TextInput1_2: + Control: TextInput + Properties: + Default: ="Default input" + DisplayMode: =DisplayMode.View + Fill: =RGBA(232, 244, 217, 1) + Tooltip: ="This is a copy of some other controls." + X: =40 + Y: =484 + + - Button1_2: + Control: Button + Properties: + DisplayMode: =DisplayMode.View + Fill: =RGBA(232, 244, 217, 1) + Text: ="A Button" + Tooltip: ="This is a copy of some other controls." + X: =40 + Y: =542 diff --git a/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/App.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/App.pa.yaml new file mode 100644 index 00000000..0faf3587 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/App.pa.yaml @@ -0,0 +1,4 @@ +App: + Properties: + Prop1: =formula1 + Prop2: =formula2 diff --git a/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/Screens-general-controls.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/Screens-general-controls.pa.yaml new file mode 100644 index 00000000..93841205 --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/Screens-general-controls.pa.yaml @@ -0,0 +1,62 @@ +Screens: + screenName2: + Properties: + Prop2: =screen1Prop2 + Prop1: =screen1Prop1 + + Groups: + Group1: + ControlNames: + - ctrlB + - ctrlA + + Children: + - ctrlB: + Control: Label + Variant: variantB + Properties: + Prop2: =ctrlBProp2 + Prop1: =ctrlBProp1 + + # Purposely set out of name sorting order to ensure ordering is maintained + - ctrlA: + Control: Label + Variant: variantA + Properties: + Prop2: =ctrlAProp2 + Prop1: =ctrlAProp1 + + screenName1: + Children: + - ctrlC: + Control: fooWithChildren + Variant: variantC + Properties: + Prop2: =ctrlAProp2 + Prop1: =ctrlAProp1 + Groups: + Group1: + ControlNames: + - ctrlC0 + - ctrlC1 + Children: + - ctrlC0: + Control: bar + Variant: variantC0 + Properties: + Prop2: =ctrlC0Prop2 + Prop1: =ctrlC0Prop1 + Children: + - ctrlC00: + Control: car + Variant: variantC00 + - ctrlC1: + Control: bar + Variant: variantC1 + Properties: + Prop2: =ctrlC1Prop2 + Prop1: =ctrlC1Prop1 + Children: + - ctrlC10: + Control: dog + Variant: variantC10 diff --git a/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/Screens-with-components.pa.yaml b/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/Screens-with-components.pa.yaml new file mode 100644 index 00000000..30a16a4b --- /dev/null +++ b/src/schemas-tests/pa-yaml/v3.0/FullSchemaUses/Screens-with-components.pa.yaml @@ -0,0 +1,22 @@ +Screens: + screenName1: + Properties: + Prop2: =screen1Prop2 + Prop1: =screen1Prop1 + + Children: + - ctrlB: + Control: Component + ComponentName: myComponent1 + Properties: + Prop2: =ctrlBProp2 + Prop1: =ctrlBProp1 + + # Purposesly set out of name sorting order to ensure ordering is maintained + - ctrlA: + Control: Component + ComponentName: externComponent2 + ComponentLibraryUniqueName: pubpref_libraryName1 + Properties: + Prop2: =ctrlAProp2 + Prop1: =ctrlAProp1 diff --git a/src/schemas/pa-yaml/v3.0/ControlLibraryVDev/ControlTypeId-1P-controls-enum.schema.yaml b/src/schemas/pa-yaml/v3.0/ControlLibraryVDev/ControlTypeId-1P-controls-enum.schema.yaml new file mode 100644 index 00000000..75cbd2fc --- /dev/null +++ b/src/schemas/pa-yaml/v3.0/ControlLibraryVDev/ControlTypeId-1P-controls-enum.schema.yaml @@ -0,0 +1,72 @@ +# Unfortunately, it also seems to make it so that schema cache doesn't get updated if the schema file updates. +# WORKAROUND: Close the VSCode window and reopen. +# +# How to add snippets for use in VS Code: https://code.visualstudio.com/docs/languages/json#_define-snippets-in-json-schemas + +$schema: http://json-schema.org/draft-07/schema# +$id: http://powerapps.com/schemas/pa-yaml/v3.0/ControlTypeId-1P-controls-enum.schema +description: >- + The schema object representing the enum of ControlTypeIds for the current version of the controls library. + +enum: + - AddMedia + - Attachments + - Audio + - Button + - Camera + - CheckBox + - Circle + - ComboBox + - DatePicker + - DropDown + - Form + - TypedDataCard + - DataCard + - Gallery + - GroupContainer + - Icon + - Image + - Label + - ListBox + - Microphone + - PDFViewer + - PowerBI + - Radio + - Rating + - Rectangle + - Slider + - StreamVideo + - TextInput + - Timer + - Toggle + - Video + - FluentV8/Button + - FluentV8/CheckBox + - FluentV8/ComboBox + - FluentV8/DatePicker + - FluentV8/Label + - FluentV8/Radio + - FluentV8/Rating + - FluentV8/Slider + - FluentV8/TextBox + - FluentV8/Toggle + - FluentV9/Badge + - FluentV9/Button + - FluentV9/CheckBox + - FluentV9/ComboBox + - FluentV9/DatePicker + - FluentV9/DropDown + - FluentV9/Header + - FluentV9/InfoButton + - FluentV9/Link + - FluentV9/NumberInput + - FluentV9/Progress + - FluentV9/Radio + - FluentV9/Slider + - FluentV9/Spinner + - FluentV9/StreamVideo + - FluentV9/TabList + - FluentV9/Text + - FluentV9/TextInput + - FluentV9/Toggle + - FluentV9/Grid diff --git a/src/schemas/pa-yaml/v2.2/pa.schema.yaml b/src/schemas/pa-yaml/v3.0/pa.schema.yaml similarity index 62% rename from src/schemas/pa-yaml/v2.2/pa.schema.yaml rename to src/schemas/pa-yaml/v3.0/pa.schema.yaml index 31c8f036..319b4c43 100644 --- a/src/schemas/pa-yaml/v2.2/pa.schema.yaml +++ b/src/schemas/pa-yaml/v3.0/pa.schema.yaml @@ -1,9 +1,11 @@ # Unfortunately, it also seems to make it so that schema cache doesn't get updated if the schema file updates. # WORKAROUND: Close the VSCode window and reopen. +# +# How to add snippets for use in VS Code: https://code.visualstudio.com/docs/languages/json#_define-snippets-in-json-schemas $schema: http://json-schema.org/draft-07/schema# -$id: http://powerapps.com/schemas/pa-yaml/v2.2/pa.schema -title: Microsoft Power Apps schema for app source yaml files (v2.2). +$id: http://powerapps.com/schemas/pa-yaml/v3.0/pa.schema +title: Microsoft Power Apps schema for app source yaml files (v3.0). description: >- The schema for all *.pa.yaml files which are used to describe a Power Apps canvas app. All *.pa.yaml files in an *.msapp are logically combined into a single *.pa.yaml file. @@ -17,6 +19,18 @@ properties: $ref: "#/definitions/Screens-name-instance-map" ComponentDefinitions: $ref: "#/definitions/ComponentDefinitions-name-instance-map" +defaultSnippets: + - label: App + body: + App: + Properties: + StartScreen: =${1:Screen1} + - label: Screens + body: + Screens: + ${1:Screen1}: + Children: + - $0 definitions: App-instance: @@ -25,20 +39,7 @@ definitions: additionalProperties: false properties: Properties: { $ref: "#/definitions/Properties-formula-map" } - - # Note: App children is fixed. - Children: - description: The App currently only supports the 'Host' child. - type: object - additionalProperties: false - properties: - Host: - type: object - additionalProperties: false - properties: - # Currently, the Control identifier is static, but may need to be exposed in order to support variants - #Control: { $ref: "#/definitions/Control-type-identifier" } - Properties: { $ref: "#/definitions/Properties-formula-map" } + # NOTE: App children is removed for now until the design for the AppHost controls is completed Screens-name-instance-map: description: |- @@ -46,15 +47,25 @@ definitions: type: object propertyNames: { $ref: "#/definitions/Screen-name" } additionalProperties: - type: object - additionalProperties: false - properties: - Properties: { $ref: "#/definitions/Properties-formula-map" } - Children: { $ref: "#/definitions/Children-Control-instance-sequence" } + $ref: "#/definitions/Screen-instance" + defaultSnippets: + - label: Add a Screen + body: + ${1:Screen1}: + Children: + - $0 Screen-name: $ref: "#/definitions/entity-name" + Screen-instance: + type: object + additionalProperties: false + properties: + Properties: { $ref: "#/definitions/Properties-formula-map" } + Groups: { $ref: "#/definitions/Groups-of-controls" } + Children: { $ref: "#/definitions/Children-Control-instance-sequence" } + Children-Control-instance-sequence: description: >- A sequence of control instances, where each item is a control's name with a control instance. @@ -67,73 +78,195 @@ definitions: propertyNames: { $ref: "#/definitions/Control-instance-name" } additionalProperties: $ref: "#/definitions/Control-instance" + defaultSnippets: + - label: Add Control + # Note: In order to get intellisense for the Control property, we can't use the ${1} in the + # default value for ${2} as vscode will put the intellisense for the first reference of the replacement. + body: + '${2:Control1}': + Control: ${1:Label} + Properties: + X: =10 + Y: =10$0 + - label: Add custom `Component` instance + body: + '${2:${1}1}': + Control: Component + ComponentName: ${1:MyComponent} + Properties: + X: =10 + Y: =10$0 + - label: Add `CodeComponent` instance (aka PCF control) + body: + '${2:${1}1}': + Control: CodeComponent + ComponentName: ${1:MyComponent} + Properties: + X: =10 + Y: =10$0 Control-instance-name: $ref: "#/definitions/entity-name" + ControlTypeId: + description: The invariant identifier for the type of control being instantiated. + allOf: + - $ref: "#/definitions/ControlTypeId-pattern" + not: + anyOf: + - $ref: "#/definitions/ControlTypeId-disallowed-types" + - $ref: "#/definitions/ControlTypeId-not-yet-supported" + oneOf: + - $ref: "#/definitions/ControlTypeId-oneOf-3P-types" + - $ref: "#/definitions/ControlTypeId-1P-controls" + + ControlTypeId-pattern: + $comment: Defines reusable schema for validating the pattern allowed for control type identifiers. + type: string + # The value of this identifier takes the format: + # ( '/' )? + # Where the set of allowed characters is limited. + pattern: |- + ^([A-Z][a-zA-Z0-9]*/)?[A-Z][a-zA-Z0-9]*$ + + ControlTypeId-disallowed-types: + enum: + - AppInfo + - HostControl + - Screen + - AppTest + - TestCase + - TestSuite + + ControlTypeId-not-yet-supported: + enum: + - CommandComponent + - DataComponent + - FunctionComponent + + ControlTypeId-oneOf-3P-types: + $comment: The set of ControlTypeIds that represent third-party controls. + oneOf: + - $ref: "#/definitions/ControlTypeId-Component" + - $ref: "#/definitions/ControlTypeId-CodeComponent" + + # TODO: Imlpement a way that some controls may define a limited subset of child control types + # e.g. The 'Form' control's children must be of type 'TypedDataCard': + # - $ref: "#/definitions/ControlTypeId-TypedDataCard" + + ControlTypeId-Component: + description: |- + Identifies a custom component instance. This control type requires additional properties to be specified. + type: string + const: Component + + ControlTypeId-CodeComponent: + description: |- + Identifies a custom code component (aka PCF control) instance. This control type requires additional properties to be specified. + type: string + const: CodeComponent + + ControlTypeId-1P-controls: + description: The invariant identifier of a first-party control published by Power Apps (aka the 'Control Library'). + allOf: + - $ref: "#/definitions/ControlTypeId-pattern" + - $ref: "#/definitions/ControlTypeId-1P-controls-enum" + not: + $comment: Exclude built-in control identifiers as these are not defined in the 'Control Library'. + $ref: "#/definitions/ControlTypeId-oneOf-3P-types" + + ControlTypeId-1P-controls-enum: + # [schema-build-remove-next-line] + $ref: "ControlLibraryVDev/ControlTypeId-1P-controls-enum.schema.yaml" + # [schema-build-uncomment-next-line] + #true + + Control-variant-name: + description: The variant of a control template being instantiated. + allOf: + - $ref: "#/definitions/entity-name" + Control-instance: type: object required: [Control] properties: - Control: { $ref: "#/definitions/Control-type-identifier" } - Variant: { $ref: "#/definitions/Control-variant-name" } + Control: { $ref: "#/definitions/ControlTypeId" } Properties: { $ref: "#/definitions/Properties-formula-map" } - Children: { $ref: "#/definitions/Children-Control-instance-sequence" } if: required: [Control] properties: - Control: { $ref: "#/definitions/Control-type-component" } + Control: { $ref: "#/definitions/ControlTypeId-oneOf-3P-types" } then: - required: [ComponentName] - properties: - Control: true - ComponentLibraryUniqueName: { $ref: "#/definitions/ComponentLibrary-unique-name" } - ComponentName: { $ref: "#/definitions/ComponentDefinition-name" } - Properties: true - # Note: Component instances do not support Variants or Children. - additionalProperties: false + allOf: + - if: + properties: + Control: { $ref: "#/definitions/ControlTypeId-Component" } + then: + required: [ComponentName] + additionalProperties: false + properties: + Control: true + ComponentLibraryUniqueName: { $ref: "#/definitions/ComponentLibrary-unique-name" } + ComponentName: { $ref: "#/definitions/ComponentDefinition-name" } + Properties: true + # Note: Component instances do not support Variants or Children. + - if: + properties: + Control: { $ref: "#/definitions/ControlTypeId-CodeComponent" } + then: + required: [ComponentName] + additionalProperties: false + properties: + Control: true + ComponentName: { $ref: "#/definitions/CodeComponent-name" } + Properties: true + # Note: CodeComponent instances do not support Variants or Children. else: # Expected to be a built-in control library template + additionalProperties: false properties: Control: true - Variant: true + Variant: { $ref: "#/definitions/Control-variant-name" } Properties: true - Children: true - additionalProperties: false - - Control-type-identifier: - description: The invariant type of control being instantiated. - type: string - oneOf: - - $ref: "#/definitions/control-library-template-name" - - $ref: "#/definitions/Control-type-component" + # Depending on the ControlTypeId, it may or may not actually support Groups + Groups: { $ref: "#/definitions/Groups-of-controls" } + Children: { $ref: "#/definitions/Children-Control-instance-sequence" } - Control-type-component: + Groups-of-controls: description: |- - Identifies a custom component instance. This control type requires additional properties to be specified. - type: string - const: component + A mapping of groups of controls under this container. The keys of this object represent the name of the Group. + + Groups do not impact the behavior of an app, but are used in the Studio to organize controls when editing. + type: object + propertyNames: { $ref: "#/definitions/Control-instance-name" } + additionalProperties: + type: object + required: [ControlNames] + additionalProperties: false + properties: + ControlNames: + description: |- + An array of the names of controls that are part of this group. + A group must have at least two (2) controls in it. + type: array + minItems: 2 + items: { $ref: "#/definitions/Control-instance-name" } + defaultSnippets: + - label: Add Group + body: + ${1:Group1}: + ControlNames: + - ${2:ControlName1} + - ${3:ControlName2} - control-library-template-name: - description: The invariant name of a control template published by Power Apps (aka the 'Control Library'). + CodeComponent-name: + description: |- + The unique name of the Code Component (aka PCF control) as it occurs in Dataverse. + The format is: '_' '.' type: string - minimum: 1 - # NOTE: The pattern here is more restrictive than a DName, to represent the actual set of chars used. - # By doing this, we can catch invalid uses. We can always expand the char set in the future iif needed. + # TODO: The JS regex pattern doesn't easily support unicode chars which are valid JS identifiers. pattern: |- - ^[a-zA-Z0-9][a-zA-Z0-9]*$ - not: - $comment: Exclude 1st class control type names which are not defined in the 'Control Library'. - enum: [component] - examples: - # TODO: Add additional well-known control types here for usability - - label - - gallery - - Control-variant-name: - description: The variant of a control template being instantiated. - allOf: - - $ref: "#/definitions/entity-name" + ^([a-z][a-z0-9]{1,7})_([a-zA-Z0-9]\.)+[a-zA-Z0-9]+*$ ComponentDefinitions-name-instance-map: type: object @@ -169,6 +302,7 @@ definitions: - Height - Width - OnReset + Groups: { $ref: "#/definitions/Groups-of-controls" } Children: { $ref: "#/definitions/Children-Control-instance-sequence" } ComponentDefinition-name: diff --git a/src/schemas/publish.cmd b/src/schemas/publish.cmd index 899f6d99..4f47ba84 100644 --- a/src/schemas/publish.cmd +++ b/src/schemas/publish.cmd @@ -1,14 +1 @@ -:: SETLOCAL will also autoreset the current directory -@SETLOCAL -@CD %~dp0 - -@SET _RepoRoot=%~dp0..\..\ -@SET _SchemasDistRoot=%_RepoRoot%schemas\ - - -:: TODO: Instead of a straight copy, we should remove yaml comments; only keeping '$comment' -:: TODO: We MAY want to also publish the *.schema.json files too. -@ECHO Copying pa-yaml/v2.2 ... -@RMDIR /S /Q "%_SchemasDistRoot%pa-yaml\v2.2\" -@MKDIR "%_SchemasDistRoot%pa-yaml\v2.2\" -@COPY pa-yaml\v2.2\pa.schema.yaml "%_SchemasDistRoot%pa-yaml\v2.2\" +@pwsh.exe -executionpolicy bypass -file %~dp0publish.ps1 diff --git a/src/schemas/publish.ps1 b/src/schemas/publish.ps1 new file mode 100644 index 00000000..90cae119 --- /dev/null +++ b/src/schemas/publish.ps1 @@ -0,0 +1,56 @@ +$ErrorActionPreference = "Stop" +Set-StrictMode -Version 3.0 + +function BuildSchemaForPublish([string]$srcSchemaFilePath, [string]$outputFilePath) { + #Copy-Item -Path $srcSchemaFilePath -Destination $outputFilePath + $contentLines = Get-Content $srcSchemaFilePath; + + # Remove whitespace at beginning of lines + # This makes the check for blank lines easier by allowing a simple IndexOf('') + $contentLines = $contentLines | foreach { + if ($_ -match '^\s+# \[schema-build-[a-z-]+\]\s*$') { + $_ -replace '^\s+', '' + } else { + $_ + } + }; + + # Uncomment lines requested. + for ($lineIdx = $contentLines.IndexOf('# [schema-build-uncomment-next-line]'); $lineIdx -ge 0; $lineIdx = $contentLines.IndexOf('# [schema-build-uncomment-next-line]')) { + $contentLines[$lineIdx] = '# '; + $contentLines[$lineIdx+1] = $contentLines[$lineIdx+1] -replace '#', ''; + } + + # Remove lines requested + for ($lineIdx = $contentLines.IndexOf('# [schema-build-remove-next-line]'); $lineIdx -ge 0; $lineIdx = $contentLines.IndexOf('# [schema-build-remove-next-line]')) { + # instead of removing the lines, just comment them out so they'll get removed later + $contentLines[$lineIdx] = '# '; + $contentLines[$lineIdx+1] = '# '; + } + + # Remove comment lines as these are intended only as source code comments. + # Schema owners should utilize $comment properties to add comments intended for public consumption. + $contentLines = $contentLines | where {$_ -notmatch '^\s*#.*?$'}; + + # Remove blank lines at the beginning + $contentLines = [Linq.Enumerable]::ToArray( ` + [Linq.Enumerable]::SkipWhile([string[]]$contentLines, [Func[string, bool]] { param($line) [string]::IsNullOrWhiteSpace($line) }) ` + ); + + Set-Content -Path $outputFilePath $contentLines +} + +$repoRoot = [IO.Path]::GetFullPath("$PSScriptRoot/../.."); +$schemaSrcRoot = "$repoRoot/src/schemas"; +$schemasDistRoot = "$repoRoot/schemas"; + +Write-Host "Copying pa-yaml/v3.0..." +$sourceFolder = "$schemaSrcRoot/pa-yaml/v3.0"; +$outputFolder = "$schemasDistRoot/pa-yaml/v3.0"; +if (Test-Path $outputFolder -PathType Container) { + [IO.Directory]::Delete($outputFolder, $true); +} + +$null = [IO.Directory]::CreateDirectory($outputFolder); + +BuildSchemaForPublish "$sourceFolder/pa.schema.yaml" "$outputFolder/pa.schema.yaml"