feat(division): impl division list+entry

feat(form): add accessibility improvements to form components

- Add labelFor prop to Label component for better form element association
- Enhance Combobox with ARIA attributes for better screen reader support
- Update form fields with proper IDs and label associations

feat(pagination): adjust button width based on page number length

Add dynamic button sizing for pagination items to accommodate different digit lengths (1-99, 100-999, 1000+). This improves visual consistency when displaying varying page numbers.

feat(modal): add reusable dialog component and refactor division form

- Create new Dialog.vue component with configurable size and outside click prevention
- Replace inline dialog implementation in division list with new Dialog component
- Fix formatting in entry-form.vue

feat(data-table): add click handling for action cells

Implement handleActionCellClick function to manage click events on action cells, triggering dropdown buttons when clicked outside interactive elements. Add cursor-pointer class and click handler to action cells for better UX.

refactor(custom-ui): centralize action event strings in types

Replace hardcoded action event strings with constants from types.ts to improve maintainability and reduce potential typos

feat(confirmation): add reusable confirmation modal components

- Implement base confirmation.vue component with customizable props
- Create record-specific record-confirmation.vue for data operations
- Add comprehensive README.md documentation for usage
- Integrate confirmation flow in division list component

refactor(components): move dialog component to base directory and update imports

The dialog component was moved from custom-ui/modal to base/modal to better reflect its shared usage across the application. All import paths referencing the old location have been updated accordingly.

refactor(select): reorganize imports and adjust conditional formatting

