Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6f3a561
Add form data retention and classification settings
jasonchung1871 Aug 26, 2025
f7750cf
Enhance submission hard deletion and retention logic
jasonchung1871 Aug 28, 2025
048e4ee
Remove unused AuditActions constant
jasonchung1871 Aug 28, 2025
44b313d
Refactor deletionService with helper methods
jasonchung1871 Aug 28, 2025
2f58bf9
Change form_audit id column to integer identity
jasonchung1871 Aug 28, 2025
89a74f1
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
jasonchung1871 Sep 2, 2025
9a25394
Fixed cronjob routes
jasonchung1871 Sep 2, 2025
4473f52
Merge branch 'FORMS-1963-hard-deletion-form-submissions' of https://g…
jasonchung1871 Sep 2, 2025
4b951f9
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
jasonchung1871 Oct 28, 2025
6ce034d
Fixing code smells
jasonchung1871 Oct 29, 2025
cf7f2ff
Fix code smells
jasonchung1871 Oct 29, 2025
9d96705
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
jasonchung1871 Oct 29, 2025
116e1be
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
usingtechnology Nov 6, 2025
015369f
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
jasonchung1871 Nov 24, 2025
8699ebd
Separates retention from form
jasonchung1871 Nov 26, 2025
ecaa4b2
Update app.cronjob.yaml
jasonchung1871 Nov 26, 2025
67c5312
Fixed some tests
jasonchung1871 Nov 26, 2025
23d8e0a
Updated frontend
jasonchung1871 Dec 1, 2025
7be2349
Added translations
jasonchung1871 Dec 1, 2025
316dce0
Refactor retention policy handling in form designer
jasonchung1871 Dec 2, 2025
f7bba8a
Integrate scheduled submission deletion with retention policy
jasonchung1871 Dec 2, 2025
f515262
Remove unused moment import
jasonchung1871 Dec 2, 2025
16009da
Update tests for form and records management modules
jasonchung1871 Dec 2, 2025
45828b6
Integrate records management for submission actions
jasonchung1871 Dec 2, 2025
fb4d7c4
Remove unused classification fields from form store
jasonchung1871 Dec 8, 2025
9cbd82b
Adjust mocks
usingtechnology Dec 8, 2025
2c9d861
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
usingtechnology Dec 8, 2025
b5f53b2
Merge branch 'bcgov:main' into FORMS-1963-hard-deletion-form-submissions
jasonchung1871 Dec 10, 2025
848e27b
Rename migration and update CREATED_BY constant
jasonchung1871 Dec 11, 2025
9a03089
Use uuidv4 for default UUID values in migrations
jasonchung1871 Dec 11, 2025
d23c0e1
Handle missing retention policy in records management
jasonchung1871 Dec 11, 2025
993b739
Add currentUser middleware to records management routes
jasonchung1871 Dec 15, 2025
cb499fe
Change deletions process endpoint to use POST
jasonchung1871 Dec 15, 2025
3b614cf
Update user property in controller test mock
jasonchung1871 Dec 15, 2025
ce801ec
Fix missing await in transaction start
jasonchung1871 Dec 15, 2025
047da88
Add and update submission audit trigger function
jasonchung1871 Dec 15, 2025
6376281
Remove retention policy setup from form creation
jasonchung1871 Dec 17, 2025
8f15765
Merge branch 'main' into FORMS-1963-hard-deletion-form-submissions
jasonchung1871 Dec 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/actions/deploy-to-environment/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,5 +138,11 @@ runs:

- name: Create Cron Jobs
shell: bash
env:
NAMESPACE_PREFIX: ${{ inputs.namespace_prefix }}
NAMESPACE_ENVIRONMENT: ${{ inputs.namespace_environment }}
JOB_NAME: ${{ inputs.job_name }}
ACRONYM: ${{ inputs.acronym }}
ROUTE_PATH: ${{ inputs.route_path }}
run: |
oc process --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f openshift/app.cronjob.yaml -p JOB_NAME=${{ inputs.job_name }} -p NAMESPACE=${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -p APP_NAME=${{ inputs.acronym }} -p ROUTE_PATH=${{ inputs.route_path }} -o yaml | oc apply --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f -
oc process --namespace ${NAMESPACE_PREFIX}-${NAMESPACE_ENVIRONMENT} -f openshift/app.cronjob.yaml -p JOB_NAME=${JOB_NAME} -p NAMESPACE=${NAMESPACE_PREFIX}-${NAMESPACE_ENVIRONMENT} -p APP_NAME=${ACRONYM} -p NAMESPACE_ENVIRONMENT=${NAMESPACE_ENVIRONMENT} -p ROUTE_PATH=${ROUTE_PATH} -o yaml | oc apply --namespace ${NAMESPACE_PREFIX}-${NAMESPACE_ENVIRONMENT} -f -
4 changes: 4 additions & 0 deletions app/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,9 @@
"matchPrecision": "occupant, unit, site, civic_number, intersection, block, street, locality, province",
"precisionPoints": 100
}
},
"recordsManagement": {
"implementation": "local",
"enabled": true
}
}
4 changes: 4 additions & 0 deletions app/frontend/src/components/designer/FormSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import FormSubmissionSettings from '~/components/designer/settings/FormSubmissio
import FormScheduleSettings from '~/components/designer/settings/FormScheduleSettings.vue';
import FormMetadataSettings from '~/components/designer/settings/FormMetadataSettings.vue';
import FormEventStreamSettings from '~/components/designer/settings/FormEventStreamSettings.vue';
import FormClassificationSettings from '~/components/designer/settings/FormClassificationSettings.vue';

import { useFormStore } from '~/store/form';

Expand Down Expand Up @@ -41,6 +42,9 @@ const { form, isFormPublished, isRTL } = storeToRefs(useFormStore());
<v-col cols="12" md="6">
<FormMetadataSettings :disabled="disabled" />
</v-col>
<v-col v-if="form.id" cols="12" md="6">
<FormClassificationSettings :disabled="disabled" />
</v-col>
</v-row>
<v-row>
<v-col v-if="form.id">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
<script setup>
import { storeToRefs } from 'pinia';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';

import { useFormStore } from '~/store/form';
import { useNotificationStore } from '~/store/notification';
import { useRecordsManagementStore } from '~/store/recordsManagement';
import { recordsManagementService } from '~/services';

const { t, locale } = useI18n({ useScope: 'global' });

const formStore = useFormStore();
const notificationStore = useNotificationStore();
const recordsManagementStore = useRecordsManagementStore();

const { isRTL } = storeToRefs(formStore);
const { formRetentionPolicy } = storeToRefs(recordsManagementStore);

// DON'T use storeToRefs for this - access directly from store
const retentionClassificationTypes = computed(
() => recordsManagementStore.retentionClassificationTypes || []
);

// Common retention periods
const retentionOptions = ref([
{ value: 30, label: t('trans.formSettings.days30') },
{ value: 90, label: t('trans.formSettings.days90') },
{ value: 180, label: t('trans.formSettings.days180') },
{ value: 365, label: t('trans.formSettings.year1') },
{ value: 730, label: t('trans.formSettings.years2') },
{ value: 1825, label: t('trans.formSettings.years5') },
{ value: null, label: t('trans.formSettings.daysCustom') },
]);

const enableHardDeletion = ref(false);
const customDays = ref(null);
const showCustomDays = ref(false);
const isCustomRetention = ref(false);
const actualRetentionDays = ref(null);
const selectedRetentionOption = ref(null);

// Computed that handles undefined safely
const CLASSIFICATION_TYPES = computed(() => {
if (
!retentionClassificationTypes.value ||
!Array.isArray(retentionClassificationTypes.value)
) {
return [];
}
return retentionClassificationTypes.value.map((type) => ({
value: type,
label: type.display,
}));
});

