Skip to content

Commit f1abf48

Browse files
committed
feat: enhance identity modals with improved UI and functionality
- Updated identity modals to include new styling and layout for better user experience. - Added computed properties for managing identity labels and selection states. - Implemented new methods for handling identity selection and synchronization. - Enhanced the mail template modal with additional variable management features. - Refactored the update identity modal to improve clarity and functionality. - Introduced a unique row key mechanism to prevent selection issues in the identities table.
1 parent 9504a1a commit f1abf48

7 files changed

Lines changed: 1134 additions & 329 deletions

File tree

apps/web/src/assets/sass/global.sass

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,14 @@ body.body--dark
190190
::-webkit-scrollbar-thumb
191191
background-color: #555
192192
border-radius: 4px
193+
194+
// Modales identités — bouton « Annuler » (outline Quasar = bordure sur ::before, pas sur le root .q-btn { border: 0 })
195+
// Couleur de trait explicite : évite un contour trop faible (ex. currentColor / ordre CSS) sur une modale isolée.
196+
.q-btn.identity-modal-btn-cancel.q-btn--outline::before
197+
border: 1px solid $separator-color !important
198+
199+
body.body--dark .q-btn.identity-modal-btn-cancel.q-btn--outline::before
200+
border-color: $separator-dark-color !important
201+
202+
body.body--dark .q-btn.identity-modal-btn-cancel.q-btn--outline
203+
color: rgba(255, 255, 255, 0.88) !important

