Skip to content

Commit 8c8a224

Browse files
committed
Merge branch 'release/4.3.0'
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).
2 parents 1fc52c0 + b226a7e commit 8c8a224

13 files changed

Lines changed: 302 additions & 121 deletions

File tree

OnTopic.Editor.AspNetCore.Host/OnTopic.Editor.AspNetCore.Host.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
</PropertyGroup>
88

99
<ItemGroup>
10-
<PackageReference Include="OnTopic" Version="4.2.0" />
11-
<PackageReference Include="OnTopic.ViewModels" Version="4.2.0" />
12-
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.2.0" />
13-
<PackageReference Include="OnTopic.Data.Caching" Version="4.2.0" />
14-
<PackageReference Include="OnTopic.Data.Sql" Version="4.2.0" />
10+
<PackageReference Include="OnTopic" Version="4.3.0" />
11+
<PackageReference Include="OnTopic.ViewModels" Version="4.3.0" />
12+
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.3.0" />
13+
<PackageReference Include="OnTopic.Data.Caching" Version="4.3.0" />
14+
<PackageReference Include="OnTopic.Data.Sql" Version="4.3.0" />
1515
</ItemGroup>
1616

1717
<ItemGroup>
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
@model TopicListAttributeViewModel
22

33
@{
4+
if (Model.TopicList.Count > 1) {
45
Layout = "~/Areas/Editor/Views/Editor/Components/_Layout.cshtml";
6+
}
57
}
68

7-
<select
8-
asp-for ="Value"
9-
asp-items ="Model.TopicList"
10-
class ="@Model.AttributeDescriptor.CssClass form-control form-inline"
11-
disabled =@(!Model.AttributeDescriptor.IsEnabled)
12-
required =@Model.AttributeDescriptor.IsRequired
13-
>
14-
</select>
9+
@if (Model.TopicList.Count > 1) {
10+
<select
11+
asp-for ="Value"
12+
asp-items ="Model.TopicList"
13+
class ="@Model.AttributeDescriptor.CssClass form-control form-inline"
14+
disabled =@(!Model.AttributeDescriptor.IsEnabled)
15+
required =@Model.AttributeDescriptor.IsRequired
16+
>
17+
</select>
18+
}