onMounted(async () => {
await fetchClassificationTypes();

if (formRetentionPolicy.value?.retentionClassificationId) {
enableHardDeletion.value = true;
}
});

async function fetchClassificationTypes() {
try {
const result =
await recordsManagementService.listRetentionClassificationTypes();
recordsManagementStore.retentionClassificationTypes = result.data;
} catch (e) {
notificationStore.addNotification({
text: t('trans.formSettings.fetchRetentionClassificationListError'),
consoleError: t(
'trans.formSettings.fetchRetentionClassificationListConsErrMsg',
{
error: e.message,
}
),
});
}
}

const selectedClassification = computed({
get() {
if (
typeof formRetentionPolicy.value?.retentionClassificationId === 'string'
) {
const match = retentionClassificationTypes.value?.find(
(type) =>
type.id === formRetentionPolicy.value.retentionClassificationId
);
return (
match || {
value: formRetentionPolicy.value.retentionClassificationId,
label: formRetentionPolicy.value.retentionClassificationId,
}
);
}
return formRetentionPolicy.value;
},
set(newValue) {
if (newValue && typeof newValue === 'object' && 'value' in newValue) {
formRetentionPolicy.value.retentionClassificationId = newValue.value.id;
} else {
formRetentionPolicy.value.retentionClassificationId = newValue;
}
},
});

const handleRetentionChange = (value) => {
if (value === null) {
isCustomRetention.value = true;
showCustomDays.value = true;
if (!customDays.value) {
customDays.value = formRetentionPolicy.value?.retentionDays || 30;
}
} else {
showCustomDays.value = false;
actualRetentionDays.value = value;
formRetentionPolicy.value.retentionDays = value;
}
};

const applyCustomDays = () => {
if (customDays.value) {
const days = Math.min(Math.max(1, customDays.value), 3650);
customDays.value = days;
actualRetentionDays.value = days;
formRetentionPolicy.value.retentionDays = days;
}
};

const isStandardRetention = (days) => {
return retentionOptions.value.some(
(option) => option.value === days && option.value !== null
);
};

const initializeRetentionUI = () => {
if (formRetentionPolicy.value?.retentionDays) {
actualRetentionDays.value = formRetentionPolicy.value.retentionDays;

if (isStandardRetention(formRetentionPolicy.value.retentionDays)) {
showCustomDays.value = false;
isCustomRetention.value = false;
selectedRetentionOption.value = formRetentionPolicy.value.retentionDays;
} else {
showCustomDays.value = true;
isCustomRetention.value = true;
customDays.value = formRetentionPolicy.value.retentionDays;
selectedRetentionOption.value = null;
}
}
};

initializeRetentionUI();

watch(actualRetentionDays, (newValue) => {
if (newValue && isCustomRetention.value) {
formRetentionPolicy.value.retentionDays = newValue;
}
});

const handleEnableChange = (enabled) => {
if (enabled) {
if (!formRetentionPolicy.value?.retentionClassificationId) {
const firstClassification = CLASSIFICATION_TYPES.value[0];
if (firstClassification?.value?.id) {
formRetentionPolicy.value.retentionClassificationId =
firstClassification.value.id;
}
}

if (!formRetentionPolicy.value?.retentionDays) {
selectedRetentionOption.value = null;
isCustomRetention.value = true;
showCustomDays.value = true;
customDays.value = 30;
actualRetentionDays.value = 30;
formRetentionPolicy.value.retentionDays = 30;
}
} else {
formRetentionPolicy.value.retentionDays = null;
actualRetentionDays.value = null;
formRetentionPolicy.value.retentionClassificationDescription = null;
formRetentionPolicy.value.retentionClassificationId = null;
showCustomDays.value = false;
customDays.value = null;
isCustomRetention.value = false;
selectedRetentionOption.value = null;
}
};

watch(
() => enableHardDeletion.value,
(newValue) => {
if (newValue && !formRetentionPolicy.value?.retentionDays) {
handleEnableChange(true);
}
}
);
</script>

