mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-04-10 15:05:35 -04:00
chore: Nuxt 4 upgrade (#7426)
This commit is contained in:
232
frontend/app/components/global/CrudTable.vue
Normal file
232
frontend/app/components/global/CrudTable.vue
Normal file
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-card-actions class="flex-wrap">
|
||||
<v-menu
|
||||
v-if="tableConfig.hideColumns"
|
||||
offset-y
|
||||
bottom
|
||||
nudge-bottom="6"
|
||||
:close-on-content-click="false"
|
||||
>
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
color="accent"
|
||||
variant="elevated"
|
||||
v-bind="activatorProps"
|
||||
>
|
||||
<v-icon>
|
||||
{{ $globals.icons.cog }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-checkbox
|
||||
v-for="itemValue in localHeaders"
|
||||
:key="itemValue.text + itemValue.show"
|
||||
v-model="itemValue.show"
|
||||
density="compact"
|
||||
flat
|
||||
inset
|
||||
:label="itemValue.text"
|
||||
hide-details
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<BaseOverflowButton
|
||||
v-if="bulkActions.length > 0"
|
||||
:disabled="selected.length < 1"
|
||||
mode="event"
|
||||
color="info"
|
||||
variant="elevated"
|
||||
:items="bulkActions"
|
||||
v-on="bulkActionListener"
|
||||
/>
|
||||
<slot name="button-row" />
|
||||
</v-card-actions>
|
||||
<div class="mx-2 clip-width">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
variant="underlined"
|
||||
:label="$t('search.search')"
|
||||
/>
|
||||
</div>
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
return-object
|
||||
:headers="activeHeaders"
|
||||
:show-select="bulkActions.length > 0"
|
||||
:sort-by="sortBy"
|
||||
:items="data || []"
|
||||
:items-per-page="15"
|
||||
:search="search"
|
||||
class="elevation-2"
|
||||
>
|
||||
<template
|
||||
v-for="header in headersWithoutActions"
|
||||
#[`item.${header.value}`]="{ item }"
|
||||
>
|
||||
<slot
|
||||
:name="'item.' + header.value"
|
||||
v-bind="{ item }"
|
||||
>
|
||||
{{ item[header.value] }}
|
||||
</slot>
|
||||
</template>
|
||||
<template #[`item.actions`]="{ item }">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.edit,
|
||||
text: $t('general.edit'),
|
||||
event: 'edit',
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: $t('general.delete'),
|
||||
event: 'delete',
|
||||
},
|
||||
]"
|
||||
@delete="$emit('delete-one', item)"
|
||||
@edit="$emit('edit-one', item)"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
<v-card-actions class="justify-end">
|
||||
<slot name="button-bottom" />
|
||||
<BaseButton
|
||||
color="info"
|
||||
@click="downloadAsJson(data, 'export.json')"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.download }}
|
||||
</template>
|
||||
{{ $t("general.download") }}
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { downloadAsJson } from "~/composables/use-utils";
|
||||
|
||||
export interface TableConfig {
|
||||
hideColumns: boolean;
|
||||
canExport: boolean;
|
||||
}
|
||||
|
||||
export interface TableHeaders {
|
||||
text: string;
|
||||
value: string;
|
||||
show: boolean;
|
||||
align?: "start" | "center" | "end";
|
||||
sortable?: boolean;
|
||||
sort?: (a: any, b: any) => number;
|
||||
}
|
||||
|
||||
export interface BulkAction {
|
||||
icon: string;
|
||||
text: string;
|
||||
event: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
tableConfig: {
|
||||
type: Object as () => TableConfig,
|
||||
default: () => ({
|
||||
hideColumns: false,
|
||||
canExport: false,
|
||||
}),
|
||||
},
|
||||
headers: {
|
||||
type: Array as () => TableHeaders[],
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Array as () => any[],
|
||||
required: true,
|
||||
},
|
||||
bulkActions: {
|
||||
type: Array as () => BulkAction[],
|
||||
default: () => [],
|
||||
},
|
||||
initialSort: {
|
||||
type: String,
|
||||
default: "id",
|
||||
},
|
||||
initialSortDesc: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete-one" | "edit-one", item: any): void;
|
||||
(e: "bulk-action", event: string, items: any[]): void;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
const sortBy = computed<{ key: string; order: "asc" | "desc" }[]>(() => [{
|
||||
key: props.initialSort,
|
||||
order: props.initialSortDesc ? "desc" : "asc",
|
||||
}]);
|
||||
|
||||
// ===========================================================
|
||||
// Reactive Headers
|
||||
// Create a local reactive copy of headers that we can modify
|
||||
const localHeaders = ref([...props.headers]);
|
||||
|
||||
// Watch for changes in props.headers and update local copy
|
||||
watch(() => props.headers, (newHeaders) => {
|
||||
localHeaders.value = [...newHeaders];
|
||||
}, { deep: true });
|
||||
|
||||
const filteredHeaders = computed<string[]>(() => {
|
||||
return localHeaders.value.filter(header => header.show).map(header => header.value);
|
||||
});
|
||||
|
||||
const headersWithoutActions = computed(() =>
|
||||
localHeaders.value
|
||||
.filter(header => filteredHeaders.value.includes(header.value))
|
||||
.map(header => ({
|
||||
...header,
|
||||
title: i18n.t(header.text),
|
||||
})),
|
||||
);
|
||||
|
||||
const activeHeaders = computed(() => [
|
||||
...headersWithoutActions.value,
|
||||
{ title: "", value: "actions", show: true, align: "end" },
|
||||
]);
|
||||
|
||||
const selected = ref<any[]>([]);
|
||||
|
||||
// ===========================================================
|
||||
// Bulk Action Event Handler
|
||||
|
||||
const bulkActionListener = computed(() => {
|
||||
const handlers: { [key: string]: () => void } = {};
|
||||
|
||||
props.bulkActions.forEach((action) => {
|
||||
handlers[action.event] = () => {
|
||||
emit("bulk-action", action.event, selected.value);
|
||||
// clear selection
|
||||
selected.value = [];
|
||||
};
|
||||
});
|
||||
|
||||
return handlers;
|
||||
});
|
||||
|
||||
const search = ref("");
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.clip-width {
|
||||
max-width: 400px;
|
||||
}
|
||||
.v-btn--disabled {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user