- Reorder imports in Select.vue for better organization
- Adjust logical operator formatting in SelectContent.vue for consistency
This commit is contained in:
Khafid Prayoga
2025-09-03 09:45:58 +07:00
parent b6d30eb154
commit a9c286bd0a
17 changed files with 598 additions and 57 deletions
+16 -10
View File
@@ -54,19 +54,23 @@ function onCancelForm({ resetForm }: { resetForm: () => void }) {
</script>
<template>
<Form v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues">
<Form
v-slot="{ handleSubmit, resetForm }" as="" keep-values :validation-schema="formSchema"
:initial-values="initialValues"
>
<form id="entry-form" @submit="handleSubmit($event, (values) => onSubmitForm(values, { resetForm }))">
<div class="mb-5 border-b border-b-slate-300 pb-3 text-lg xl:text-xl">
<div class="flex flex-col justify-between">
<FieldGroup>
<Label>Nama</Label>
<Label label-for="name">Nama</Label>
<Field id="name" :errors="errors">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormControl>
<Input type="text" placeholder="Masukkan nama divisi" autocomplete="organization"
v-bind="componentField" />
<Input
id="name" type="text" placeholder="Masukkan nama divisi" autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -75,12 +79,12 @@ function onCancelForm({ resetForm }: { resetForm: () => void }) {
</FieldGroup>
<FieldGroup>
<Label>Kode</Label>
<Label label-for="code">Kode</Label>
<Field id="code" :errors="errors">
<FormField v-slot="{ componentField }" name="code">
<FormItem>
<FormControl>
<Input type="text" placeholder="Masukkan kode divisi" autocomplete="off" v-bind="componentField" />
<Input id="code" type="text" placeholder="Masukkan kode divisi" autocomplete="off" v-bind="componentField" />
</FormControl>
<FormMessage />
</FormItem>
@@ -89,14 +93,16 @@ function onCancelForm({ resetForm }: { resetForm: () => void }) {
</FieldGroup>
<FieldGroup :column="2">
<Label>Kelompok</Label>
<Label label-for="parentId">Kelompok</Label>
<Field id="parentId" :errors="errors">
<FormField v-slot="{ componentField }" name="parentId">
<FormItem>
<FormControl>
<Combobox id="parentId" v-bind="componentField" :items="props.division.items"
<Combobox
id="parentId" v-bind="componentField" :items="props.division.items"
:placeholder="props.division.msg.placeholder" :search-placeholder="props.division.msg.search"
:empty-message="props.division.msg.empty" />
:empty-message="props.division.msg.empty"
/>
</FormControl>
<FormMessage />
</FormItem>
+83 -24
View File
@@ -4,6 +4,9 @@ import type { HeaderPrep } from '~/components/pub/custom-ui/data/types'
import type { PaginationMeta } from '~/components/pub/custom-ui/pagination/pagination.type'
import { refDebounced, useUrlSearchParams } from '@vueuse/core'
import AppDivisonEntryForm from '~/components/app/divison/entry-form.vue'
import Dialog from '~/components/pub/base/modal/dialog.vue'
import RecordConfirmation from '~/components/pub/custom-ui/confirmation/record-confirmation.vue'
import { ActionEvents } from '~/components/pub/custom-ui/data/types'
import Header from '~/components/pub/custom-ui/nav-header/header.vue'
import { division as divisionConf, schema as schemaConf } from './entry'
import { defaultQuery, querySchema } from './schema.query'
@@ -13,7 +16,10 @@ const data = ref([])
const isLoading = reactive<DataTableLoader>({
isTableLoading: false,
})
const isDialogOpen = ref(false)
// Dialog state
const isFormEntryDialogOpen = ref(false)
const isRecordConfirmationOpen = ref(false)
// URL state management
const queryParams = useUrlSearchParams('history', {
@@ -67,7 +73,7 @@ const headerPrep: HeaderPrep = {
label: 'Tambah Divisi',
icon: 'i-lucide-send',
onClick: () => {
isDialogOpen.value = true
isFormEntryDialogOpen.value = true
},
},
}
@@ -136,12 +142,38 @@ function handlePageChange(page: number) {
queryParams.page = page
}
async function handleDeleteRow(record: any) {
try {
// TODO : hit backend request untuk delete
console.log('Deleting record:', record)
// Simulate API call
// const response = await xfetch(`/api/v1/division/${record.id}`, {
// method: 'DELETE'
// })
// Refresh data setelah berhasil delete
await getDivisionList()
// TODO: Show success message
console.log('Record deleted successfully')
} catch (error) {
console.error('Error deleting record:', error)
// TODO: Show error message
} finally {
// Reset record state
recId.value = 0
recAction.value = ''
recItem.value = null
}
}
// #endregion region
// #region Form event handlers
function onCancelForm(resetForm: () => void) {
isDialogOpen.value = false
isFormEntryDialogOpen.value = false
setTimeout(() => {
resetForm()
}, 500)
@@ -160,7 +192,7 @@ async function onSubmitForm(values: any, resetForm: () => void) {
// })
// If successful, mark as success and close dialog
isDialogOpen.value = false
isFormEntryDialogOpen.value = false
isSuccess = true
// Refresh data after successful submission
@@ -208,34 +240,61 @@ watch(debouncedSearch, (newValue) => {
queryParams.page = 1 // Reset to first page when searching
}
})
// Watch for row actions
watch(recId, () => {
switch (recAction.value) {
case ActionEvents.showEdit:
// TODO: Handle edit action
// isFormEntryDialogOpen.value = true
break
case ActionEvents.showConfirmDelete:
// Trigger confirmation modal open
isRecordConfirmationOpen.value = true
break
}
})
// Handle confirmation result
function handleConfirmDelete(record: any, action: string) {
console.log('Confirmed action:', action, 'for record:', record)
handleDeleteRow(record)
}
function handleCancelConfirmation() {
// Reset record state when cancelled
recId.value = 0
recAction.value = ''
recItem.value = null
}
// #endregion
</script>
<template>
<div class="rounded-md border p-4">
<div class="rounded-md border p-4">
<Header v-model="searchInput" :prep="headerPrep" @search="handleSearch" />
<AppDivisonList :data="data" :pagination-meta="paginationMeta" @page-change="handlePageChange" />
<Dialog v-model:open="isDialogOpen">
<DialogContent
class="sm:max-w-[425px]"
@interact-outside="(e) => e.preventDefault()"
@pointer-down-outside="(e) => e.preventDefault()"
>
<DialogHeader>
<DialogTitle>Tambah Divisi</DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<AppDivisonEntryForm
:division="divisionConf"
:schema="schemaConf"
:initial-values="{ name: '', code: '', parentId: '' }"
@submit="onSubmitForm"
@cancel="onCancelForm"
/>
</DialogContent>
<Dialog v-model:open="isFormEntryDialogOpen" title="Tambah Divisi" size="lg">
<AppDivisonEntryForm
:division="divisionConf" :schema="schemaConf"
:initial-values="{ name: '', code: '', parentId: '' }" @submit="onSubmitForm" @cancel="onCancelForm"
/>
</Dialog>
<!-- Record Confirmation Modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen" action="delete" :record="recItem"
@confirm="handleConfirmDelete" @cancel="handleCancelConfirmation"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p v-if="record?.firstName"><strong>Nama:</strong> {{ record.firstName }}</p>
<p v-if="record?.code"><strong>Kode:</strong> {{ record.cellphone }}</p>
</div>
</template>
</RecordConfirmation>
</div>
</template>
@@ -15,6 +15,21 @@ defineProps<{
}>()
const loader = inject('table_data_loader') as DataTableLoader
function handleActionCellClick(event: Event, _cellRef: string) {
// Prevent event if clicked directly on the button/dropdown
const target = event.target as HTMLElement
if (target.closest('button') || target.closest('[role="button"]')) {
return
}
// Find the dropdown trigger button and click it
const cell = event.currentTarget as HTMLElement
const triggerButton = cell.querySelector('button[data-state]') || cell.querySelector('button')
if (triggerButton) {
(triggerButton as HTMLButtonElement).click()
}
}
</script>
<template>
@@ -50,11 +65,18 @@ v-for="(h, idx) in header[0]" :key="`head-${idx}`" class="border"
</TableBody>
<TableBody v-else>
<TableRow v-for="(row, rowIndex) in rows" :key="`row-${rowIndex}`">
<TableCell v-for="(key, cellIndex) in keys" :key="`cell-${rowIndex}-${cellIndex}`" class="border">
<TableCell
v-for="(key, cellIndex) in keys"
:key="`cell-${rowIndex}-${cellIndex}`"
class="border"
:class="{ 'cursor-pointer': key === 'action' && funcComponent[key] }"
@click="key === 'action' && funcComponent[key] ? handleActionCellClick($event, `cell-${rowIndex}-${cellIndex}`) : null"
>
<!-- If funcComponent has a renderer -->
<component
:is="funcComponent[key]?.(row, rowIndex).component"
v-if="funcComponent[key]"
:ref="key === 'action' ? `actionComponent-${rowIndex}-${cellIndex}` : undefined"
:rec="row"
:idx="rowIndex"
v-bind="funcComponent[key]?.(row, rowIndex).props"
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
import { Dialog } from '~/components/pub/ui/dialog'
interface DialogProps {
title: string
description?: string
preventOutside?: boolean
open?: boolean
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
}
const props = withDefaults(defineProps<DialogProps>(), {
preventOutside: false,
open: false,
size: 'md',
})
const emit = defineEmits<{
'update:open': [value: boolean]
}>()
// Computed untuk menentukan class size berdasarkan prop size
const sizeClass = computed(() => {
const sizeMap = {
sm: 'sm:max-w-[350px]',
md: 'sm:max-w-[425px]',
lg: 'sm:max-w-[600px]',
xl: 'sm:max-w-[800px]',
full: 'sm:max-w-[95vw]',
}
return sizeMap[props.size]
})
// Computed untuk state dialog
const isOpen = computed({
get: () => props.open,
set: (value) => emit('update:open', value),
})
</script>
<template>
<Dialog v-model:open="isOpen">
<DialogContent
:class="sizeClass" @interact-outside="(e: any) => preventOutside && e.preventDefault()"
@pointer-down-outside="(e: any) => preventOutside && e.preventDefault()"
>
<DialogHeader>
<DialogTitle>{{ props.title }}</DialogTitle>
<DialogDescription v-if="props.description">{{ props.description }}</DialogDescription>
</DialogHeader>
<slot />
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,137 @@
# Confirmation Modal Components
Sistem confirmation modal yang modular dan dapat digunakan kembali di seluruh aplikasi.
## Components
### 1. `confirmation.vue` - Base Confirmation Modal
Komponen dasar untuk modal konfirmasi yang dapat dikustomisasi.
**Props:**
- `open?: boolean` - Status modal (terbuka/tertutup)
- `title?: string` - Judul modal (default: "Konfirmasi")
- `message?: string` - Pesan konfirmasi (default: "Apakah Anda yakin ingin melanjutkan?")
- `confirmText?: string` - Text tombol konfirmasi (default: "Ya")
- `cancelText?: string` - Text tombol batal (default: "Batal")
- `variant?: 'default' | 'destructive' | 'warning'` - Varian tampilan (default: "default")
- `size?: 'sm' | 'md' | 'lg' | 'xl'` - Ukuran modal (default: "md")
**Events:**
- `@confirm` - Dipanggil saat tombol konfirmasi diklik
- `@cancel` - Dipanggil saat tombol batal diklik
- `@update:open` - Dipanggil saat status modal berubah
**Contoh penggunaan:**
```vue
<template>
<Confirmation
v-model:open="isConfirmOpen"
title="Hapus Data"
message="Apakah Anda yakin ingin menghapus data ini?"
confirm-text="Hapus"
variant="destructive"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</template>
```
### 2. `record-confirmation.vue` - Record-specific Confirmation
Komponen khusus untuk konfirmasi operasi pada record/data tertentu.
**Props:**
- `open?: boolean` - Status modal
- `action?: 'delete' | 'deactivate' | 'activate' | 'archive' | 'restore'` - Jenis aksi (default: "delete")
- `record?: RecordData | null` - Data record yang akan diproses
- `customTitle?: string` - Custom judul (opsional)
- `customMessage?: string` - Custom pesan (opsional)
- `customConfirmText?: string` - Custom text tombol konfirmasi (opsional)
- `customCancelText?: string` - Custom text tombol batal (opsional)
**Events:**
- `@confirm` - Dipanggil dengan parameter `(record, action)`
- `@cancel` - Dipanggil saat batal
- `@update:open` - Update status modal
**Contoh penggunaan:**
```vue
<template>
<RecordConfirmation
v-model:open="isRecordConfirmOpen"
action="delete"
:record="selectedRecord"
@confirm="handleDeleteRecord"
@cancel="handleCancel"
>
<template #default="{ record }">
<div class="text-sm">
<p><strong>ID:</strong> {{ record?.id }}</p>
<p><strong>Nama:</strong> {{ record?.name }}</p>
</div>
</template>
</RecordConfirmation>
</template>
```
## Action Types
Record confirmation mendukung beberapa jenis aksi dengan konfigurasi default:
- **delete**: Hapus data (variant: destructive, warna merah)
- **deactivate**: Nonaktifkan data (variant: warning, warna kuning)
- **activate**: Aktifkan data (variant: default, warna biru)
- **archive**: Arsipkan data (variant: warning, warna kuning)
- **restore**: Pulihkan data (variant: default, warna biru)
## Integration Example
Contoh implementasi di komponen list:
```vue
<script setup>
// State management
const isRecordConfirmationOpen = ref(false)
const selectedRecord = ref(null)
const confirmAction = ref('delete')
// Handle action dari table
function handleRowAction(action, record) {
selectedRecord.value = record
confirmAction.value = action
isRecordConfirmationOpen.value = true
}
// Handle konfirmasi
async function handleConfirmAction(record, action) {
try {
// API call berdasarkan action
await performAction(action, record.id)
// Refresh data
await refreshData()
} catch (error) {
// Handle error
}
}
</script>
<template>
<!-- Your list component -->
<DataTable @action="handleRowAction" />
<!-- Confirmation modal -->
<RecordConfirmation
v-model:open="isRecordConfirmationOpen"
:action="confirmAction"
:record="selectedRecord"
@confirm="handleConfirmAction"
/>
</template>
```
## Styling
Komponen menggunakan Tailwind CSS dan shadcn/ui components. Pastikan dependencies berikut tersedia:
- `~/components/pub/custom-ui/modal/dialog.vue`
- `~/components/pub/ui/button`
@@ -0,0 +1,99 @@
<script setup lang="ts">
import Dialog from '~/components/pub/base/modal/dialog.vue'
import { Button } from '~/components/pub/ui/button'
interface ConfirmationProps {
open?: boolean
title?: string
message?: string
confirmText?: string
cancelText?: string
variant?: 'default' | 'destructive' | 'warning'
size?: 'sm' | 'md' | 'lg' | 'xl'
}
interface ConfirmationEmits {
'update:open': [value: boolean]
'confirm': []
'cancel': []
}
const props = withDefaults(defineProps<ConfirmationProps>(), {
open: false,
title: 'Konfirmasi',
message: 'Apakah Anda yakin ingin melanjutkan?',
confirmText: 'Ya',
cancelText: 'Batal',
variant: 'default',
size: 'md',
})
const emit = defineEmits<ConfirmationEmits>()
const isOpen = computed({
get: () => props.open,
set: (value) => emit('update:open', value),
})
const variantClasses = computed(() => {
const variants = {
default: {
icon: 'i-lucide-help-circle',
iconColor: 'text-blue-500',
confirmVariant: 'default' as const,
},
destructive: {
icon: 'i-lucide-alert-triangle',
iconColor: 'text-red-500',
confirmVariant: 'destructive' as const,
},
warning: {
icon: 'i-lucide-alert-circle',
iconColor: 'text-yellow-500',
confirmVariant: 'default' as const,
},
}
return variants[props.variant]
})
function handleConfirm() {
emit('confirm')
emit('update:open', false)
}
function handleCancel() {
emit('cancel')
emit('update:open', false)
}
</script>
<template>
<Dialog v-model:open="isOpen" :title="title" :size="size">
<div class="space-y-4">
<!-- Icon dan pesan -->
<div class="flex items-start gap-3">
<div :class="[variantClasses.icon, variantClasses.iconColor]" class="w-6 h-6 mt-1 flex-shrink-0" />
<div class="flex-1">
<p class="text-sm text-muted-foreground leading-relaxed">
{{ message }}
</p>
</div>
</div>
<!-- Slot untuk konten custom -->
<div v-if="$slots.default">
<slot />
</div>
<!-- Footer buttons -->
<div class="flex justify-end gap-3 pt-4">
<Button variant="outline" @click="handleCancel">
{{ cancelText }}
</Button>
<Button :variant="variantClasses.confirmVariant" @click="handleConfirm">
{{ confirmText }}
</Button>
</div>
</div>
</Dialog>
</template>
@@ -0,0 +1,131 @@
<script setup lang="ts">
import Confirmation from '~/components/pub/custom-ui/confirmation/confirmation.vue'
interface RecordData {
id: number | string
name?: string
title?: string
code?: string
[key: string]: any
}
interface RecordConfirmationProps {
open?: boolean
action?: 'delete' | 'deactivate' | 'activate' | 'archive' | 'restore'
record?: RecordData | null
customTitle?: string
customMessage?: string
customConfirmText?: string
customCancelText?: string
}
interface RecordConfirmationEmits {
'update:open': [value: boolean]
'confirm': [record: RecordData, action: string]
'cancel': []
}
const props = withDefaults(defineProps<RecordConfirmationProps>(), {
open: false,
action: 'delete',
record: null,
customTitle: '',
customMessage: '',
customConfirmText: '',
customCancelText: '',
})
const emit = defineEmits<RecordConfirmationEmits>()
const isOpen = computed({
get: () => props.open,
set: (value) => emit('update:open', value),
})
const actionConfig = computed(() => {
const configs = {
delete: {
title: 'Hapus Data',
message: 'Apakah Anda yakin ingin menghapus data ini? Tindakan ini tidak dapat dibatalkan.',
confirmText: 'Hapus',
variant: 'destructive' as const,
},
deactivate: {
title: 'Nonaktifkan Data',
message: 'Apakah Anda yakin ingin menonaktifkan data ini?',
confirmText: 'Nonaktifkan',
variant: 'warning' as const,
},
activate: {
title: 'Aktifkan Data',
message: 'Apakah Anda yakin ingin mengaktifkan data ini?',
confirmText: 'Aktifkan',
variant: 'default' as const,
},
archive: {
title: 'Arsipkan Data',
message: 'Apakah Anda yakin ingin mengarsipkan data ini?',
confirmText: 'Arsipkan',
variant: 'warning' as const,
},
restore: {
title: 'Pulihkan Data',
message: 'Apakah Anda yakin ingin memulihkan data ini?',
confirmText: 'Pulihkan',
variant: 'default' as const,
},
}
return configs[props.action]
})
const finalTitle = computed(() => {
return props.customTitle || actionConfig.value.title
})
const finalMessage = computed(() => {
if (props.customMessage) {
return props.customMessage
}
const baseMessage = actionConfig.value.message
// if (props.record) {
// const recordName = props.record.name || props.record.title || props.record.code || `ID: ${props.record.id}`
// return `${baseMessage}\n\nData: ${recordName}`
// }
return baseMessage
})
const finalConfirmText = computed(() => {
return props.customConfirmText || actionConfig.value.confirmText
})
const finalCancelText = computed(() => {
return props.customCancelText || 'Batal'
})
function handleConfirm() {
if (props.record) {
emit('confirm', props.record, props.action)
}
emit('update:open', false)
}
function handleCancel() {
emit('cancel')
emit('update:open', false)
}
</script>
<template>
<Confirmation
v-model:open="isOpen" :title="finalTitle" :message="finalMessage" :confirm-text="finalConfirmText"
:cancel-text="finalCancelText" :variant="actionConfig.variant" size="md" @confirm="handleConfirm"
@cancel="handleCancel"
>
<!-- Slot untuk informasi tambahan record -->
<div v-if="record && $slots.default" class="mt-4 p-3 bg-muted rounded-md">
<slot :record="record" :action="action" />
</div>
</Confirmation>
</template>
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './types'
const props = defineProps<{
rec: ListItemDto
@@ -11,13 +12,13 @@ const recItem = inject<Ref<any>>('rec_item')!
function detail() {
recId.value = props.rec.id || 0
recAction.value = 'showDetail'
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = 'showEdit'
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './types'
const props = defineProps<{
rec: ListItemDto
@@ -11,19 +12,19 @@ const recItem = inject<Ref<any>>('rec_item')!
function detail() {
recId.value = props.rec.id || 0
recAction.value = 'showDetail'
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = 'showEdit'
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = 'showConfirmDel'
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './types'
const props = defineProps<{
rec: ListItemDto
@@ -11,19 +12,19 @@ const recItem = inject<Ref<any>>('rec_item')!
function detail() {
recId.value = props.rec.id || 0
recAction.value = 'showDetail'
recAction.value = ActionEvents.showDetail
recItem.value = props.rec
}
function edit() {
recId.value = props.rec.id || 0
recAction.value = 'showEdit'
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = 'showConfirmDel'
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { LinkItem, ListItemDto } from './types'
import { ActionEvents } from './types'
interface Props {
rec: ListItemDto
@@ -16,13 +17,13 @@ const recItem = inject<Ref<any>>('rec_item')!
function edit() {
recId.value = props.rec.id || 0
recAction.value = 'showEdit'
recAction.value = ActionEvents.showEdit
recItem.value = props.rec
}
function del() {
recId.value = props.rec.id || 0
recAction.value = 'showConfirmDel'
recAction.value = ActionEvents.showConfirmDelete
recItem.value = props.rec
}
@@ -100,3 +100,9 @@ export interface LinkItem {
onClick?: (event: Event) => void
headerStatus?: boolean
}
export const ActionEvents = {
showConfirmDelete: 'showConfirmDel',
showEdit: 'showEdit',
showDetail: 'showDetail',
}
+13 -3
View File
@@ -63,7 +63,12 @@ function onSelect(item: Item) {
<PopoverTrigger as-child>
<Button
:id="props.id"
variant="outline" role="combobox" :aria-expanded="open" :class="cn(
variant="outline"
role="combobox"
:aria-expanded="open"
:aria-controls="`${props.id}-list`"
:aria-describedby="`${props.id}-search`"
:class="cn(
'w-full justify-between border-black bg-white hover:bg-gray-50 text-sm font-normal',
!modelValue && 'text-muted-foreground',
props.class,
@@ -75,9 +80,14 @@ function onSelect(item: Item) {
</PopoverTrigger>
<PopoverContent class="w-full p-0">
<Command>
<CommandInput class="h-9" :placeholder="searchPlaceholder || 'Cari...'" />
<CommandInput
:id="`${props.id}-search`"
class="h-9"
:placeholder="searchPlaceholder || 'Cari...'"
:aria-label="`Cari ${displayText}`"
/>
<CommandEmpty>{{ emptyMessage || 'Item tidak ditemukan.' }}</CommandEmpty>
<CommandList>
<CommandList :id="`${props.id}-list`" role="listbox">
<CommandGroup>
<CommandItem v-for="item in searchableItems" :key="item.value" :value="item.searchValue" @select="onSelect(item)">
<div class="flex items-center justify-between w-full">
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
labelFor: string
size?: 'default' | 'narrow' | 'wide'
height?: 'default' | 'compact'
position?: 'default' | 'dynamic'
@@ -45,7 +46,7 @@ const labelClass = computed(() => positionChildMap[props.position])
<template>
<div :class="wrapperClass">
<label :class="labelClass">
<label :class="labelClass" :for="labelFor">
<slot />
</label>
</div>
@@ -71,6 +71,19 @@ const startRecord = computed(() => {
const endRecord = computed(() => {
return Math.min(props.paginationMeta.page * props.paginationMeta.pageSize, props.paginationMeta.recordCount)
})
// Function to determine button width based on page number
function getButtonClass(pageNumber: number) {
const digits = pageNumber.toString().length
if (digits >= 4) { // 1000+ (1k+)
return 'h-9 px-4 min-w-12'
} else if (digits === 3) { // 100-999
return 'h-9 px-3 min-w-10'
} else { // 1-99
return 'w-9 h-9 p-0'
}
}
</script>
<template>
@@ -97,7 +110,7 @@ v-slot="{ page }" :total="paginationMeta.recordCount" :sibling-count="1" :page="
v-if="item.type === 'page'" :key="index" :value="item.value" as-child
@click="handlePageChange(item.value)"
>
<Button class="w-9 h-9 p-0" :variant="item.value === page ? 'default' : 'outline'">
<Button :class="getButtonClass(item.value)" :variant="item.value === page ? 'default' : 'outline'">
{{ item.value }}
</Button>
</PaginationListItem>
+4 -5
View File
@@ -1,15 +1,14 @@
<script setup lang="ts">
import { SelectRoot } from 'radix-vue'
import type { SelectRootEmits, SelectRootProps } from 'radix-vue'
import { SelectRoot, useForwardPropsEmits } from 'radix-vue'
import {
SelectTrigger,
SelectContent,
SelectValue,
SelectGroup,
SelectItem,
SelectSeparator,
SelectTrigger,
SelectValue,
} from '~/components/pub/ui/select'
import type { SelectRootProps, SelectRootEmits } from 'radix-vue'
import { useForwardPropsEmits } from 'radix-vue'
interface Item {
value: string
@@ -32,8 +32,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
:class="
cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border shadow-md',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
position === 'popper'
&& 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
props.class,
)
"