First step to ingest template engine (#5065)
* First step to ingest template engine Please do not merge yet @piotrp @seancpeters @livarocc * Localization * Attempt to get a build going Rename program.cs to New3Command.cs Move TableFormatter into its own file Consume template engine packages version 1.0.0-beta1-20161218-24 Temporarily add MyGet templating feed so that package restore will work * Update ExtendedCommandParser Make short form args prefer longer runs of characters in the source parameter before falling back to p:shortname syntax Change scoping for a few methods to get rid of inconsistent visiblity errors * Fix package installation wildcards * Cleanup New3Command, fix review issue, bump version, sorting for template list * Installation, loc fix, help formatting * Use latest TemplateEngine packages * New3 unit tests * Fixed formatting on csproj files * Add the build steps to add templates to the layout * Change tests slightly to make comparisons easier Also fixes the wrong flag getting passed to set language * Fixes for 127, 128, 130, 131 - help display * Sync to latest TemplateEngine version Absorbs new search logic * All tests passing Cleaner New3Command Support for project/item template contexts * Try to make tests more durable * Disable test parallelization for dotnet-new tests * Update web SDK and template engine versions * Remove AI package feed * Simplify CLI interface to Template Engine * Fix host identifier and update to latest packages * Update template engine & Web SDK versions * Fix template engine version * Remove UsingTask and redeclaration of property
This commit is contained in:
parent
5e7b946ad7
commit
decec5f8da
15 changed files with 328 additions and 5 deletions
|
@ -3,6 +3,7 @@
|
|||
<packageSources>
|
||||
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
|
||||
<clear />
|
||||
<add key="templating" value="https://dotnet.myget.org/F/templating/api/v3/index.json" />
|
||||
<add key="dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
|
||||
<add key="cli-deps" value="https://dotnet.myget.org/F/cli-deps/api/v3/index.json" />
|
||||
<add key="roslyn" value="https://dotnet.myget.org/f/roslyn/api/v3/index.json" />
|
||||
|
|
39
build/Microsoft.DotNet.Cli.BundledTemplates.proj
Normal file
39
build/Microsoft.DotNet.Cli.BundledTemplates.proj
Normal file
|
@ -0,0 +1,39 @@
|
|||
<Project ToolsVersion="15.0" DefaultTargets="CopyTemplateToOutput">
|
||||
<!-- workaround for https://github.com/Microsoft/msbuild/issues/885 -->
|
||||
<!-- renaming the property because the original property is a global property and therefore
|
||||
cannot be redefined at runtime. -->
|
||||
|
||||
<Import Project="Microsoft.DotNet.Cli.tasks" />
|
||||
|
||||
<Target Name="CopyTemplateToOutput"
|
||||
DependsOnTargets="PrepareBundledTemplateProps;
|
||||
EnsureTemplateRestored;
|
||||
GetTemplateItemsToCopy"
|
||||
Inputs="@(TemplateContent)"
|
||||
Outputs="@(TemplateContent->'$(TemplateLayoutDirectory)/%(RecursiveDir)%(FileName)%(Extension)')">
|
||||
<Copy SourceFiles="@(TemplateContent)"
|
||||
DestinationFiles="@(TemplateContent->'$(TemplateLayoutDirectory)/%(RecursiveDir)%(FileName)%(Extension)')" />
|
||||
|
||||
<Message Text="Copied template $(TemplatePackageName) from $(TemplateNuPkgPath) to $(TemplateLayoutDirectory)."
|
||||
Importance="High" />
|
||||
</Target>
|
||||
|
||||
<Target Name="GetTemplateItemsToCopy">
|
||||
<ItemGroup>
|
||||
<TemplateContent Include="$(TemplateNuPkgPath)/$(TemplatePackageName.ToLower()).$(TemplatePackageVersion.ToLower()).nupkg" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="EnsureTemplateRestored"
|
||||
Condition="!Exists('$(TemplateNuPkgPath)/$(TemplatePackageName.ToLower()).nuspec')">
|
||||
<DotNetRestore ToolPath="$(Stage0Directory)"
|
||||
ProjectPath="$(MSBuildThisFileDirectory)/templates/templates.csproj"
|
||||
AdditionalParameters="/p:TemplatePackageName=$(TemplatePackageName) /p:TemplatePackageVersion=$(TemplatePackageVersion)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="PrepareBundledTemplateProps">
|
||||
<PropertyGroup>
|
||||
<TemplateNuPkgPath>$(NuGetPackagesDir)/$(TemplatePackageName.ToLower())/$(TemplatePackageVersion.ToLower())</TemplateNuPkgPath>
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
</Project>
|
10
build/Microsoft.DotNet.Cli.BundledTemplates.props
Normal file
10
build/Microsoft.DotNet.Cli.BundledTemplates.props
Normal file
|
@ -0,0 +1,10 @@
|
|||
<Project ToolsVersion="15.0">
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
|
||||
|
||||
<ItemGroup>
|
||||
<BundledTemplate Include="Microsoft.DotNet.Common.ItemTemplates" Version="$(TemplateEngineVersion)" />
|
||||
<BundledTemplate Include="Microsoft.DotNet.Common.ProjectTemplates" Version="$(TemplateEngineVersion)" />
|
||||
<BundledTemplate Include="Microsoft.DotNet.Test.ProjectTemplates" Version="$(TemplateEngineVersion)" />
|
||||
<BundledTemplate Include="Microsoft.DotNet.Web.ProjectTemplates" Version="$(TemplateEngineVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -18,6 +18,7 @@
|
|||
SetupStage;
|
||||
CompileStage;
|
||||
PublishSdks;
|
||||
PublishTemplates;
|
||||
BuildProjectsForNuGetPackages;
|
||||
GetNuGetPackagesArchive;" />
|
||||
|
||||
|
@ -263,4 +264,25 @@
|
|||
Projects="@(SdksToBundle)">
|
||||
</MSBuild>
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishTemplates"
|
||||
DependsOnTargets="Prepare">
|
||||
<ItemGroup>
|
||||
<TemplatesToBundle Include="build/Microsoft.DotNet.Cli.BundledTemplates.proj">
|
||||
<Properties>
|
||||
CLIBuildDll=$(CLIBuildDll);
|
||||
NuGetPackagesDir=$(NuGetPackagesDir);
|
||||
TemplateLayoutDirectory=$(SdkOutputDirectory)/Templates;
|
||||
TemplatePackageName=%(BundledTemplate.Identity);
|
||||
TemplatePackageVersion=%(BundledTemplate.Version);
|
||||
Stage0Directory=$(Stage0Directory)
|
||||
</Properties>
|
||||
</TemplatesToBundle>
|
||||
</ItemGroup>
|
||||
|
||||
<MSBuild
|
||||
BuildInParallel="False"
|
||||
Projects="@(TemplatesToBundle)">
|
||||
</MSBuild>
|
||||
</Target>
|
||||
</Project>
|
|
@ -4,7 +4,8 @@
|
|||
<CLI_MSBuild_Version>15.1.0-preview-000503-01</CLI_MSBuild_Version>
|
||||
<CLI_Roslyn_Version>2.0.0-rc3-61212-03</CLI_Roslyn_Version>
|
||||
<CLI_NETSDK_Version>1.0.0-alpha-20170105-1</CLI_NETSDK_Version>
|
||||
<CLI_WEBSDK_Version>1.0.0-alpha-20170104-1-189</CLI_WEBSDK_Version>
|
||||
<CLI_WEBSDK_Version>1.0.0-alpha-20170106-1-203</CLI_WEBSDK_Version>
|
||||
<CLI_TestPlatform_Version>15.0.0-preview-20161227-02</CLI_TestPlatform_Version>
|
||||
<TemplateEngineVersion>1.0.0-beta1-20170106-79</TemplateEngineVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<Import Project="Microsoft.DotNet.Cli.tasks" />
|
||||
<Import Project="prepare/CheckPrereqs.targets" />
|
||||
<Import Project="Microsoft.DotNet.Cli.BundledSdks.props" />
|
||||
<Import Project="Microsoft.DotNet.Cli.BundledTemplates.props" />
|
||||
<Import Project="Microsoft.DotNet.Cli.DependencyVersions.props" />
|
||||
|
||||
<Target Name="Prepare"
|
||||
|
|
17
build/templates/templates.csproj
Normal file
17
build/templates/templates.csproj
Normal file
|
@ -0,0 +1,17 @@
|
|||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>netcoreapp1.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="$(TemplatePackageName)">
|
||||
<Version>$(TemplatePackageVersion)</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
|
@ -16,6 +16,7 @@ using Microsoft.DotNet.Tools.List;
|
|||
using Microsoft.DotNet.Tools.Migrate;
|
||||
using Microsoft.DotNet.Tools.MSBuild;
|
||||
using Microsoft.DotNet.Tools.New;
|
||||
using Microsoft.DotNet.Tools.New3;
|
||||
using Microsoft.DotNet.Tools.NuGet;
|
||||
using Microsoft.DotNet.Tools.Pack;
|
||||
using Microsoft.DotNet.Tools.Publish;
|
||||
|
@ -42,6 +43,7 @@ namespace Microsoft.DotNet.Cli
|
|||
["migrate"] = MigrateCommand.Run,
|
||||
["msbuild"] = MSBuildCommand.Run,
|
||||
["new"] = NewCommand.Run,
|
||||
["new3"] = New3CommandShim.Run,
|
||||
["nuget"] = NuGetCommand.Run,
|
||||
["pack"] = PackCommand.Run,
|
||||
["publish"] = PublishCommand.Run,
|
||||
|
|
71
src/dotnet/commands/dotnet-new3/New3CommandShim.cs
Normal file
71
src/dotnet/commands/dotnet-new3/New3CommandShim.cs
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.TemplateEngine.Abstractions;
|
||||
using Microsoft.TemplateEngine.Cli;
|
||||
using Microsoft.TemplateEngine.Edge;
|
||||
using Microsoft.TemplateEngine.Orchestrator.RunnableProjects;
|
||||
using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Config;
|
||||
using Microsoft.TemplateEngine.Orchestrator.RunnableProjects.Macros;
|
||||
using Microsoft.TemplateEngine.Utils;
|
||||
|
||||
namespace Microsoft.DotNet.Tools.New3
|
||||
{
|
||||
internal class New3CommandShim
|
||||
{
|
||||
private const string HostIdentifier = "dotnetcli";
|
||||
private const string HostVersion = "1.0.0";
|
||||
private const string CommandName = "new3";
|
||||
|
||||
public static int Run(string[] args)
|
||||
{
|
||||
return New3Command.Run(CommandName, CreateHost(), FirstRun, args);
|
||||
}
|
||||
|
||||
private static ITemplateEngineHost CreateHost()
|
||||
{
|
||||
var builtIns = new Dictionary<Guid, Func<Type>>
|
||||
{
|
||||
{ new Guid("0C434DF7-E2CB-4DEE-B216-D7C58C8EB4B3"), () => typeof(RunnableProjectGenerator) },
|
||||
{ new Guid("3147965A-08E5-4523-B869-02C8E9A8AAA1"), () => typeof(BalancedNestingConfig) },
|
||||
{ new Guid("3E8BCBF0-D631-45BA-A12D-FBF1DE03AA38"), () => typeof(ConditionalConfig) },
|
||||
{ new Guid("A1E27A4B-9608-47F1-B3B8-F70DF62DC521"), () => typeof(FlagsConfig) },
|
||||
{ new Guid("3FAE1942-7257-4247-B44D-2DDE07CB4A4A"), () => typeof(IncludeConfig) },
|
||||
{ new Guid("3D33B3BF-F40E-43EB-A14D-F40516F880CD"), () => typeof(RegionConfig) },
|
||||
{ new Guid("62DB7F1F-A10E-46F0-953F-A28A03A81CD1"), () => typeof(ReplacementConfig) },
|
||||
{ new Guid("370996FE-2943-4AED-B2F6-EC03F0B75B4A"), () => typeof(ConstantMacro) },
|
||||
{ new Guid("BB625F71-6404-4550-98AF-B2E546F46C5F"), () => typeof(EvaluateMacro) },
|
||||
{ new Guid("10919008-4E13-4FA8-825C-3B4DA855578E"), () => typeof(GuidMacro) },
|
||||
{ new Guid("F2B423D7-3C23-4489-816A-41D8D2A98596"), () => typeof(NowMacro) },
|
||||
{ new Guid("011E8DC1-8544-4360-9B40-65FD916049B7"), () => typeof(RandomMacro) },
|
||||
{ new Guid("8A4D4937-E23F-426D-8398-3BDBD1873ADB"), () => typeof(RegexMacro) },
|
||||
{ new Guid("B57D64E0-9B4F-4ABE-9366-711170FD5294"), () => typeof(SwitchMacro) },
|
||||
{ new Guid("10919118-4E13-4FA9-825C-3B4DA855578E"), () => typeof(CaseChangeMacro) }
|
||||
}.ToList();
|
||||
|
||||
var preferences = new Dictionary<string, string>
|
||||
{
|
||||
{ "prefs:language", "C#" }
|
||||
};
|
||||
|
||||
return new DefaultTemplateEngineHost(HostIdentifier, HostVersion, CultureInfo.CurrentCulture.Name, preferences, builtIns);
|
||||
}
|
||||
|
||||
private static void FirstRun(ITemplateEngineHost host, IInstaller installer)
|
||||
{
|
||||
var templatesDir = Path.Combine(Paths.Global.BaseDir, "Templates");
|
||||
|
||||
if (templatesDir.Exists())
|
||||
{
|
||||
var layoutIncludedPackages = host.FileSystem.EnumerateFiles(templatesDir, "*.nupkg", SearchOption.TopDirectoryOnly);
|
||||
installer.InstallPackages(layoutIncludedPackages);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,6 +71,8 @@
|
|||
<PackageReference Include="Microsoft.DotNet.PlatformAbstractions">
|
||||
<Version>1.0.1-beta-000933</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.TemplateEngine.Cli" Version="$(TemplateEngineVersion)" />
|
||||
<PackageReference Include="Microsoft.TemplateEngine.Orchestrator.RunnableProjects" Version="$(TemplateEngineVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
|
|
|
@ -2,12 +2,10 @@
|
|||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.Build.Construction;
|
||||
using Microsoft.DotNet.Tools.Test.Utilities;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.New.Tests
|
||||
{
|
||||
|
|
68
test/dotnet-new.Tests/New3/GivenThatIWantANewApp.cs
Normal file
68
test/dotnet-new.Tests/New3/GivenThatIWantANewApp.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.DotNet.Tools.Test.Utilities;
|
||||
using System;
|
||||
using System.IO;
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.DotNet.New3.Tests
|
||||
{
|
||||
public class GivenThatIWantANewApp : New3TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void When_dotnet_new_is_invoked_mupliple_times_it_should_fail()
|
||||
{
|
||||
var rootPath = TestAssetsManager.CreateTestDirectory(identifier: "new3").Path;
|
||||
|
||||
new TestCommand("dotnet") { WorkingDirectory = rootPath }
|
||||
.Execute($"new3 console");
|
||||
|
||||
DateTime expectedState = Directory.GetLastWriteTime(rootPath);
|
||||
|
||||
var result = new TestCommand("dotnet") { WorkingDirectory = rootPath }
|
||||
.ExecuteWithCapturedOutput($"new3 console");
|
||||
|
||||
DateTime actualState = Directory.GetLastWriteTime(rootPath);
|
||||
|
||||
Assert.Equal(expectedState, actualState);
|
||||
|
||||
result.Should().Fail();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RestoreDoesNotUseAnyCliProducedPackagesOnItsTemplates()
|
||||
{
|
||||
string[] cSharpTemplates = new[] { "console", "classlib", "mstest", "xunit", "web", "mvc", "webapi" };
|
||||
|
||||
var rootPath = TestAssetsManager.CreateTestDirectory(identifier: "new3").Path;
|
||||
var packagesDirectory = Path.Combine(rootPath, "packages");
|
||||
|
||||
foreach (string cSharpTemplate in cSharpTemplates)
|
||||
{
|
||||
var projectFolder = Path.Combine(rootPath, cSharpTemplate + "1");
|
||||
Directory.CreateDirectory(projectFolder);
|
||||
CreateAndRestoreNewProject(cSharpTemplate, projectFolder, packagesDirectory);
|
||||
}
|
||||
|
||||
Directory.EnumerateFiles(packagesDirectory, $"*.nupkg", SearchOption.AllDirectories)
|
||||
.Should().NotContain(p => p.Contains("Microsoft.DotNet.Cli.Utils"));
|
||||
}
|
||||
|
||||
private void CreateAndRestoreNewProject(
|
||||
string projectType,
|
||||
string projectFolder,
|
||||
string packagesDirectory)
|
||||
{
|
||||
new TestCommand("dotnet") { WorkingDirectory = projectFolder }
|
||||
.Execute($"new3 {projectType}")
|
||||
.Should().Pass();
|
||||
|
||||
new RestoreCommand()
|
||||
.WithWorkingDirectory(projectFolder)
|
||||
.Execute($"--packages {packagesDirectory} /p:SkipInvalidConfigurations=true")
|
||||
.Should().Pass();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.DotNet.TestFramework;
|
||||
using Microsoft.DotNet.Tools.Test.Utilities;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Microsoft.DotNet.New3.Tests
|
||||
{
|
||||
public class GivenThatIWantANewAppWithSpecifiedType : New3TestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("C#", "console", false)]
|
||||
[InlineData("C#", "classlib", false)]
|
||||
[InlineData("C#", "mstest", false)]
|
||||
[InlineData("C#", "xunit", false)]
|
||||
[InlineData("C#", "web", true)]
|
||||
[InlineData("C#", "mvc", true)]
|
||||
[InlineData("C#", "webapi", true)]
|
||||
[InlineData("F#", "console", false)]
|
||||
[InlineData("F#", "classlib", false)]
|
||||
[InlineData("F#", "mstest", false)]
|
||||
[InlineData("F#", "xunit", false)]
|
||||
[InlineData("F#", "mvc", true)]
|
||||
public void TemplateRestoresAndBuildsWithoutWarnings(
|
||||
string language,
|
||||
string projectType,
|
||||
bool useNuGetConfigForAspNet)
|
||||
{
|
||||
string rootPath = TestAssetsManager.CreateTestDirectory(identifier: $"new3_{language}_{projectType}").Path;
|
||||
|
||||
new TestCommand("dotnet") { WorkingDirectory = rootPath }
|
||||
.Execute($"new3 {projectType} -lang {language}")
|
||||
.Should().Pass();
|
||||
|
||||
if (useNuGetConfigForAspNet)
|
||||
{
|
||||
File.Copy("NuGet.tempaspnetpatch.config", Path.Combine(rootPath, "NuGet.Config"));
|
||||
}
|
||||
|
||||
new TestCommand("dotnet")
|
||||
.WithWorkingDirectory(rootPath)
|
||||
.Execute($"restore")
|
||||
.Should().Pass();
|
||||
|
||||
var buildResult = new TestCommand("dotnet")
|
||||
.WithWorkingDirectory(rootPath)
|
||||
.ExecuteWithCapturedOutput("build")
|
||||
.Should().Pass()
|
||||
.And.NotHaveStdErr();
|
||||
}
|
||||
}
|
||||
}
|
36
test/dotnet-new.Tests/New3/New3TestBase.cs
Normal file
36
test/dotnet-new.Tests/New3/New3TestBase.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) .NET Foundation and contributors. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
|
||||
using Microsoft.DotNet.Tools.Test.Utilities;
|
||||
using Xunit;
|
||||
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
|
||||
namespace Microsoft.DotNet.New3.Tests
|
||||
{
|
||||
public class New3TestBase : TestBase
|
||||
{
|
||||
private static readonly object InitializationSync = new object();
|
||||
private static bool _isInitialized;
|
||||
|
||||
protected New3TestBase()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (InitializationSync)
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//Force any previously computed configuration to be cleared
|
||||
new TestCommand("dotnet").Execute("new3 --debug:reinit");
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" />
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp1.0</TargetFramework>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
|
|
Loading…
Add table
Reference in a new issue