|
| 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