feat: Page ordering

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

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

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

@ -9,6 +9,7 @@ query($id: Int!) {
isPrivate
isPublished
privateNS
orderPriority
publishStartDate
publishEndDate
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',
'description',
'isPublished',
'orderPriority',
'isPrivate',
'privateNS',
'contentType',
@ -281,36 +282,36 @@ module.exports = {
builder.orWhereIn('id', _.isString(curPage.ancestors) ? JSON.parse(curPage.ancestors) : curPage.ancestors)
}
}
})
// 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
}
}).orderBy([{ column: 'isFolder', order: 'desc' }, 'orderPriority'])
if (a.path.startsWith(userPrefix) || a.path.startsWith(userPrefix)) {
// Emoji first
const aIsEmoji = emojiRegex.test(a.title)
const bIsEmoji = emojiRegex.test(b.title)
if (aIsEmoji && !bIsEmoji) return -1
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)
})
// // 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)) {
// // Emoji first
// const aIsEmoji = emojiRegex.test(a.title)
// const bIsEmoji = emojiRegex.test(b.title)
//
// if (aIsEmoji && !bIsEmoji) return -1
// 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 WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
@ -454,6 +455,25 @@ module.exports = {
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
*/

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

@ -10,7 +10,7 @@ module.exports = async (pageId) => {
await WIKI.configSvc.loadFromDb()
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 pik = 0
@ -32,6 +32,7 @@ module.exports = async (pageId) => {
pik++
tree.push({
id: pik,
orderPriority: page.orderPriority,
localeCode: page.localeCode,
path: currentPath,
depth: depth,

@ -47,6 +47,7 @@ module.exports = class Page extends Model {
publishEndDate: {type: 'string'},
content: {type: 'string'},
contentType: {type: 'string'},
orderPriority: {type: 'integer'},
createdAt: {type: 'string'},
updatedAt: {type: 'string'}
@ -145,6 +146,7 @@ module.exports = class Page extends Model {
description: 'string',
editorKey: 'string',
isPrivate: 'boolean',
orderPriority: 'uint',
isPublished: 'boolean',
publishEndDate: 'string',
publishStartDate: 'string',
@ -430,6 +432,7 @@ module.exports = class Page extends Model {
isPublished: opts.isPublished === true || opts.isPublished === 1,
publishEndDate: opts.publishEndDate || '',
publishStartDate: opts.publishStartDate || '',
orderPriority: opts.orderPriority ?? ogPage.orderPriority,
title: opts.title,
extra: JSON.stringify({
...ogPage.extra,
@ -488,6 +491,54 @@ module.exports = class Page extends Model {
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
*
@ -724,10 +775,6 @@ module.exports = class Page extends Model {
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
const destinationTitle = (page.title === _.last(page.path.split('/')) ? _.last(opts.destinationPath.split('/')) : page.title)
await WIKI.models.pages.query().patch({
@ -988,6 +1035,7 @@ module.exports = class Page extends Model {
'pages.hash',
'pages.title',
'pages.description',
'pages.orderPriority',
'pages.isPrivate',
'pages.isPublished',
'pages.privateNS',
@ -1023,23 +1071,6 @@ module.exports = class Page extends Model {
'pages.path': opts.path,
'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()
} catch (err) {
WIKI.logger.warn(err)
@ -1063,6 +1094,7 @@ module.exports = class Page extends Model {
creatorId: page.creatorId,
creatorName: page.creatorName,
description: page.description,
orderPriority: page.orderPriority,
editorKey: page.editorKey,
extra: {
css: _.get(page, 'extra.css', ''),

Loading…
Cancel
Save