3
0

Add navigation for previous and next page on the record page

This commit is contained in:
Kelani Tolulope
2023-01-31 10:11:22 +01:00
parent 5f216c9adb
commit 31572e33dc
10 changed files with 395 additions and 108 deletions

View File

@@ -5,11 +5,12 @@
class="bg-white p-3"
>
<b-row
align-v="stretch"
no-gutters
class="wrap-with-vertical-gutters align-items-center"
class="wrap-with-vertical-gutters"
>
<div
class="wrap-with-vertical-gutters align-items-center"
<b-col
class="d-flex align-items-center justify-content-start"
>
<b-button
v-if="!settings.hideBack"
@@ -25,112 +26,149 @@
/>
{{ showRecordModal ? $t('label.close') : labels.back || $t('label.back') }}
</b-button>
</div>
</b-col>
<div
v-if="module"
class="d-flex wrap-with-vertical-gutters align-items-center ml-auto"
<b-col
class="d-flex align-items-center justify-content-center"
>
<c-input-confirm
v-if="(isCreated && !settings.hideDelete && !isDeleted)"
:disabled="!canDeleteRecord"
size="lg"
size-confirm="lg"
variant="danger"
:borderless="false"
@confirmed="$emit('delete')"
>
<b-spinner
v-if="processingDelete"
small
type="grow"
/>
<span v-else>
{{ labels.delete || $t('label.delete') }}
</span>
</c-input-confirm>
<c-input-confirm
v-if="isDeleted"
:disabled="!canUndeleteRecord"
size="lg"
size-confirm="lg"
variant="warning"
variant-ok="warning"
:borderless="false"
@confirmed="$emit('undelete')"
>
<b-spinner
v-if="processingUndelete"
small
type="grow"
/>
<span v-else>
{{ $t('label.restore') }}
</span>
</c-input-confirm>
<b-button
v-if="!inEditing && module.canCreateRecord && !hideClone && isCreated && !settings.hideClone"
data-test-id="button-clone"
variant="light"
size="lg"
:disabled="!record || processing"
class="ml-2"
@click.prevent="$emit('clone')"
>
{{ labels.clone || $t('label.clone') }}
</b-button>
<b-button
v-if="!inEditing && !settings.hideEdit && isCreated"
data-test-id="button-edit"
:disabled="!record.canUpdateRecord || processing"
variant="light"
size="lg"
class="ml-2"
@click.prevent="$emit('edit')"
>
{{ labels.edit || $t('label.edit') }}
</b-button>
<b-button
v-if="module.canCreateRecord && !hideAdd && !inEditing && !settings.hideNew"
data-test-id="button-add-new"
variant="primary"
size="lg"
:disabled="processing"
class="ml-2"
@click.prevent="$emit('add')"
>
{{ labels.new || $t('label.addNew') }}
</b-button>
<b-button
v-if="inEditing && !settings.hideSubmit"
data-test-id="button-submit"
:disabled="!canSaveRecord || processing"
class="d-flex align-items-center justify-content-center ml-2"
variant="primary"
size="lg"
@click.prevent="$emit('submit')"
>
<b-spinner
v-if="processingSubmit"
small
type="grow"
/>
<span
v-else
data-test-id="button-save"
<b-button-group v-if="recordNavigation.prev || recordNavigation.next">
<b-button
id="tooltip-target-prev-page"
v-b-tooltip.hover
pill
size="lg"
variant="outline-primary"
class="mr-2"
:disabled="!recordNavigation.prev"
:title="$t('recordNavigation.prev')"
@click="$emit('update-navigation', recordNavigation.prev)"
>
{{ labels.submit || $t('label.save') }}
</span>
</b-button>
</div>
<font-awesome-icon :icon="['fas', 'angle-left']" />
</b-button>
<b-button
id="tooltip-target-next-page"
v-b-tooltip.hover
size="lg"
pill
variant="outline-primary"
:disabled="!recordNavigation.next"
:title="$t('recordNavigation.next')"
@click="$emit('update-navigation', recordNavigation.next)"
>
<font-awesome-icon :icon="['fas', 'angle-right']" />
</b-button>
</b-button-group>
</b-col>
<b-col
class="d-flex align-items-center justify-content-end"
>
<template
v-if="module"
class="d-flex wrap-with-vertical-gutters align-items-center"
>
<c-input-confirm
v-if="(isCreated && !settings.hideDelete && !isDeleted)"
:disabled="!canDeleteRecord"
size="lg"
size-confirm="lg"
variant="danger"
:borderless="false"
@confirmed="$emit('delete')"
>
<b-spinner
v-if="processingDelete"
small
type="grow"
/>
<span v-else>
{{ labels.delete || $t('label.delete') }}
</span>
</c-input-confirm>
<c-input-confirm
v-if="isDeleted"
:disabled="!canUndeleteRecord"
size="lg"
size-confirm="lg"
variant="warning"
variant-ok="warning"
:borderless="false"
@confirmed="$emit('undelete')"
>
<b-spinner
v-if="processingUndelete"
small
type="grow"
/>
<span v-else>
{{ $t('label.restore') }}
</span>
</c-input-confirm>
<b-button
v-if="!inEditing && module.canCreateRecord && !hideClone && isCreated && !settings.hideClone"
data-test-id="button-clone"
variant="light"
size="lg"
:disabled="!record || processing"
class="ml-2"
@click.prevent="$emit('clone')"
>
{{ labels.clone || $t('label.clone') }}
</b-button>
<b-button
v-if="!inEditing && !settings.hideEdit && isCreated"
data-test-id="button-edit"
:disabled="!record.canUpdateRecord || processing"
variant="light"
size="lg"
class="ml-2"
@click.prevent="$emit('edit')"
>
{{ labels.edit || $t('label.edit') }}
</b-button>
<b-button
v-if="module.canCreateRecord && !hideAdd && !inEditing && !settings.hideNew"
data-test-id="button-add-new"
variant="primary"
size="lg"
:disabled="processing"
class="ml-2"
@click.prevent="$emit('add')"
>
{{ labels.new || $t('label.addNew') }}
</b-button>
<b-button
v-if="inEditing && !settings.hideSubmit"
data-test-id="button-submit"
:disabled="!canSaveRecord || processing"
class="d-flex align-items-center justify-content-center ml-2"
variant="primary"
size="lg"
@click.prevent="$emit('submit')"
>
<b-spinner
v-if="processingSubmit"
small
type="grow"
/>
<span
v-else
data-test-id="button-save"
>
{{ labels.submit || $t('label.save') }}
</span>
</b-button>
</template>
</b-col>
</b-row>
</b-container>
</template>
@@ -200,6 +238,12 @@ export default {
type: Boolean,
required: false,
},
recordNavigation: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {

View File

@@ -622,7 +622,7 @@
</template>
<script>
import { debounce } from 'lodash'
import { mapGetters } from 'vuex'
import { mapGetters, mapActions } from 'vuex'
import base from './base'
import FieldViewer from 'corteza-webapp-compose/src/components/ModuleFields/Viewer'
import FieldEditor from 'corteza-webapp-compose/src/components/ModuleFields/Editor'
@@ -920,6 +920,10 @@ export default {
},
methods: {
...mapActions({
loadPaginationRecords: 'ui/loadPaginationRecords',
}),
createEvents () {
const { pageID = NoID } = this.page
const { recordID = NoID } = this.record || {}
@@ -1214,6 +1218,26 @@ export default {
},
handleRowClicked ({ r: { recordID } }) {
const { moduleID, namespaceID } = this.recordListModule
if (this.block.options.enableRecordPageNavigation) {
const { pageCursor, nextPage, prevPage } = this.filter
this.loadPaginationRecords({
recordListModule: this.recordListModule,
moduleID,
namespaceID,
filterCursors: [prevPage, pageCursor, nextPage],
filter: {
...this.filter,
limit: Math.min(this.pagination.count, 100),
},
options: {
showRecordNavigationTooltip: this.block.options.showRecordNavigationTooltip,
},
})
}
if ((this.options.editable && this.editing) || (!this.recordPageID && !this.options.rowViewUrl)) {
return
}

View File

@@ -306,6 +306,11 @@
>
{{ $t('recordList.hideRecordPermissionsButton') }}
</b-form-checkbox>
<b-form-checkbox
v-model="options.enableRecordPageNavigation"
>
{{ $t('recordList.enableRecordPageNavigation') }}
</b-form-checkbox>
</b-form-group>
</b-tab>

View File

@@ -7,6 +7,7 @@ import page from './page'
import chart from './chart'
import user from './user'
import languages from './languages'
import ui from './ui'
import { store as cvStore } from '@cortezaproject/corteza-vue'
Vue.use(Vuex)
@@ -21,6 +22,7 @@ export default new Vuex.Store({
chart: chart(Vue.prototype.$ComposeAPI),
user: user(Vue.prototype.$SystemAPI),
languages: languages(Vue.prototype.$SystemAPI),
ui: ui(Vue.prototype.$ComposeAPI),
rbac: {
namespaced: true,
...cvStore.RBAC(

View File

@@ -0,0 +1,115 @@
const types = {
loading: 'loading',
loaded: 'loaded',
pending: 'pending',
completed: 'completed',
setRecordPagination: 'setRecordPagination',
clearRecordPagination: 'clearRecordPagination',
clearRecordPageNavigation: 'clearRecordPageNavigation',
setClearRecordPageNavigation: 'setClearRecordPageNavigation',
}
export default function (ComposeAPI) {
return {
namespaced: true,
state: {
loading: false,
pending: false,
recordPaginationIds: [],
recordPageVisited: null,
clearRecordPageNavigation: true,
},
getters: {
loading: (state) => state.loading,
pending: (state) => state.pending,
clearRecordPageNavigation: (state) => state.clearRecordPageNavigation,
getRecordNavigationIndex: (state) => (recordID) => {
return state.recordPaginationIds.indexOf(recordID)
},
nextRecordNavigation: ({ recordPaginationIds }, { getRecordNavigationIndex }) => (recordID) => {
const recordIndex = getRecordNavigationIndex(recordID)
const index = recordIndex !== undefined ? recordIndex : 1
return recordPaginationIds[index - 1]
},
prevRecordNavigation: ({ recordPaginationIds }, { getRecordNavigationIndex }) => (recordID) => {
const recordIndex = getRecordNavigationIndex(recordID)
const index = recordIndex !== undefined ? recordIndex : 1
return recordPaginationIds[index + 1]
},
getNextAndPrevRecord: (_, { nextRecordNavigation, prevRecordNavigation }) => (recordID) => {
return {
next: prevRecordNavigation(recordID),
prev: nextRecordNavigation(recordID),
}
},
},
actions: {
loadPaginationRecords ({ commit }, { filter, moduleID, namespaceID, filterCursors, incTotal, incPageNavigation, options } = {}) {
commit(types.pending)
commit(types.setClearRecordPageNavigation, true)
return Promise.all(filterCursors.map((cursor) => {
filter.pageCursor = cursor
return ComposeAPI.recordList({ ...filter, moduleID, namespaceID, incTotal, incPageNavigation })
.then(({ set }) => {
return set.map(({ recordID }) => recordID)
})
})).finally(() => {
commit(types.completed)
}).then(([prevRecords, nextRecords]) => {
commit(types.setRecordPagination, [...new Set([...prevRecords, ...nextRecords])])
})
},
clearRecordIds ({ commit }) {
commit(types.clearRecordPagination)
},
setClearRecordPageNavigation ({ commit }, value) {
commit(types.setClearRecordPageNavigation, value)
},
},
mutations: {
[types.loading] (state) {
state.loading = true
},
[types.loaded] (state) {
state.loading = false
},
[types.pending] (state) {
state.pending = true
},
[types.completed] (state) {
state.pending = false
},
[types.setRecordPagination] (state, recordIds) {
state.recordPaginationIds = recordIds
},
[types.clearRecordPagination] (state) {
state.recordPaginationIds = []
},
[types.setClearRecordPageNavigation] (state, value) {
state.clearRecordPageNavigation = value
},
},
}
}

View File

@@ -78,6 +78,7 @@
:processing-delete="processingDelete"
:processing-undelete="processingUndelete"
:in-editing="inEditing"
:record-navigation="recordNavigation"
@add="handleAdd()"
@clone="handleClone()"
@edit="handleEdit()"
@@ -85,12 +86,14 @@
@undelete="handleUndelete()"
@back="handleBack()"
@submit="handleFormSubmitSimple('admin.modules.record.view')"
@update-navigation="handleRedirectToPrevOrNext"
/>
</portal>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import RecordToolbar from 'corteza-webapp-compose/src/components/Common/RecordToolbar'
import users from 'corteza-webapp-compose/src/mixins/users'
import record from 'corteza-webapp-compose/src/mixins/record'
@@ -132,6 +135,11 @@ export default {
},
computed: {
...mapGetters({
clearRecordPageNavigation: 'ui/clearRecordPageNavigation',
getNextAndPrevRecord: 'ui/getNextAndPrevRecord',
}),
title () {
const { name, handle } = this.module
return this.$t('allRecords.view.title', { name: name || handle, interpolation: { escapeValue: false } })
@@ -188,6 +196,12 @@ export default {
return undefined
},
recordNavigation () {
if (!this.record) return
return this.getNextAndPrevRecord(this.record.recordID)
},
},
watch: {
@@ -201,9 +215,21 @@ export default {
created () {
this.createBlocks()
if (this.clearRecordPageNavigation) {
this.setClearRecordPageNavigation(false)
this.clearRecordIds()
} else {
this.clearRecordIds()
}
},
methods: {
...mapActions({
setClearRecordPageNavigation: 'ui/setClearRecordPageNavigation',
clearRecordIds: 'ui/clearRecordIds',
}),
createBlocks () {
this.fields.forEach(f => {
const block = new compose.PageBlockRecord()
@@ -246,6 +272,23 @@ export default {
handleEdit () {
this.$router.push({ name: 'admin.modules.record.edit', params: this.$route.params })
},
handleRedirectToPrevOrNext (value) {
const recordID = value
if (!recordID) return
const route = {
name: 'admin.modules.record.view',
params: {
moduleID: this.$route.params.moduleID,
recordID,
},
query: null,
}
this.$router.push(route)
},
},
}
</script>

View File

@@ -38,6 +38,7 @@
:processing-undelete="processingUndelete"
:in-editing="inEditing"
:show-record-modal="showRecordModal"
:record-navigation="recordNavigation"
@add="handleAdd()"
@clone="handleClone()"
@edit="handleEdit()"
@@ -45,11 +46,13 @@
@undelete="handleUndelete()"
@back="handleBack()"
@submit="handleFormSubmit('page.record')"
@update-navigation="handleRedirectToPrevOrNext"
/>
</portal>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
import Grid from 'corteza-webapp-compose/src/components/Public/Page/Grid'
import RecordToolbar from 'corteza-webapp-compose/src/components/Common/RecordToolbar'
import record from 'corteza-webapp-compose/src/mixins/record'
@@ -109,6 +112,11 @@ export default {
},
computed: {
...mapGetters({
clearRecordPageNavigation: 'ui/clearRecordPageNavigation',
getNextAndPrevRecord: 'ui/getNextAndPrevRecord',
}),
portalTopbarTitle () {
return this.showRecordModal ? 'record-modal-header' : 'topbar-title'
},
@@ -143,6 +151,10 @@ export default {
return this.$t(`page:public.record.${titlePrefix}.title`, { name: name || handle, interpolation: { escapeValue: false } })
},
recordNavigation () {
return this.getNextAndPrevRecord(this.recordID)
},
},
watch: {
@@ -166,6 +178,13 @@ export default {
this.loadRecord()
this.$root.$emit(`refetch-non-record-blocks:${this.page.pageID}`)
})
if (this.clearRecordPageNavigation) {
this.setClearRecordPageNavigation(false)
this.clearRecordIds()
} else {
this.clearRecordIds()
}
},
// Destroy event before route leave to ensure it doesn't destroy the newly created one
@@ -175,6 +194,11 @@ export default {
},
methods: {
...mapActions({
setClearRecordPageNavigation: 'ui/setClearRecordPageNavigation',
clearRecordIds: 'ui/clearRecordIds',
}),
async loadRecord () {
this.record = undefined
@@ -242,6 +266,29 @@ export default {
this.$router.push({ name: 'page.record.edit', params: this.$route.params })
}
},
handleRedirectToPrevOrNext (value) {
const recordID = value
if (!recordID) return
if (this.showRecordModal) {
this.$router.replace({
query: { recordID, recordPageID: this.$route.query.recordPageID },
})
} else {
const route = {
name: 'page.record',
params: {
pageID: this.page.pageID,
recordID,
},
query: null,
}
this.$router.push(route)
}
},
},
}
</script>

View File

@@ -22,6 +22,7 @@ interface Options {
hideRecordEditButton: boolean;
hideRecordViewButton: boolean;
hideRecordPermissionsButton: boolean;
enableRecordPageNavigation: boolean;
allowExport: boolean;
perPage: number;
recordDisplayOption: string;
@@ -72,6 +73,7 @@ const defaults: Readonly<Options> = Object.freeze({
hideRecordEditButton: false,
hideRecordViewButton: true,
hideRecordPermissionsButton: true,
enableRecordPageNavigation: true,
allowExport: false,
perPage: 20,
recordDisplayOption: 'sameTab',
@@ -146,6 +148,7 @@ export class PageBlockRecordList extends PageBlock {
'hideRecordEditButton',
'hideRecordViewButton',
'hideRecordPermissionsButton',
'enableRecordPageNavigation',
'editable',
'draggable',
'linkToParent',

View File

@@ -313,6 +313,7 @@ recordList:
hideRecordPermissionsButton: Hide record permissions button
hideRecordReminderButton: Hide record reminder button
hideRecordViewButton: Hide view record button
enableRecordPageNavigation: Enable record page navigation
import:
dropzoneFileAdded: '{{name}} was uploaded and is ready for import ({{count}} record)'
dropzoneFileAdded_plural: '{{name}} was uploaded and is ready for import ({{count}} records)'

View File

@@ -143,4 +143,7 @@ resourceList:
prev: Previous
showing: '{{from}} - {{to}} of {{count}} items'
single: One item
single_plural: '{{count}} items'
single_plural: '{{count}} items'
recordNavigation:
next: Next record
prev: Previous record