Changes to UI and other stuff

This commit is contained in:
Simon Pocrnjič
2025-11-20 18:11:43 +01:00
parent b7fa2d261b
commit 3b284fa4bd
87 changed files with 7872 additions and 2330 deletions
@@ -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>