Skip to content

Commit 47afe27

Browse files
committed
feat(Settings): implement SignatureFlowGroupPolicy behavior
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 1e77af4 commit 47afe27

1 file changed

Lines changed: 290 additions & 0 deletions

File tree

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<NcSettingsSection
8+
:name="t('libresign', 'Signing order by group')"
9+
:description="t('libresign', 'Select a group to define a signing order override that is applied before user and request-level preferences.')">
10+
<NcNoteCard v-if="errorMessage" type="error">
11+
{{ errorMessage }}
12+
</NcNoteCard>
13+
14+
<NcSelect
15+
v-model="selectedGroup"
16+
label="displayname"
17+
:no-wrap="false"
18+
:aria-label-combobox="t('libresign', 'Select a group to configure signing order policy')"
19+
:disabled="loadingGroups || saving"
20+
:loading="loadingGroups"
21+
:options="groups"
22+
:searchable="true"
23+
:show-no-options="false"
24+
@search-change="searchGroup"
25+
@update:modelValue="onGroupChange" />
26+
27+
<div v-if="selectedGroup" class="signature-flow-group-policy">
28+
<div class="signature-flow-group-policy__toggle">
29+
<NcCheckboxRadioSwitch
30+
type="switch"
31+
v-model="enabled"
32+
:disabled="saving"
33+
@update:modelValue="onToggleChange">
34+
<span>{{ t('libresign', 'Use a custom signing order for this group') }}</span>
35+
</NcCheckboxRadioSwitch>
36+
<span v-if="saving" class="signature-flow-group-policy__status">
37+
<NcLoadingIcon :size="20" />
38+
</span>
39+
<span v-else-if="saved" class="signature-flow-group-policy__status">
40+
<NcSavingIndicatorIcon :size="20" />
41+
</span>
42+
</div>
43+
44+
<div v-if="enabled" class="signature-flow-group-policy__content">
45+
<NcCheckboxRadioSwitch
46+
type="switch"
47+
v-model="allowChildOverride"
48+
:disabled="saving"
49+
@update:modelValue="onAllowChildOverrideChange">
50+
<span>{{ t('libresign', 'Allow user and request-level overrides for this group default') }}</span>
51+
</NcCheckboxRadioSwitch>
52+
53+
<NcCheckboxRadioSwitch
54+
v-for="flow in availableFlows"
55+
:key="flow.value"
56+
type="radio"
57+
v-model="selectedFlowValue"
58+
:value="flow.value"
59+
:disabled="saving"
60+
name="signature_flow_group_policy"
61+
@update:modelValue="onFlowChange">
62+
<div class="signature-flow-group-policy__option">
63+
<div>
64+
<strong>{{ flow.label }}</strong>
65+
<p>{{ flow.description }}</p>
66+
</div>
67+
</div>
68+
</NcCheckboxRadioSwitch>
69+
</div>
70+
</div>
71+
</NcSettingsSection>
72+
</template>
73+
74+
<script setup lang="ts">
75+
import axios from '@nextcloud/axios'
76+
import { t } from '@nextcloud/l10n'
77+
import { confirmPassword } from '@nextcloud/password-confirmation'
78+
import { generateOcsUrl } from '@nextcloud/router'
79+
import { onMounted, ref } from 'vue'
80+
81+
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
82+
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
83+
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
84+
import NcSavingIndicatorIcon from '@nextcloud/vue/components/NcSavingIndicatorIcon'
85+
import NcSelect from '@nextcloud/vue/components/NcSelect'
86+
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
87+
88+
import { usePoliciesStore } from '../../store/policies'
89+
import type { GroupPolicyState, SignatureFlowMode } from '../../types/index'
90+
91+
import '@nextcloud/password-confirmation/style.css'
92+
93+
defineOptions({
94+
name: 'SignatureFlowGroupPolicy',
95+
})
96+
97+
type GroupRow = {
98+
id: string
99+
displayname: string
100+
}
101+
102+
type FlowOption = {
103+
value: SignatureFlowMode
104+
label: string
105+
description: string
106+
}
107+
108+
const policiesStore = usePoliciesStore()
109+
const groups = ref<GroupRow[]>([])
110+
const selectedGroup = ref<GroupRow | null>(null)
111+
const loadingGroups = ref(false)
112+
const saving = ref(false)
113+
const saved = ref(false)
114+
const errorMessage = ref('')
115+
const enabled = ref(false)
116+
const allowChildOverride = ref(true)
117+
const selectedFlow = ref<FlowOption | null>(null)
118+
119+
const availableFlows: FlowOption[] = [
120+
{
121+
value: 'parallel',
122+
label: t('libresign', 'Simultaneous (Parallel)'),
123+
description: t('libresign', 'All signers receive the document at the same time and can sign in any order.'),
124+
},
125+
{
126+
value: 'ordered_numeric',
127+
label: t('libresign', 'Sequential'),
128+
description: t('libresign', 'Signers are organized by signing order number. Only those with the lowest pending order number can sign.'),
129+
},
130+
]
131+
132+
const defaultFlow = availableFlows[0]!
133+
const selectedFlowValue = ref<SignatureFlowMode>(defaultFlow.value)
134+
135+
function applyGroupPolicy(policy: GroupPolicyState | null) {
136+
const value = policy?.value
137+
if (policy && (value === 'parallel' || value === 'ordered_numeric')) {
138+
enabled.value = true
139+
allowChildOverride.value = policy.allowChildOverride
140+
selectedFlow.value = availableFlows.find((flow) => flow.value === value) ?? defaultFlow
141+
selectedFlowValue.value = selectedFlow.value.value
142+
return
143+
}
144+
145+
enabled.value = false
146+
allowChildOverride.value = true
147+
selectedFlow.value = defaultFlow
148+
selectedFlowValue.value = defaultFlow.value
149+
}
150+
151+
async function searchGroup(query: string) {
152+
loadingGroups.value = true
153+
try {
154+
const response = await axios.get(generateOcsUrl('cloud/groups/details'), {
155+
params: {
156+
search: query,
157+
limit: 20,
158+
offset: 0,
159+
},
160+
})
161+
groups.value = response.data.ocs.data.groups.sort((a: GroupRow, b: GroupRow) => a.displayname.localeCompare(b.displayname))
162+
} catch (error) {
163+
console.error('Could not search groups for signature flow policy', error)
164+
errorMessage.value = t('libresign', 'Could not load groups. Try again.')
165+
} finally {
166+
loadingGroups.value = false
167+
}
168+
}
169+
170+
async function onGroupChange(group: GroupRow | null) {
171+
selectedGroup.value = group
172+
errorMessage.value = ''
173+
if (!group) {
174+
applyGroupPolicy(null)
175+
return
176+
}
177+
178+
try {
179+
const policy = await policiesStore.fetchGroupPolicy(group.id, 'signature_flow')
180+
applyGroupPolicy(policy)
181+
} catch (error) {
182+
console.error('Could not load group signature flow policy', error)
183+
errorMessage.value = t('libresign', 'Could not load the selected group policy.')
184+
applyGroupPolicy(null)
185+
}
186+
}
187+
188+
async function persistPolicy() {
189+
if (!selectedGroup.value) {
190+
return
191+
}
192+
193+
saving.value = true
194+
errorMessage.value = ''
195+
saved.value = false
196+
197+
try {
198+
await confirmPassword()
199+
if (!enabled.value) {
200+
const clearedPolicy = await policiesStore.clearGroupPolicy(selectedGroup.value.id, 'signature_flow')
201+
applyGroupPolicy(clearedPolicy)
202+
} else {
203+
const policy = await policiesStore.saveGroupPolicy(
204+
selectedGroup.value.id,
205+
'signature_flow',
206+
selectedFlowValue.value,
207+
allowChildOverride.value,
208+
)
209+
applyGroupPolicy(policy)
210+
}
211+
saved.value = true
212+
setTimeout(() => {
213+
saved.value = false
214+
}, 3000)
215+
} catch (error) {
216+
console.error('Could not save group signature flow policy', error)
217+
errorMessage.value = t('libresign', 'Could not save the group policy. Try again.')
218+
} finally {
219+
saving.value = false
220+
}
221+
}
222+
223+
async function onToggleChange() {
224+
await persistPolicy()
225+
}
226+
227+
async function onAllowChildOverrideChange() {
228+
await persistPolicy()
229+
}
230+
231+
async function onFlowChange() {
232+
selectedFlow.value = availableFlows.find((flow) => flow.value === selectedFlowValue.value) ?? defaultFlow
233+
await persistPolicy()
234+
}
235+
236+
onMounted(async () => {
237+
await searchGroup('')
238+
})
239+
240+
defineExpose({
241+
groups,
242+
selectedGroup,
243+
loadingGroups,
244+
saving,
245+
saved,
246+
errorMessage,
247+
enabled,
248+
allowChildOverride,
249+
selectedFlow,
250+
selectedFlowValue,
251+
availableFlows,
252+
searchGroup,
253+
onGroupChange,
254+
onToggleChange,
255+
onAllowChildOverrideChange,
256+
onFlowChange,
257+
})
258+
</script>
259+
260+
<style lang="scss" scoped>
261+
.signature-flow-group-policy {
262+
margin-top: 1rem;
263+
display: flex;
264+
flex-direction: column;
265+
gap: 1rem;
266+
267+
&__toggle {
268+
display: flex;
269+
align-items: center;
270+
gap: 0.5rem;
271+
}
272+
273+
&__status {
274+
display: flex;
275+
align-items: center;
276+
}
277+
278+
&__content {
279+
display: flex;
280+
flex-direction: column;
281+
gap: 0.75rem;
282+
}
283+
284+
&__option p {
285+
margin: 0.25rem 0 0;
286+
color: var(--color-text-maxcontrast);
287+
font-size: 90%;
288+
}
289+
}
290+
</style>

0 commit comments

Comments
 (0)