diff --git a/client/web/admin/src/components/Settings/Compose/CComposeEditorBasic.vue b/client/web/admin/src/components/Settings/Compose/CComposeEditorBasic.vue index 44723a106..2a7188f48 100644 --- a/client/web/admin/src/components/Settings/Compose/CComposeEditorBasic.vue +++ b/client/web/admin/src/components/Settings/Compose/CComposeEditorBasic.vue @@ -24,9 +24,7 @@ label-cols="2" class="mb-0" > - - - +
@@ -36,13 +34,11 @@ :label="$t('attachments.max-size')" label-cols="2" > - - - + - - - + + + +
+ +
{{ $t('attachments.icon') }}
+ + + + + @@ -131,6 +147,16 @@ export default { this.basic['compose.record.attachments.mimetypes'] = this.convertToExternal(value) }, }, + + iconAttachmentWhitelist: { + get () { + return (this.basic['compose.icon.attachments.mimetypes'] || []).join(',') + }, + + set (value) { + this.basic['compose.icon.attachments.mimetypes'] = this.convertToExternal(value) + }, + }, }, methods: { diff --git a/client/web/compose/src/components/Namespaces/NamespaceSidebar.vue b/client/web/compose/src/components/Namespaces/NamespaceSidebar.vue index 3173916a1..e452b67ca 100644 --- a/client/web/compose/src/components/Namespaces/NamespaceSidebar.vue +++ b/client/web/compose/src/components/Namespaces/NamespaceSidebar.vue @@ -103,36 +103,6 @@ import { Portal } from 'portal-vue' import { VueSelect } from 'vue-select' const { CSidebarNavItems, CInputSearch } = components -const publicPageWrap = ({ pageID, selfID, title, visible }) => ({ - page: { - // name omitted as default is provided - pageID, - selfID, - title, - visible, - }, - children: [], - params: { - pageID, - }, -}) - -const adminPageWrap = (page) => { - return { - page: { - name: 'admin.pages.builder', - pageID: `page-${page.pageID}`, - selfID: page.selfID !== NoID ? `page-${page.selfID}` : 'pages', - rootSelfID: 'pages', - title: page.title || page.handle, - visible: true, - }, - children: [], - params: { - pageID: page.pageID, - }, - } -} const moduleWrap = (module) => { return { page: { @@ -241,8 +211,7 @@ export default { filteredPages () { if (this.namespace) { // If on admin page, show admin pages. Otherwise show public pages - const pages = [...(this.isAdminPage ? this.adminRoutes() : this.publicRoutes.map(publicPageWrap))] - + const pages = [...(this.isAdminPage ? this.adminRoutes() : this.publicPageWrap(this.publicRoutes))] if (!this.query) { return pages } @@ -259,7 +228,7 @@ export default { navItems () { const current = this.filteredPages - const ax = this.pageIndex(this.isAdminPage ? this.adminRoutes() : this.pages.map(publicPageWrap)) + const ax = this.pageIndex(this.isAdminPage ? this.adminRoutes() : this.publicPageWrap(this.pages)) // Correct potentially missing parent references for (const cp of current) { @@ -374,7 +343,7 @@ export default { }, children: [], }, - ...this.pages.map(adminPageWrap), + ...this.adminPageWrap(this.pages), { page: { pageID: 'charts', @@ -388,6 +357,68 @@ export default { ...this.charts.map(chartWrap), ] }, + + publicPageWrap (pages) { + return pages.map(({ pageID, selfID, title, visible, config }) => { + const { navItem = {} } = config + const { icon = {}, expanded = '' } = navItem + const { type = '', src = '' } = icon + + const iconType = 'attachment' + let iconSrc = src + + if (type === iconType) { + iconSrc = `${this.$ComposeAPI.baseURL}${src}` + } + + return { + page: { + // name omitted as default is provided + pageID, + selfID, + title, + visible, + expanded, + icon: iconSrc, + }, + children: [], + params: { + pageID, + }, + } + }) + }, + + adminPageWrap (pages) { + return pages.map(({ pageID, selfID, title, handle, config }) => { + const { navItem = {} } = config + const { icon = {} } = navItem + const { type = '', src = '' } = icon + + const iconType = 'attachment' + let iconSrc = src + + if (type === iconType) { + iconSrc = `${this.$ComposeAPI.baseURL}${src}` + } + + return { + page: { + name: 'admin.pages.builder', + pageID: `page-${pageID}`, + selfID: selfID !== NoID ? `page-${selfID}` : 'pages', + rootSelfID: 'pages', + title: title || handle, + visible: true, + icon: iconSrc, + }, + children: [], + params: { + pageID: pageID, + }, + } + }) + }, }, } diff --git a/client/web/compose/src/components/Public/Page/Attachment/Uploader.vue b/client/web/compose/src/components/Public/Page/Attachment/Uploader.vue index 1084937a5..c7b882642 100644 --- a/client/web/compose/src/components/Public/Page/Attachment/Uploader.vue +++ b/client/web/compose/src/components/Public/Page/Attachment/Uploader.vue @@ -81,6 +81,10 @@ export default { required: false, default: () => ({}), }, + paramName: { + type: String, + default: 'upload', + }, }, data () { @@ -100,7 +104,7 @@ export default { const vm = this return { - paramName: 'upload', + paramName: this.paramName, maxFilesize: this.maxFilesize, // mb url: () => this.baseUrl + this.endpoint, thumbnailMethod: 'contain', diff --git a/client/web/compose/src/views/Admin/Pages/Edit.vue b/client/web/compose/src/views/Admin/Pages/Edit.vue index 94f9074fe..b8bfd0923 100644 --- a/client/web/compose/src/views/Admin/Pages/Edit.vue +++ b/client/web/compose/src/views/Admin/Pages/Edit.vue @@ -111,6 +111,33 @@ /> + + + + {{ $t('icon.not-set') }} + + + {{ $t('icon.set') }} + + + @@ -122,6 +149,118 @@ {{ $t('edit.visible') }} + + + + {{ $t('showSubPages') }} + + + + + + + + + + + + + + + + + + + + + + + @@ -171,6 +310,7 @@ import { mapGetters, mapActions } from 'vuex' import EditorToolbar from 'corteza-webapp-compose/src/components/Admin/EditorToolbar' import PageTranslator from 'corteza-webapp-compose/src/components/Admin/Page/PageTranslator' import pages from 'corteza-webapp-compose/src/mixins/pages' +import Uploader from 'corteza-webapp-compose/src/components/Public/Page/Attachment/Uploader' import { compose, NoID } from '@cortezaproject/corteza-js' import { handle } from '@cortezaproject/corteza-vue' @@ -184,6 +324,7 @@ export default { components: { EditorToolbar, PageTranslator, + Uploader, }, mixins: [ @@ -206,6 +347,13 @@ export default { return { modulesList: [], page: new compose.Page(), + showIconModal: false, + attachments: [], + selectedIconID: '', + isSelected: false, + linkUrl: '', + + processing: false, } }, @@ -250,6 +398,29 @@ export default { return this.hasChildren && this.page.canDeletePage && !this.page.deletedAt }, + endpoint () { + return this.$ComposeAPI.iconUploadEndpoint({ + namespaceID: this.namespace.namespaceID, + }) + }, + + icon: { + get () { + return this.page.config.navItem.icon || {} + }, + + set (icon) { + this.page.config.navItem = icon + }, + }, + + isIconSet () { + return !!this.attachments.find(a => a.attachmentID === this.selectedIconID) + }, + + pageIcon () { + return this.icon.type === 'link' ? this.icon.src : this.makeAttachmentUrl(this.icon.src) + }, }, watch: { @@ -257,8 +428,7 @@ export default { immediate: true, handler (pageID) { if (pageID) { - const { namespaceID } = this.namespace - this.findPageByID({ namespaceID, pageID }).then((page) => { + this.findPageByID({ namespaceID: this.namespace.namespaceID, pageID }).then((page) => { this.page = page.clone() }).catch(this.toastErrorHandler(this.$t('notification:page.loadFailed'))) } @@ -275,14 +445,14 @@ export default { loadPages: 'page/load', }), - handleSave ({ closeOnSuccess = false } = {}) { + async handleSave ({ closeOnSuccess = false } = {}) { /** * Pass a special tag alongside payload that * instructs store layer to add content-language header to the API request */ const resourceTranslationLanguage = this.currentLanguage - const { namespaceID } = this.namespace - this.updatePage({ namespaceID, ...this.page, resourceTranslationLanguage }).then((page) => { + await this.setIcon().then(icon => { this.page.config.navItem = { icon, expanded: this.page.config.navItem.expanded } }) + this.updatePage({ namespaceID: this.namespace.namespaceID, ...this.page, resourceTranslationLanguage }).then((page) => { this.page = page.clone() this.toastSuccess(this.$t('notification:page.saved')) if (closeOnSuccess) { @@ -296,6 +466,95 @@ export default { this.$router.push({ name: 'admin.pages' }) }).catch(this.toastErrorHandler(this.$t('notification:page.deleteFailed'))) }, + + uploadAttachment () { + this.fetchAttachments() + }, + + fetchAttachments () { + this.processing = true + + this.$ComposeAPI.iconList({ sort: 'id DESC' }) + .then(({ set: attachments = [] }) => { + const baseURL = this.$ComposeAPI.baseURL + this.attachments = [] + + if (attachments) { + attachments.forEach(a => { + const src = !a.url.includes(baseURL) ? this.makeAttachmentUrl(a.url) : a.url + const currSelectedIcon = a.url === this.icon.src + + if (currSelectedIcon) { + this.selectedIconID = a.attachmentID + } + + this.attachments.push({ ...a, src }) + }) + } + }) + .catch(this.toastErrorHandler(this.$t('notification:page.iconFetchFailed'))) + .finally(() => { + this.processing = false + }) + }, + + async setIcon () { + const selectedAttachmentUrl = this.isSelected ? this.attachments.find(att => att.attachmentID === this.selectedIconID).url : '' + + let attachmentType = 'link' + let attachmentSource = this.linkUrl + + if (!attachmentSource) { + attachmentType = 'attachment' + attachmentSource = selectedAttachmentUrl + } + + const navItem = { + icon: { + type: '', + src: '', + }, + } + + if (attachmentType && attachmentSource) { + await this.$ComposeAPI.pageUpdateIcon({ + namespaceID: this.namespace.namespaceID, + pageID: this.pageID, + type: attachmentType, + source: attachmentSource, + }).then(({ type, src }) => { + navItem.icon.type = type + navItem.icon.src = src + }) + } + + return !this.icon.src ? navItem.icon : this.icon + }, + + toggleSelectedIcon (attachmentID = '') { + if (!this.linkUrl) { + this.isSelected = this.selectedIconID !== attachmentID + this.selectedIconID = this.isSelected ? attachmentID : '' + } + }, + + closeIconModal () { + this.isSelected = this.icon.src + this.selectedIconID = this.isSelected ? this.isIconSet : '' + if (this.icon.type === 'link') { + this.linkUrl = this.icon.src + } + }, + + makeAttachmentUrl (src) { + return `${this.$ComposeAPI.baseURL}${src}` + }, + + openIconModal () { + this.linkUrl = this.icon.type === 'link' ? this.icon.src : '' + + this.fetchAttachments() + }, }, } diff --git a/lib/js/src/api-clients/compose.ts b/lib/js/src/api-clients/compose.ts index fbdb894b3..7f7acf3ac 100644 --- a/lib/js/src/api-clients/compose.ts +++ b/lib/js/src/api-clients/compose.ts @@ -919,6 +919,94 @@ export default class Compose { return `/namespace/${namespaceID}/page/${pageID}/translation` } + // Update icon for page + async pageUpdateIcon (a: KV, extra: AxiosRequestConfig = {}): Promise { + const { + namespaceID, + pageID, + type, + source, + style, + } = (a as KV) || {} + if (!namespaceID) { + throw Error('field namespaceID is empty') + } + if (!pageID) { + throw Error('field pageID is empty') + } + if (!type) { + throw Error('field type is empty') + } + const cfg: AxiosRequestConfig = { + ...extra, + method: 'patch', + url: this.pageUpdateIconEndpoint({ + namespaceID, pageID, + }), + } + cfg.data = { + type, + source, + style, + } + return this.api().request(cfg).then(result => stdResolve(result)) + } + + pageUpdateIconEndpoint (a: KV): string { + const { + namespaceID, + pageID, + } = a || {} + return `/namespace/${namespaceID}/page/${pageID}/icon` + } + + // List icons + async iconList (a: KV, extra: AxiosRequestConfig = {}): Promise { + const { + limit, + incTotal, + pageCursor, + sort, + } = (a as KV) || {} + const cfg: AxiosRequestConfig = { + ...extra, + method: 'get', + url: this.iconListEndpoint(), + } + cfg.params = { + limit, + incTotal, + pageCursor, + sort, + } + + return this.api().request(cfg).then(result => stdResolve(result)) + } + + iconListEndpoint (): string { + return '/icon/' + } + + // Upload icon + async iconUpload (a: KV, extra: AxiosRequestConfig = {}): Promise { + const { + icon, + } = (a as KV) || {} + const cfg: AxiosRequestConfig = { + ...extra, + method: 'post', + url: this.iconUploadEndpoint(), + } + cfg.data = { + icon, + } + return this.api().request(cfg).then(result => stdResolve(result)) + } + + iconUploadEndpoint (): string { + return '/icon/' + } + // List modules async moduleList (a: KV, extra: AxiosRequestConfig = {}): Promise { const { diff --git a/lib/js/src/compose/types/page.ts b/lib/js/src/compose/types/page.ts index 900d4d1e0..aaa279d9f 100644 --- a/lib/js/src/compose/types/page.ts +++ b/lib/js/src/compose/types/page.ts @@ -22,6 +22,14 @@ interface PageConfig { clone: Button; back: Button; }; + attachments: []; + navItem: { + icon: { + type: string; + src: string; + }; + expanded: false; + }; } export class Page { @@ -52,6 +60,14 @@ export class Page { clone: { enabled: true }, back: { enabled: true }, }, + attachments: [], + navItem: { + icon: { + type: '', + src: '', + }, + expanded: false, + }, } public createdAt?: Date = undefined; diff --git a/lib/vue/src/components/filePreview/common/base.vue b/lib/vue/src/components/filePreview/common/base.vue index 80623ed1d..f2557ef78 100644 --- a/lib/vue/src/components/filePreview/common/base.vue +++ b/lib/vue/src/components/filePreview/common/base.vue @@ -7,7 +7,6 @@ export default { props: { inline: { type: Boolean, - default: false, }, }, diff --git a/lib/vue/src/components/filePreview/common/types/IMG/InlineIMG.vue b/lib/vue/src/components/filePreview/common/types/IMG/InlineIMG.vue index be744a8b1..540b0c402 100644 --- a/lib/vue/src/components/filePreview/common/types/IMG/InlineIMG.vue +++ b/lib/vue/src/components/filePreview/common/types/IMG/InlineIMG.vue @@ -25,12 +25,11 @@ export default { props: { alt: { - required: false, type: String, default: null, }, + title: { - required: false, type: String, default: null, }, @@ -81,6 +80,7 @@ export default {