diff --git a/zero.Core/Routing/ILinkListProvider.cs b/zero.Core/Routing/ILinkListProvider.cs new file mode 100644 index 00000000..33c582ab --- /dev/null +++ b/zero.Core/Routing/ILinkListProvider.cs @@ -0,0 +1,18 @@ +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/ILinkTreeProvider.cs b/zero.Core/Routing/ILinkTreeProvider.cs new file mode 100644 index 00000000..7130c45e --- /dev/null +++ b/zero.Core/Routing/ILinkTreeProvider.cs @@ -0,0 +1,18 @@ +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.Web.UI/App/components/forms/select.vue b/zero.Web.UI/App/components/forms/select.vue index 331f7482..0f04195c 100644 --- a/zero.Web.UI/App/components/forms/select.vue +++ b/zero.Web.UI/App/components/forms/select.vue @@ -15,10 +15,7 @@ props: { value: [String, Number, Object], items: [Array, Function], - entity: { - type: Object, - required: true - }, + entity: Object, disabled: Boolean, emptyOption: { type: Boolean, @@ -57,22 +54,18 @@ methods: { rebuild() { - let items = []; - - if (!this.entity || !this.items) - { - this.options = items; - return; - } - - if (typeof this.items === 'function') + if (this.entity && typeof this.items === 'function') { this.options = this.items(this.entity); } - else + else if (typeof this.items !== 'function' && this.items) { this.options = [...this.items]; } + else + { + this.options = []; + } }, onChange(ev) diff --git a/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue b/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue index 9c0c2277..46c0a9a2 100644 --- a/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue +++ b/zero.Web.UI/App/components/pickers/linkPicker/linkpicker.vue @@ -18,7 +18,7 @@ import { extend as _extend, isArray as _isArray, isEmpty as _isEmpty, clone as _clone } from 'underscore'; export default { - name: 'uiPagepicker', + name: 'uiLinkpicker', props: { value: { @@ -29,6 +29,26 @@ type: Number, default: 1 }, + title: { + type: Boolean, + default: true + }, + label: { + type: Boolean, + default: false + }, + target: { + type: Boolean, + default: true + }, + suffix: { + type: Boolean, + default: false + }, + areas: { + type: Array, + default: () => [] + }, disabled: { type: Boolean, default: false @@ -121,7 +141,15 @@ closeLabel: '@ui.close', component: LinkpickerOverlay, display: 'editor', - model: this.multiple ? id : this.value + model: this.multiple ? id : this.value, + options: { + limit: this.limit, + label: this.label, + title: this.title, + target: this.target, + suffix: this.suffix, + areas: this.areas + } }, typeof this.options === 'object' ? this.options : {}); return Overlay.open(options).then(value => diff --git a/zero.Web.UI/App/components/pickers/linkPicker/overlay.vue b/zero.Web.UI/App/components/pickers/linkPicker/overlay.vue index ac44584e..995385d3 100644 --- a/zero.Web.UI/App/components/pickers/linkPicker/overlay.vue +++ b/zero.Web.UI/App/components/pickers/linkPicker/overlay.vue @@ -1,15 +1,45 @@  @@ -25,34 +55,100 @@ }, data: () => ({ - opened: false + opened: false, + current: null, + area: null, + areas: [], + areaItems: [], + treeConfig: { + parent: null, + active: null, + onSelect: (ev) => { console.info(ev); } + }, + link: null, + template: { + area: null, + target: 'default', + urlSuffix: null, + label: null, + title: null, + values: {} + }, + showOptions: false }), - computed: { - disabledIds() + + watch: { + current() { - return this.config.disabledIds || []; + this.reloadSelector(); } }, mounted() { + this.areas = this.zero.config.linkPicker.areas; + this.areaItems = this.areas.map(x => + { + return { + key: x.alias, + value: x.name + }; + }); + this.area = this.areas[0]; + this.current = this.area.alias; + + this.link = JSON.parse(JSON.stringify(this.template)); + this.link.area = this.current; + setTimeout(() => this.opened = true, 300); }, methods: { + reloadSelector() + { + this.area = this.areas.find(x => x.alias === this.current); + + if (!this.opened) + { + return; + } + if (this.area.display === 'tree' && this.$refs.tree) + { + this.$refs.tree.refresh(); + } + else if (this.area.display === 'list') + { + + } + else if (this.area.display === 'media') + { + + } + else + { + // custom + } + }, + onSelect(item) { this.config.confirm(item); }, - // get tree items - getItems(parent) + // get list items + getListItems(search) { - return PageTreeApi.getChildren(parent, this.model).then(res => + + }, + + // get tree items + getTreeItems(parent) + { + return PageTreeApi.getChildren(parent, null).then(res => { res = res.filter(x => x.id !== 'recyclebin'); @@ -62,55 +158,73 @@ { item.isSelected = true; } - if (this.disabledIds.indexOf(item.id) > -1) - { - item.disabled = true; - } item.hasActions = false; }); return res; }); + }, + + + onTargetChange(ev) + { + this.link.target = ev ? 'blank' : 'default'; } } } \ No newline at end of file diff --git a/zero.Web.UI/App/components/tabs/tabs.vue b/zero.Web.UI/App/components/tabs/tabs.vue index 12c2c6f8..41b6c6c7 100644 --- a/zero.Web.UI/App/components/tabs/tabs.vue +++ b/zero.Web.UI/App/components/tabs/tabs.vue @@ -149,7 +149,7 @@ color: var(--color-text); background: var(--color-box); box-shadow: var(--shadow-short); - border-top: 3px solid var(--color-primary); + //border-top: 3px solid var(--color-primary); .ui-tabs-list-item-count { diff --git a/zero.Web.UI/App/components/tree/tree.vue b/zero.Web.UI/App/components/tree/tree.vue index 6accfca6..b46f2ad9 100644 --- a/zero.Web.UI/App/components/tree/tree.vue +++ b/zero.Web.UI/App/components/tree/tree.vue @@ -98,6 +98,7 @@ }) .catch(error => { + console.error(error); this.setStatus('error', this.items, error); // TODO handle errors }); diff --git a/zero.Web.UI/App/core/link-provider.js b/zero.Web.UI/App/core/link-provider.js new file mode 100644 index 00000000..118407e4 --- /dev/null +++ b/zero.Web.UI/App/core/link-provider.js @@ -0,0 +1,32 @@ +//import MediaApi from 'zero/api/media.js'; +//import Strings from 'zero/helpers/strings.js'; +//import Localization from 'zero/helpers/localization.js'; +//class LinkProvider +//{ +// key; +// label; +// icon; +// autoclose = true; +// action = () => +// { +// console.warn(`[zero] A list action needs a "action" callback`); +// }; +// constructor(alias: string, name: string, type: string) +// { +// this.key = key; +// this.label = label; +// this.icon = icon; +// this.action = action; +// } +// /** +// * Calls the action +// * @returns {ListColumn} +// */ +// call(options) +// { +// console.info(options); +// this.action(options); +// } +//} +//export default ListAction; +//# sourceMappingURL=link-provider.js.map \ No newline at end of file diff --git a/zero.Web.UI/App/core/link-provider.js.map b/zero.Web.UI/App/core/link-provider.js.map new file mode 100644 index 00000000..af4749f5 --- /dev/null +++ b/zero.Web.UI/App/core/link-provider.js.map @@ -0,0 +1 @@ +{"version":3,"file":"link-provider.js","sourceRoot":"","sources":["link-provider.ts"],"names":[],"mappings":"AACA,2CAA2C;AAC3C,gDAAgD;AAChD,0DAA0D;AAE1D,oBAAoB;AACpB,GAAG;AACH,QAAQ;AACR,UAAU;AACV,SAAS;AACT,qBAAqB;AACrB,kBAAkB;AAClB,KAAK;AACL,qEAAqE;AACrE,MAAM;AAEN,0DAA0D;AAC1D,KAAK;AACL,qBAAqB;AACrB,yBAAyB;AACzB,uBAAuB;AACvB,2BAA2B;AAC3B,KAAK;AAEL,OAAO;AACP,uBAAuB;AACvB,4BAA4B;AAC5B,OAAO;AACP,iBAAiB;AACjB,KAAK;AACL,4BAA4B;AAC5B,2BAA2B;AAC3B,KAAK;AACL,GAAG;AAGH,4BAA4B"} \ No newline at end of file diff --git a/zero.Web.UI/App/core/link-provider.ts b/zero.Web.UI/App/core/link-provider.ts new file mode 100644 index 00000000..0ddde1e9 --- /dev/null +++ b/zero.Web.UI/App/core/link-provider.ts @@ -0,0 +1,37 @@ + +//import MediaApi from 'zero/api/media.js'; +//import Strings from 'zero/helpers/strings.js'; +//import Localization from 'zero/helpers/localization.js'; + +//class LinkProvider +//{ +// key; +// label; +// icon; +// autoclose = true; +// action = () => +// { +// console.warn(`[zero] A list action needs a "action" callback`); +// }; + +// constructor(alias: string, name: string, type: string) +// { +// this.key = key; +// this.label = label; +// this.icon = icon; +// this.action = action; +// } + +// /** +// * Calls the action +// * @returns {ListColumn} +// */ +// call(options) +// { +// console.info(options); +// this.action(options); +// } +//} + + +//export default ListAction; \ No newline at end of file diff --git a/zero.Web.UI/App/core/options.js b/zero.Web.UI/App/core/options.js index 46df02cc..f76c148f 100644 --- a/zero.Web.UI/App/core/options.js +++ b/zero.Web.UI/App/core/options.js @@ -12,5 +12,20 @@ export default { images_natural: ['.jpg', '.jpeg', '.webp'], images_artificial: ['.png', '.webp', '.svg'] } + }, + + linkPicker: { + areas: [ + { + alias: 'zero.pages', + name: '@zero.config.linkareas.pages', + display: 'tree' + }, + { + alias: 'zero.media', + name: '@zero.config.linkareas.media', + display: 'media' + } + ] } } \ No newline at end of file diff --git a/zero.Web.UI/App/editor/fields/linkpicker.vue b/zero.Web.UI/App/editor/fields/linkpicker.vue index 9549ae43..390a2db9 100644 --- a/zero.Web.UI/App/editor/fields/linkpicker.vue +++ b/zero.Web.UI/App/editor/fields/linkpicker.vue @@ -1,5 +1,5 @@  @@ -18,6 +18,26 @@ type: Number, default: 1 }, + title: { + type: Boolean, + default: true + }, + label: { + type: Boolean, + default: false + }, + target: { + type: Boolean, + default: true + }, + suffix: { + type: Boolean, + default: false + }, + areas: { + type: Array, + default: () => [] + }, config: Object }, } diff --git a/zero.Web.UI/Sass/Settings/_dimensions.scss b/zero.Web.UI/Sass/Settings/_dimensions.scss index 333d2436..52db0b94 100644 --- a/zero.Web.UI/Sass/Settings/_dimensions.scss +++ b/zero.Web.UI/Sass/Settings/_dimensions.scss @@ -7,5 +7,5 @@ --padding-l: 64px; --height-top: 74px; - --radius: 3px; + --radius: 4px; } \ No newline at end of file diff --git a/zero.Web.UI/app/core/editor-field.ts b/zero.Web.UI/app/core/editor-field.ts index bc012a56..9399af7a 100644 --- a/zero.Web.UI/app/core/editor-field.ts +++ b/zero.Web.UI/app/core/editor-field.ts @@ -411,6 +411,11 @@ class EditorField * Render a link picker * @param {object} [options] - Custom options * @param {number} [options.limit=1] - Limit of selection + * @param {boolean} [options.title=true] - Allow input of custom link title + * @param {boolean} [options.target=true] - Allow selection of the link target + * @param {boolean} [options.label=false] - Allow input of a custom label for button generation + * @param {boolean} [options.suffix=false] - Allow input of custom link URL suffix (query or hash) + * @param {string[]} [options.areas] - Limit link areas to the specified values (built-in are zero.pages, zero.media and zero.url) * @returns {EditorField} */ linkPicker(options) diff --git a/zero.Web.UI/app/core/plugin.ts b/zero.Web.UI/app/core/plugin.ts index ed87246c..bc642dd9 100644 --- a/zero.Web.UI/app/core/plugin.ts +++ b/zero.Web.UI/app/core/plugin.ts @@ -7,6 +7,7 @@ class Plugin routes = []; editors = []; lists = []; + linkAreas = []; constructor(name) @@ -54,9 +55,9 @@ class Plugin /* * Add a new vue list renderer to the global configuration */ - addList(config) + addList(list) { - this.lists.push(config); + this.lists.push(list); } @@ -87,6 +88,15 @@ class Plugin { routes.forEach(route => this.addRoute(route)); } + + + /* + * Add a new link area + */ + addLinkArea(area) + { + this.linkAreas.push(area); + } }; export default Plugin; \ No newline at end of file diff --git a/zero.Web.UI/app/core/zero.ts b/zero.Web.UI/app/core/zero.ts index c8fc1134..9db734c0 100644 --- a/zero.Web.UI/app/core/zero.ts +++ b/zero.Web.UI/app/core/zero.ts @@ -128,6 +128,8 @@ class Zero // append lists plugin.lists.forEach(x => this.addOrReplace(this.#lists, x, 'alias')); + // append + console.log(`[zero] Installed %c${plugin.name}%cplugin`, 'font-style:italic;'); } diff --git a/zero.Web/Controllers/LinkPickerController.cs b/zero.Web/Controllers/LinkPickerController.cs new file mode 100644 index 00000000..df55dc0e --- /dev/null +++ b/zero.Web/Controllers/LinkPickerController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using zero.Core.Api; +using zero.Core.Entities; + +namespace zero.Web.Controllers +{ + public class LinkPickerController : BackofficeController + { + IPageTreeApi Api; + IPagesApi PagesApi; + + public LinkPickerController(IPageTreeApi api, IPagesApi pagesApi) + { + Api = api; + PagesApi = pagesApi; + } + + + public async Task> GetChildren([FromQuery] string areaAlias, [FromQuery] string parent = null, [FromQuery] string active = null) + { + return await Api.GetChildren(parent, active); + } + + //public async Task> GetPreviews([FromQuery] List ids) + //{ + // return Previews(await PagesApi.GetByIds(ids.ToArray()), PreviewTransform); + //} + } +} diff --git a/zero.Web/Resources/Localization/zero.en-us.json b/zero.Web/Resources/Localization/zero.en-us.json index bfb92d46..11dcaacb 100644 --- a/zero.Web/Resources/Localization/zero.en-us.json +++ b/zero.Web/Resources/Localization/zero.en-us.json @@ -1,6 +1,13 @@ { "zero": { - "name": "zero" + "name": "zero", + + "config": { + "linkareas": { + "pages": "Pages", + "media": "Media" + } + } }, "ui": {