OnTopic.Editor.AspNetCore/Areas/Editor/Views/Editor/Components/TopicReference/Default.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
@{
44
var attributeDescriptor = new TokenizedTopicListAttributeTopicViewModel() {
5-
Key = "TopicID",
5+
Key = Model.AttributeDescriptor.Key,
66
ContentType = "TokenizedTopicListAttribute",
77
Description = Model.AttributeDescriptor.Description,
88
Title = Model.AttributeDescriptor.Title,

OnTopic.Editor.AspNetCore/Areas/Editor/Views/Editor/Edit.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
<h3 class="h5">Topic Information</h3>
7272
<dl>
7373
<dt><i class="fa fa-cogs"></i> Content Type</dt>
74-
<dd><a href="/Configuration/ContentTypes/@Model.Topic.ContentType">@Model.Topic.ContentType</a></dd>
74+
<dd><a href="/OnTopic/Edit/@Model.ContentTypeDescriptor.WebPath">@Model.Topic.ContentType</a></dd>
7575
<dt><i class="fa fa-database"></i> Topic ID</dt>
7676
<dd><a href="/Topic/@Model.Topic.Id/">@Model.Topic.Id</a></dd>
7777
<dt><i class="fa fa-eye"></i> Current</dt>

OnTopic.Editor.AspNetCore/Areas/Editor/Views/Editor/Export.cshtml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@
3737
<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>
3838
</section>
3939

40+
<section class="attribute">
41+
<input type="checkbox" asp-for="ExportOptions.TranslateTopicPointers" />
42+
<label asp-for="ExportOptions.TranslateTopicPointers">Translate Topic Pointers?</label>
43+
<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>
44+
</section>
45+
4046
</div>
4147

4248
@if (!Model.IsModal) {

OnTopic.Editor.AspNetCore/Components/ContentTypeListViewComponent.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,20 @@ public IViewComponentResult Invoke(
9090
);
9191
}
9292

93+
/*------------------------------------------------------------------------------------------------------------------------
94+
| Get content type
95+
>-------------------------------------------------------------------------------------------------------------------------
96+
| If the database is uninitialized, the content type won't be found. In that case, return an empty view, which will
97+
| effectively hide the component.
98+
\-----------------------------------------------------------------------------------------------------------------------*/
99+
var contentTypes = _topicRepository.GetContentTypeDescriptors();
100+
var actualTopic = _topicRepository.Load(currentTopic.Id);
101+
var actualContentType = contentTypes.GetTopic(currentTopic.ContentType);
102+
103+
if (actualContentType == null) {
104+
return View(viewModel);
105+
}
106+
93107
/*------------------------------------------------------------------------------------------------------------------------
94108
| Get permitted content types for container
95109
>-------------------------------------------------------------------------------------------------------------------------
@@ -98,10 +112,6 @@ public IViewComponentResult Invoke(
98112
| to organize a specific type of content. For example, a Container called "Forms" might be used exclusively to organized
99113
| Form topics.
100114
\-----------------------------------------------------------------------------------------------------------------------*/
101-
var contentTypes = _topicRepository.GetContentTypeDescriptors();
102-
var actualTopic = _topicRepository.Load(currentTopic.Id);
103-
var actualContentType = contentTypes.GetTopic(currentTopic.ContentType);
104-
105115
if (actualContentType.Key.Equals("Container", StringComparison.InvariantCultureIgnoreCase)) {
106116
viewModel.TopicList.AddRange(
107117
actualTopic

OnTopic.Editor.AspNetCore/Components/TopicListViewComponent.cs

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using OnTopic.Editor.Models;
1313
using OnTopic.Editor.Models.Metadata;
1414
using OnTopic.Editor.Models.Queryable;
15+
using OnTopic.Querying;
1516
using OnTopic.Repositories;
1617

1718
namespace OnTopic.Editor.AspNetCore.Components {
@@ -76,7 +77,7 @@ public IViewComponentResult Invoke(
7677
\-----------------------------------------------------------------------------------------------------------------------*/
7778
viewModel.TopicList.Add(
7879
new SelectListItem {
79-
Value = null,
80+
Value = "",
8081
Text = attribute.DefaultLabel
8182
}
8283
);
@@ -87,35 +88,42 @@ public IViewComponentResult Invoke(
8788
var defaultValue = currentTopic.Attributes.ContainsKey(attribute.Key)? currentTopic.Attributes[attribute.Key] : null;
8889

8990
/*------------------------------------------------------------------------------------------------------------------------
90-
| Get values
91+
| Get root topic
9192
\-----------------------------------------------------------------------------------------------------------------------*/
92-
var topics = (List<QueryResultTopicViewModel>)null;
93+
var rootTopic = (Topic)null;
9394

9495
if (attribute.RelativeTopicBase != null) {
9596
var baseTopic = _topicRepository.Load(currentTopic.UniqueKey);
96-
var rootTopic = attribute.RelativeTopicBase switch {
97+
if (String.IsNullOrEmpty(currentTopic.Key)) {
98+
baseTopic = TopicFactory.Create("NewTopic", currentTopic.ContentType, baseTopic);
99+
baseTopic.Parent.Children.Remove(baseTopic);
100+
}
101+
rootTopic = attribute.RelativeTopicBase switch {
97102
"CurrentTopic" => baseTopic,
98103
"ParentTopic" => baseTopic.Parent,
99104
"GrandparentTopic" => (Topic)baseTopic.Parent?.Parent,
100-
"ContentTypeDescriptor" => (Topic)_topicRepository.GetContentTypeDescriptors().Where(t => t.Key.Equals(baseTopic.ContentType)),
105+
"ContentTypeDescriptor" => (Topic)_topicRepository.GetContentTypeDescriptors().FirstOrDefault(t => t.Key.Equals(baseTopic.ContentType)),
101106
_ => baseTopic
102107
};
103-
topics = GetTopics(
104-
rootTopic,
105-
attribute.AttributeKey,
106-
attribute.AttributeValue,
107-
allowedKeys
108-
);
109108
}
110109
else {
111-
topics = GetTopics(
112-
_topicRepository.Load(attribute.RootTopic?.UniqueKey?? attribute.RootTopicKey),
113-
attribute.AttributeKey,
114-
attribute.AttributeValue,
115-
allowedKeys
116-
);
110+
rootTopic = _topicRepository.Load(attribute.RootTopic?.UniqueKey?? attribute.RootTopicKey);
111+
}
112+
113+
if (rootTopic != null && !String.IsNullOrEmpty(attribute.RelativeTopicPath)) {
114+
rootTopic = rootTopic.GetByUniqueKey(rootTopic.GetUniqueKey() + ":" + attribute.RelativeTopicPath);
117115
}
118116

117+
/*------------------------------------------------------------------------------------------------------------------------
118+
| Get values
119+
\-----------------------------------------------------------------------------------------------------------------------*/
120+
var topics = GetTopics(
121+
rootTopic,
122+
attribute.AttributeKey,
123+
attribute.AttributeValue,
124+
allowedKeys
125+
);
126+
119127
/*------------------------------------------------------------------------------------------------------------------------
120128
| Get values from repository
121129
\-----------------------------------------------------------------------------------------------------------------------*/
@@ -158,6 +166,13 @@ public static List<QueryResultTopicViewModel> GetTopics(
158166
string allowedKeys = ""
159167
) {
160168

169+
/*------------------------------------------------------------------------------------------------------------------------
170+
| Swallow missing topic
171+
\-----------------------------------------------------------------------------------------------------------------------*/
172+
if (topic == null) {
173+
return new List<QueryResultTopicViewModel>();
174+
}
175+
161176
/*------------------------------------------------------------------------------------------------------------------------
162177
| Establish query options
163178
\-----------------------------------------------------------------------------------------------------------------------*/

OnTopic.Editor.AspNetCore/Controllers/EditorController.cs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,9 @@ protected Topic CurrentTopic {
104104
/// <returns>The Content Type associated with the current request.</returns>
105105
protected ContentTypeDescriptor GetContentType(string contentType) => _topicRepository
106106
.GetContentTypeDescriptors()
107-
.Where(t => t.Key.Equals(contentType))
108-
.First();
107+
.Where(t => t.Key.Equals(contentType?? ""))
108+
.FirstOrDefault()??
109+
(ContentTypeDescriptor)TopicFactory.Create(contentType, "ContentTypeDescriptor");
109110

110111
/*==========================================================================================================================
111112
| GET EDITOR VIEW MODEL
@@ -685,7 +686,18 @@ public async Task<IActionResult> Import(IFormFile jsonFile, [Bind(Prefix = "Impo
685686
/*------------------------------------------------------------------------------------------------------------------------
686687
| IDENTIFY TARGET TOPIC
687688
\-----------------------------------------------------------------------------------------------------------------------*/
688-
var target = TopicRepository.Load(topicData.UniqueKey);
689+
var uniqueKey = topicData.UniqueKey;
690+
var target = TopicRepository.Load(uniqueKey);
691+
692+
//Create target if it doesn't exist
693+
if (target == null) {
694+
var parentKey = uniqueKey.Substring(0, uniqueKey.LastIndexOf(":", StringComparison.InvariantCulture));
695+
var parent = TopicRepository.Load(parentKey);
696+
697+
if (parent != null) {
698+
target = TopicFactory.Create(topicData.Key, topicData.ContentType, parent);
699+
}
700+
}
689701

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

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

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

723730
foreach (var unmatchedTopic in unmatchedTopics) {
724731
TopicRepository.Delete(unmatchedTopic);
725732
}
726733

727734
/*------------------------------------------------------------------------------------------------------------------------
728-
| SAVE
735+
| SET SAVE SCOPE
736+
>-------------------------------------------------------------------------------------------------------------------------
737+
| ### HACK JJC20200519: If the parent hasn't been saved, then it should be set to the target to be saved. This should only
738+
| happen when working with an empty database, in which case the Root topic will be autogenerated by TopicRepositoryBase.
739+
| Otherwise, Save() will generate an error since the parent ID won't be found.
740+
\-----------------------------------------------------------------------------------------------------------------------*/
741+
var saveRoot = target;
742+
if (target.Parent.IsNew) {
743+
saveRoot = target.Parent;
744+
}
745+
746+
/*------------------------------------------------------------------------------------------------------------------------
747+
| INITIAL SAVE
748+
\-----------------------------------------------------------------------------------------------------------------------*/
749+
TopicRepository.Save(saveRoot, topicData.Children.Count > 0);
750+
751+
/*------------------------------------------------------------------------------------------------------------------------
752+
| RESOLVE TOPIC REFERENCES
729753
>-------------------------------------------------------------------------------------------------------------------------
730-
| ### HACK JJC20200123: Because the graph may include references to objects that won't be saved until later in the import,
731-
| we need to save the topic tree twice. The first will ensure all objects have an TopicId. The second will ensure all
732-
| saved references refer to the correct TopicId.
754+
| ### HACK JJC20200522: When the first Import() is done, topic references may be pointing to objects that haven't yet been
755+
| imported (i.e., they occur later in the graph traversal). Likewise, when the first Save() is done, those same topic
756+
| references have not yet been saved, and so they can't be resolved to a valid TopicID. To mitigate this, we do a second
757+
| Import() followed by a second Save(). This shouldn't impact the items that have already been imported, but it will
758+
| ensure that topic references are resolved. This includes relationships, derived topics, and topic pointers from
759+
| attribute types such as TokenizedTopicList, TopicList, and TopicReference.
733760
\-----------------------------------------------------------------------------------------------------------------------*/
734-
TopicRepository.Save(target, topicData.Children.Count > 0);
735-
TopicRepository.Save(target, topicData.Children.Count > 0);
761+
target.Import(topicData, options);
762+
TopicRepository.Save(saveRoot, topicData.Children.Count > 0);
736763

737764
/*------------------------------------------------------------------------------------------------------------------------
738765
| RETURN JSON

OnTopic.Editor.AspNetCore/EditorServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ this IEndpointRouteBuilder routes
6060
name: "TopicEditor",
6161
areaName: "Editor",
6262
pattern: "OnTopic/{action}/{**path}",
63-
defaults: new { controller = "Editor", action="Edit", path = "Root/Web/" }
63+
defaults: new { controller = "Editor", action="Edit", path = "Root" }
6464
);
6565

6666
} // Class

OnTopic.Editor.AspNetCore/OnTopic.Editor.AspNetCore.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626

2727
<ItemGroup>
2828
<FrameworkReference Include="Microsoft.AspNetCore.App" />
29-
<PackageReference Include="GitVersionTask" Version="5.2.4">
29+
<PackageReference Include="GitVersionTask" Version="5.3.5">
3030
<PrivateAssets>all</PrivateAssets>
3131
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
3232
</PackageReference>
33-
<PackageReference Include="OnTopic" Version="4.2.0" />
34-
<PackageReference Include="OnTopic.Data.Transfer" Version="1.1.0" />
35-
<PackageReference Include="OnTopic.ViewModels" Version="4.2.0" />
36-
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.2.0" />
33+
<PackageReference Include="OnTopic" Version="4.3.0" />
34+
<PackageReference Include="OnTopic.Data.Transfer" Version="1.2.0" />
35+
<PackageReference Include="OnTopic.ViewModels" Version="4.3.0" />
36+
<PackageReference Include="OnTopic.AspNetCore.Mvc" Version="4.3.0" />
3737
</ItemGroup>
3838

3939
<ItemGroup>

0 commit comments

Comments
 (0)