mirror of
https://github.com/hay-kot/mealie.git
synced 2025-07-07 13:32:21 -07:00
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Docker Nightly Production / Backend Server Tests (push) Waiting to run
Docker Nightly Production / Frontend Tests (push) Waiting to run
Docker Nightly Production / Build Package (push) Waiting to run
Docker Nightly Production / Build Tagged Release (push) Blocked by required conditions
Docker Nightly Production / Notify Discord (push) Blocked by required conditions
Release Drafter / ✏️ Draft release (push) Waiting to run
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com> Co-authored-by: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com>
235 lines
5.4 KiB
Vue
235 lines
5.4 KiB
Vue
<template>
|
|
<div>
|
|
<v-card-actions>
|
|
<v-menu
|
|
v-if="tableConfig.hideColumns"
|
|
offset-y
|
|
bottom
|
|
nudge-bottom="6"
|
|
:close-on-content-click="false"
|
|
>
|
|
<template #activator="{ props }">
|
|
<v-btn
|
|
color="accent"
|
|
variant="elevated"
|
|
v-bind="props"
|
|
>
|
|
<v-icon>
|
|
{{ $globals.icons.cog }}
|
|
</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<v-card>
|
|
<v-card-text>
|
|
<v-checkbox
|
|
v-for="itemValue in headers"
|
|
:key="itemValue.text + itemValue.show"
|
|
v-model="filteredHeaders"
|
|
:value="itemValue.value"
|
|
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-bind="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"
|
|
item-key="id"
|
|
: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 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?: string;
|
|
sortable?: boolean;
|
|
sort?: (a: any, b: any) => number;
|
|
}
|
|
|
|
export interface BulkAction {
|
|
icon: string;
|
|
text: string;
|
|
event: string;
|
|
}
|
|
|
|
export default defineNuxtComponent({
|
|
props: {
|
|
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,
|
|
},
|
|
},
|
|
emits: ["delete-one", "edit-one"],
|
|
setup(props, context) {
|
|
const i18n = useI18n();
|
|
const sortBy = computed(() => [{
|
|
key: props.initialSort,
|
|
order: props.initialSortDesc ? "desc" : "asc",
|
|
}]);
|
|
|
|
// ===========================================================
|
|
// Reactive Headers
|
|
const filteredHeaders = computed<string[]>(() => {
|
|
return props.headers.filter(header => header.show).map(header => header.value);
|
|
});
|
|
|
|
const headersWithoutActions = computed(() =>
|
|
props.headers
|
|
.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] = () => {
|
|
context.emit(action.event, selected.value);
|
|
// clear selection
|
|
selected.value = [];
|
|
};
|
|
});
|
|
|
|
return handlers;
|
|
});
|
|
|
|
const search = ref("");
|
|
|
|
return {
|
|
sortBy,
|
|
selected,
|
|
filteredHeaders,
|
|
headersWithoutActions,
|
|
activeHeaders,
|
|
bulkActionListener,
|
|
search,
|
|
downloadAsJson,
|
|
};
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.clip-width {
|
|
max-width: 400px;
|
|
}
|
|
.v-btn--disabled {
|
|
opacity: 0.5 !important;
|
|
}
|
|
</style>
|