diff --git a/zero.Core/Extensions/DictionaryExtensions.cs b/zero.Core/Extensions/DictionaryExtensions.cs new file mode 100644 index 00000000..ed18611c --- /dev/null +++ b/zero.Core/Extensions/DictionaryExtensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace zero.Core.Extensions +{ + public static class DictionaryExtensions + { + public static bool TryGetValue(this Dictionary model, string key, out T value) + { + if (!model.TryGetValue(key, out object valueObj) || !(valueObj is T)) + { + value = default; + return false; + } + + value = (T)valueObj; + return true; + } + + + public static T GetValueOrDefault(this Dictionary model, string key) + { + object? value = model.GetValueOrDefault(key); + return value == default || !(value is T) ? default : (T)value; + } + } +} diff --git a/zero.Core/Routing/ILinkListProvider.cs b/zero.Core/Routing/ILinkListProvider.cs deleted file mode 100644 index 33c582ab..00000000 --- a/zero.Core/Routing/ILinkListProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Entities; - -namespace zero.Core.Routing -{ - public interface ILinkListProvider : ILinkProvider - { - /// - /// Get paged list items. - /// Current document session - /// Current page number (one-based) - /// Search query - /// - Task> GetLinkListItems(IAsyncDocumentSession session, int page = 1, string search = null); - } -} diff --git a/zero.Core/Routing/ILinkProvider.cs b/zero.Core/Routing/ILinkProvider.cs index 2c1cc3e4..a8cfed73 100644 --- a/zero.Core/Routing/ILinkProvider.cs +++ b/zero.Core/Routing/ILinkProvider.cs @@ -1,17 +1,24 @@ -using System.Threading.Tasks; +using Raven.Client.Documents.Session; +using System.Threading.Tasks; using zero.Core.Entities; namespace zero.Core.Routing { public interface ILinkProvider { - string Name { get; } - - string Alias { get; } + /// + /// + /// + bool CanProcess(ILink link); /// /// /// - Task ResolveLink(ILink link); + Task Resolve(ILink link); + + /// + /// + /// + Task Preview(IAsyncDocumentSession session, ILink link); } } diff --git a/zero.Core/Routing/ILinkTreeProvider.cs b/zero.Core/Routing/ILinkTreeProvider.cs deleted file mode 100644 index 7130c45e..00000000 --- a/zero.Core/Routing/ILinkTreeProvider.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Raven.Client.Documents.Session; -using System.Collections.Generic; -using System.Threading.Tasks; -using zero.Core.Entities; - -namespace zero.Core.Routing -{ - public interface ILinkTreeProvider : ILinkProvider - { - /// - /// Get tree children for the current parent id. - /// Current document session - /// Parent node id - /// Selected node so parents can be set to open for the tree to load correclty - /// - Task> GetLinkTreeItems(IAsyncDocumentSession session, string parentId = null, string activeId = null); - } -} diff --git a/zero.Core/Routing/Links.cs b/zero.Core/Routing/Links.cs new file mode 100644 index 00000000..6c42a3a9 --- /dev/null +++ b/zero.Core/Routing/Links.cs @@ -0,0 +1,79 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using zero.Core.Database; +using zero.Core.Entities; + +namespace zero.Core.Routing +{ + public class Links : ILinks + { + protected IZeroStore Store { get; set; } + protected ILogger Logger { get; set; } + protected IEnumerable Providers { get; set; } + + public Links(IZeroStore store, ILogger logger, IEnumerable providers) + { + Store = store; + Logger = logger; + Providers = providers; + } + + + /// + public async Task GetUrl(ILink link) + { + ILinkProvider provider = Providers.LastOrDefault(x => x.CanProcess(link)); + + if (provider == null) + { + Logger.LogWarning("Could not find provider for link with area {area}", link.Area); + return null; + } + + return await provider.Resolve(link); + } + + + /// + public async Task> GetUrls(params ILink[] links) + { + Dictionary result = new(); + + foreach (ILink link in links) + { + result.Add(link, await GetUrl(link)); + } + + return result; + } + + + /// + public ILinkProvider GetProvider(ILink link) + { + return Providers.LastOrDefault(x => x.CanProcess(link)); + } + } + + public interface ILinks + { + /// + /// Get URL from a link object by finding a provider which can resolve the link + /// + Task GetUrl(ILink link); + + /// + /// Get URLs from link objects by finding matching providers + /// + Task> GetUrls(params ILink[] links); + + /// + /// Get the provider for a specific link + /// + ILinkProvider GetProvider(ILink link); + } +} diff --git a/zero.Core/Routing/Page/PageLinkProvider.cs b/zero.Core/Routing/Page/PageLinkProvider.cs index 3b07efc0..0e948b72 100644 --- a/zero.Core/Routing/Page/PageLinkProvider.cs +++ b/zero.Core/Routing/Page/PageLinkProvider.cs @@ -1,49 +1,65 @@ using Raven.Client.Documents.Session; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using zero.Core.Database; using zero.Core.Entities; +using zero.Core.Extensions; +using zero.Core.Options; namespace zero.Core.Routing { public class PageLinkProvider : ILinkProvider { - /// - public string Name { get; } = "@links.providers.page"; - - /// - public string Alias { get; } = "zero.pages"; - - protected IZeroStore Store { get; set; } - protected IRoutes Routes { get; set; } + protected IZeroOptions Options { get; set; } - - public PageLinkProvider(IZeroStore store, IRoutes routes) + public PageLinkProvider(IRoutes routes, IZeroOptions options) { - Store = store; Routes = routes; + Options = options; } /// - public async Task ResolveLink(ILink link) + public bool CanProcess(ILink link) => link.Area == "zero.pages"; + + + /// + public async Task Resolve(ILink link) { - if (!link.Values.TryGetValue("pageId", out object pageIdObj)) + return await Routes.GetUrl(link.Values.GetValueOrDefault("id")); + } + + + /// + public async Task Preview(IAsyncDocumentSession session, ILink link) + { + string id = link.Values.GetValueOrDefault("id"); + + if (id.IsNullOrEmpty()) { return null; } - string pageId = pageIdObj.ToString(); + IPage page = await session.LoadAsync(id); - using IAsyncDocumentSession session = Store.OpenAsyncSession(); - IPage page = await session.LoadAsync(pageId); - IRoute route = await Routes.GetRoute(page); + if (page == null) + { + return null; + } - return route.Url; + PageType pageType = Options.Pages.GetAllItems().FirstOrDefault(x => x.Alias == page.PageTypeAlias); + + string url = await Routes.GetUrl(page); + + return new() + { + Id = page.Id, + Icon = pageType?.Icon ?? "fth-folder", + Name = page.Name, + Text = url + }; } } } diff --git a/zero.Core/Routing/Routes.cs b/zero.Core/Routing/Routes.cs index d1f10273..e19aa5e6 100644 --- a/zero.Core/Routing/Routes.cs +++ b/zero.Core/Routing/Routes.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using zero.Core.Api; using zero.Core.Database; using zero.Core.Entities; using zero.Core.Extensions; @@ -39,10 +38,42 @@ namespace zero.Core.Routing public async Task GetUrl(T model) => (await GetRoute(model))?.Url; + /// + public async Task GetUrl(string id) where T : IZeroIdEntity => (await GetRoute(id))?.Url; + + /// public async Task> GetUrls(params T[] models) => (await GetRoutes(models)).ToDictionary(x => x.Key, x => x.Value?.Url); + /// + public async Task GetRoute(string id) where T : IZeroIdEntity + { + if (id.IsNullOrEmpty()) + { + return null; + } + + Type type = typeof(T); + IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.AffectedTypes.Any(t => t.IsAssignableFrom(type))); + + if (routeProvider == null) + { + return null; + } + + using IAsyncDocumentSession session = Store.OpenAsyncSession(); + T model = await session.LoadAsync(id); + + if (model == null) + { + return null; + } + + return await routeProvider.GetRoute(session, model); + } + + /// public async Task GetRoute(T model) { @@ -246,11 +277,21 @@ namespace zero.Core.Routing /// Task GetUrl(T model); + /// + /// Get the URL for an entity + /// + Task GetUrl(string id) where T : IZeroIdEntity; + /// /// Get the route object for an entity /// Task GetRoute(T model); + /// + /// Get the route object for an entity + /// + Task GetRoute(string id) where T : IZeroIdEntity; + /// /// Get URLs for multiple entities /// diff --git a/zero.Web.UI/App/api/links.js b/zero.Web.UI/App/api/links.js new file mode 100644 index 00000000..0c28d92c --- /dev/null +++ b/zero.Web.UI/App/api/links.js @@ -0,0 +1,7 @@ +import { get, post, del } from '../helpers/request.ts'; + +const base = 'links/'; + +export default { + getPreviews: async links => await post(base + 'getPreviews', links) +}; \ No newline at end of file diff --git a/zero.Web.UI/App/components/pickers/linkPicker/areas/pages.vue b/zero.Web.UI/App/components/pickers/linkPicker/areas/pages.vue index 9687623b..8f8c0ed1 100644 --- a/zero.Web.UI/App/components/pickers/linkPicker/areas/pages.vue +++ b/zero.Web.UI/App/components/pickers/linkPicker/areas/pages.vue @@ -4,7 +4,7 @@ - + @@ -29,9 +29,11 @@ }, data: () => ({ + selection: [], treeConfig: { parent: null, - active: null + active: null, + mode: 'select' }, search: null, debouncedSearch: null @@ -41,22 +43,40 @@ search() { this.debouncedSearch(); + }, + value() + { + this.selection = [this.value.values.id]; } }, mounted() { this.debouncedSearch = _debounce(() => this.$refs.tree.refresh(), 300); + this.selection = this.value.values.id ? [this.value.values.id] : []; }, methods: { - onSelect(item) + isValid() { - this.value.values = { - id: item.id - }; + return this.selection.length > 0; + }, + + onSelect(id) + { + if (id) + { + this.selection = [id]; + this.value.values = { id }; + } + else + { + this.selection = []; + this.value.values = { id: null }; + } this.$emit('change', this.value); + this.$emit('input', this.value); }, getTreeItems(parent) @@ -67,10 +87,6 @@ res.forEach(item => { - //if (item.id === this.model) - //{ - // item.isSelected = true; - //} item.hasActions = false; }); @@ -92,4 +108,33 @@ { background: var(--color-tree-selected); } + + .ui-linkpicker-area-pages-tree + { + .ui-tree-item.is-disabled + { + opacity: .5; + } + + .ui-tree-item.is-selected, .ui-tree-item:hover:not(.is-disabled) + { + background: var(--color-tree-selected); + } + + .ui-tree-item.is-selected + { + &:after + { + font-family: "Feather"; + content: "\e83e"; + font-size: 16px; + color: var(--color-primary); + } + + .ui-tree-item-text + { + font-weight: bold; + } + } + } \ No newline at end of file diff --git a/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue b/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue index 4a1d83a3..f4cce62c 100644 --- a/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue +++ b/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue @@ -1,12 +1,12 @@  @@ -14,6 +14,7 @@