Skip to content

Commit 17c53e7

Browse files
committed
CCP-4997: Implement Offline Submission UI changes
1 parent 928cf7c commit 17c53e7

14 files changed

Lines changed: 861 additions & 308 deletions

File tree

.devcontainer/chefs_local/config/jetstream-single.conf

Whitespace-only changes.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
<script setup>
2+
import { storeToRefs } from 'pinia';
3+
import { computed, ref } from 'vue';
4+
import { useI18n } from 'vue-i18n';
5+
import { useRoute } from 'vue-router';
6+
7+
import PendingSubmissionsModal from '~/components/forms/offline/PendingSubmissionsModal.vue';
8+
import { offlineQueue } from '~/offline/queue';
9+
import { useSimulationToggle } from '~/offline/useSimulationToggle';
10+
import { useFormStore } from '~/store/form';
11+
12+
const { t, locale } = useI18n({ useScope: 'global' });
13+
14+
const route = useRoute();
15+
const { form } = storeToRefs(useFormStore());
16+
const { networkOnline, canSimulateOffline, simulatingOffline } =
17+
useSimulationToggle();
18+
19+
const OFFLINE_ROUTES = ['FormSubmit', 'FormSuccess'];
20+
const queuedCount = computed(() => offlineQueue.entries.value.length);
21+
const showPending = ref(false);
22+
23+
// Only show when an offline-enabled form is actually being rendered. Login /
24+
// forms-list / admin / etc. must never see this control even when simulation
25+
// is on or the queue has items; those are unrelated contexts.
26+
const visible = computed(
27+
() =>
28+
!!form.value.enableOfflineSubmission && OFFLINE_ROUTES.includes(route.name)
29+
);
30+
31+
const realOffline = computed(() => !networkOnline.value);
32+
// Toggle button is visible when the URL grants the simulate gate OR whenever
33+
// the user is already simulating (so they can always exit even after
34+
// navigating away from a route that carried ?simulateOffline=1). Hidden
35+
// during real offline; there's no manual "go online" to flip to.
36+
const showChevron = computed(
37+
() =>
38+
networkOnline.value && (canSimulateOffline.value || simulatingOffline.value)
39+
);
40+
41+
const state = computed(() => {
42+
if (realOffline.value) {
43+
return {
44+
color: 'white',
45+
variant: 'outlined',
46+
icon: 'mdi:mdi-cloud-off-outline',
47+
iconColor: 'warning',
48+
label: t('trans.offlineSubmission.offlineBadge'),
49+
dataTest: 'offlineBadge',
50+
};
51+
}
52+
if (simulatingOffline.value) {
53+
return {
54+
color: 'white',
55+
variant: 'outlined',
56+
icon: 'mdi:mdi-cloud-off-outline',
57+
iconColor: 'warning',
58+
label: t('trans.offlineSubmission.simulatingBadge'),
59+
dataTest: 'simulatingOfflineBadge',
60+
};
61+
}
62+
return {
63+
color: 'white',
64+
variant: 'outlined',
65+
icon: 'mdi:mdi-cloud-check-outline',
66+
iconColor: '#00e676',
67+
label: t('trans.offlineSubmission.onlineBadge'),
68+
dataTest: 'onlineBadge',
69+
};
70+
});
71+
72+
// External toggle icon shows the state you'll switch TO, not the current one.
73+
const toggleIcon = computed(() =>
74+
simulatingOffline.value
75+
? 'mdi:mdi-cloud-check-outline'
76+
: 'mdi:mdi-cloud-off-outline'
77+
);
78+
const toggleTooltip = computed(() =>
79+
simulatingOffline.value
80+
? t('trans.offlineSubmission.goOnline')
81+
: t('trans.offlineSubmission.goOffline')
82+
);
83+
84+
function openQueue() {
85+
showPending.value = true;
86+
}
87+
88+
function toggleSimulate() {
89+
simulatingOffline.value = !simulatingOffline.value;
90+
}
91+
</script>
92+
93+
<template>
94+
<div v-if="visible" class="offline-control d-flex align-center">
95+
<v-tooltip v-if="showChevron" location="bottom" :text="toggleTooltip">
96+
<template #activator="{ props: toggleProps }">
97+
<v-btn
98+
color="white"
99+
variant="text"
100+
class="offline-toggle-btn"
101+
data-test="simulateOfflineToggle"
102+
v-bind="toggleProps"
103+
@click="toggleSimulate"
104+
>
105+
<v-icon :icon="toggleIcon" size="28" />
106+
</v-btn>
107+
</template>
108+
</v-tooltip>
109+
<v-tooltip
110+
location="bottom"
111+
:text="
112+
t('trans.offlineSubmission.headerButtonTooltip', queuedCount, {
113+
count: queuedCount,
114+
})
115+
"
116+
:open-delay="400"
117+
>
118+
<template #activator="{ props: tipProps }">
119+
<v-badge
120+
:model-value="queuedCount > 0"
121+
color="warning"
122+
location="top end"
123+
offset-x="0"
124+
offset-y="0"
125+
data-test="offlineSubmissionsBadge"
126+
>
127+
<template #badge>
128+
<v-tooltip
129+
location="bottom"
130+
:text="
131+
t('trans.offlineSubmission.headerButtonTooltip', queuedCount, {
132+
count: queuedCount,
133+
})
134+
"
135+
>
136+
<template #activator="{ props: badgeTipProps }">
137+
<span v-bind="badgeTipProps" @click="openQueue">{{
138+
queuedCount
139+
}}</span>
140+
</template>
141+
</v-tooltip>
142+
</template>
143+
<v-btn
144+
:color="state.color"
145+
:variant="state.variant"
146+
:data-test="state.dataTest"
147+
class="offline-status-btn"
148+
v-bind="tipProps"
149+
@click="openQueue"
150+
>
151+
<template #prepend>
152+
<v-icon
153+
:color="state.iconColor"
154+
:icon="state.icon"
155+
size="28"
156+
class="offline-status-icon"
157+
/>
158+
</template>
159+
<span :lang="locale" class="offline-status-label">{{
160+
state.label
161+
}}</span>
162+
</v-btn>
163+
</v-badge>
164+
</template>
165+
</v-tooltip>
166+
<PendingSubmissionsModal v-model="showPending" />
167+
</div>
168+
</template>
169+
170+
<style scoped lang="scss">
171+
.offline-status-btn {
172+
height: 40px !important;
173+
width: 120px;
174+
min-width: 120px;
175+
padding-inline: 12px !important;
176+
letter-spacing: 0;
177+
178+
@media (max-width: 599px) {
179+
min-width: 40px;
180+
padding-inline: 8px !important;
181+
182+
.offline-status-label {
183+
display: none;
184+
}
185+
}
186+
}
187+
188+
.offline-toggle-btn {
189+
height: 32px !important;
190+
width: 32px !important;
191+
min-width: 32px !important;
192+
padding: 0 !important;
193+
margin-inline-end: -4px;
194+
}
195+
196+
:deep(.v-badge__badge) {
197+
cursor: pointer;
198+
}
199+
</style>

