Changes to UI and other stuff
This commit is contained in:
@@ -1,318 +1,182 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from 'vue';
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import { faSearch, faTimes, faDownload, faEllipsisVertical, faGear, faPlus, faFilter } from '@fortawesome/free-solid-svg-icons';
|
||||
import Dropdown from '../Dropdown.vue';
|
||||
import { Input } from '@/Components/ui/input';
|
||||
import { Button } from '@/Components/ui/button';
|
||||
import { computed, ref } from "vue";
|
||||
import { X, Settings2 } from "lucide-vue-next";
|
||||
import { Input } from "@/Components/ui/input";
|
||||
import { Button } from "@/Components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/Components/ui/select';
|
||||
import { Label } from '@/Components/ui/label';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/Components/ui/popover';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/Components/ui/dropdown-menu';
|
||||
} from "@/Components/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/Components/ui/popover";
|
||||
import DataTableViewOptions from "./DataTableViewOptions.vue";
|
||||
|
||||
/**
|
||||
* DataTable Toolbar Component
|
||||
* Simplified toolbar following shadcn-vue patterns for TanStack Table integration
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
search: { type: String, default: '' },
|
||||
showSearch: { type: Boolean, default: false },
|
||||
showPageSize: { type: Boolean, default: false },
|
||||
pageSize: { type: Number, default: 10 },
|
||||
pageSizeOptions: { type: Array, default: () => [10, 25, 50, 100] },
|
||||
selectedCount: { type: Number, default: 0 },
|
||||
showSelectedCount: { type: Boolean, default: false }, // Control visibility of selected count badge
|
||||
showExport: { type: Boolean, default: false },
|
||||
showAdd: { type: Boolean, default: false }, // Control visibility of add buttons dropdown
|
||||
showOptions: { type: Boolean, default: false }, // Control visibility of custom options slot
|
||||
showFilters: { type: Boolean, default: false }, // Control visibility of filters button
|
||||
showOptionsMenu: { type: Boolean, default: false }, // Control visibility of options menu (three dots)
|
||||
compact: { type: Boolean, default: false }, // New prop to toggle compact menu mode
|
||||
hasActiveFilters: { type: Boolean, default: false }, // External indicator for active filters
|
||||
// TanStack Table instance
|
||||
table: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
// Column to filter on (e.g., 'email', 'name')
|
||||
filterColumn: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
// Placeholder text for filter input
|
||||
filterPlaceholder: {
|
||||
type: String,
|
||||
default: "Filter...",
|
||||
},
|
||||
// Show view options (column visibility toggle)
|
||||
showViewOptions: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// Show per-page selector
|
||||
showPerPageSelector: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// Current per page value
|
||||
perPage: {
|
||||
type: Number,
|
||||
default: 15,
|
||||
},
|
||||
// Per page options
|
||||
pageSizeOptions: {
|
||||
type: Array,
|
||||
default: () => [10, 15, 25, 50, 100],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:search', 'update:page-size', 'export']);
|
||||
const emit = defineEmits(["update:perPage"]);
|
||||
|
||||
const internalSearch = ref(props.search);
|
||||
const menuOpen = ref(false);
|
||||
// Popover state
|
||||
const settingsPopoverOpen = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.search,
|
||||
(newVal) => {
|
||||
internalSearch.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
const hasActiveFilters = computed(() => {
|
||||
return !!internalSearch.value || props.selectedCount > 0;
|
||||
// Check if any filters are active
|
||||
const isFiltered = computed(() => {
|
||||
if (!props.filterColumn) return false;
|
||||
const column = props.table.getColumn(props.filterColumn);
|
||||
return column && column.getFilterValue();
|
||||
});
|
||||
|
||||
function clearSearch() {
|
||||
internalSearch.value = '';
|
||||
emit('update:search', '');
|
||||
}
|
||||
// Get/set filter value
|
||||
const filterValue = computed({
|
||||
get() {
|
||||
if (!props.filterColumn) return "";
|
||||
const column = props.table.getColumn(props.filterColumn);
|
||||
return column?.getFilterValue() ?? "";
|
||||
},
|
||||
set(value) {
|
||||
if (!props.filterColumn) return;
|
||||
const column = props.table.getColumn(props.filterColumn);
|
||||
column?.setFilterValue(value);
|
||||
},
|
||||
});
|
||||
|
||||
function handleSearchInput() {
|
||||
emit('update:search', internalSearch.value);
|
||||
}
|
||||
|
||||
function handlePageSizeChange(value) {
|
||||
emit('update:page-size', Number(value));
|
||||
}
|
||||
|
||||
function handleExport(format) {
|
||||
emit('export', format);
|
||||
menuOpen.value = false;
|
||||
// Reset all filters
|
||||
function resetFilters() {
|
||||
props.table.resetColumnFilters();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<!-- Left side: Search and Add buttons dropdown -->
|
||||
<div class="flex items-center gap-3 flex-1">
|
||||
<!-- Search (always visible if showSearch is true) -->
|
||||
<div v-if="showSearch && !compact" class="flex-1 max-w-sm">
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 z-10">
|
||||
<FontAwesomeIcon :icon="faSearch" class="h-4 w-4 text-gray-400" />
|
||||
</div>
|
||||
<Input
|
||||
v-model="internalSearch"
|
||||
@input="handleSearchInput"
|
||||
type="text"
|
||||
placeholder="Iskanje..."
|
||||
class="pl-10"
|
||||
:class="internalSearch ? 'pr-10' : ''"
|
||||
/>
|
||||
<Button
|
||||
v-if="internalSearch"
|
||||
@click="clearSearch"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="absolute inset-y-0 right-0 h-full w-auto px-3 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTimes" class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Left side: Search and Filters -->
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<!-- Filter Input -->
|
||||
<Input
|
||||
v-if="filterColumn"
|
||||
v-model="filterValue"
|
||||
:placeholder="filterPlaceholder"
|
||||
class="h-8 w-[150px] lg:w-[250px]"
|
||||
/>
|
||||
|
||||
<!-- Add buttons dropdown (after search input) -->
|
||||
<Dropdown v-if="$slots.add && showAdd && !compact" align="left">
|
||||
<template #trigger>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPlus" class="h-4 w-4" />
|
||||
<span class="sr-only">Dodaj</span>
|
||||
</Button>
|
||||
</template>
|
||||
<template #content>
|
||||
<slot name="add" />
|
||||
</template>
|
||||
</Dropdown>
|
||||
<!-- Custom filter slots -->
|
||||
<slot name="filters" :table="table" />
|
||||
|
||||
<!-- Custom options dropdown (after search input and add buttons) -->
|
||||
<div v-if="$slots.options && showOptions && !compact" class="flex items-center">
|
||||
<slot name="options" />
|
||||
</div>
|
||||
<!-- Reset filters button -->
|
||||
<Button
|
||||
v-if="isFiltered"
|
||||
variant="ghost"
|
||||
@click="resetFilters"
|
||||
class="h-8 px-2 lg:px-3"
|
||||
>
|
||||
Reset
|
||||
<X class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filters button (after options, before right side) -->
|
||||
<Popover v-if="showFilters && $slots.filters && !compact">
|
||||
<!-- Right side: Actions and View Options -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Custom action slots -->
|
||||
<slot name="actions" :table="table" />
|
||||
|
||||
<!-- Settings Popover (Per-page selector + View Options) -->
|
||||
<Popover v-model:open="settingsPopoverOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="outline" size="icon" class="relative">
|
||||
<FontAwesomeIcon :icon="faFilter" class="h-4 w-4" />
|
||||
<span
|
||||
v-if="hasActiveFilters"
|
||||
class="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-primary-600"
|
||||
></span>
|
||||
<span class="sr-only">Filtri</span>
|
||||
<Button variant="outline" size="sm" class="gap-2">
|
||||
<Settings2 class="h-4 w-4" />
|
||||
Pogled
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-auto p-4" align="start">
|
||||
<slot name="filters" />
|
||||
<PopoverContent class="w-[300px]" align="end">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h4 class="font-medium text-sm">Nastavitve pogleda</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Per page selector -->
|
||||
<div
|
||||
v-if="showPerPageSelector"
|
||||
class="flex items-center justify-between gap-4"
|
||||
>
|
||||
<label class="text-sm whitespace-nowrap">Elementov na stran</label>
|
||||
<Select
|
||||
:model-value="String(perPage)"
|
||||
@update:model-value="
|
||||
(value) => {
|
||||
emit('update:perPage', Number(value));
|
||||
settingsPopoverOpen = false;
|
||||
}
|
||||
"
|
||||
>
|
||||
<SelectTrigger class="h-9 w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem
|
||||
v-for="size in pageSizeOptions"
|
||||
:key="size"
|
||||
:value="String(size)"
|
||||
>
|
||||
{{ size }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Column visibility -->
|
||||
<div v-if="showViewOptions" class="flex items-center justify-between gap-4">
|
||||
<label class="text-sm whitespace-nowrap">Vidnost stolpcev</label>
|
||||
<DataTableViewOptions
|
||||
:table="table"
|
||||
@column-toggle="settingsPopoverOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<!-- Right side: Selected count, Page size, Menu & Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Selected count badge -->
|
||||
<div
|
||||
v-if="selectedCount > 0 && showSelectedCount"
|
||||
class="inline-flex items-center rounded-md bg-primary-50 px-3 py-1.5 text-sm font-medium text-primary-700"
|
||||
>
|
||||
{{ selectedCount }} izbran{{ selectedCount === 1 ? 'o' : 'ih' }}
|
||||
</div>
|
||||
|
||||
<!-- Page size selector (visible when not in compact mode) -->
|
||||
<div v-if="showPageSize && !compact" class="flex items-center gap-2">
|
||||
<Label for="page-size" class="text-sm text-gray-600 whitespace-nowrap">Na stran:</Label>
|
||||
<Select
|
||||
:model-value="String(pageSize)"
|
||||
@update:model-value="handlePageSizeChange"
|
||||
>
|
||||
<SelectTrigger id="page-size" class="w-[100px]">
|
||||
<SelectValue :placeholder="String(pageSize)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="opt in pageSizeOptions"
|
||||
:key="opt"
|
||||
:value="String(opt)"
|
||||
>
|
||||
{{ opt }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Table Options Menu (compact mode or always as dropdown) -->
|
||||
<DropdownMenu v-if="showOptionsMenu" v-model:open="menuOpen">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
:class="hasActiveFilters && !compact ? 'relative' : ''"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faEllipsisVertical" class="h-4 w-4" />
|
||||
<span
|
||||
v-if="hasActiveFilters && !compact"
|
||||
class="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-primary-600"
|
||||
></span>
|
||||
<span class="sr-only">Možnosti tabele</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-56">
|
||||
<!-- Search in menu (only in compact mode) -->
|
||||
<div v-if="compact && showSearch" class="p-2 border-b">
|
||||
<Label for="menu-search" class="text-xs font-medium mb-1.5 block">Iskanje</Label>
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5 z-10">
|
||||
<FontAwesomeIcon :icon="faSearch" class="h-3.5 w-3.5 text-gray-400" />
|
||||
</div>
|
||||
<Input
|
||||
id="menu-search"
|
||||
v-model="internalSearch"
|
||||
@input="handleSearchInput"
|
||||
type="text"
|
||||
placeholder="Iskanje..."
|
||||
class="pl-8 h-8 text-sm"
|
||||
:class="internalSearch ? 'pr-8' : ''"
|
||||
/>
|
||||
<Button
|
||||
v-if="internalSearch"
|
||||
@click="clearSearch"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="absolute inset-y-0 right-0 h-full w-auto px-2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTimes" class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page size in menu (only in compact mode) -->
|
||||
<div v-if="compact && showPageSize" class="p-2 border-b">
|
||||
<Label for="menu-page-size" class="text-xs font-medium mb-1.5 block">Elementov na stran</Label>
|
||||
<Select
|
||||
:model-value="String(pageSize)"
|
||||
@update:model-value="handlePageSizeChange"
|
||||
>
|
||||
<SelectTrigger id="menu-page-size" class="w-full h-8 text-sm">
|
||||
<SelectValue :placeholder="String(pageSize)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="opt in pageSizeOptions"
|
||||
:key="opt"
|
||||
:value="String(opt)"
|
||||
>
|
||||
{{ opt }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<!-- Export options -->
|
||||
<template v-if="showExport">
|
||||
<DropdownMenuLabel>Izvozi</DropdownMenuLabel>
|
||||
<DropdownMenuItem @select="handleExport('csv')">
|
||||
<FontAwesomeIcon :icon="faDownload" class="mr-2 h-4 w-4" />
|
||||
CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @select="handleExport('xlsx')">
|
||||
<FontAwesomeIcon :icon="faDownload" class="mr-2 h-4 w-4" />
|
||||
Excel
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<!-- Custom actions slot in menu -->
|
||||
<template v-if="$slots.actions">
|
||||
<DropdownMenuSeparator />
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<template v-else>
|
||||
<!-- If options menu is hidden but we have content to show, render it inline -->
|
||||
<div v-if="showExport && !compact" class="flex items-center gap-2">
|
||||
<Dropdown v-if="showExport" align="right">
|
||||
<template #trigger>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="gap-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4" />
|
||||
Izvozi
|
||||
</Button>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="py-1">
|
||||
<button
|
||||
type="button"
|
||||
@click="handleExport('csv')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
CSV
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="handleExport('xlsx')"
|
||||
class="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Excel
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Custom actions slot (visible when not in compact mode) -->
|
||||
<div v-if="$slots.actions && !compact" class="flex items-center gap-2">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user