apps/web/src/components/core/twopan.vue

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export default defineNuxtComponent({
124124
default: 33,
125125
},
126126
rowKey: {
127-
type: String,
127+
type: [String, Function] as PropType<string | ((row: Record<string, unknown>) => string | number)>,
128128
default: 'id',
129129
},
130130
refresh: {
@@ -156,9 +156,14 @@ export default defineNuxtComponent({
156156
default: 'sesame-table-td-highlight',
157157
},
158158
rowClassHandler: {
159-
type: Function as PropType<(payload: { row: any; targetId: string; rowKey: string; rowClassHighlight: string }) => string>,
159+
type: Function as PropType<
160+
(payload: { row: any; targetId: string; rowKey: string | ((row: any) => string | number); rowClassHighlight: string }) => string
161+
>,
160162
default: ({ row, targetId, rowKey, rowClassHighlight }) => {
161-
if (`${row[rowKey]}` === targetId) {
163+
const keyVal = typeof rowKey === 'function' ? rowKey(row) : row?.[rowKey as string]
164+
const keyStr = String(keyVal ?? '')
165+
const baseKey = keyStr.includes('::') ? keyStr.slice(0, keyStr.indexOf('::')) : keyStr
166+
if (baseKey === String(targetId)) {
162167
return rowClassHighlight
163168
}
164169
@@ -229,6 +234,9 @@ export default defineNuxtComponent({
229234
if (this.isSimple) {
230235
this.splitterModel = !t ? 100 : 0
231236
}
237+
// Même jeu de `rows` que la navigation précédente : éviter une sélection figée
238+
// sur d’anciennes lignes au retour liste / changement de fiche.
239+
this.clearSelection()
232240
},
233241
},
234242
total: {
@@ -237,6 +245,11 @@ export default defineNuxtComponent({
237245
},
238246
immediate: true,
239247
},
248+
rows: {
249+
handler() {
250+
this.syncSelectionWithRows()
251+
},
252+
},
240253
},
241254
computed: {
242255
visibleColumnsSelected(): QTableProps['visibleColumns'] {
@@ -251,7 +264,7 @@ export default defineNuxtComponent({
251264
return this.rowClassHandler({
252265
row,
253266
targetId: this.targetId,
254-
rowKey: this.rowKey,
267+
rowKey: this.rowKey as string | ((row: any) => string | number),
255268
rowClassHighlight: this.rowClassHighlight,
256269
}) as string
257270
}
@@ -277,16 +290,38 @@ export default defineNuxtComponent({
277290
},
278291
},
279292
methods: {
293+
/**
294+
* Garde uniquement les lignes encore présentes dans `rows` (même clé `rowKey`)
295+
* et réaligne sur les objets courants. Sinon la sélection peut rester bloquée
296+
* sur d’anciennes lignes après changement de route / filtres / rechargement.
297+
*/
298+
tableRowKey(row: Record<string, unknown>): string {
299+
const rk = this.rowKey
300+
return typeof rk === 'function' ? String(rk(row)) : String(row?.[rk as string] ?? '')
301+
},
302+
syncSelectionWithRows() {
303+
if (!this.selection || this.selection === 'none') return
304+
if (!Array.isArray(this.selected) || this.selected.length === 0) return
305+
const rows = this.rows || []
306+
const rowByKey = new Map(rows.map((r) => [this.tableRowKey(r), r]))
307+
const next = this.selected.map((s) => rowByKey.get(this.tableRowKey(s))).filter((r) => r !== undefined)
308+
if (next.length !== this.selected.length || next.some((r, i) => r !== this.selected[i])) {
309+
this.selected = next
310+
}
311+
},
280312
clearSelection() {
281313
this.selected = []
282314
},
315+
getSelectedRows(): unknown[] {
316+
return Array.isArray(this.selected) ? [...this.selected] : []
317+
},
283318
async onRequestEvent(props: { pagination: QTableProps['pagination']; filter: string; getCellValue: Function }) {
284319
// console.log('onRequestEvent', props)
285320
await this.onRequest(props as any)
286321
},
287322
},
288323
async mounted() {
289-
const table = <{ $el?: HTMLElement }>this.$refs.table
324+
const table = this.$refs.table as { $el?: HTMLElement }
290325
const internalChildren = table?.$el?.querySelector('.q-table__middle')
291326
const internalInnerHeight = internalChildren?.clientHeight || table?.$el?.offsetHeight || window.innerHeight
292327

apps/web/src/components/pages/identities/modals/delete-many.vue

Lines changed: 231 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,66 @@
22
q-dialog(
33
ref="dialogRef",
44
@hide="onDialogHide",
5+
transition-show="scale",
6+
transition-hide="scale",
57
)
6-
q-card.q-dialog-plugin
7-
q-card-section
8-
.text-h6 Suppression en masse des identités
9-
q-card-section {{ mainText }}
10-
q-card-actions
11-
q-space
12-
q-btn(
13-
color="positive",
14-
label="Valider",
15-
@click="syncIdentities"
8+
q-card.identity-modal-card(flat bordered)
9+
q-card-section.identity-modal-header.row.items-center.no-wrap.bg-negative.text-white
10+
q-avatar(round color="white" text-color="negative" icon="mdi-delete-alert" size="32px")
11+
.column.q-ml-md.col
12+
.text-h6.text-weight-medium Suppression en masse
13+
.text-caption(style="opacity: 0.92") Les identités listées seront mises dans la corbeille
14+
q-separator
15+
q-card-section.identity-modal-body
16+
.identity-modal-body-top
17+
p.text-body2.identity-modal-lede {{ mainText }}
18+
.row.items-center.no-wrap.q-mb-sm
19+
q-icon(color="negative" name="mdi-account-multiple-outline" size="22px")
20+
.text-subtitle1.q-ml-sm.text-weight-medium Identités concernées
21+
q-space
22+
q-badge(color="negative" text-color="white" rounded) {{ selectedRows.length }}
23+
q-banner(
24+
v-if="selectedRows.length === 0"
25+
dense
26+
rounded
27+
class="bg-amber-2 text-dark"
28+
)
29+
span Aucune identité dans la sélection.
30+
.identity-modal-list-wrap(v-if="selectedRows.length > 0" :class="listWrapClass")
31+
q-list(dense separator padding)
32+
q-item.identity-modal-list-item(
33+
v-for="item in identityListItems"
34+
:key="item.key"
35+
)
36+
q-item-section(side)
37+
q-avatar(:color="item.avatarColor" text-color="white" size="36px") {{ item.initials }}
38+
q-item-section
39+
q-item-label.text-weight-medium(lines="2") {{ item.label }}
40+
q-item-label.text-caption.text-grey-6(lines="1" style="font-family: ui-monospace, monospace") {{ item.idShort }}
41+
q-card-actions.identity-modal-actions(align="right")
42+
q-btn.identity-modal-btn-cancel(
43+
outline
44+
color="grey-8"
45+
no-caps
46+
padding="sm lg"
47+
label="Annuler"
48+
@click="cancelSync"
1649
)
1750
q-btn(
18-
color="negative",
19-
label="Annuler",
20-
@click="cancelSync"
51+
unelevated
52+
no-caps
53+
padding="sm lg"
54+
color="negative"
55+
icon-right="mdi-check"
56+
label="Supprimer"
57+
:disable="selectedRows.length === 0"
58+
@click="syncIdentities"
2159
)
2260
</template>
2361

2462
<script lang="ts" setup>
25-
import { ref, computed } from 'vue'
26-
import { useDialogPluginComponent } from 'quasar'
63+
import { computed } from 'vue'
64+
import { useDialogPluginComponent, useQuasar } from 'quasar'
2765
2866
const props = defineProps({
2967
selectedIdentities: {
@@ -34,17 +72,190 @@ const props = defineProps({
3472
3573
defineEmits([...useDialogPluginComponent.emits])
3674
37-
const mainText = computed(() => `Vous êtes sur le point de supprimer ${props.selectedIdentities.length} identités. Voulez-vous continuer ?`)
75+
const AVATAR_COLORS = ['orange-8', 'teal', 'indigo', 'deep-orange', 'purple', 'cyan', 'brown']
76+
77+
function idToString(id: unknown): string {
78+
if (id == null) return ''
79+
if (typeof id === 'string') return id
80+
if (typeof id === 'object' && id !== null && '$oid' in (id as Record<string, unknown>)) {
81+
return String((id as Record<string, unknown>).$oid)
82+
}
83+
return String(id)
84+
}
85+
86+
function strOrJoin(v: unknown): string {
87+
if (v == null) return ''
88+
if (typeof v === 'string') return v.trim()
89+
if (Array.isArray(v)) {
90+
return v
91+
.map((x) => (typeof x === 'string' ? x.trim() : x != null ? String(x).trim() : ''))
92+
.filter(Boolean)
93+
.join(' ')
94+
}
95+
return String(v).trim()
96+
}
97+
98+
function buildIdentityLabel(row: Record<string, unknown>): string {
99+
const p = row?.inetOrgPerson as Record<string, unknown> | undefined
100+
if (p && typeof p === 'object') {
101+
const fromCn = strOrJoin(p.cn)
102+
if (fromCn) return fromCn
103+
const dn = strOrJoin(p.displayName)
104+
if (dn) return dn
105+
const gn = strOrJoin(p.givenName)
106+
const sn = strOrJoin(p.sn)
107+
if (gn || sn) return [gn, sn].filter(Boolean).join(' ')
108+
const mail = strOrJoin(p.mail)
109+
if (mail) return mail
110+
const uid = strOrJoin(p.uid)
111+
if (uid) return uid
112+
}
113+
const id = idToString(row?._id)
114+
if (id) return `Identité ${id.length > 14 ? `${id.slice(0, 14)}…` : id}`
115+
return 'Identité (sans identifiant)'
116+
}
117+
118+
function initialsFromLabel(label: string): string {
119+
const t = label.trim()
120+
if (!t) return '?'
121+
const parts = t.split(/\s+/).filter(Boolean)
122+
if (parts.length >= 2) {
123+
const a = parts[0][0] || ''
124+
const b = parts[1][0] || ''
125+
return `${a}${b}`.toUpperCase()
126+
}
127+
if (t.length >= 2) return t.slice(0, 2).toUpperCase()
128+
return t.charAt(0).toUpperCase()
129+
}
130+
131+
function shortId(id: string): string {
132+
if (!id) return ''
133+
if (id.length <= 22) return id
134+
return `${id.slice(0, 10)}…${id.slice(-8)}`
135+
}
136+
137+
const selectedRows = computed(() => {
138+
const raw = props.selectedIdentities
139+
if (!Array.isArray(raw)) return [] as Record<string, unknown>[]
140+
return raw as Record<string, unknown>[]
141+
})
142+
143+
/* eslint-disable @typescript-eslint/no-unused-vars -- template Pug */
144+
const $q = useQuasar()
145+
146+
const listWrapClass = computed(() => ($q.dark.isActive ? 'identity-modal-list-wrap--dark' : 'identity-modal-list-wrap--light'))
147+
148+
const identityListItems = computed(() => {
149+
return selectedRows.value.map((row, idx) => {
150+
const label = buildIdentityLabel(row)
151+
const id = idToString(row?._id)
152+
return {
153+
key: `${id || 'noid'}_${idx}`,
154+
label,
155+
idShort: shortId(id),
156+
initials: initialsFromLabel(label),
157+
avatarColor: AVATAR_COLORS[idx % AVATAR_COLORS.length],
158+
}
159+
})
160+
})
161+
162+
const mainText = computed(
163+
() =>
164+
`Vous allez supprimer ${selectedRows.value.length} identité${selectedRows.value.length > 1 ? 's' : ''}. Cette action est irréversible. Vérifiez la liste puis confirmez.`,
165+
)
166+
167+
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
38168
39169
const syncIdentities = () => {
40-
console.log('syncIdentities')
41170
onDialogOK({ success: true })
42171
}
43172
44173
const cancelSync = () => {
45-
console.log('cancelSync')
46174
onDialogCancel()
47175
}
48-
49-
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
176+
/* eslint-enable @typescript-eslint/no-unused-vars */
50177
</script>
178+
179+
<style scoped>
180+
.identity-modal-card {
181+
min-width: 340px;
182+
max-width: min(520px, 96vw);
183+
display: flex;
184+
flex-direction: column;
185+
max-height: min(580px, 88vh);
186+
border-radius: 12px;
187+
overflow: hidden;
188+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.14);
189+
}
190+
191+
.body--dark .identity-modal-card {
192+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
193+
}
194+
195+
.identity-modal-header {
196+
padding: 1rem 1.25rem;
197+
}
198+
199+
.identity-modal-body {
200+
flex: 1 1 auto;
201+
min-height: 0;
202+
display: flex;
203+
flex-direction: column;
204+
overflow: hidden;
205+
padding-top: 1rem;
206+
padding-bottom: 0.5rem;
207+
}
208+
209+
.identity-modal-body-top {
210+
flex-shrink: 0;
211+
}
212+
213+
.identity-modal-lede {
214+
line-height: 1.55;
215+
margin-bottom: 1rem;
216+
opacity: 0.92;
217+
}
218+
219+
.identity-modal-list-wrap--light {
220+
background: rgba(0, 0, 0, 0.03);
221+
border: 1px solid rgba(0, 0, 0, 0.08);
222+
}
223+
224+
.identity-modal-list-wrap--dark {
225+
background: rgba(255, 255, 255, 0.06);
226+
border: 1px solid rgba(255, 255, 255, 0.12);
227+
}
228+
229+
.identity-modal-list-wrap--light,
230+
.identity-modal-list-wrap--dark {
231+
flex: 1 1 auto;
232+
min-height: 0;
233+
border-radius: 10px;
234+
overflow-x: hidden;
235+
overflow-y: auto;
236+
}
237+
238+
.identity-modal-list-item {
239+
border-radius: 8px;
240+
margin-bottom: 2px;
241+
transition: background-color 0.15s ease;
242+
}
243+
244+
.identity-modal-list-wrap--light .identity-modal-list-item:hover {
245+
background-color: rgba(0, 0, 0, 0.04);
246+
}
247+
248+
.identity-modal-list-wrap--dark .identity-modal-list-item:hover {
249+
background-color: rgba(255, 255, 255, 0.06);
250+
}
251+
252+
.identity-modal-actions {
253+
padding: 0.5rem 1rem 1rem;
254+
background: rgba(0, 0, 0, 0.02);
255+
}
256+
257+
.body--dark .identity-modal-actions {
258+
background: rgba(255, 255, 255, 0.04);
259+
}
260+
261+
</style>

0 commit comments

Comments
 (0)