app/frontend/src/components/bcgov/BCGovHeader.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ const showTenantDropdown = computed(() => {
110110
<div v-if="showTenantDropdown" class="tenant-dropdown-wrapper">
111111
<TenantDropdown />
112112
</div>
113+
<BaseOfflineControl data-test="base-offline-control" />
113114
<BaseAuthButton data-test="base-auth-btn" />
114115
<BaseInternationalization data-test="base-internationalization" />
115116
</div>

app/frontend/src/components/designer/FormViewer.vue

Lines changed: 106 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ const authStore = useAuthStore();
118118
const formStore = useFormStore();
119119
const notificationStore = useNotificationStore();
120120
121-
const { canSimulateOffline, simulatingOffline, online } = useSimulationToggle();
121+
const { canSimulateOffline, online } = useSimulationToggle();
122122
123123
const { config } = storeToRefs(appStore);
124124
const { authenticated, keycloak, tokenParsed, user } = storeToRefs(authStore);
@@ -1086,11 +1086,7 @@ async function uploadFile(file, config = {}) {
10861086
:submission-id="submissionId"
10871087
:wide-form-layout="form.wideFormLayout"
10881088
:public-form="isFormPublic(form)"
1089-
:offline-enabled="!!form.enableOfflineSubmission"
1090-
:can-simulate-offline="canSimulateOffline"
1091-
:simulating-offline="simulatingOffline"
10921089
class="d-print-none"
1093-
@toggle-simulate-offline="simulatingOffline = !simulatingOffline"
10941090
@showdoYouWantToSaveTheDraftModal="showdoYouWantToSaveTheDraftModal"
10951091
@save-draft="saveDraft"
10961092
@switchView="switchView"
@@ -1125,7 +1121,78 @@ async function uploadFile(file, config = {}) {
11251121
11261122
<slot name="alert" :form="form" :class="{ 'dir-rtl': isRTL }" />
11271123
1124+
<v-dialog
1125+
v-if="form.enableOfflineSubmission && !online"
1126+
v-model="showSubmitConfirmDialog"
1127+
max-width="560"
1128+
persistent
1129+
@keydown.esc="showSubmitConfirmDialog = false"
1130+
>
1131+
<v-card class="offline-confirm-card" elevation="4">
1132+
<v-card-title
1133+
class="offline-confirm-title"
1134+
:class="{ 'dir-rtl': isRTL }"
1135+
>
1136+
<span :lang="locale">{{
1137+
$t('trans.offlineSubmission.queueConfirmTitle')
1138+
}}</span>
1139+
</v-card-title>
1140+
<v-card-text
1141+
class="offline-confirm-body"
1142+
:class="{ 'dir-rtl': isRTL }"
1143+
>
1144+
<p class="offline-confirm-message" :lang="locale">
1145+
{{ $t('trans.offlineSubmission.queueConfirmMessage') }}
1146+
</p>
1147+
<v-text-field
1148+
v-model="queueNote"
1149+
class="offline-confirm-note"
1150+
density="comfortable"
1151+
variant="outlined"
1152+
hide-details
1153+
:label="$t('trans.offlineSubmission.queueConfirmNoteLabel')"
1154+
:placeholder="
1155+
$t('trans.offlineSubmission.queueConfirmNotePlaceholder')
1156+
"
1157+
maxlength="100"
1158+
:lang="locale"
1159+
/>
1160+
</v-card-text>
1161+
<v-card-actions
1162+
class="offline-confirm-actions"
1163+
:class="{ 'dir-rtl': isRTL }"
1164+
>
1165+
<v-spacer />
1166+
<v-btn
1167+
color="primary"
1168+
variant="flat"
1169+
rounded="lg"
1170+
size="large"
1171+
class="px-6"
1172+
data-test="queue-confirm-submit"
1173+
@click="continueSubmit"
1174+
>
1175+
<span :lang="locale">{{
1176+
$t('trans.offlineSubmission.queueConfirmQueue')
1177+
}}</span>
1178+
</v-btn>
1179+
<v-btn
1180+
variant="outlined"
1181+
rounded="lg"
1182+
size="large"
1183+
class="px-6"
1184+
data-test="queue-confirm-cancel"
1185+
@click="showSubmitConfirmDialog = false"
1186+
>
1187+
<span :lang="locale">{{
1188+
$t('trans.baseDialog.cancel')
1189+
}}</span>
1190+
</v-btn>
1191+
</v-card-actions>
1192+
</v-card>
1193+
</v-dialog>
11281194
<BaseDialog
1195+
v-else
11291196
v-model="showSubmitConfirmDialog"
11301197
type="CONTINUE"
11311198
:enable-custom-button="canSaveDraft"
@@ -1139,31 +1206,11 @@ async function uploadFile(file, config = {}) {
11391206
>
11401207
<template #text>
11411208
<span :lang="locale">{{
1142-
form.enableOfflineSubmission && !online
1143-
? $t('trans.offlineSubmission.queueConfirmMessage')
1144-
: $t('trans.formViewer.submitFormWarningMsg')
1209+
$t('trans.formViewer.submitFormWarningMsg')
11451210
}}</span>
1146-
<v-text-field
1147-
v-if="form.enableOfflineSubmission && !online"
1148-
v-model="queueNote"
1149-
class="mt-3"
1150-
density="compact"
1151-
variant="outlined"
1152-
hide-details
1153-
:label="$t('trans.offlineSubmission.queueConfirmNoteLabel')"
1154-
:placeholder="
1155-
$t('trans.offlineSubmission.queueConfirmNotePlaceholder')
1156-
"
1157-
maxlength="100"
1158-
:lang="locale"
1159-
/>
11601211
</template>
11611212
<template #button-text-continue>
1162-
<span :lang="locale">{{
1163-
form.enableOfflineSubmission && !online
1164-
? $t('trans.offlineSubmission.queueConfirmQueue')
1165-
: $t('trans.formViewer.submit')
1166-
}}</span>
1213+
<span :lang="locale">{{ $t('trans.formViewer.submit') }}</span>
11671214
</template>
11681215
</BaseDialog>
11691216
@@ -1266,4 +1313,36 @@ async function uploadFile(file, config = {}) {
12661313
}
12671314
}
12681315
}
1316+
1317+
.offline-confirm-card {
1318+
padding: 8px 4px;
1319+
border-radius: 14px !important;
1320+
}
1321+
.offline-confirm-title {
1322+
font-size: 1.5rem !important;
1323+
font-weight: 700 !important;
1324+
padding: 20px 28px 8px !important;
1325+
line-height: 1.3 !important;
1326+
letter-spacing: normal !important;
1327+
}
1328+
.offline-confirm-body {
1329+
padding: 8px 28px 12px;
1330+
font-size: 1rem;
1331+
}
1332+
.offline-confirm-message {
1333+
margin: 0 0 20px;
1334+
color: rgba(0, 0, 0, 0.72);
1335+
line-height: 1.5;
1336+
}
1337+
.offline-confirm-note {
1338+
margin-top: 4px;
1339+
1340+
:deep(.v-field) {
1341+
border-radius: 8px;
1342+
}
1343+
}
1344+
.offline-confirm-actions {
1345+
padding: 8px 24px 20px;
1346+
gap: 12px;
1347+
}
12691348
</style>

0 commit comments

Comments
 (0)