Skip to content

Commit

Permalink
Merge pull request #1043 from getodk/features/delete-submissions
Browse files Browse the repository at this point in the history
View deleted Submissions and delete/undelete Submissions.
  • Loading branch information
sadiqkhoja authored Nov 4, 2024
2 parents e80cbb6 + 0b74e3c commit acd5cf6
Show file tree
Hide file tree
Showing 34 changed files with 1,611 additions and 109 deletions.
24 changes: 22 additions & 2 deletions src/components/date-range-picker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ except according to the terms contained in the LICENSE file.
https://github.com/ankurk91/vue-flatpickr-component/issues/47 -->
<flatpickr ref="flatpickr" v-model="flatpickrValue" :config="config"
class="form-control" :class="{ required }"
:aria-disabled="disabled" v-tooltip.aria-describedby="disabledMessage"
:placeholder="requiredLabel(placeholder, required)" autocomplete="off"
@keydown="stopPropagationIfDisabled"
@on-close="close"/>
<template v-if="!required">
<button v-show="modelValue.length === 2" type="button" class="close"
<button v-show="modelValue.length === 2 && !disabled" type="button" class="close"
:aria-label="$t('action.clear')" @click="clear">
<span aria-hidden="true">&times;</span>
</button>
Expand Down Expand Up @@ -70,6 +72,14 @@ export default {
placeholder: {
type: String,
required: true
},
disabled: {
type: Boolean,
default: false
},
disabledMessage: {
type: String,
required: false
}
},
emits: ['update:modelValue'],
Expand All @@ -90,7 +100,8 @@ export default {
mode: 'range',
// See https://github.com/flatpickr/flatpickr/issues/1549
dateFormat: 'Y/m/d',
locale: l10ns[this.$i18n.locale] ?? l10ns[this.$i18n.fallbackLocale]
locale: l10ns[this.$i18n.locale] ?? l10ns[this.$i18n.fallbackLocale],
clickOpens: !this.disabled
};
}
},
Expand Down Expand Up @@ -149,6 +160,11 @@ export default {
// https://github.com/ankurk91/vue-flatpickr-component/issues/33
this.$refs.flatpickr.$el.focus();
this.$refs.flatpickr.fp.close();
},
stopPropagationIfDisabled(e) {
if (this.disabled) {
e.stopPropagation();
}
}
}
};
Expand All @@ -165,6 +181,10 @@ export default {
&::placeholder { color: $color-text; }
}

.form-group .flatpickr-input[aria-disabled="true"]::placeholder {
color: $color-input-inactive;
}

