feat: Page ordering

pull/7619/head
Ruslan Semak 8 months ago
parent 552e6136de
commit 60875ff2e2

@ -33,9 +33,9 @@
flat flat
hide-details hide-details
dense dense
label='Paths' label='Groups'
:items='paths' :items='groups'
v-model='selectedPath' v-model='selectedGroup'
style='max-width: 250px;' style='max-width: 250px;'
) )
v-spacer v-spacer
@ -65,30 +65,23 @@
:headers='headers' :headers='headers'
:search='search' :search='search'
:page.sync='pagination' :page.sync='pagination'
:items-per-page='50' :items-per-page='500'
:loading='loading' :loading='loading'
must-sort, must-sort,
sort-by='updatedAt', sort-by='orderPriority',
sort-desc, sort,
hide-default-footer hide-default-footer
@page-count="pageTotal = $event" @page-count="pageTotal = $event"
) )
template(slot='item', slot-scope='props') template(slot='item', slot-scope='props')
tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)') tr.is-clickable(:active='props.selected', @click='$router.push(`/pages/` + props.item.id)')
td.text-xs-right {{ props.item.id }} td.text-xs-right {{ props.item.id }}
td
.body-2: strong {{ props.item.title }}
.caption {{ props.item.description }}
td.admin-pages-path
v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
td {{ props.item.createdAt | moment('calendar') }}
td {{ props.item.updatedAt | moment('calendar') }}
td td
v-edit-dialog( v-edit-dialog(
:return-value.sync='props.item.orderPriority' :return-value.sync='props.item.orderPriority'
:disabled='!selectedGroup'
@open='startEdit(props.item)' @open='startEdit(props.item)'
@save='savePriority(props.item)' @save='saveEdit(props.item)'
@cancel='cancelEdit()' @cancel='cancelEdit()'
large large
persistent persistent
@ -102,9 +95,18 @@
single-line single-line
autofocus autofocus
:rules='[v => !!v || "Priority is required", v => v >= 0 || "Must be positive"]' :rules='[v => !!v || "Priority is required", v => v >= 0 || "Must be positive"]'
:disabled='!selectedGroup'
@keydown.enter='saveEdit(props.item)' @keydown.enter='saveEdit(props.item)'
@keydown.esc='cancelEdit' @keydown.esc='cancelEdit()'
) )
td
.body-2: strong {{ props.item.title }}
.caption {{ props.item.description }}
td.admin-pages-path
v-chip(label, small, :color='$vuetify.theme.dark ? `grey darken-4` : `grey lighten-4`') {{ props.item.locale }}
span.ml-2.grey--text(:class='$vuetify.theme.dark ? `text--lighten-1` : `text--darken-2`') / {{ props.item.path }}
td {{ props.item.createdAt | moment('calendar') }}
td {{ props.item.updatedAt | moment('calendar') }}
template(slot='no-data') template(slot='no-data')
v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display. v-alert.ma-3(icon='mdi-alert', :value='true', outlined) No pages to display.
.text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1') .text-center.py-2.animated.fadeInDown(v-if='this.pageTotal > 1')
@ -114,6 +116,7 @@
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import pagesQuery from 'gql/admin/pages/pages-query-list.gql' import pagesQuery from 'gql/admin/pages/pages-query-list.gql'
import updatePagePriorityMutation from 'gql/admin/pages/update-page-priority.gql'
export default { export default {
data() { data() {
@ -124,22 +127,22 @@ export default {
pageTotal: 0, pageTotal: 0,
headers: [ headers: [
{ text: 'ID', value: 'id', width: 80, sortable: true }, { text: 'ID', value: 'id', width: 80, sortable: true },
{ text: 'Order', value: 'orderPriority', width: 100 },
{ text: 'Title', value: 'title' }, { text: 'Title', value: 'title' },
{ text: 'Path', value: 'path' }, { text: 'Path', value: 'path' },
{ text: 'Created', value: 'createdAt', width: 250 }, { text: 'Created', value: 'createdAt', width: 250 },
{ text: 'Last Updated', value: 'updatedAt', width: 250 }, { text: 'Last Updated', value: 'updatedAt', width: 250 }
{ text: 'Order Priority', value: 'orderPriority', width: 150 }
], ],
search: '', search: '',
selectedLang: null, selectedLang: null,
selectedState: null, selectedState: null,
selectedPath: null, selectedGroup: null,
states: [ states: [
{ text: 'All Publishing States', value: null }, { text: 'All Publishing States', value: null },
{ text: 'Published', value: true }, { text: 'Published', value: true },
{ text: 'Not Published', value: false } { text: 'Not Published', value: false }
], ],
paths: [], groups: [],
editPriorityValue: null, editPriorityValue: null,
editingItem: null, editingItem: null,
originalPriorities: new Map(), originalPriorities: new Map(),
@ -149,7 +152,7 @@ export default {
computed: { computed: {
filteredPages () { filteredPages () {
return _.filter(this.pages, pg => { return _.filter(this.pages, pg => {
if (this.selectedPath !== null && !pg.path.startsWith(this.selectedPath)) { if (this.selectedGroup !== null && pg.group !== this.selectedGroup) {
return false return false
} }
if (this.selectedLang !== null && this.selectedLang !== pg.locale) { if (this.selectedLang !== null && this.selectedLang !== pg.locale) {
@ -179,7 +182,13 @@ export default {
}, },
saveEdit(item) { saveEdit(item) {
if (this.editingItem && this.editingItem.id === item.id) { if (!this.selectedGroup || !item.group) {
this.$store.commit('showNotification', {
message: 'You should select group',
style: 'error',
icon: 'error'
})
} else if (this.editingItem && this.editingItem.id === item.id) {
item.orderPriority = this.editPriorityValue item.orderPriority = this.editPriorityValue
this.savePriority(item) this.savePriority(item)
} }
@ -198,15 +207,14 @@ export default {
async savePriority(item) { async savePriority(item) {
try { try {
// ruslan: И на стороне сервера сделать чтобы в текущей папке всё пересчитывалось await this.$apollo.mutate({
// Только здесь делаем фактическое сохранение mutation: updatePagePriorityMutation,
// await this.$apollo.mutate({ variables: {
// mutation: updatePagePriorityMutation, id: item.id,
// variables: { orderPriority: item.orderPriority,
// id: item.id, group: item.group
// priority: item.orderPriority }
// } })
// })
this.$store.commit('showNotification', { this.$store.commit('showNotification', {
message: 'Priority updated successfully', message: 'Priority updated successfully',
@ -235,12 +243,12 @@ export default {
icon: 'cached' icon: 'cached'
}) })
}, },
updatePathSelector(pages) { updateGroupSelector(pages) {
const paths = Array.from(new Set(pages.filter(p => p.path.includes('/')).map(p => p.path.split('/')[0]))) const groups = Array.from(new Set(pages.filter(p => p.group).map(p => p.group)))
this.paths = [ this.groups = [
{ text: 'Select path', value: null }, { text: 'Select group', value: null },
...paths.sort().map(p => ({ text: p, value: p })) ...groups.sort().map(p => ({ text: p, value: p }))
] ]
}, },
newpage() { newpage() {
@ -254,11 +262,12 @@ export default {
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
update: function (data) { update: function (data) {
const pages = data.pages.list.map(p => { const pages = data.pages.list.map(p => {
p.orderPriority = Math.round(Math.random() * 100) p.group = p.path.includes('/') ? p.path.split('/')[0] : null
return p return p
}) })
this.updatePathSelector(pages) this.updateGroupSelector(pages)
return pages return pages
}, },

@ -9,6 +9,7 @@ query {
contentType contentType
isPublished isPublished
isPrivate isPrivate
orderPriority
privateNS privateNS
createdAt createdAt
updatedAt updatedAt

@ -9,6 +9,7 @@ query($id: Int!) {
isPrivate isPrivate
isPublished isPublished
privateNS privateNS
orderPriority
publishStartDate publishStartDate
publishEndDate publishEndDate
contentType contentType

@ -0,0 +1,17 @@
mutation (
$id: Int!
$orderPriority: Int!
$group: String!
) {
pages {
updatePriority(
id: $id
orderPriority: $orderPriority
group: $group
) {
responseResult {
succeeded
}
}
}
}

@ -0,0 +1,8 @@
exports.up = async knex => {
await knex.schema
.alterTable('pageTree', table => {
table.integer('orderPriority').unsigned().notNullable().defaultTo(0)
})
}
exports.down = knex => { }

@ -0,0 +1,8 @@
exports.up = async knex => {
await knex('pages')
.update({
orderPriority: knex.raw('(SELECT row_number FROM (SELECT id, row_number() OVER (ORDER BY title ASC) FROM pages) AS sorted WHERE sorted.id = pages.id)')
})
}
exports.down = knex => { }

@ -81,6 +81,7 @@ module.exports = {
'title', 'title',
'description', 'description',
'isPublished', 'isPublished',
'orderPriority',
'isPrivate', 'isPrivate',
'privateNS', 'privateNS',
'contentType', 'contentType',
@ -281,36 +282,36 @@ module.exports = {
builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors) builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors)
} }
} }
}) }).orderBy([{ column: 'isFolder', order: 'desc' }, 'orderPriority'])
// Ruslan: Custom sorting for "Tree Navigation" for folder "Users"
const emojiRegex = /^[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u
const russianRegex = /[\u0400-\u04FF]/
const userPrefix = 'Users/'
results.sort((a, b) => {
if (a.isFolder !== b.isFolder) {
return b.isFolder - a.isFolder
}
if (a.path.startsWith(userPrefix) || a.path.startsWith(userPrefix)) { // // Ruslan: Custom sorting for "Tree Navigation" for folder "Users"
// Emoji first // const emojiRegex = /^[\u{1F300}-\u{1F6FF}\u{1F900}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u
const aIsEmoji = emojiRegex.test(a.title) // const russianRegex = /[\u0400-\u04FF]/
const bIsEmoji = emojiRegex.test(b.title) //
// const userPrefix = 'Users/'
if (aIsEmoji && !bIsEmoji) return -1 // results.sort((a, b) => {
if (!aIsEmoji && bIsEmoji) return 1 // if (a.isFolder !== b.isFolder) {
// return b.isFolder - a.isFolder
// Russian second, only by first letter due to title must contain russian + english // }
const aIsRussian = russianRegex.test(a.title[0]) //
const bIsRussian = russianRegex.test(b.title[1]) // if (a.path.startsWith(userPrefix) || a.path.startsWith(userPrefix)) {
// // Emoji first
if (aIsRussian && !bIsRussian) return -1 // const aIsEmoji = emojiRegex.test(a.title)
if (!aIsRussian && bIsRussian) return 1 // const bIsEmoji = emojiRegex.test(b.title)
} //
// if (aIsEmoji && !bIsEmoji) return -1
return a.title.localeCompare(b.title) // if (!aIsEmoji && bIsEmoji) return 1
}) //
// // Russian second, only by first letter due to title must contain russian + english
// const aIsRussian = russianRegex.test(a.title[0])
// const bIsRussian = russianRegex.test(b.title[1])
//
// if (aIsRussian && !bIsRussian) return -1
// if (!aIsRussian && bIsRussian) return 1
// }
//
// return a.title.localeCompare(b.title)
// })
return results.filter(r => { return results.filter(r => {
return WIKI.auth.checkAccess(context.req.user, ['read:pages'], { return WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
@ -454,6 +455,25 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
/**
* UPDATE PAGE PRIORITY
*/
async updatePriority(obj, args, context) {
try {
const page = await WIKI.models.pages.updatePriority({
...args,
user: context.req.user
})
return {
responseResult: graphHelper.generateSuccess('Page has been updated.'),
page
}
} catch (err) {
return graphHelper.generateError(err)
}
},
/** /**
* CONVERT PAGE * CONVERT PAGE
*/ */

@ -117,6 +117,12 @@ type PageMutation {
title: String title: String
): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"]) ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
updatePriority(
id: Int!
orderPriority: Int!
group: String!
): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
convert( convert(
id: Int! id: Int!
editor: String! editor: String!
@ -194,6 +200,7 @@ type Page {
content: String! @auth(requires: ["read:source", "write:pages", "manage:system"]) content: String! @auth(requires: ["read:source", "write:pages", "manage:system"])
render: String render: String
toc: String toc: String
orderPriority: Int
contentType: String! contentType: String!
createdAt: Date! createdAt: Date!
updatedAt: Date! updatedAt: Date!
@ -275,6 +282,7 @@ type PageListItem {
title: String title: String
description: String description: String
contentType: String! contentType: String!
orderPriority: Int
isPublished: Boolean! isPublished: Boolean!
isPrivate: Boolean! isPrivate: Boolean!
privateNS: String privateNS: String

@ -10,7 +10,7 @@ module.exports = async (pageId) => {
await WIKI.configSvc.loadFromDb() await WIKI.configSvc.loadFromDb()
await WIKI.configSvc.applyFlags() await WIKI.configSvc.applyFlags()
const pages = await WIKI.models.pages.query().select('id', 'path', 'localeCode', 'title', 'isPrivate', 'privateNS').orderBy(['localeCode', 'path']) const pages = await WIKI.models.pages.query().select('id', 'path', 'localeCode', 'orderPriority', 'title', 'isPrivate', 'privateNS').orderBy(['localeCode', 'path'])
let tree = [] let tree = []
let pik = 0 let pik = 0
@ -32,6 +32,7 @@ module.exports = async (pageId) => {
pik++ pik++
tree.push({ tree.push({
id: pik, id: pik,
orderPriority: page.orderPriority,
localeCode: page.localeCode, localeCode: page.localeCode,
path: currentPath, path: currentPath,
depth: depth, depth: depth,

@ -47,6 +47,7 @@ module.exports = class Page extends Model {
publishEndDate: {type: 'string'}, publishEndDate: {type: 'string'},
content: {type: 'string'}, content: {type: 'string'},
contentType: {type: 'string'}, contentType: {type: 'string'},
orderPriority: {type: 'integer'},
createdAt: {type: 'string'}, createdAt: {type: 'string'},
updatedAt: {type: 'string'} updatedAt: {type: 'string'}
@ -145,6 +146,7 @@ module.exports = class Page extends Model {
description: 'string', description: 'string',
editorKey: 'string', editorKey: 'string',
isPrivate: 'boolean', isPrivate: 'boolean',
orderPriority: 'uint',
isPublished: 'boolean', isPublished: 'boolean',
publishEndDate: 'string', publishEndDate: 'string',
publishStartDate: 'string', publishStartDate: 'string',
@ -430,6 +432,7 @@ module.exports = class Page extends Model {
isPublished: opts.isPublished === true || opts.isPublished === 1, isPublished: opts.isPublished === true || opts.isPublished === 1,
publishEndDate: opts.publishEndDate || '', publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '', publishStartDate: opts.publishStartDate || '',
orderPriority: opts.orderPriority ?? ogPage.orderPriority,
title: opts.title, title: opts.title,
extra: JSON.stringify({ extra: JSON.stringify({
...ogPage.extra, ...ogPage.extra,
@ -488,6 +491,54 @@ module.exports = class Page extends Model {
return page return page
} }
/**
* Update an Priority of Existing Page
* @param {Object} opts Page Properties
* @returns {Promise} Promise of the Page Model Instance
*/
static async updatePriority(opts) {
// 1. Для текущего пути запрашиваем все страницы по порядку
// 2. Устанавливаем новый порядок
// 3. Обновляем все страницы и проставляем новый приоритет (только если он изменился)
// 4. rebuildTree()
// -> Fetch original page
const page = await WIKI.models.pages.query().findById(opts.id)
if (!page) {
throw new Error('Invalid Page Id')
}
// -> Check for page access
if (!WIKI.auth.checkAccess(opts.user, ['write:pages'], {
locale: page.localeCode,
path: page.path
})) {
throw new WIKI.Error.PageUpdateForbidden()
}
page.orderPriority = opts.orderPriority
// -> Original pages, sorted by orderPriority, without target (currently updating) page
const pages = await WIKI.models.pages.query()
.select('*')
.where('path', 'ilike', `${opts.group}%`)
.whereNot('id', page.id)
.orderBy('orderPriority', 'asc')
const insertIndex = pages.findIndex((p) => p.orderPriority >= opts.orderPriority)
pages.splice(insertIndex, 0, page)
const newPriorities = pages.map((p, idx) => ({id: p.id, orderPriority: idx + 1}))
for (const { id, orderPriority } of newPriorities) {
await WIKI.models.pages.query()
.where('id', id)
.patch({ orderPriority })
}
await WIKI.models.pages.rebuildTree()
return page
}
/** /**
* Convert an Existing Page * Convert an Existing Page
* *
@ -724,10 +775,6 @@ module.exports = class Page extends Model {
const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' }) const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale, privateNS: opts.isPrivate ? 'TODO' : '' })
/**
* RUSLAN: Here should be checking all exists pages for links for moving page.
*/
// -> Move page // -> Move page
const destinationTitle = (page.title === _.last(page.path.split('/')) ? _.last(opts.destinationPath.split('/')) : page.title) const destinationTitle = (page.title === _.last(page.path.split('/')) ? _.last(opts.destinationPath.split('/')) : page.title)
await WIKI.models.pages.query().patch({ await WIKI.models.pages.query().patch({
@ -988,6 +1035,7 @@ module.exports = class Page extends Model {
'pages.hash', 'pages.hash',
'pages.title', 'pages.title',
'pages.description', 'pages.description',
'pages.orderPriority',
'pages.isPrivate', 'pages.isPrivate',
'pages.isPublished', 'pages.isPublished',
'pages.privateNS', 'pages.privateNS',
@ -1023,23 +1071,6 @@ module.exports = class Page extends Model {
'pages.path': opts.path, 'pages.path': opts.path,
'pages.localeCode': opts.locale 'pages.localeCode': opts.locale
}) })
// .andWhere(builder => {
// if (queryModeID) return
// builder.where({
// 'pages.isPublished': true
// }).orWhere({
// 'pages.isPublished': false,
// 'pages.authorId': opts.userId
// })
// })
// .andWhere(builder => {
// if (queryModeID) return
// if (opts.isPrivate) {
// builder.where({ 'pages.isPrivate': true, 'pages.privateNS': opts.privateNS })
// } else {
// builder.where({ 'pages.isPrivate': false })
// }
// })
.first() .first()
} catch (err) { } catch (err) {
WIKI.logger.warn(err) WIKI.logger.warn(err)
@ -1063,6 +1094,7 @@ module.exports = class Page extends Model {
creatorId: page.creatorId, creatorId: page.creatorId,
creatorName: page.creatorName, creatorName: page.creatorName,
description: page.description, description: page.description,
orderPriority: page.orderPriority,
editorKey: page.editorKey, editorKey: page.editorKey,
extra: { extra: {
css: _.get(page, 'extra.css', ''), css: _.get(page, 'extra.css', ''),

Loading…
Cancel
Save