319 lines
11 KiB
Vue
319 lines
11 KiB
Vue
<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 {
|
|
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';
|
|
|
|
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
|
|
});
|
|
|
|
const emit = defineEmits(['update:search', 'update:page-size', 'export']);
|
|
|
|
const internalSearch = ref(props.search);
|
|
const menuOpen = ref(false);
|
|
|
|
watch(
|
|
() => props.search,
|
|
(newVal) => {
|
|
internalSearch.value = newVal;
|
|
}
|
|
);
|
|
|
|
const hasActiveFilters = computed(() => {
|
|
return !!internalSearch.value || props.selectedCount > 0;
|
|
});
|
|
|
|
function clearSearch() {
|
|
internalSearch.value = '';
|
|
emit('update:search', '');
|
|
}
|
|
|
|
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;
|
|
}
|
|
</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>
|
|
|
|
<!-- 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 options dropdown (after search input and add buttons) -->
|
|
<div v-if="$slots.options && showOptions && !compact" class="flex items-center">
|
|
<slot name="options" />
|
|
</div>
|
|
|
|
<!-- Filters button (after options, before right side) -->
|
|
<Popover v-if="showFilters && $slots.filters && !compact">
|
|
<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>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-auto p-4" align="start">
|
|
<slot name="filters" />
|
|
</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>
|
|
|
|
|