.form-inline .flatpickr-input {
// Leave space for the .close button.
width: 205px;
Expand Down
4 changes: 4 additions & 0 deletions src/components/form-draft/testing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import EnketoFill from '../enketo/fill.vue';
import Loading from '../loading.vue';
import SentenceSeparator from '../sentence-separator.vue';
import SubmissionList from '../submission/list.vue';
import useSubmissions from '../../request-data/submissions';

import { apiPaths } from '../../util/request';
import { noop } from '../../util/util';
Expand Down Expand Up @@ -104,6 +105,9 @@ export default {
},
setup() {
const { resourceView, createResource } = useRequestData();

// SubmissionList expects submission stores to be created!
useSubmissions();
const formDraft = resourceView('formDraft', (data) => data.get());
const keys = createResource('keys');
return { formDraft, keys };
Expand Down
101 changes: 93 additions & 8 deletions src/components/form/submissions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ including this file, may be copied, modified, propagated, or distributed
except according to the terms contained in the LICENSE file.
-->
<template>
<div>
<div id="form-submissions">
<loading :state="keys.initiallyLoading"/>
<page-section v-show="keys.dataExists">
<template #heading>
<span>{{ $t('resource.submissions') }}</span>
<enketo-fill v-if="rendersEnketoFill" :form-version="form">
<span class="icon-plus-circle"></span>{{ $t('action.createSubmission') }}
</enketo-fill>
<template v-if="deletedSubmissionCount.dataExists">
<button v-if="canUpdate && (deletedSubmissionCount.value > 0 || deleted)" type="button"
class="btn toggle-deleted-submissions" :class="{ 'btn-danger': deleted, 'btn-link': !deleted }"
@click="toggleDeleted">
<span class="icon-trash"></span>{{ $tcn('action.toggleDeletedSubmissions', deletedSubmissionCount.value) }}
<span v-show="deleted" class="icon-close"></span>
</button>
</template>
<p v-show="deleted" class="purge-description">{{ $t('purgeDescription') }}</p>
<odata-data-access :analyze-disabled="analyzeDisabled"
:analyze-disabled-message="analyzeDisabledMessage"
@analyze="analyzeModal.show()"/>
</template>
<template #body>
<submission-list :project-id="projectId" :xml-form-id="xmlFormId" @fetch-keys="fetchData"/>
<submission-list :project-id="projectId" :xml-form-id="xmlFormId"
:deleted="deleted" @fetch-keys="fetchKeys"
@fetch-deleted-count="fetchDeletedCount"/>
</template>
</page-section>
<odata-analyze v-bind="analyzeModal" :odata-url="odataUrl"
Expand All @@ -32,12 +43,17 @@ except according to the terms contained in the LICENSE file.
</template>

<script>
import { watchEffect, computed } from 'vue';
import { useRouter } from 'vue-router';

import EnketoFill from '../enketo/fill.vue';
import Loading from '../loading.vue';
import PageSection from '../page/section.vue';
import OdataAnalyze from '../odata/analyze.vue';
import OdataDataAccess from '../odata/data-access.vue';
import SubmissionList from '../submission/list.vue';
import useQueryRef from '../../composables/query-ref';
import useSubmissions from '../../request-data/submissions';

import { apiPaths } from '../../util/request';
import { modalData } from '../../util/reactivity';
Expand Down Expand Up @@ -66,8 +82,32 @@ export default {
},
setup() {
const { project, form, createResource } = useRequestData();
const { deletedSubmissionCount } = useSubmissions();
const keys = createResource('keys');
return { project, form, keys, analyzeModal: modalData() };
const router = useRouter();

const deleted = useQueryRef({
fromQuery: (query) => {
if (typeof query.deleted === 'string' && query.deleted === 'true') {
return true;
}
return false;
},
toQuery: (value) => ({
deleted: value === true ? 'true' : null
})
});

const canUpdate = computed(() => project.dataExists && project.permits('submission.update'));

watchEffect(() => {
if (deleted.value && project.dataExists && !canUpdate.value) router.push('/');
});

return {
project, form, keys, analyzeModal: modalData(),
deletedSubmissionCount, canUpdate, deleted
};
},
computed: {
rendersEnketoFill() {
Expand All @@ -80,17 +120,19 @@ export default {
- There are encrypted submissions, or
- There are no submissions yet, but the form is encrypted. In that case,
there will never be decrypted submissions available to OData (as long as
the form remains encrypted).
the form remains encrypted), or
- Showing deleted Submissions
*/
analyzeDisabled() {
if (this.keys.dataExists && this.keys.length !== 0) return true;
if (this.form.dataExists && this.form.keyId != null &&
this.form.submissions === 0)
return true;
if (this.deleted) return true;
return false;
},
analyzeDisabledMessage() {
return this.$t('analyzeDisabled');
return this.deleted ? this.$t('analyzeDisabledDeletedData') : this.$t('analyzeDisabled');
},
odataUrl() {
if (!this.form.dataExists) return '';
Expand All @@ -99,26 +141,69 @@ export default {
}
},
created() {
this.fetchData();
this.fetchKeys();
if (!this.deleted) this.fetchDeletedCount();
},
methods: {
fetchData() {
fetchKeys() {
this.keys.request({
url: apiPaths.submissionKeys(this.projectId, this.xmlFormId)
}).catch(noop);
},
fetchDeletedCount() {
this.deletedSubmissionCount.request({
method: 'GET',
url: apiPaths.odataSubmissions(
this.projectId,
this.xmlFormId,
this.draft,
{
$top: 0,
$count: true,
$filter: '__system/deletedAt ne null',
}
),
}).catch(noop);
},
toggleDeleted() {
const { path } = this.$route;
this.$router.push(this.deleted ? path : `${path}?deleted=true`);
}
}
};
</script>

<style lang="scss">
@import '../../assets/scss/variables';

#odata-data-access { float: right; }
#form-submissions {
.toggle-deleted-submissions {
margin-left: 5px;

&.btn-link{
color: $color-danger;
}
}
.purge-description {
display: inline;
position: relative;
top: -5px;
left: 5px;
font-size: 14px;
}
}
</style>

<i18n lang="json5">
{
"en": {
"analyzeDisabled": "OData access is unavailable due to Form encryption"
"analyzeDisabled": "OData access is unavailable due to Form encryption",
"analyzeDisabledDeletedData": "OData access is unavailable for deleted Submissions",
"purgeDescription": "Submissions and Submission-related data are deleted after 30 days in the Trash",
"action": {
"toggleDeletedSubmissions": "{count} deleted Submission | {count} deleted Submissions"
}
}
}
</i18n>
Expand Down
3 changes: 0 additions & 3 deletions src/components/form/trash-row.vue
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,6 @@ export default {
<i18n lang="json5">
{
"en": {
"action": {
"restore": "Undelete"
},
// This text shows when the Form was deleted. {dateTime} shows
// the date and time, for example: "2020/01/01 01:23". It may show a
// formatted date like "2020/01/01", or it may use a word like "today",
Expand Down
14 changes: 13 additions & 1 deletion src/components/multiselect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ except according to the terms contained in the LICENSE file.
not show a menu with the placeholder option. This approach seems to work
across browsers. -->
<select :id="toggleId" ref="toggle" class="form-control"
:aria-disabled="options == null" data-toggle="dropdown" role="button"
:aria-disabled="options == null || disabled"
:data-toggle="(options == null || disabled) ? null : 'dropdown'" role="button"
v-tooltip.aria-describedby="disabledMessage"
aria-haspopup="true" aria-expanded="false" :aria-label="label"
@keydown="toggleAfterEnter" @mousedown.prevent @click="verifyAttached">
<option value="">{{ selectOption }}</option>
Expand Down Expand Up @@ -150,6 +152,16 @@ const props = defineProps({
empty: {
type: String,
required: false
},

// disabled the control
disabled: {
type: Boolean,
default: false
},
disabledMessage: {
type: String,
required: false
}
});
const emit = defineEmits(['update:modelValue']);
Expand Down
11 changes: 10 additions & 1 deletion src/components/submission/activity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ except according to the terms contained in the LICENSE file.
<span class="icon-pencil"></span>{{ $t('action.edit') }}
</button>
</template>
<button v-if="project.dataExists && project.permits('submission.delete')"
id="submission-activity-delete-button" type="button" class="btn btn-default"
@click="$emit('delete')">
<span class="icon-trash"></span>{{ $t('action.delete') }}
</button>
</template>
</template>
<template #body>
Expand Down Expand Up @@ -73,7 +78,7 @@ export default {
required: true
}
},
emits: ['review', 'comment'],
emits: ['review', 'comment', 'delete'],
setup() {
// The component does not assume that this data will exist when the
// component is created.
Expand Down Expand Up @@ -105,6 +110,10 @@ export default {
</script>

<style lang="scss">
@import '../../assets/scss/variables';

#submission-activity { margin-bottom: 35px; }
#submission-activity-review-button { margin-right: 5px; }
#submission-activity-edit-button { margin-right: 5px; }
#submission-activity-delete-button .icon-trash { color: $color-danger; }
</style>
Loading

0 comments on commit acd5cf6

Please sign in to comment.