Skip to content

Commit 0b2debf

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

1 file changed

Lines changed: 329 additions & 0 deletions

File tree

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
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', 'Policy catalog')"
9+
:description="t('libresign', 'Browse policy-managed settings in one place. Signing order is already connected to the live policy store.')">
10+
<div class="policy-catalog">
11+
<header class="policy-catalog__header">
12+
<div>
13+
<p class="policy-catalog__eyebrow">
14+
{{ t('libresign', 'Unified settings catalog') }}
15+
</p>
16+
<h3>{{ t('libresign', 'One list, live settings') }}</h3>
17+
<p>
18+
{{ t('libresign', 'Start from one catalog and open the setting you need. Signing order already uses the backend contract.') }}
19+
</p>
20+
</div>
21+
<div class="policy-catalog__summary">
22+
<span class="policy-catalog__summary-value">{{ itemCount }}</span>
23+
<span>{{ t('libresign', 'settings listed here') }}</span>
24+
</div>
25+
</header>
26+
27+
<div class="policy-catalog__layout">
28+
<section class="policy-catalog__list" :aria-label="t('libresign', 'Policy-managed settings')">
29+
<button
30+
v-for="item in items"
31+
:key="item.key"
32+
type="button"
33+
class="policy-catalog__item"
34+
:class="{
35+
'policy-catalog__item--selected': item.key === selectedItemKey,
36+
'policy-catalog__item--available': item.available,
37+
}"
38+
@click="selectedItemKey = item.key">
39+
<div class="policy-catalog__item-header">
40+
<div>
41+
<p class="policy-catalog__item-key">{{ item.key }}</p>
42+
<h4>{{ item.label }}</h4>
43+
</div>
44+
<span class="policy-catalog__status" :class="`policy-catalog__status--${item.status}`">
45+
{{ item.statusLabel }}
46+
</span>
47+
</div>
48+
<p class="policy-catalog__item-description">
49+
{{ item.description }}
50+
</p>
51+
<p class="policy-catalog__item-summary">
52+
{{ item.summary }}
53+
</p>
54+
</button>
55+
</section>
56+
57+
<section class="policy-catalog__detail">
58+
<div class="policy-catalog__detail-header">
59+
<p class="policy-catalog__eyebrow">
60+
{{ t('libresign', 'Current setting') }}
61+
</p>
62+
<h4>{{ selectedItem.label }}</h4>
63+
<p>
64+
{{ selectedItem.description }}
65+
</p>
66+
</div>
67+
68+
<SignatureFlow v-if="selectedItem.key === 'signature_flow'" />
69+
70+
<NcNoteCard v-else type="info">
71+
{{ t('libresign', 'This setting already appears in the catalog, but its dedicated policy editor is not wired to live persistence yet.') }}
72+
</NcNoteCard>
73+
</section>
74+
</div>
75+
</div>
76+
</NcSettingsSection>
77+
</template>
78+
79+
<script setup lang="ts">
80+
import { t } from '@nextcloud/l10n'
81+
import { computed, onMounted, ref } from 'vue'
82+
83+
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
84+
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
85+
86+
import { usePoliciesStore } from '../../../store/policies'
87+
import type { EffectivePolicyState } from '../../../types/index'
88+
import SignatureFlow from '../SignatureFlow.vue'
89+
90+
defineOptions({
91+
name: 'PolicyCatalog',
92+
})
93+
94+
type PolicyCatalogItem = {
95+
key: string
96+
label: string
97+
description: string
98+
summary: string
99+
status: 'ready' | 'planned'
100+
statusLabel: string
101+
available: boolean
102+
}
103+
104+
const policiesStore = usePoliciesStore()
105+
const selectedItemKey = ref('signature_flow')
106+
107+
const signatureFlowPolicy = computed(() => policiesStore.getPolicy('signature_flow'))
108+
109+
function getSignatureFlowSummary(policy: EffectivePolicyState | null): string {
110+
switch (policy?.effectiveValue) {
111+
case 'parallel':
112+
return t('libresign', 'Current effective value: Simultaneous (Parallel).')
113+
case 'ordered_numeric':
114+
return t('libresign', 'Current effective value: Sequential.')
115+
case 'none':
116+
return t('libresign', 'Current effective value: Disabled.')
117+
default:
118+
return t('libresign', 'Waiting for the effective policy value.')
119+
}
120+
}
121+
122+
const items = computed<PolicyCatalogItem[]>(() => [
123+
{
124+
key: 'signature_flow',
125+
label: t('libresign', 'Signing order'),
126+
description: t('libresign', 'Define whether signers work in parallel or in a sequential order.'),
127+
summary: getSignatureFlowSummary(signatureFlowPolicy.value),
128+
status: 'ready',
129+
statusLabel: t('libresign', 'Available now'),
130+
available: true,
131+
},
132+
{
133+
key: 'signature_stamp',
134+
label: t('libresign', 'Signature stamp'),
135+
description: t('libresign', 'Reserve a dedicated policy shell for stamp placement and defaults.'),
136+
summary: t('libresign', 'Catalog entry created. Implementation wiring is the next step.'),
137+
status: 'planned',
138+
statusLabel: t('libresign', 'Next step'),
139+
available: false,
140+
},
141+
{
142+
key: 'identify_factors',
143+
label: t('libresign', 'Identification factors'),
144+
description: t('libresign', 'Prepare policy-level control over the required identification methods.'),
145+
summary: t('libresign', 'Catalog entry created. Implementation wiring is the next step.'),
146+
status: 'planned',
147+
statusLabel: t('libresign', 'Next step'),
148+
available: false,
149+
},
150+
])
151+
152+
const selectedItem = computed(() => {
153+
return items.value.find(item => item.key === selectedItemKey.value) ?? items.value[0]
154+
})
155+
156+
const itemCount = computed(() => items.value.length)
157+
158+
onMounted(async () => {
159+
if (!signatureFlowPolicy.value) {
160+
await policiesStore.fetchEffectivePolicies()
161+
}
162+
})
163+
</script>
164+
165+
<style scoped lang="scss">
166+
.policy-catalog {
167+
margin-top: 1rem;
168+
display: flex;
169+
flex-direction: column;
170+
gap: 1.5rem;
171+
172+
&__header {
173+
display: flex;
174+
justify-content: space-between;
175+
gap: 1rem;
176+
align-items: flex-start;
177+
padding: 1.25rem;
178+
border-radius: 20px;
179+
background:
180+
radial-gradient(circle at top left, color-mix(in srgb, var(--color-primary-element) 16%, transparent), transparent 50%),
181+
linear-gradient(180deg, color-mix(in srgb, var(--color-main-background) 92%, white), var(--color-main-background));
182+
border: 1px solid color-mix(in srgb, var(--color-primary-element) 16%, var(--color-border-maxcontrast));
183+
184+
h3,
185+
p {
186+
margin: 0;
187+
}
188+
}
189+
190+
&__eyebrow,
191+
&__item-key {
192+
margin: 0 0 0.4rem;
193+
text-transform: uppercase;
194+
letter-spacing: 0.04em;
195+
font-size: 0.72rem;
196+
color: var(--color-text-maxcontrast);
197+
}
198+
199+
&__summary {
200+
display: flex;
201+
flex-direction: column;
202+
align-items: flex-end;
203+
text-align: right;
204+
color: var(--color-text-maxcontrast);
205+
}
206+
207+
&__summary-value {
208+
font-size: 2rem;
209+
line-height: 1;
210+
font-weight: 700;
211+
color: var(--color-main-text);
212+
}
213+
214+
&__layout {
215+
display: grid;
216+
grid-template-columns: minmax(280px, 0.8fr) minmax(0, 1.2fr);
217+
gap: 1.25rem;
218+
align-items: start;
219+
}
220+
221+
&__list,
222+
&__detail {
223+
display: flex;
224+
flex-direction: column;
225+
gap: 0.75rem;
226+
}
227+
228+
&__item,
229+
&__detail {
230+
padding: 1rem;
231+
border-radius: 18px;
232+
border: 1px solid var(--color-border-maxcontrast);
233+
background: var(--color-main-background);
234+
}
235+
236+
&__item {
237+
text-align: left;
238+
cursor: pointer;
239+
display: flex;
240+
flex-direction: column;
241+
gap: 0.75rem;
242+
transition: border-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
243+
244+
&:hover {
245+
border-color: color-mix(in srgb, var(--color-primary-element) 30%, var(--color-border-maxcontrast));
246+
transform: translateY(-1px);
247+
}
248+
249+
&--selected {
250+
border-color: var(--color-primary-element);
251+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary-element) 16%, transparent);
252+
}
253+
}
254+
255+
&__item-header {
256+
display: flex;
257+
justify-content: space-between;
258+
gap: 1rem;
259+
align-items: flex-start;
260+
261+
h4,
262+
p {
263+
margin: 0;
264+
}
265+
}
266+
267+
&__item-description,
268+
&__item-summary,
269+
&__detail-header p {
270+
margin: 0;
271+
color: var(--color-text-maxcontrast);
272+
}
273+
274+
&__item-summary {
275+
font-weight: 600;
276+
color: var(--color-main-text);
277+
}
278+
279+
&__status {
280+
padding: 0.3rem 0.7rem;
281+
border-radius: 999px;
282+
font-size: 0.8rem;
283+
white-space: nowrap;
284+
background: color-mix(in srgb, var(--color-background-dark) 15%, var(--color-main-background));
285+
286+
&--ready {
287+
background: color-mix(in srgb, #1e7a46 18%, var(--color-main-background));
288+
}
289+
290+
&--planned {
291+
background: color-mix(in srgb, #9b6a18 18%, var(--color-main-background));
292+
}
293+
}
294+
295+
&__detail {
296+
gap: 1rem;
297+
background:
298+
linear-gradient(180deg, color-mix(in srgb, var(--color-primary-element) 8%, var(--color-main-background)), var(--color-main-background));
299+
position: sticky;
300+
top: 1rem;
301+
}
302+
303+
&__detail-header {
304+
h4,
305+
p {
306+
margin: 0;
307+
}
308+
}
309+
}
310+
311+
@media (max-width: 1024px) {
312+
.policy-catalog {
313+
&__header,
314+
&__layout {
315+
display: flex;
316+
flex-direction: column;
317+
}
318+
319+
&__summary {
320+
align-items: flex-start;
321+
text-align: left;
322+
}
323+
324+
&__detail {
325+
position: static;
326+
}
327+
}
328+
}
329+
</style>

0 commit comments

Comments
 (0)