diff --git a/client/components/admin.vue b/client/components/admin.vue index 8691a1e7..b5ebcee3 100644 --- a/client/components/admin.vue +++ b/client/components/admin.vue @@ -152,12 +152,13 @@ const router = new VueRouter({ { path: '/locale', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-locale.vue') }, { path: '/navigation', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-navigation.vue') }, { path: '/pages', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages.vue') }, - { path: '/pages/:id', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-edit.vue') }, + { path: '/pages/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-edit.vue') }, + { path: '/pages/visualize', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-pages-visualize.vue') }, { path: '/theme', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-theme.vue') }, { path: '/groups', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups.vue') }, - { path: '/groups/:id', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups-edit.vue') }, + { path: '/groups/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-groups-edit.vue') }, { path: '/users', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users.vue') }, - { path: '/users/:id', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users-edit.vue') }, + { path: '/users/:id(\\d+)', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-users-edit.vue') }, { path: '/analytics', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-analytics.vue') }, { path: '/auth', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-auth.vue') }, { path: '/rendering', component: () => import(/* webpackChunkName: "admin" */ './admin/admin-rendering.vue') }, diff --git a/client/components/admin/admin-pages-visualize.vue b/client/components/admin/admin-pages-visualize.vue new file mode 100644 index 00000000..568ef91f --- /dev/null +++ b/client/components/admin/admin-pages-visualize.vue @@ -0,0 +1,358 @@ + + + + + diff --git a/client/components/admin/admin-pages.vue b/client/components/admin/admin-pages.vue index ab21ba69..66ab328b 100644 --- a/client/components/admin/admin-pages.vue +++ b/client/components/admin/admin-pages.vue @@ -13,9 +13,9 @@ v-btn.animated.fadeInDown.mx-3(color='primary', outlined, large, @click='recyclebin', disabled) v-icon(left) mdi-delete-outline span Recycle Bin - v-btn.animated.fadeInDown(color='primary', depressed, large, @click='newpage', disabled) - v-icon(left) mdi-plus - span New Page + v-btn.animated.fadeInDown(color='primary', depressed, large, to='pages/visualize') + v-icon(left) mdi-graph + span Visualize v-card.wiki-form.mt-3.animated.fadeInUp v-toolbar(flat, :color='$vuetify.theme.dark ? `grey darken-3-d5` : `grey lighten-5`', height='80') v-spacer diff --git a/client/components/editor/editor-modal-properties.vue b/client/components/editor/editor-modal-properties.vue index eb399112..53a2b259 100644 --- a/client/components/editor/editor-modal-properties.vue +++ b/client/components/editor/editor-modal-properties.vue @@ -20,6 +20,7 @@ v-tabs(color='white', background-color='blue darken-1', dark, centered) v-tab {{$t('editor:props.info')}} v-tab {{$t('editor:props.scheduling')}} + v-tab(disabled) {{$t('editor:props.scripts')}} v-tab {{$t('editor:props.social')}} v-tab-item v-card-text.pt-5 @@ -177,6 +178,25 @@ @click='$refs.menuPublishEnd.save(publishEndDate)' ) {{$t('common:actions.ok')}} + v-tab-item + v-card-text + .overline.pb-3 {{$t('editor:props.js')}} + v-textarea( + outlined + rows='5' + :hint='$t(`editor:props.jsHint`)' + persistent-hint + ) + v-divider + v-card-text.grey.pt-5(:class='darkMode ? `darken-3-d3` : `lighten-5`') + .overline.pb-3 {{$t('editor:props.css')}} + v-textarea( + outlined + rows='5' + :hint='$t(`editor:props.cssHint`)' + persistent-hint + ) + v-tab-item v-card-text .overline.pb-5 {{$t('editor:props.socialFeatures')}} #[v-chip.ml-3(label, color='grey', small, outlined).white--text coming soon] diff --git a/package.json b/package.json index 4c39004c..c4ce2a63 100644 --- a/package.json +++ b/package.json @@ -215,6 +215,7 @@ "core-js": "3.6.1", "css-loader": "3.4.0", "cssnano": "4.1.10", + "d3": "5.15.0", "duplicate-package-checker-webpack-plugin": "3.0.0", "epic-spinners": "1.1.0", "eslint": "6.8.0", diff --git a/server/graph/resolvers/page.js b/server/graph/resolvers/page.js index 5c8ee704..6568e7ee 100644 --- a/server/graph/resolvers/page.js +++ b/server/graph/resolvers/page.js @@ -137,8 +137,12 @@ module.exports = { async tree (obj, args, context, info) { let results = [] let conds = { - localeCode: args.locale, - parent: (args.parent < 1) ? null : args.parent + localeCode: args.locale + } + if (args.parent) { + conds.parent = (args.parent < 1) ? null : args.parent + } else if (args.path) { + // conds.parent = (args.parent < 1) ? null : args.parent } switch (args.mode) { case 'FOLDERS': @@ -162,6 +166,44 @@ module.exports = { parent: r.parent || 0, locale: r.localeCode })) + }, + /** + * FETCH PAGE LINKS + */ + async links (obj, args, context, info) { + let results = [] + + results = await WIKI.models.knex('pages') + .column({ id: 'pages.id' }, { path: 'pages.path' }, 'title', { link: 'pageLinks.path' }, { locale: 'pageLinks.localeCode' }) + .fullOuterJoin('pageLinks', 'pages.id', 'pageLinks.pageId') + .where({ + 'pages.localeCode': args.locale + }) + + return _.reduce(results, (result, val) => { + // -> Check if user has access to source and linked page + if ( + !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.path, locale: args.locale }) || + !WIKI.auth.checkAccess(context.req.user, ['read:pages'], { path: val.link, locale: val.locale }) + ) { + return result + } + + const existingEntry = _.findIndex(result, ['id', val.id]) + if (existingEntry >= 0) { + if (val.link) { + result[existingEntry].links.push(`${val.locale}/${val.link}`) + } + } else { + result.push({ + id: val.id, + title: val.title, + path: `${args.locale}/${val.path}`, + links: val.link ? [`${val.locale}/${val.link}`] : [] + }) + } + return result + }, []) } }, PageMutation: { diff --git a/server/graph/schemas/page.graphql b/server/graph/schemas/page.graphql index 9640633f..fe757e6a 100644 --- a/server/graph/schemas/page.graphql +++ b/server/graph/schemas/page.graphql @@ -42,10 +42,16 @@ type PageQuery { tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"]) tree( - parent: Int! + path: String + parent: Int mode: PageTreeMode! locale: String! + includeParents: Boolean ): [PageTreeItem] @auth(requires: ["manage:system", "read:pages"]) + + links( + locale: String! + ): [PageLinkItem] @auth(requires: ["manage:system", "read:pages"]) } # ----------------------------------------------- @@ -209,6 +215,13 @@ type PageTreeItem { locale: String! } +type PageLinkItem { + id: Int! + path: String! + title: String! + links: [String]! +} + enum PageOrderBy { CREATED ID diff --git a/server/helpers/page.js b/server/helpers/page.js index eb941527..a677ad98 100644 --- a/server/helpers/page.js +++ b/server/helpers/page.js @@ -4,7 +4,7 @@ const crypto = require('crypto') const path = require('path') const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i -const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?)\/?(.*)/i +const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i const contentToExt = { markdown: 'md', @@ -125,7 +125,7 @@ module.exports = { const result = localeFolderRegex.exec(meta.path) if (result[1]) { meta = { - locale: result[1], + locale: result[1].replace('/', ''), path: result[2] } } diff --git a/server/modules/storage/disk/common.js b/server/modules/storage/disk/common.js index 8b28f546..9ed0b986 100644 --- a/server/modules/storage/disk/common.js +++ b/server/modules/storage/disk/common.js @@ -72,7 +72,8 @@ module.exports = { }, async processPage ({ user, fullPath, relPath, contentType, moduleName }) { - const contentPath = pageHelper.getPagePath(relPath) + const normalizedRelPath = relPath.replace(/\\/g, '/') + const contentPath = pageHelper.getPagePath(normalizedRelPath) const itemContents = await fs.readFile(path.join(fullPath, relPath), 'utf8') const pageData = WIKI.models.pages.parseMetadata(itemContents, contentType) const currentPage = await WIKI.models.pages.getPageFromDb({ @@ -82,7 +83,7 @@ module.exports = { const newTags = !_.isNil(pageData.tags) ? _.get(pageData, 'tags', '').split(', ') : false if (currentPage) { // Already in the DB, can mark as modified - WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as modified: ${relPath}`) + WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as modified: ${normalizedRelPath}`) await WIKI.models.pages.updatePage({ id: currentPage.id, title: _.get(pageData, 'title', currentPage.title), @@ -96,7 +97,7 @@ module.exports = { }) } else { // Not in the DB, can mark as new - WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as new: ${relPath}`) + WIKI.logger.info(`(STORAGE/${moduleName}) Page marked as new: ${normalizedRelPath}`) const pageEditor = await WIKI.models.editors.getDefaultEditor(contentType) await WIKI.models.pages.createPage({ path: contentPath.path, diff --git a/yarn.lock b/yarn.lock index 24d5de07..e7070238 100644 Binary files a/yarn.lock and b/yarn.lock differ