Move most all table views to new action menu pattern

This commit is contained in:
Johannes Zellner
2025-08-18 17:25:50 +02:00
parent 4d8b6c5ea7
commit 72fdc707ee
6 changed files with 188 additions and 42 deletions
+22 -4
View File
@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef } from 'vue';
import { Button, TableView, Dialog } from '@cloudron/pankow';
import { Button, Menu, TableView, Dialog } from '@cloudron/pankow';
import { eachLimit } from 'async';
import Section from '../components/Section.vue';
import MailinglistDialog from '../components/MailinglistDialog.vue';
@@ -29,6 +29,24 @@ const columns = {
actions: {}
};
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onActionMenu(mailinglist, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-pencil-alt',
label: t('main.action.edit'),
action: onAddOrEdit.bind(null, mailinglist),
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
action: onRemove.bind(null, mailinglist),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
const busy = ref(true);
const mailinglists = ref([]);
const domains = ref([]);
@@ -108,6 +126,7 @@ onMounted(async () => {
<template>
<div class="content">
<Menu ref="actionMenuElement" :model="actionMenuModel" />
<Dialog ref="removeDialog"
:title="$t('email.deleteMailinglistDialog.title', { name: removeMailinglist.name, domain: removeMailinglist.domain })"
:confirm-label="$t('email.deleteMailinglistDialog.deleteAction')"
@@ -139,9 +158,8 @@ onMounted(async () => {
{{ mailinglist.members.join(', ') }}
</template>
<template #actions="mailinglist">
<div class="table-actions">
<Button tool secondary small icon="fa fa-pencil-alt" @click.stop="onAddOrEdit(mailinglist)"></Button>
<Button tool danger small icon="fa-solid fa-trash-alt" @click.stop="onRemove(mailinglist)"></Button>
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onActionMenu(mailinglist, $event)" icon="fa-solid fa-ellipsis" />
</div>
</template>
</TableView>
+28 -7
View File
@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { computed, reactive, onMounted, ref, useTemplateRef } from 'vue';
import { Button, TableView, ProgressBar, ButtonGroup, FormGroup, Checkbox, Dialog } from '@cloudron/pankow';
import { Button, Menu, TableView, ProgressBar, FormGroup, Checkbox, Dialog } from '@cloudron/pankow';
import { prettyBinarySize } from '@cloudron/pankow/utils';
import { each } from 'async';
import Section from '../components/Section.vue';
@@ -37,6 +37,30 @@ const columns = {
actions: {}
};
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onActionMenu(service, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-pencil-alt',
label: t('main.action.edit'),
visible: service.status !== 'disabled' && service.memoryLimit,
action: onEdit.bind(null, service),
}, {
icon: 'fa-solid fa-sync-alt',
label: t('services.restartActionTooltip'),
visible: service.id !== 'box',
disabled: (service.status === 'starting' && !service.config.recoveryMode),
action: onRestart.bind(null, service.id),
}, {
icon: 'fa-solid fa-fw fa-file-alt',
label: t('logs.title'),
target: '_blank',
href: `/logs.html?id=${service.id}`,
}];
actionMenuElement.value.open(event, event.currentTarget);
}
const services = reactive({
box: {
name: 'cloudron',
@@ -178,6 +202,7 @@ onMounted(async () => {
<template>
<div class="content">
<Menu ref="actionMenuElement" :model="actionMenuModel" />
<Dialog ref="dialog"
:title="$t('services.configure.title', { name: editService.name })"
:confirm-busy="editBusy"
@@ -220,12 +245,8 @@ onMounted(async () => {
<span v-show="service.memoryLimit">{{ prettyBinarySize(service.memoryLimit) }}</span>
</template>
<template #actions="service">
<div class="table-actions">
<ButtonGroup>
<Button small tool secondary v-if="service.status !== 'disabled' && service.config.memoryLimit" @click="onEdit(service)" v-tooltip="$t('services.configureActionTooltip')" icon="fa-solid fa fa-pencil-alt"/>
<Button small tool secondary v-if="service.id !== 'box'" @click="onRestart(service.id)" :loading="service.status === 'starting' && !service.config.recoveryMode" v-tooltip="$t('services.restartActionTooltip')" icon="fa-solid fa-sync-alt"/>
<Button tool small secondary :href="`/logs.html?id=${service.id}`" target="_blank" v-tooltip="$t('logs.title')" icon="fa-solid fa-file-alt" />
</ButtonGroup>
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onActionMenu(service, $event)" icon="fa-solid fa-ellipsis" />
</div>
</template>
</TableView>
@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, useTemplateRef, computed } from 'vue';
import { Button, Dialog, TableView, FormGroup, TextInput, InputGroup, InputDialog } from '@cloudron/pankow';
import { Button, Menu, Dialog, TableView, FormGroup, TextInput, InputGroup, InputDialog } from '@cloudron/pankow';
import { copyToClipboard } from '@cloudron/pankow/utils';
import Section from '../components/Section.vue';
import DashboardModel from '../models/DashboardModel.js';
@@ -19,6 +19,24 @@ const columns = {
actions: {}
};
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onActionMenu(client, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-pencil-alt',
label: t('main.action.edit'),
action: onEdit.bind(null, client),
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
action: onRemove.bind(null, client),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
const inputDialog = useTemplateRef('inputDialog');
const editDialog = useTemplateRef('editDialog');
@@ -83,7 +101,8 @@ async function onSubmit() {
async function onRemove(client) {
const yes = await inputDialog.value.confirm({
message: t('oidc.deleteClientDialog.title', { client: client.name }) + ' ' + t('oidc.deleteClientDialog.description'),
title: t('oidc.deleteClientDialog.title', { client: client.name }),
message: t('oidc.deleteClientDialog.description'),
confirmStyle: 'danger',
confirmLabel: t('main.dialog.delete'),
rejectLabel: t('main.dialog.cancel')
@@ -119,6 +138,7 @@ onMounted(async () => {
<template>
<div class="content">
<Menu ref="actionMenuElement" :model="actionMenuModel" />
<InputDialog ref="inputDialog" />
<Dialog ref="editDialog"
:title="clientId ? $t('oidc.editClientDialog.title', { client: clientName }) : $t('oidc.newClientDialog.title')"
@@ -184,9 +204,8 @@ onMounted(async () => {
<TableView :columns="columns" :model="clients" :placeholder="$t('oidc.clients.empty')">
<template #actions="client">
<div class="table-actions">
<Button tool secondary small icon="fa-solid fa-pencil-alt" @click.stop="onEdit(client)"></Button>
<Button tool danger small icon="fa-solid fa-trash-alt" @click.stop="onRemove(client)"></Button>
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onActionMenu(client, $event)" icon="fa-solid fa-ellipsis" />
</div>
</template>
</TableView>
+59 -14
View File
@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { ref, onMounted, computed, useTemplateRef, inject } from 'vue';
import { Button, ButtonGroup, TextInput, SingleSelect, TableView, InputDialog } from '@cloudron/pankow';
import { Button, Menu, TextInput, SingleSelect, TableView, InputDialog } from '@cloudron/pankow';
import { ROLES } from '../constants.js';
import Section from '../components/Section.vue';
import UserDialog from '../components/UserDialog.vue';
@@ -37,6 +37,43 @@ const usersColumns = {
actions: {}
};
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onUserActionMenu(user, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-paper-plane',
label: t('users.users.invitationTooltip'),
visible: !user.inviteAccepted && !isMe(user) && !user.source,
disabled: !canEdit(user),
action: onInvitation.bind(null, user),
}, {
icon: 'fa-solid fa-key',
label: t('users.users.resetPasswordTooltip'),
visible: user.inviteAccepted && !user.source,
disabled: !canEdit(user),
action: onPasswordReset.bind(null, user),
}, {
icon: 'fa-solid fa-user-secret',
label: t('users.users.setGhostTooltip'),
visible: canImpersonate(user),
action: onImpersonate.bind(null, user),
}, {
icon: 'fa fa-pencil-alt',
label: t('main.action.edit'),
disabled: !canEdit(user),
action: onEditOrAddUser.bind(null, user),
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
disabled: !canEdit(user),
action: onRemoveUser.bind(null, user),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
const groupsColumns = {
name: {
label: t('users.groups.name'),
@@ -50,6 +87,22 @@ const groupsColumns = {
actions: {}
};
function onGroupActionMenu(group, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-pencil-alt',
label: t('main.action.edit'),
action: onEditOrAddGroup.bind(null, group),
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
action: onRemoveGroup.bind(null, group),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
const profile = ref({});
const busy = ref(true);
const filterOptions = ref([
@@ -221,6 +274,7 @@ onMounted(async () => {
<template>
<div class="content">
<Menu ref="actionMenuElement" :model="actionMenuModel" />
<InputDialog ref="inputDialog" />
<UserDialog ref="userDialog" @success="refreshUsers()"/>
<GroupDialog ref="groupDialog" @success="refreshGroups()"/>
@@ -255,14 +309,8 @@ onMounted(async () => {
</span>
</template>
<template #actions="user">
<div class="table-actions">
<ButtonGroup>
<Button small tool secondary :disabled="!canEdit(user)" v-if="!user.inviteAccepted && !isMe(user) && !user.source" @click.stop="onInvitation(user)" v-tooltip="$t('users.users.invitationTooltip')" icon="fa-solid fa-paper-plane" />
<Button small tool secondary :disabled="!canEdit(user)" v-if="user.inviteAccepted && !user.source" @click.stop="onPasswordReset(user)" v-tooltip="$t('users.users.resetPasswordTooltip')" icon="fa-solid fa-key" />
<Button small tool secondary v-if="canImpersonate(user)" @click.stop="onImpersonate(user)" v-tooltip="$t('users.users.setGhostTooltip')" icon="fa-solid fa-user-secret" />
<Button small tool secondary :disabled="!canEdit(user)" @click.stop="onEditOrAddUser(user)" v-tooltip="$t('users.users.editUserTooltip')" icon="fa fa-pencil-alt" />
</ButtonGroup>
<Button small tool danger :disabled="!canEdit(user) || isMe(user)" @click.stop="onRemoveUser(user)" v-tooltip="$t('users.users.removeUserTooltip')" icon="far fa-trash-alt" />
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onUserActionMenu(user, $event)" icon="fa-solid fa-ellipsis" />
</div>
</template>
</TableView>
@@ -283,11 +331,8 @@ onMounted(async () => {
{{ groupMembers(group) }}
</template>
<template #actions="group">
<div class="table-actions">
<ButtonGroup>
<Button tool small secondary @click.stop="onEditOrAddGroup(group)" v-tooltip="'Edit Group'" icon="fa fa-pencil-alt" />
</ButtonGroup>
<Button tool small danger @click.stop="onRemoveGroup(group)" v-tooltip="'Remove Group'" icon="far fa-trash-alt" />
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onGroupActionMenu(group, $event)" icon="fa-solid fa-ellipsis" />
</div>
</template>
</TableView>
+33 -8
View File
@@ -5,7 +5,7 @@ const i18n = useI18n();
const t = i18n.t;
import { computed, ref, useTemplateRef, onMounted } from 'vue';
import { Button, ButtonGroup, Checkbox, Dialog, SingleSelect, FormGroup, InputDialog, NumberInput, PasswordInput, TableView, TextInput } from '@cloudron/pankow';
import { Button, Menu, Checkbox, Dialog, SingleSelect, FormGroup, InputDialog, NumberInput, PasswordInput, TableView, TextInput } from '@cloudron/pankow';
import Section from '../components/Section.vue';
import StateLED from '../components/StateLED.vue';
import VolumesModel from '../models/VolumesModel.js';
@@ -31,6 +31,35 @@ const columns = {
actions: {}
};
const actionMenuModel = ref([]);
const actionMenuElement = useTemplateRef('actionMenuElement');
function onActionMenu(volume, event) {
actionMenuModel.value = [{
icon: 'fa-solid fa-sync-alt',
label: t('volumes.remountActionTooltip'),
visible: volume.mountType === 'sshfs' || volume.mountType === 'cifs' || volume.mountType === 'nfs' || volume.mountType === 'ext4' || volume.mountType === 'xfs',
action: remount.bind(null, volume),
}, {
icon: 'fa-solid fa-pencil-alt',
label: t('main.action.edit'),
visible: volume.mountType === 'sshfs' || volume.mountType === 'cifs' || volume.mountType === 'nfs',
action: openVolumeDialog.bind(null, volume),
}, {
icon: 'fa-solid fa-folder',
label: t('volumes.openFileManagerActionTooltip'),
target: '_blank',
href: '/filemanager.html#/home/volume/' + volume.id,
}, {
separator: true,
}, {
icon: 'fa-solid fa-trash-alt',
label: t('main.action.remove'),
action: onRemove.bind(null, volume),
}];
actionMenuElement.value.open(event, event.currentTarget);
}
const busy = ref(true);
const volumes = ref([]);
const volumeDialogData = ref({
@@ -202,6 +231,7 @@ onMounted(async () =>{
<template>
<div class="content">
<Menu ref="actionMenuElement" :model="actionMenuModel" />
<InputDialog ref="inputDialog" />
<Dialog ref="volumeDialog"
@@ -298,13 +328,8 @@ onMounted(async () =>{
</div>
</template>
<template #actions="volume">
<div class="table-actions">
<ButtonGroup>
<Button tool secondary small icon="fa fa-sync-alt" v-if="volume.mountType === 'sshfs' || volume.mountType === 'cifs' || volume.mountType === 'nfs' || volume.mountType === 'ext4' || volume.mountType === 'xfs'" v-tooltip="$t('volumes.remountActionTooltip')" @click="remount(volume)"></Button>
<Button tool secondary small icon="fa fa-pencil-alt" v-if="volume.mountType === 'sshfs' || volume.mountType === 'cifs' || volume.mountType === 'nfs'" v-tooltip="$t('volumes.editActionTooltip')" @click="openVolumeDialog(volume)"></Button>
<Button tool secondary small icon="fas fa-folder" v-tooltip="$t('volumes.openFileManagerActionTooltip')" :href="'/filemanager.html#/home/volume/' + volume.id" target="_blank"></Button>
</ButtonGroup>
<Button tool danger small icon="fa-solid fa-trash-alt" v-tooltip="$t('volumes.removeVolumeActionTooltip')" @click="onRemove(volume)"></Button>
<div style="text-align: right;">
<Button tool plain secondary @click.capture="onActionMenu(volume, $event)" icon="fa-solid fa-ellipsis" />
</div>
</template>
</TableView>