Compare commits

...

5 commits

Author SHA1 Message Date
buttle 84f10d7365 adds 'No data' message 2024-01-27 19:59:23 +01:00
buttle 7da41b56fe adds 'Display deleted fields' 2024-01-27 16:38:44 +01:00
buttle 52672a944d renames component 2024-01-27 15:27:38 +01:00
buttle bc064e438e adds delete item modal 2024-01-27 15:16:29 +01:00
buttle e8c7438c52 reworks display panel layout 2024-01-27 14:00:24 +01:00
18 changed files with 318 additions and 181 deletions

View file

@ -6,22 +6,15 @@ This file is part of LiberaForms.
-->
<template>
<div v-if="is_loading===true">
<div class="ds-loading">
{{ $t('Loading...') }}
</div>
<div v-if="is_loading===true" class="ds-loading">
{{ $t('Loading...') }}
</div>
<div v-else>
<DisplayPanel />
</div>
<DisplayPanel v-else />
</template>
<script>
import axios from 'axios';
import { inject, computed, provide } from "vue";
import { inject, computed } from "vue";
import { dataDisplayStore } from '@/store.js'
import DisplayPanel from '@/components/panel/DisplayPanel.vue'

View file

@ -7,25 +7,35 @@ This file is part of LiberaForms.
<template>
<ItemsGrid v-if="display_items" />
<div v-if="!is_loading">
<h2 v-if="!has_items" class="ds-no-data">
{{ $t('No data') }}
</h2>
<template v-else-if="display_items">
<ItemsGrid class="mt-4" />
<FieldModal v-if="!is_loading"
:show_modal="show_field_modal"
:data="field_modal_data"
@closeFieldModal="closeFieldModal" />
<FieldModal v-if="display_items"
:show_modal="show_field_modal"
:data="field_modal_data"
@closeFieldModal="closeFieldModal" />
<DeleteItemModal v-if="delete_item_initiated" />
</template>
</div>
</template>
<script>
import { computed, provide, ref, toRefs, reactive } from "vue";
import { computed, provide, ref, reactive } from "vue";
import { dataDisplayStore } from '@/store.js'
import ItemsGrid from '@/components/itemsRenderer/ItemsGrid.vue'
import FieldModal from '@/components/itemsRenderer/FieldModal.vue'
import DeleteItemModal from '@/components/itemsRenderer/DeleteItemModal.vue'
export default {
name: 'ItemsRenderer',
components: {
ItemsGrid, FieldModal
ItemsGrid, FieldModal, DeleteItemModal
},
setup() {
@ -39,6 +49,7 @@ export default {
item_id: null
})
const has_items = computed(() => {return store.itemsToDisplay.length})
function showFieldEditor(data) {
//console.log("showFieldEditor:", data.item_id, data.field_name, data.field_value)
@ -58,7 +69,7 @@ export default {
return {
display_items: computed(() => {
if (store.downloading_items) {
if (store.downloading_items || !has_items) {
return false
}
if (store.pdf_builder_mode) {
@ -69,13 +80,31 @@ export default {
}
return false
}),
has_items,
field_modal_data, show_field_modal, closeFieldModal,
is_loading: computed(() => store.downloading_items),
delete_item_initiated: computed(() => { return store.delete_item_initiated }),
}
},
}
</script>
<style scoped>
<style>
.ds-empty-field {
font-style: italic;
}
</style>
<style scoped>
.ds-loading {
font-style: italic;
color: var(--lf-gray-800);
margin-top: 1.5em;
}
.ds-no-data {
font-style: italic;
color: var(--lf-gray-600);
margin-top: 1em;
font-size: 1.75em;
}
</style>

View file

@ -1,35 +0,0 @@
<!--
This file is part of LiberaForms.
# SPDX-FileCopyrightText: 2024 LiberaForms.org
# SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
</template>
<script>
import { computed } from "vue";
import { dataDisplayStore } from '@/store.js'
export default {
name: 'DeleteItem',
components: {
},
setup() {
const store = dataDisplayStore()
return {
}
},
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,108 @@
<!--
This file is part of LiberaForms.
# SPDX-FileCopyrightText: 2024 LiberaForms.org
# SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div tabindex="-1" aria-labelledby="delete-modal-label" aria-hidden="true"
class="modal fade d-block show"
@keydown.esc="closeModal()"
role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="delete-modal-label">
{{ $t("Delete answer")}}
</h5>
<button type="button" aria-label="Close"
v-on:click="closeModal()">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="modal-body">
{{ $t("This cannot be undone!") }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger"
v-on:click="deleteItem()">
{{ $t("Delete") }}
</button>
<button type="button" class="btn btn-secondary"
v-on:click="closeModal()">
{{ $t("Cancel") }}
</button>
</div>
</div>
</div>
</div>
<div class="modal-backdrop fade show"></div>
</template>
<script>
import axios from 'axios';
import { computed } from "vue";
import { dataDisplayStore } from '@/store.js'
export default {
name: 'DeleteItemModal',
components: {
},
setup() {
const store = dataDisplayStore()
const item_id = store.delete_item_initiated
function deleteItem() {
function removeFromStore() {
let item_pos = store.items.findIndex(item => item.id === item_id)
store.items.splice(item_pos, 1)
store.meta.total = store.meta.total - 1
if (store.filtered_items) {
let item_pos = store.filtered_items.findIndex(item => item.id === item_id)
store.filtered_items.splice(item_pos, 1)
}
}
let endpoint = store.item_endpoint + item_id + '/delete'
axios.delete(endpoint)
.then(response => {
if (response.data.deleted == true) {
removeFromStore()
if (response.data.remove_alert != false) {
let alert = document.getElementById(response.data.remove_alert.disk_alert)
if (alert !== null) {
alert.remove()
let alert_menu = document.getElementById(response.data.remove_alert.alerts)
let alerts = alert_menu.querySelectorAll("li");
if (alerts.length == 0) {
alert_menu.remove()
}
}
}
}
})
.catch(e => {
console.log(e)
})
.finally(() => {
this.closeModal()
});
}
function closeModal() {
store.delete_item_initiated = null
}
return {
deleteItem, closeModal,
}
},
}
</script>
<style scoped>
</style>

View file

@ -24,7 +24,7 @@ This file is part of LiberaForms.
data-dismiss="modal"
aria-label="Close"
v-on:click="closeModal()">
<XIcon aria-hidden="true" />
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="modal-body">
@ -131,12 +131,12 @@ This file is part of LiberaForms.
import axios from 'axios';
import { computed, ref, watch } from "vue";
import { dataDisplayStore } from '@/store.js'
import { XIcon } from '@zhuowenli/vue-feather-icons'
//import { XIcon } from '@zhuowenli/vue-feather-icons'
export default {
name: 'FieldModal',
components: {
XIcon
//XIcon
},
props: {
data: Object,

View file

@ -31,7 +31,6 @@ export default {
props: {
field_name: String,
field_value: undefined,
edit_mode: Boolean,
item_id: Number,
},
setup(props) {

View file

@ -12,8 +12,7 @@ This file is part of LiberaForms.
class="p-0 stickyColumn first_sticky_col">
<RowControls :item_id="item.id"
:marked="item.marked"
:edit_mode="edit_mode" />
:marked="item.marked" />
</td>
<td v-else
@ -21,9 +20,7 @@ This file is part of LiberaForms.
<div :style="column_width">
<FieldRender :field_name="field.name"
:field_value="field.name == 'created' ? item.created : item.data[field.name]"
:item_id="item.id"
:edit_mode="field.name == 'created' ? false : edit_mode">
</FieldRender>
:item_id="item.id" />
</div>
</td>
</template>
@ -44,7 +41,6 @@ export default {
props: {
item: Object,
field_index: Object,
edit_mode: Boolean,
is_data_sticky: Boolean,
delete_initiated: Boolean,
column_width: Object,

View file

@ -7,7 +7,7 @@ This file is part of LiberaForms.
<template>
<div v-if="is_loading === false"
class="d-flex align-items-center justify-content-end my-4">
class="d-flex align-items-center justify-content-end mb-2">
<small class="page_count_display me-2">
{{ getPaginationText() }}
</small>

View file

@ -7,8 +7,6 @@ This file is part of LiberaForms.
<template>
<div>
<ItemPagination />
{{ column_width }}
<div class="table-responsive">
@ -38,7 +36,6 @@ This file is part of LiberaForms.
<GridRow v-for="item in items" :key="item.id"
:item="item"
:field_index="field_index"
:edit_mode="edit_mode"
:is_data_sticky="is_data_sticky"
:highlighted_row="highlighted_row"
@click="highlighted_row = item.id"
@ -47,12 +44,13 @@ This file is part of LiberaForms.
</tbody>
</table>
</div>
<ItemPagination />
<NumericTotals />
</div>
</div>
</template>
<script>
import { computed, ref } from "vue";
import { computed, ref, inject } from "vue";
import { dataDisplayStore } from '@/store.js'
import { orderItems } from '@/modules/items.js'
import { ArrowDownIcon, CheckCircleIcon } from '@zhuowenli/vue-feather-icons'
@ -72,11 +70,10 @@ export default {
const data_type = store.data_type
const field_index = computed(() => {
return store.user_prefs.field_index
return store.field_index
})
const highlighted_row = ref(0)
let edit_mode = false
var is_data_sticky = false
let grid_width = 400 //$refs.table.clientWidth
@ -125,7 +122,7 @@ export default {
return "stickyColumn first_sticky_col sticky_data_col"
}
if (col_index == 1) {
if (edit_mode) {
if (store.show_other_options) {
return "stickyColumn sticky_data_col sticky_data_col_with_controls_expanded";
}
if (store.has_row_controls) {
@ -146,8 +143,6 @@ export default {
return {
store,
edit_mode,
items: computed(() => store.paginatedItems),
field_index,
setOrder,

View file

@ -12,11 +12,11 @@ This file is part of LiberaForms.
:aria-label="$t('Mark for reference')">
<CheckCircleIcon size="1.3x" />
</button>
<button v-if="edit_mode==true"
<button v-if="with_other_options"
class="ds-delete-button"
:class="{'ds-card-button':is_card==true}"
:aria-label="$t('Delete answer')"
v-on:click="deleteItem">
v-on:click="initiateDelete()">
<TrashIcon size="1.3x" />
</button>
</div>
@ -36,18 +36,18 @@ export default {
props: {
item_id: Number,
marked: Boolean,
edit_mode: Boolean,
is_card: Boolean,
},
setup(props) {
const store = dataDisplayStore()
const with_other_options = computed(() => { return store.show_other_options })
const controlClass = computed(() => {
if (props.is_card==true) {
return props.edit_mode ? 'card-controls editMode' : 'card-controls nonEditMode'
return with_other_options ? 'card-controls editMode' : 'card-controls nonEditMode'
} else {
return props.edit_mode ? 'grid-controls editMode' : 'grid-controls nonEditMode'
return with_other_options ? 'grid-controls editMode' : 'grid-controls nonEditMode'
}
})
@ -66,9 +66,16 @@ export default {
}
}
function initiateDelete() {
store.delete_item_initiated = props.item_id
}
return {
controlClass,
toggleBookmarked,
with_other_options,
initiateDelete
}
},
}

View file

@ -18,7 +18,7 @@ This file is part of LiberaForms.
v-on:click="displayItemsAs('cards')">
{{ $t("Cards") }}
</button>
<button v-if="include_graphs"
<button v-if="data_type='answer'"
class="btn btn-sm"
:class="button_classes.graphs"
:disabled="disabled"
@ -56,7 +56,8 @@ export default {
'grid': store.display_items_as == 'grid' ? 'btn-primary' : 'btn-outline-secondary',
'graphs': store.display_items_as == 'graphs' ? 'btn-primary' : 'btn-outline-secondary'
}
})
}),
data_type: store.data_type,
}
},
}

View file

@ -6,34 +6,69 @@ This file is part of LiberaForms.
-->
<template>
<div>
<PanelControls />
<div class="ds-panel-desktop-container-1">
<ExportOptions />
</div>
<div class="mt-2 ds-panel-desktop-container-1">
<!--<Filters :mq="$mq" :field_index="field_index" />-->
<ItemFilters />
</div>
</div>
<div v-if="data_type=='answer'"
class="mt-3 ds-panel-desktop-container-1"
:class="controlGroupClasses()">
<div class="ds-controls-height">
<DisplayModes />
</div>
<div class="ds-controls-height">
<OtherOptions />
</div>
</div>
</template>
<script>
import { computed } from "vue";
import { computed, ref, provide } from "vue";
import { dataDisplayStore } from '@/store.js'
import ItemFilters from '@/components/panel/ItemFilters.vue'
import PanelControls from '@/components/panel/PanelControls.vue'
import ExportOptions from '@/components/panel/ExportOptions.vue'
import DisplayModes from '@/components/panel/DisplayModes.vue'
import OtherOptions from '@/components/panel/OtherOptions.vue'
export default {
name: 'DisplayPanel',
components: {
ItemFilters, PanelControls
ItemFilters, ExportOptions, DisplayModes, OtherOptions
},
setup() {
const store = dataDisplayStore()
provide('has_items', computed(() => {return store.itemsToDisplay.length}))
let mq = "lg"
function controlGroupClasses() {
if (mq == "sm") {
return "mb-3"
}
if (store.data_type=='answer') {
return 'd-flex align-items-center justify-content-between'
}
if (store.data_type=='user') {
return 'd-flex align-items-center justify-content-end'
}
return "mt-1";
}
return {
controlGroupClasses,
data_type: store.data_type,
has_items: computed(() => store.itemsToDisplay.length),
}
},
@ -41,5 +76,14 @@ export default {
</script>
<style scoped>
.ds-controls-height {
min-height: 31px !important;
}
</style>
<style>
.ds-controls-title svg {
color: var(--lf-link-color);
cursor: pointer;
}
</style>

View file

@ -47,7 +47,7 @@ export default {
function generateCSV() {
var columns = []
let field_index = store.user_prefs.field_index
let field_index = store.field_index
field_index.forEach((field, i) => {
columns.push(field.label)
@ -86,7 +86,7 @@ export default {
function generateJSON() {
var result = []
let items = store.itemsToDisplay
let field_index = store.user_prefs.field_index
let field_index = store.field_index
items.forEach((item) => {
var obj = new Object();
for(let i = 0; i < field_index.length; i++) {

View file

@ -211,7 +211,7 @@ export default {
return {
filter_text,
default_field_index: computed(() => store.default_field_index),
default_field_index: store.default_field_index,
order_by: computed(() => store.user_prefs.order_by),
first_field,

View file

@ -0,0 +1,78 @@
<!--
This file is part of LiberaForms.
# SPDX-FileCopyrightText: 2024 LiberaForms.org
# SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<button v-if="available_options"
class="btn btn-sm"
:class="show_options && has_items ? 'btn-primary' : 'btn-outline-primary'"
:disabled="!has_items"
v-on:click="toggleOptions()">
{{ $t("Options") }}
</button>
<a v-if="show_options && can_edit && has_items"
class="btn btn-sm btn-outline-danger modes"
role="button"
:href="endpoint+'/delete-all-items'">
{{ $t("Delete all") }}
</a>
<button v-if="has_deleted_fields && has_items"
class="btn btn-sm"
:class="show_deleted==true ? 'btn-primary' : 'btn-outline-secondary'"
v-on:click="displayDeletedFields()">
{{ $t("Include deleted fields") }}
</button>
</template>
<script>
import { computed, inject } from "vue";
import { dataDisplayStore } from '@/store.js'
export default {
name: 'OtherOptions',
components: {
},
setup() {
const store = dataDisplayStore()
const has_items = inject('has_items')
function toggleOptions() {
store.show_other_options = !store.show_other_options
}
function displayDeletedFields() {
store.include_deleted_fields = !store.include_deleted_fields
store.filtered_items = null
}
return {
has_items,
can_edit: store.can_edit,
data_type: store.data_type,
endpoint: store.endpoint,
has_deleted_fields: store.deleted_fields.length,
available_options: store.can_edit || store.deleted_fields,
show_options: computed(() => store.show_other_options),
toggleOptions, displayDeletedFields,
}
},
}
</script>
<style scoped>
.ds-controls-height {
min-height: 31px !important;
}
.ds-controls-title svg {
color: var(--lf-link-color);
cursor: pointer;
}
</style>

View file

@ -1,86 +0,0 @@
<!--
This file is part of LiberaForms.
# SPDX-FileCopyrightText: 2024 LiberaForms.org
# SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="ds-modes-export-container-1"
:class="controlGroupClasses()">
<div v-if="data_type=='answer'" class="ds-controls-height">
<DisplayModes :disabled="pdf_builder_mode"
:include_graphs="data_type=='answer'" />
<!--<EditOptions v-if="can_edit" :edit_mode="edit_mode" :can_edit="can_edit" />-->
</div>
<div v-if="pdf_builder_mode"
class="ds-controls-title ds-controls-height fs-5 fw-light">
<ArrowLeftIcon stroke-width="1" size="1.2x"
v-if="mq=='sm'"
@click="togglePdfBuilderMode()"/>
{{ $t("Export PDF") }}
</div>
<ExportOptions />
</div>
</template>
<script>
import { computed } from "vue";
import { dataDisplayStore } from '@/store.js'
import { ArrowLeftIcon } from '@zhuowenli/vue-feather-icons'
import DisplayModes from '@/components/panel/DisplayModes.vue'
import EditOptions from '@/components/panel/EditOptions.vue'
import ExportOptions from '@/components/panel/ExportOptions.vue'
export default {
name: 'PanelControls',
components: {
DisplayModes, EditOptions, ExportOptions,
ArrowLeftIcon,
},
setup() {
const store = dataDisplayStore()
let mq = "lg"
function controlGroupClasses() {
if (mq == "sm") {
return "mb-3"
}
if (store.data_type=='answer') {
return 'd-flex align-items-center justify-content-between mb-4'
}
if (store.data_type=='user') {
return 'd-flex align-items-center justify-content-end mb-3'
}
return "mt-1";
}
return {
controlGroupClasses,
can_edit: store.can_edit,
data_type: store.data_type,
pdf_builder_mode: computed(() => { return store.pdf_builder_mode }),
}
},
}
</script>
<style scoped>
.ds-controls-height {
min-height: 31px !important;
}
.ds-controls-title svg {
color: var(--lf-link-color);
cursor: pointer;
}
</style>

View file

@ -37,7 +37,9 @@ export const dataDisplayStore = defineStore("dataDisplayStore", {
page_length: 10,
can_edit: false,
show_modes: false,
show_other_options: false,
has_row_controls: false,
delete_item_initiated: null, // is the id of the item
include_deleted_fields: false,
display_items_as: "grid",
pdf_builder_mode: false,
@ -104,7 +106,7 @@ export const dataDisplayStore = defineStore("dataDisplayStore", {
return field_value
},
getFieldLabel: (state) => (field_name) => {
var field = state.user_prefs.field_index.find(x => x.name === field_name)
var field = state.field_index.find(x => x.name === field_name)
if (field !== undefined) {
return field.label
}
@ -125,6 +127,12 @@ export const dataDisplayStore = defineStore("dataDisplayStore", {
});
return label
},
field_index: (state) => {
if (state.include_deleted_fields && state.deleted_fields.length) {
return state.user_prefs.field_index.concat(state.deleted_fields)
}
return state.user_prefs.field_index
},
getFieldStructure: (state) => (field_name) => {
return _.findWhere(state.meta.form_structure, {name: field_name})
},

View file

@ -29,7 +29,7 @@
"Cards": "Cards",
"Options": "Options",
"Delete all": "Delete all",
"Deleted fields": "Deleted fields",
"Include deleted fields": "Include deleted fields",
"True": "True",
"False": "False",
"Previous": "Previous",