Frontend update

This commit is contained in:
Antoine Nguyen
2025-07-25 12:01:09 +02:00
parent df490d9ca4
commit ade5461f52
4 changed files with 177 additions and 73 deletions
@@ -348,11 +348,20 @@ defineExpose({
})
</script>
<style scoped>
<style scoped lang="scss">
.v-text-field--outlined :deep(fieldset) {
border-color: #bfc5d2;
}
.v-input--checkbox :deep(.v-label) {
font-size: 0.875rem !important;
}
a {
text-decoration: none;
color: rgb(var(--v-theme-primary));
&:visited {
color: rgb(var(--v-theme-primary));
}
}
</style>
@@ -29,6 +29,12 @@
class="flex-grow-0 w-33 mr-4"
></v-text-field>
<slot name="extraActions" />
<v-btn
variant="text"
icon="mdi-format-columns"
:title="$gettext('Customize displayed columns')"
@click="showColumnsForm = true"
></v-btn>
<v-menu location="bottom">
<template #activator="{ props }">
<v-btn
@@ -49,44 +55,45 @@
icon="mdi-reload"
@click="fetchIdentities"
></v-btn>
<v-radio-group
v-model="identityType"
inline
@update:model-value="updateType"
:readonly="loading"
>
<v-radio
color="primary"
:label="$gettext('Accounts')"
value="account"
></v-radio>
<v-radio
color="primary"
:label="$gettext('Alias')"
value="alias"
></v-radio>
</v-radio-group>
<v-combobox
v-model="columns"
:items="currentAvailableColumns"
multiple
return-object
>
</v-combobox>
<v-spacer></v-spacer>
<template #extension>
<v-radio-group
v-model="identityType"
inline
:readonly="loading"
hide-details
@update:model-value="updateType"
>
<v-radio
color="primary"
:label="$gettext('Accounts')"
value="account"
></v-radio>
<v-radio
color="primary"
:label="$gettext('Alias')"
value="alias"
></v-radio>
</v-radio-group>
</template>
</v-toolbar>
</template>
<template #[`item.identity`]="{ item }">
<template v-if="identityType === 'account'">
<router-link :to="{ name: 'AccountDetail', params: { id: item.pk } }">
{{ item.identity }}
</router-link>
</template>
<template v-else>
<router-link :to="{ name: 'AliasDetail', params: { id: item.pk } }">
{{ item.identity }}
</router-link>
</template>
<template #[`item.username`]="{ item }">
<router-link :to="{ name: 'AccountDetail', params: { id: item.pk } }">
{{ item.username }}
</router-link>
</template>
<template #[`item.address`]="{ item }">
<router-link :to="{ name: 'AliasDetail', params: { id: item.pk } }">
{{ item.address }}
</router-link>
</template>
<template #[`item.is_active`]="{ item }">
<v-icon
:color="item.is_active ? 'success' : 'error'"
:icon="item.is_active ? 'mdi-check-circle' : 'mdi-close-circle'"
variant="flat"
/>
</template>
<template #[`item.enabled`]="{ item }">
<v-icon
@@ -95,15 +102,24 @@
variant="flat"
/>
</template>
<template #[`item.tags`]="{ item }">
<v-chip
v-for="(tag, index) in item.tags"
:key="tag.name"
:color="tag.type !== 'idt' ? 'primary' : 'default'"
:class="index > 0 ? 'ml-2' : ''"
size="small"
>
{{ tag.label }}
<template #[`item.recipients`]="{ item }">
{{ displayRecipients(item) }}
</template>
<template #[`item.date_joined`]="{ item }">
{{ $date(item.date_joined) }}
</template>
<template #[`item.creation`]="{ item }">
{{ $date(item.creation) }}
</template>
<template #[`item.last_modification`]="{ item }">
{{ $date(item.last_modification) }}
</template>
<template #[`item.last_login`]="{ item }">
{{ $date(item.last_login) }}
</template>
<template #[`item.role`]="{ item }">
<v-chip color="default" size="small">
{{ item.role }}
</v-chip>
</template>
<template #[`item.actions`]="{ item }">
@@ -137,6 +153,13 @@
hide-details
/>
</ConfirmDialog>
<v-dialog v-model="showColumnsForm" max-width="500">
<ChooseColumnsForm
:columns="columns"
:available-columns="currentAvailableColumns"
@apply="updateDisplayedColumns"
/>
</v-dialog>
</v-card>
</template>
@@ -145,44 +168,39 @@ import { ref, onMounted, computed } from 'vue'
import { useGettext } from 'vue3-gettext'
import MenuItems from '@/components/tools/MenuItems.vue'
import ConfirmDialog from '@/components/tools/ConfirmDialog.vue'
import ChooseColumnsForm from '@/components/tools/ChooseColumnsForm.vue'
import { useRouter } from 'vue-router'
import { useBusStore } from '@/stores'
import accountsApi from '@/api/accounts'
import aliasesApi from '@/api/aliases'
import debounce from 'debounce'
// import identitiesApi from '@/api/identities'
const { $gettext, $pgettext } = useGettext()
const { $gettext } = useGettext()
const router = useRouter()
const { displayNotification } = useBusStore()
const availableAliasesColumns = {
address: { title: $gettext('Address'), key: 'address', value: 'address' },
enabled: { title: $gettext('Enabled'), key: 'enabled', value: 'enabled' },
address: { title: $gettext('Address'), key: 'address' },
enabled: { title: $gettext('Enabled'), key: 'enabled' },
recipients: {
title: $gettext('Recipients'),
key: 'recipients',
value: 'recipients',
},
expire_at: {
title: $gettext('Expire at'),
key: 'expire_at',
value: 'expire_at',
},
description: {
title: $gettext('Description'),
key: 'description',
value: 'description',
},
creation: {
title: $gettext('Creation date'),
key: 'creation',
value: 'creation',
},
last_modification: {
title: $gettext('Last modification'),
key: 'last_modification',
value: 'last_modification',
},
}
@@ -193,54 +211,45 @@ const defaultAliasColumns = [
]
const availableAccountsColumns = {
username: { title: $gettext('Name'), key: 'username', value: 'username' },
username: { title: $gettext('Name'), key: 'username' },
first_name: {
title: $gettext('First name'),
key: 'first_name',
value: 'first_name',
},
last_name: {
title: $gettext('Last Name'),
key: 'last_name',
value: 'last_name',
},
is_active: { title: $gettext('Enabled'), key: 'enabled', value: 'is_active' },
role: { title: $gettext('Role'), key: 'role', value: 'role' },
language: { title: $gettext('Language'), key: 'language', value: 'language' },
is_active: { title: $gettext('Enabled'), key: 'is_active' },
role: { title: $gettext('Role'), key: 'role' },
language: { title: $gettext('Language'), key: 'language' },
phone_number: {
title: $gettext('Phone number'),
key: 'phone_number',
value: 'phone_number',
},
secondary_email: {
title: $gettext('Secondary email'),
key: 'secondary_email',
value: 'secondary_email',
},
tfa_enabled: {
title: $gettext('TFA enabled'),
key: 'tfa_enabled',
value: 'tfa_enabled',
},
totp_enabled: {
title: $gettext('TOTP enabled'),
key: 'totp_enabled',
value: 'totp_enabled',
},
webauth_enabled: {
title: $gettext('WebAutn enabled'),
key: 'webauth_enabled',
value: 'webauth_enabled',
},
date_joined: {
title: $gettext('Created date'),
title: $gettext('Creation date'),
key: 'date_joined',
value: 'date_joined',
},
last_login: {
title: $gettext('Last login'),
key: 'last_login',
value: 'last_login',
},
}
@@ -263,6 +272,7 @@ const currentPage = ref(1)
const itemsPerPageR = ref(10)
const sortByR = ref([])
const columns = ref(defaultAccountsColumns)
const showColumnsForm = ref(false)
const headers = computed(() => {
const result = columns.value
@@ -292,7 +302,7 @@ async function updateType(value) {
} else {
columns.value = defaultAliasColumns
}
fetchIdentities()
await fetchIdentities()
}
async function updatedOptions({ page, itemsPerPage, sortBy }) {
@@ -329,9 +339,16 @@ async function fetchIdentities() {
}
}
function updateDisplayedColumns(selection) {
columns.value = currentAvailableColumns.value.filter((col) =>
selection.includes(col.key)
)
showColumnsForm.value = false
}
function getMenuItems(item) {
const result = []
if (identityType === 'account') {
if (identityType.value === 'account') {
item.possible_actions.forEach((element) => {
result.push({
label: element.label,
@@ -355,7 +372,7 @@ function getMenuItems(item) {
onClick: deleteAccount,
color: 'red',
})
} else if (identityType === 'alias') {
} else if (identityType.value === 'alias') {
result.push({
label: $gettext('Edit'),
icon: 'mdi-circle-edit-outline',
@@ -439,6 +456,13 @@ async function deleteAlias(alias) {
}
}
function displayRecipients(alias) {
if (!alias.recipients) {
return ''
}
return alias.recipients.join(', ')
}
onMounted(() => {
fetchIdentities()
})
@@ -449,3 +473,14 @@ defineExpose({
fetchIdentities = debounce(fetchIdentities, 500)
</script>
<style scoped lang="scss">
a {
text-decoration: none;
color: rgb(var(--v-theme-primary));
&:visited {
color: rgb(var(--v-theme-primary));
}
}
</style>
@@ -0,0 +1,43 @@
<template>
<v-card :title="$gettext('Choose columns to display')">
<v-card-text>
<div v-for="column in props.availableColumns" :key="column.key">
<v-checkbox
v-model="model"
:value="column.key"
:label="column.title"
color="primary"
hide-details
density="compact"
/>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" variant="outlined" @click="apply">
{{ $gettext('Apply') }}
</v-btn>
</v-card-actions>
</v-card>
</template>
<script setup>
const props = defineProps({
availableColumns: {
type: Array,
default: null,
},
columns: {
type: Array,
default: null,
},
})
const model = defineModel({ type: Array })
const emit = defineEmits(['apply'])
const apply = () => {
emit('apply', model.value)
}
model.value = props.columns.map((col) => col.key)
</script>
+18 -1
View File
@@ -402,9 +402,13 @@ class AccountSerializer(v1_serializers.AccountSerializer):
aliases = serializers.ListField(
child=lib_fields.DRFEmailFieldUTF8(), source="mailbox.alias_addresses"
)
possible_actions = serializers.SerializerMethodField()
class Meta(v1_serializers.AccountSerializer.Meta):
fields = v1_serializers.AccountSerializer.Meta.fields + ("aliases",)
fields = v1_serializers.AccountSerializer.Meta.fields + (
"aliases",
"possible_actions",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -412,6 +416,19 @@ class AccountSerializer(v1_serializers.AccountSerializer):
return
self.fields["resources"] = serializers.SerializerMethodField()
def get_possible_actions(self, identity) -> list[IdPossibleActionsSerializer]:
actions = admin_signals.extra_account_identities_actions.send(
self.__class__, account=identity
)
cleaned_actions = []
for action in actions:
try:
serialized_data = IdPossibleActionsSerializer(action[1]).data
cleaned_actions.append(serialized_data)
except (ValidationError, AttributeError):
continue
return cleaned_actions
def get_resources(self, account) -> list[AccountResourceSerializer]:
if account.role == "SimpleUsers":
return []