Skip to content

Commit

Permalink
Merge branch 'release/4.3.0'
Browse files Browse the repository at this point in the history
This update builds off of OnTopic Library 4.3.0 and the OnTopic Data Transfer 1.2.0 library in order to provide support for connecting to and bootstrapping empty repositories. Notably, this includes the ability to dynamically add or remove content type and attribute descriptors. Not only does this allow some configuration changes made via the **OnTopic Editor** to be immediately available to the interface, it also allows new or reference configurations to be imported using the **OnTopic Data Transfer** library integration. Finally, this update takes advantage of many performance and reliability updates to the underlying services, particularly regarding recursive saves (as done during `Import()`).

Features
- The editor will now tolerate binding to topics with missing content type descriptors (06f065b, 34cdeec). This allows it to display a default `Root` topic even if the `Container` content type hasn't yet been created. Of course, no editor form will be present in these scenarios.
- Allow creation of the root topic on `Import()` (c51a141). Previously, the editor assumed that the root topic already existed.

Bug Fixes
- Ensured implicit topic references (i.e., attributes ending in 'Id' and pointing to a `Topic.Id`) can be resolved based on a single `Import()` by rearranging the `Import()` and `Save()` logic (a01e92e).
- Changed the implicit default root from `Web` to `Root`; this not only allows support for an empty database (where `Web` won't yet exist), but also fixes issues when querying the `/JSON` service where it would only query the `Web` branch, thus making it impossible to reference e.g. the `Configuration` branch with a `QueryableTopicListAttribute` or `TopicReferenceAttribute` (1069b34).
- Ensure the `TopicReferenceAttributeViewComponent` honors the current key, instead of being hard-coded to use `TopicID` (eb84431).
- Expose new `ExportOptions.TranslateTopicPointers` opt-out to the `Export()` interface (5630bb7).
- Fix link to the `ContentTypeDescriptor` from the editor interface (9b4ebf6).

`TopicListAttributeViewComponent`
- Ensure `DefaultLabel` isn't persisted as the value if not topic is selected (1be3eb3)
- Resolve runtime exception when using `RelativeTopicBase`'s `ContentTypeDescriptor` (339fa6d)
- Gracefully fail if the `TopicList`'s scope cannot be resolved (9ecf880)
- Ensure `RelativeTopicBase` works correctly when creating new topics (7f027c6)
- Introduce missing support for the `RelativeTopicPath` attribute (b727cf1)
- Hide the `TopicListViewComponent` if no values are returned (753d0a4)

Code Changes
- Updated to use newly created `Topic.IsNew` property for detecting if topic references are valid (9a79cc8).

Maintenance
- Updated various dependencies, including client-side dependencies (4b996cc).
  • Loading branch information
JeremyCaney committed Jun 1, 2020
2 parents 1fc52c0 + b226a7e commit 8c8a224
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OnTopic" Version="4.2.0" />
<PackageReference Include="OnTopic.ViewModels" Version="4.2.0" />
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.2.0" />
<PackageReference Include="OnTopic.Data.Caching" Version="4.2.0" />
<PackageReference Include="OnTopic.Data.Sql" Version="4.2.0" />
<PackageReference Include="OnTopic" Version="4.3.0" />
<PackageReference Include="OnTopic.ViewModels" Version="4.3.0" />
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.3.0" />
<PackageReference Include="OnTopic.Data.Caching" Version="4.3.0" />
<PackageReference Include="OnTopic.Data.Sql" Version="4.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
@model TopicListAttributeViewModel

@{
if (Model.TopicList.Count > 1) {
Layout = "~/Areas/Editor/Views/Editor/Components/_Layout.cshtml";
}
}

<select
asp-for ="Value"
asp-items ="Model.TopicList"
class ="@Model.AttributeDescriptor.CssClass form-control form-inline"
disabled =@(!Model.AttributeDescriptor.IsEnabled)
required =@Model.AttributeDescriptor.IsRequired
>
</select>
@if (Model.TopicList.Count > 1) {
<select
asp-for ="Value"
asp-items ="Model.TopicList"
class ="@Model.AttributeDescriptor.CssClass form-control form-inline"
disabled =@(!Model.AttributeDescriptor.IsEnabled)
required =@Model.AttributeDescriptor.IsRequired
>
</select>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

@{
var attributeDescriptor = new TokenizedTopicListAttributeTopicViewModel() {
Key = "TopicID",
Key = Model.AttributeDescriptor.Key,
ContentType = "TokenizedTopicListAttribute",
Description = Model.AttributeDescriptor.Description,
Title = Model.AttributeDescriptor.Title,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
<h3 class="h5">Topic Information</h3>
<dl>
<dt><i class="fa fa-cogs"></i> Content Type</dt>
<dd><a href="/Configuration/ContentTypes/@Model.Topic.ContentType">@Model.Topic.ContentType</a></dd>
<dd><a href="/OnTopic/Edit/@Model.ContentTypeDescriptor.WebPath">@Model.Topic.ContentType</a></dd>
<dt><i class="fa fa-database"></i> Topic ID</dt>
<dd><a href="/Topic/@Model.Topic.Id/">@Model.Topic.Id</a></dd>
<dt><i class="fa fa-eye"></i> Current</dt>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" title="Includes relationships even if the topic they are referencing falls outside the scope of the export."></i>
</section>

<section class="attribute">
<input type="checkbox" asp-for="ExportOptions.TranslateTopicPointers" />
<label asp-for="ExportOptions.TranslateTopicPointers">Translate Topic Pointers?</label>
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right" title="Attributes which end in 'Id' and have a numeric value mapping to an existing topic will be translated to their unique key (e.g., 'Root:Web:Contact') on export, and then translated back to topic identifiers on import. This is enabled by default, but can optionally be disabled."></i>
</section>

</div>

@if (!Model.IsModal) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,20 @@ public IViewComponentResult Invoke(
);
}

/*------------------------------------------------------------------------------------------------------------------------
| Get content type
>-------------------------------------------------------------------------------------------------------------------------
| If the database is uninitialized, the content type won't be found. In that case, return an empty view, which will
| effectively hide the component.
\-----------------------------------------------------------------------------------------------------------------------*/
var contentTypes = _topicRepository.GetContentTypeDescriptors();
var actualTopic = _topicRepository.Load(currentTopic.Id);
var actualContentType = contentTypes.GetTopic(currentTopic.ContentType);

if (actualContentType == null) {
return View(viewModel);
}

/*------------------------------------------------------------------------------------------------------------------------
| Get permitted content types for container
>-------------------------------------------------------------------------------------------------------------------------
Expand All @@ -98,10 +112,6 @@ public IViewComponentResult Invoke(
| to organize a specific type of content. For example, a Container called "Forms" might be used exclusively to organized
| Form topics.
\-----------------------------------------------------------------------------------------------------------------------*/
var contentTypes = _topicRepository.GetContentTypeDescriptors();
var actualTopic = _topicRepository.Load(currentTopic.Id);
var actualContentType = contentTypes.GetTopic(currentTopic.ContentType);

if (actualContentType.Key.Equals("Container", StringComparison.InvariantCultureIgnoreCase)) {
viewModel.TopicList.AddRange(
actualTopic
Expand Down
49 changes: 32 additions & 17 deletions OnTopic.Editor.AspNetCore/Components/TopicListViewComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using OnTopic.Editor.Models;
using OnTopic.Editor.Models.Metadata;
using OnTopic.Editor.Models.Queryable;
using OnTopic.Querying;
using OnTopic.Repositories;

namespace OnTopic.Editor.AspNetCore.Components {
Expand Down Expand Up @@ -76,7 +77,7 @@ public IViewComponentResult Invoke(
\-----------------------------------------------------------------------------------------------------------------------*/
viewModel.TopicList.Add(
new SelectListItem {
Value = null,
Value = "",
Text = attribute.DefaultLabel
}
);
Expand All @@ -87,35 +88,42 @@ public IViewComponentResult Invoke(
var defaultValue = currentTopic.Attributes.ContainsKey(attribute.Key)? currentTopic.Attributes[attribute.Key] : null;

/*------------------------------------------------------------------------------------------------------------------------
| Get values
| Get root topic
\-----------------------------------------------------------------------------------------------------------------------*/
var topics = (List<QueryResultTopicViewModel>)null;
var rootTopic = (Topic)null;

if (attribute.RelativeTopicBase != null) {
var baseTopic = _topicRepository.Load(currentTopic.UniqueKey);
var rootTopic = attribute.RelativeTopicBase switch {
if (String.IsNullOrEmpty(currentTopic.Key)) {
baseTopic = TopicFactory.Create("NewTopic", currentTopic.ContentType, baseTopic);
baseTopic.Parent.Children.Remove(baseTopic);
}
rootTopic = attribute.RelativeTopicBase switch {
"CurrentTopic" => baseTopic,
"ParentTopic" => baseTopic.Parent,
"GrandparentTopic" => (Topic)baseTopic.Parent?.Parent,
"ContentTypeDescriptor" => (Topic)_topicRepository.GetContentTypeDescriptors().Where(t => t.Key.Equals(baseTopic.ContentType)),
"ContentTypeDescriptor" => (Topic)_topicRepository.GetContentTypeDescriptors().FirstOrDefault(t => t.Key.Equals(baseTopic.ContentType)),
_ => baseTopic
};
topics = GetTopics(
rootTopic,
attribute.AttributeKey,
attribute.AttributeValue,
allowedKeys
);
}
else {
topics = GetTopics(
_topicRepository.Load(attribute.RootTopic?.UniqueKey?? attribute.RootTopicKey),
attribute.AttributeKey,
attribute.AttributeValue,
allowedKeys
);
rootTopic = _topicRepository.Load(attribute.RootTopic?.UniqueKey?? attribute.RootTopicKey);
}

if (rootTopic != null && !String.IsNullOrEmpty(attribute.RelativeTopicPath)) {
rootTopic = rootTopic.GetByUniqueKey(rootTopic.GetUniqueKey() + ":" + attribute.RelativeTopicPath);
}

/*------------------------------------------------------------------------------------------------------------------------
| Get values
\-----------------------------------------------------------------------------------------------------------------------*/
var topics = GetTopics(
rootTopic,
attribute.AttributeKey,
attribute.AttributeValue,
allowedKeys
);

/*------------------------------------------------------------------------------------------------------------------------
| Get values from repository
\-----------------------------------------------------------------------------------------------------------------------*/
Expand Down Expand Up @@ -158,6 +166,13 @@ public static List<QueryResultTopicViewModel> GetTopics(
string allowedKeys = ""
) {

/*------------------------------------------------------------------------------------------------------------------------
| Swallow missing topic
\-----------------------------------------------------------------------------------------------------------------------*/
if (topic == null) {
return new List<QueryResultTopicViewModel>();
}

/*------------------------------------------------------------------------------------------------------------------------
| Establish query options
\-----------------------------------------------------------------------------------------------------------------------*/
Expand Down
61 changes: 44 additions & 17 deletions OnTopic.Editor.AspNetCore/Controllers/EditorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ protected Topic CurrentTopic {
/// <returns>The Content Type associated with the current request.</returns>
protected ContentTypeDescriptor GetContentType(string contentType) => _topicRepository
.GetContentTypeDescriptors()
.Where(t => t.Key.Equals(contentType))
.First();
.Where(t => t.Key.Equals(contentType?? ""))
.FirstOrDefault()??
(ContentTypeDescriptor)TopicFactory.Create(contentType, "ContentTypeDescriptor");

/*==========================================================================================================================
| GET EDITOR VIEW MODEL
Expand Down Expand Up @@ -685,7 +686,18 @@ public async Task<IActionResult> Import(IFormFile jsonFile, [Bind(Prefix = "Impo
/*------------------------------------------------------------------------------------------------------------------------
| IDENTIFY TARGET TOPIC
\-----------------------------------------------------------------------------------------------------------------------*/
var target = TopicRepository.Load(topicData.UniqueKey);
var uniqueKey = topicData.UniqueKey;
var target = TopicRepository.Load(uniqueKey);

//Create target if it doesn't exist
if (target == null) {
var parentKey = uniqueKey.Substring(0, uniqueKey.LastIndexOf(":", StringComparison.InvariantCulture));
var parent = TopicRepository.Load(parentKey);

if (parent != null) {
target = TopicFactory.Create(topicData.Key, topicData.ContentType, parent);
}
}

if (target == null) {
ModelState.AddModelError(
Expand All @@ -698,17 +710,12 @@ public async Task<IActionResult> Import(IFormFile jsonFile, [Bind(Prefix = "Impo
/*------------------------------------------------------------------------------------------------------------------------
| INDEX TOPICS IN SCOPE
\-----------------------------------------------------------------------------------------------------------------------*/
var topics = target.FindAll(t => t.Id >= 0).ToList();
var topics = target.FindAll(t => !t.IsNew).ToList();

/*------------------------------------------------------------------------------------------------------------------------
| IMPORT INTO TOPIC GRAPH
>-------------------------------------------------------------------------------------------------------------------------
| ### HACK JJC20200123: Because the graph may include references to objects that won't be created until later in the
| import, we need to import the topic data twice. The first will ensure all objects are created. The second will ensure
| all references are restored.
| INITIAL IMPORT
\-----------------------------------------------------------------------------------------------------------------------*/
target.Import(topicData, options);
target.Import(topicData, options);

/*------------------------------------------------------------------------------------------------------------------------
| DELETE UNMATCHED TOPICS
Expand All @@ -718,21 +725,41 @@ public async Task<IActionResult> Import(IFormFile jsonFile, [Bind(Prefix = "Impo
| removed topics during a recursive save and, therefore, the deletions aren't persited to the database. To mitigate this,
| we evaluate the topic graph after the save, and then delete any orphans.
\-----------------------------------------------------------------------------------------------------------------------*/
var unmatchedTopics = topics.Except(target.FindAll(t => t.Id >= 0));
var unmatchedTopics = topics.Except(target.FindAll(t => !t.IsNew));

foreach (var unmatchedTopic in unmatchedTopics) {
TopicRepository.Delete(unmatchedTopic);
}

/*------------------------------------------------------------------------------------------------------------------------
| SAVE
| SET SAVE SCOPE
>-------------------------------------------------------------------------------------------------------------------------
| ### HACK JJC20200519: If the parent hasn't been saved, then it should be set to the target to be saved. This should only
| happen when working with an empty database, in which case the Root topic will be autogenerated by TopicRepositoryBase.
| Otherwise, Save() will generate an error since the parent ID won't be found.
\-----------------------------------------------------------------------------------------------------------------------*/
var saveRoot = target;
if (target.Parent.IsNew) {
saveRoot = target.Parent;
}

/*------------------------------------------------------------------------------------------------------------------------
| INITIAL SAVE
\-----------------------------------------------------------------------------------------------------------------------*/
TopicRepository.Save(saveRoot, topicData.Children.Count > 0);

/*------------------------------------------------------------------------------------------------------------------------
| RESOLVE TOPIC REFERENCES
>-------------------------------------------------------------------------------------------------------------------------
| ### HACK JJC20200123: Because the graph may include references to objects that won't be saved until later in the import,
| we need to save the topic tree twice. The first will ensure all objects have an TopicId. The second will ensure all
| saved references refer to the correct TopicId.
| ### HACK JJC20200522: When the first Import() is done, topic references may be pointing to objects that haven't yet been
| imported (i.e., they occur later in the graph traversal). Likewise, when the first Save() is done, those same topic
| references have not yet been saved, and so they can't be resolved to a valid TopicID. To mitigate this, we do a second
| Import() followed by a second Save(). This shouldn't impact the items that have already been imported, but it will
| ensure that topic references are resolved. This includes relationships, derived topics, and topic pointers from
| attribute types such as TokenizedTopicList, TopicList, and TopicReference.
\-----------------------------------------------------------------------------------------------------------------------*/
TopicRepository.Save(target, topicData.Children.Count > 0);
TopicRepository.Save(target, topicData.Children.Count > 0);
target.Import(topicData, options);
TopicRepository.Save(saveRoot, topicData.Children.Count > 0);

/*------------------------------------------------------------------------------------------------------------------------
| RETURN JSON
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ this IEndpointRouteBuilder routes
name: "TopicEditor",
areaName: "Editor",
pattern: "OnTopic/{action}/{**path}",
defaults: new { controller = "Editor", action="Edit", path = "Root/Web/" }
defaults: new { controller = "Editor", action="Edit", path = "Root" }
);

} // Class
Expand Down
10 changes: 5 additions & 5 deletions OnTopic.Editor.AspNetCore/OnTopic.Editor.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="GitVersionTask" Version="5.2.4">
<PackageReference Include="GitVersionTask" Version="5.3.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="OnTopic" Version="4.2.0" />
<PackageReference Include="OnTopic.Data.Transfer" Version="1.1.0" />
<PackageReference Include="OnTopic.ViewModels" Version="4.2.0" />
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.2.0" />
<PackageReference Include="OnTopic" Version="4.3.0" />
<PackageReference Include="OnTopic.Data.Transfer" Version="1.2.0" />
<PackageReference Include="OnTopic.ViewModels" Version="4.3.0" />
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.3.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 8c8a224

Please sign in to comment.