<template>
<BasePanel class="fill-height">
<template #title>
<span :lang="locale">
{{ $t('trans.formSettings.dataRetention') }}
</span>
</template>

<v-checkbox
v-model="enableHardDeletion"
hide-details="auto"
data-test="enableHardDeletionCheckbox"
class="my-0"
:class="{ 'dir-rtl': isRTL }"
@update:model-value="handleEnableChange"
>
<template #label>
<div :class="{ 'mr-2': isRTL }">
<span :lang="locale">
{{ $t('trans.formSettings.enableHardDeletion') }}
</span>
<v-tooltip location="bottom">
<template #activator="{ props }">
<v-icon
color="primary"
class="ml-3"
:class="{ 'mr-2': isRTL }"
v-bind="props"
icon="mdi:mdi-help-circle-outline"
/>
</template>
<span>
<span
:lang="locale"
v-html="$t('trans.formSettings.hardDeletionTooltip')"
/>
<ul>
<li :lang="locale">
{{ $t('trans.formSettings.hardDeletionWarning') }}
</li>
<li :lang="locale">
{{ $t('trans.formSettings.permanentDeletion') }}
</li>
</ul>
</span>
</v-tooltip>
</div>
</template>
</v-checkbox>

<div v-if="enableHardDeletion" class="mt-4">
<v-combobox
v-model="selectedClassification"
:items="CLASSIFICATION_TYPES"
item-title="label"
item-value="value"
:label="$t('trans.formSettings.dataClassification')"
:hint="$t('trans.formSettings.dataClassificationHint')"
persistent-hint
variant="outlined"
class="mb-4"
:class="{ 'dir-rtl': isRTL }"
clearable
return-object
></v-combobox>

<v-select
v-model="selectedRetentionOption"
:items="retentionOptions"
item-title="label"
item-value="value"
:label="$t('trans.formSettings.retentionPeriod')"
:hint="$t('trans.formSettings.retentionPeriodHint')"
persistent-hint
variant="outlined"
class="mb-4"
:class="{ 'dir-rtl': isRTL }"
@update:model-value="handleRetentionChange"
></v-select>

<v-text-field
v-if="showCustomDays"
v-model="customDays"
type="number"
min="1"
max="3650"
:label="$t('trans.formSettings.customDaysLabel')"
:hint="$t('trans.formSettings.customDaysHint')"
persistent-hint
variant="outlined"
class="mb-4"
:class="{ 'dir-rtl': isRTL }"
@blur="applyCustomDays"
></v-text-field>

<v-textarea
v-model="formRetentionPolicy.retentionClassificationDescription"
:label="$t('trans.formSettings.classificationDescription')"
:hint="$t('trans.formSettings.classificationDescriptionHint')"
persistent-hint
variant="outlined"
rows="3"
auto-grow
class="mb-4"
:class="{ 'dir-rtl': isRTL }"
></v-textarea>

<v-alert
v-if="formRetentionPolicy.retentionDays"
type="warning"
variant="tonal"
class="mt-4"
>
<span :lang="locale">
{{
$t('trans.formSettings.deletionDisclaimerWithDays', {
days: formRetentionPolicy.retentionDays,
})
}}
</span>
</v-alert>

<v-alert v-else type="info" variant="tonal" class="mt-4">
<span :lang="locale">
{{ $t('trans.formSettings.setRetentionPrompt') }}
</span>
</v-alert>
</div>
</BasePanel>
</template>
5 changes: 4 additions & 1 deletion app/frontend/src/components/forms/SubmissionsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,10 @@ async function restoreSub() {

async function deleteSingleSubs() {
showDeleteDialog.value = false;
await formStore.deleteSubmission(deleteItem.value.submissionId);
await formStore.deleteSubmission(
form.value.id,
deleteItem.value.submissionId
);
refreshSubmissions();
}

Expand Down
Loading
Loading