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 {