Enhance form column settings and table display
- Add "Show all" and "Hide all" buttons in column settings modal - Improve column width handling and default width management - Update OpenSelect and OpenText components to support more flexible prop types - Add z-index to table header for better visual hierarchy - Refactor column width property from `cell_width` to `width`
This commit is contained in:
parent
0d6bd1bfde
commit
55b0f57741
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
description: Laravel Back-end
|
||||
globs: api/**.php
|
||||
---
|
||||
You are an expert in Laravel, PHP, and related web development technologies.
|
||||
|
||||
Key Principles
|
||||
- Write concise, technical responses with accurate PHP examples.
|
||||
- Adhere to Laravel 11+ best practices and conventions.
|
||||
- Use object-oriented programming with a focus on SOLID principles.
|
||||
- Prefer iteration and modularization over duplication.
|
||||
- Use descriptive variable and method names.
|
||||
- Use lowercase with dashes for directories (e.g., app/Http/Controllers).
|
||||
- Favor dependency injection and service containers.
|
||||
|
||||
PHP/Laravel
|
||||
- Use PHP 8.2+ features when appropriate (e.g., typed properties, match expressions).
|
||||
- Follow PSR-12 coding standards.
|
||||
- Utilize Laravel's built-in features and helpers when possible.
|
||||
- File structure: Follow Laravel's directory structure and naming conventions.
|
||||
- Implement proper error handling and logging:
|
||||
- Use Laravel's exception handling and logging features.
|
||||
- Create custom exceptions when necessary.
|
||||
- Use try-catch blocks for expected exceptions.
|
||||
- Use Laravel's validation features for form and request validation.
|
||||
- Implement middleware for request filtering and modification.
|
||||
- Utilize Laravel's Eloquent ORM for database interactions.
|
||||
- Use Laravel's query builder for complex database queries.
|
||||
- Implement proper database migrations and seeders.
|
||||
|
||||
Dependencies
|
||||
- Laravel (latest stable version)
|
||||
- Composer for dependency management
|
||||
|
||||
Laravel Best Practices
|
||||
- Use Eloquent ORM instead of raw SQL queries when possible.
|
||||
- Use Laravel's built-in authentication and authorization features.
|
||||
- Utilize Laravel's caching mechanisms for improved performance.
|
||||
- Implement job queues for long-running tasks.
|
||||
- Use Pest for unit and feature tests.
|
||||
- Use Laravel's localization features for multi-language support.
|
||||
- Implement proper database indexing for improved query performance.
|
||||
- Use Laravel's built-in pagination features.
|
||||
- Implement proper error logging and monitoring.
|
||||
|
||||
Key Conventions
|
||||
1. Follow Laravel's MVC architecture.
|
||||
2. Use Laravel's routing system for defining application endpoints.
|
||||
3. Implement proper request validation using Form Requests.
|
||||
4. Implement proper database relationships using Eloquent.
|
||||
5. Use Laravel's event and listener system for decoupled code.
|
||||
6. Implement proper database transactions for data integrity.
|
||||
7. Use Laravel's built-in scheduling features for recurring tasks.
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
description: Vue and Nuxt guidelines
|
||||
globs: client/**.*
|
||||
---
|
||||
You are an expert in Nuxt, Vue.js, Vue Router, Pinia, VueUse, NuxtUI library and Tailwind, with a deep understanding of best practices and performance optimization techniques in these technologies.
|
||||
|
||||
Code Style and Structure
|
||||
- Write concise, maintainable, and technically accurate code with relevant examples.
|
||||
- Use functional and declarative programming patterns; avoid classes.
|
||||
- Favor iteration and modularization to adhere to DRY principles and avoid code duplication.
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
|
||||
- Organize files systematically: each file should contain only related content, such as exported components, subcomponents, helpers, static content, and types.
|
||||
|
||||
Naming Conventions
|
||||
- Use lowercase with dashes for directories (e.g., components/auth-wizard).
|
||||
- Favor named exports for functions.
|
||||
|
||||
We don't use typescript, javascript only (except when really required for config file for instance).
|
||||
|
||||
Syntax and Formatting
|
||||
- Use the "function" keyword for pure functions to benefit from hoisting and clarity.
|
||||
- Always use the Vue Composition API script setup style. If a legacy file still used
|
||||
|
||||
UI and Styling
|
||||
- Use Nuxt UI components (https://ui.nuxt.com/components) and Tailwind for components and styling.
|
||||
- Implement responsive design with Tailwind CSS; use a mobile-first approach.
|
||||
- Build UI components using atomic design principles, organizing them from smallest to largest (e.g., atoms, molecules, organisms, pages).
|
||||
|
||||
Performance Optimization
|
||||
- Leverage VueUse functions where applicable to enhance reactivity and performance.
|
||||
- Wrap asynchronous components in Suspense with a fallback UI made with <USkeleton/> components.
|
||||
- Use dynamic loading for non-critical components.
|
||||
|
||||
Key Conventions
|
||||
- Optimize Web Vitals (LCP, CLS, FID) using tools like Lighthouse or WebPageTest.
|
||||
- Implement proper error boundaries or try-catch mechanisms to handle errors gracefully, especially in asynchronous operations.
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<UButtonGroup size="xs" orientation="horizontal">
|
||||
<UButtonGroup
|
||||
size="xs"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<UButton
|
||||
v-track.delete_record_click
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@
|
|||
<modal
|
||||
compact-header
|
||||
:show="show"
|
||||
v-bind="$attrs"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon name="heroicons:adjustments-horizontal" class="w-8 h-8" />
|
||||
<Icon
|
||||
name="heroicons:adjustments-horizontal"
|
||||
class="w-8 h-8"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
Manage Columns
|
||||
|
|
@ -21,7 +25,24 @@
|
|||
class="font-semibold mb-2"
|
||||
:class="{ 'mt-4': sectionIndex > 0 }"
|
||||
>
|
||||
{{ section.title }}
|
||||
<div class="flex items-center justify-between">
|
||||
<span>{{ section.title }}</span>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||||
<button
|
||||
class="hover:text-gray-700"
|
||||
@click="toggleAllColumns(section.fields, true)"
|
||||
>
|
||||
Show all
|
||||
</button>
|
||||
<span class="text-gray-300">|</span>
|
||||
<button
|
||||
class="hover:text-gray-700"
|
||||
@click="toggleAllColumns(section.fields, false)"
|
||||
>
|
||||
Hide all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="border border-gray-300">
|
||||
<div class="grid grid-cols-[1fr,auto,auto] gap-4 px-4 py-2 bg-gray-50 border-b border-gray-300">
|
||||
|
|
@ -46,8 +67,8 @@
|
|||
</p>
|
||||
<div class="flex justify-center w-20">
|
||||
<ToggleSwitchInput
|
||||
v-model="displayColumns[field.id]"
|
||||
wrapper-class="mb-0"
|
||||
v-model="computedDisplayColumns[field.id]"
|
||||
wrapper-class="my-0"
|
||||
label=""
|
||||
:name="`display-${field.id}`"
|
||||
@update:model-value="onChangeDisplayColumns"
|
||||
|
|
@ -55,8 +76,8 @@
|
|||
</div>
|
||||
<div class="flex justify-center w-20">
|
||||
<ToggleSwitchInput
|
||||
v-model="wrapColumns[field.id]"
|
||||
wrapper-class="mb-0"
|
||||
v-model="computedWrapColumns[field.id]"
|
||||
wrapper-class="my-0"
|
||||
label=""
|
||||
:name="`wrap-${field.id}`"
|
||||
/>
|
||||
|
|
@ -85,6 +106,14 @@ const props = defineProps({
|
|||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
displayColumns: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
wrapColumns: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -122,16 +151,24 @@ const sections = computed(() => [
|
|||
])
|
||||
|
||||
// Column preferences storage
|
||||
const storageKey = computed(() => `column-preferences-formid-${props.form.id}`)
|
||||
|
||||
const columnPreferences = useStorage(
|
||||
computed(() => props.form ? `column-preferences-formid-${props.form.id}` : null),
|
||||
storageKey.value,
|
||||
{
|
||||
display: {},
|
||||
wrap: {},
|
||||
widths: {}
|
||||
},
|
||||
localStorage,
|
||||
{
|
||||
onError: (error) => {
|
||||
console.error('Storage error:', error)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const displayColumns = computed({
|
||||
const computedDisplayColumns = computed({
|
||||
get: () => columnPreferences.value.display,
|
||||
set: (val) => {
|
||||
columnPreferences.value.display = val
|
||||
|
|
@ -139,7 +176,7 @@ const displayColumns = computed({
|
|||
}
|
||||
})
|
||||
|
||||
const wrapColumns = computed({
|
||||
const computedWrapColumns = computed({
|
||||
get: () => columnPreferences.value.wrap,
|
||||
set: (val) => {
|
||||
columnPreferences.value.wrap = val
|
||||
|
|
@ -164,12 +201,18 @@ function preserveColumnWidths(newColumns, existingColumns = []) {
|
|||
// Then fallback to form properties
|
||||
const existing = existingColumns?.find(e => e.id === col.id)
|
||||
|
||||
const width = storedWidth || currentCol?.cell_width || currentCol?.width || existing?.cell_width || existing?.width || col.width || 150
|
||||
// Convert any non-numeric width to default
|
||||
const defaultWidth = 250
|
||||
let width = storedWidth || currentCol?.width || existing?.width || defaultWidth
|
||||
|
||||
// If width is not a number or is 'full', use default width
|
||||
if (typeof width !== 'number' || isNaN(width)) {
|
||||
width = defaultWidth
|
||||
}
|
||||
|
||||
return {
|
||||
...col,
|
||||
width,
|
||||
cell_width: width
|
||||
width
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -187,8 +230,8 @@ watch(() => props.columns, (newColumns) => {
|
|||
|
||||
const widths = {}
|
||||
newColumns.forEach(col => {
|
||||
if (col.cell_width) {
|
||||
widths[col.id] = col.cell_width
|
||||
if (col.width) {
|
||||
widths[col.id] = col.width
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -199,42 +242,63 @@ watch(() => props.columns, (newColumns) => {
|
|||
watch(() => props.form, (newForm) => {
|
||||
if (!newForm) return
|
||||
|
||||
const properties = newForm.properties || []
|
||||
const properties = candidatesProperties.value
|
||||
const storedPrefs = columnPreferences.value
|
||||
const removedProperties = newForm.removed_properties || []
|
||||
|
||||
// Initialize display columns if not set
|
||||
if (!Object.keys(storedPrefs.display).length) {
|
||||
// Set all non-removed properties to visible by default
|
||||
properties.forEach((field) => {
|
||||
storedPrefs.display[field.id] = true
|
||||
})
|
||||
// Also handle removed properties
|
||||
removedProperties.forEach((field) => {
|
||||
storedPrefs.display[field.id] = false
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize wrap columns if not set
|
||||
if (!Object.keys(storedPrefs.wrap).length) {
|
||||
properties.forEach((field) => {
|
||||
[...properties, ...removedProperties].forEach((field) => {
|
||||
storedPrefs.wrap[field.id] = false
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize widths if not set
|
||||
if (!Object.keys(storedPrefs.widths).length) {
|
||||
[...properties, ...removedProperties].forEach((field) => {
|
||||
const defaultWidth = 150
|
||||
storedPrefs.widths[field.id] = field.width || defaultWidth
|
||||
})
|
||||
}
|
||||
|
||||
// Emit initial values
|
||||
emit('update:displayColumns', storedPrefs.display)
|
||||
emit('update:wrapColumns', storedPrefs.wrap)
|
||||
|
||||
// Emit initial columns (all visible by default)
|
||||
const initialColumns = clonedeep(candidatesProperties.value)
|
||||
.concat(props.form?.removed_properties || [])
|
||||
.filter((field) => storedPrefs.display[field.id] !== false) // Show all columns by default unless explicitly hidden
|
||||
// Emit initial columns (all non-removed visible by default)
|
||||
const initialColumns = clonedeep(properties)
|
||||
.concat(removedProperties)
|
||||
.filter((field) => storedPrefs.display[field.id] !== false)
|
||||
|
||||
// Preserve any existing column widths
|
||||
const columnsWithWidths = preserveColumnWidths(initialColumns, props.form.properties)
|
||||
const columnsWithWidths = preserveColumnWidths(initialColumns, properties)
|
||||
emit('update:columns', columnsWithWidths)
|
||||
}, { immediate: true })
|
||||
|
||||
function toggleAllColumns(fields, show) {
|
||||
fields.forEach((field) => {
|
||||
computedDisplayColumns.value[field.id] = show
|
||||
})
|
||||
onChangeDisplayColumns()
|
||||
}
|
||||
|
||||
function onChangeDisplayColumns() {
|
||||
if (!import.meta.client) return
|
||||
const properties = clonedeep(candidatesProperties.value)
|
||||
.concat(props.form?.removed_properties || [])
|
||||
.filter((field) => displayColumns.value[field.id] === true)
|
||||
.filter((field) => computedDisplayColumns.value[field.id] === true)
|
||||
|
||||
// Preserve existing column widths when toggling visibility
|
||||
const columnsWithWidths = preserveColumnWidths(properties, props.form.properties)
|
||||
|
|
|
|||
|
|
@ -10,11 +10,14 @@
|
|||
|
||||
<!-- Settings Modal -->
|
||||
<form-columns-settings-modal
|
||||
v-if="form"
|
||||
:show="showColumnsModal"
|
||||
:form="form"
|
||||
:columns="properties"
|
||||
v-model:display-columns="displayColumns"
|
||||
v-model:wrap-columns="wrapColumns"
|
||||
:display-columns="displayColumns"
|
||||
:wrap-columns="wrapColumns"
|
||||
@update:display-columns="displayColumns = $event"
|
||||
@update:wrap-columns="wrapColumns = $event"
|
||||
@close="showColumnsModal = false"
|
||||
@update:columns="onColumnUpdated"
|
||||
/>
|
||||
|
|
@ -225,6 +228,7 @@ export default {
|
|||
},
|
||||
onColumnUpdated(columns) {
|
||||
this.properties = columns
|
||||
this.dataChanged()
|
||||
},
|
||||
onUpdateRecord(submission) {
|
||||
this.recordStore.save(submission)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<thead
|
||||
:id="'table-header-' + tableHash"
|
||||
ref="header"
|
||||
class="n-table-head top-0"
|
||||
class="n-table-head top-0 z-10"
|
||||
:class="{ absolute: data.length !== 0 }"
|
||||
style="will-change: transform; transform: translate3d(0px, 0px, 0px)"
|
||||
>
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
:key="col.id"
|
||||
scope="col"
|
||||
:allow-resize="allowResize"
|
||||
:width="col.cell_width ? col.cell_width + 'px' : 'auto'"
|
||||
:width="col.width ? col.width + 'px' : '150px'"
|
||||
class="n-table-cell p-0 relative"
|
||||
@resize-width="resizeCol(col, $event)"
|
||||
>
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
<td
|
||||
v-for="(col, colIndex) in columns"
|
||||
:key="col.id"
|
||||
:style="{ width: col.cell_width + 'px' }"
|
||||
:style="{ width: col.width ? col.width + 'px' : '150px' }"
|
||||
class="n-table-cell border-gray-100 dark:border-gray-900 text-sm p-2 overflow-hidden"
|
||||
:class="[
|
||||
{
|
||||
|
|
@ -179,7 +179,8 @@ export default {
|
|||
type: Boolean,
|
||||
},
|
||||
scrollParent: {
|
||||
type: [Boolean]
|
||||
type: [Boolean, Object],
|
||||
default: null
|
||||
},
|
||||
},
|
||||
emits: ["updated", "deleted", "resize", "update-columns"],
|
||||
|
|
@ -294,7 +295,7 @@ export default {
|
|||
if (this.internalColumns) {
|
||||
this.$nextTick(() => {
|
||||
this.internalColumns.forEach((col) => {
|
||||
if (!_has(col, "cell_width")) {
|
||||
if (!_has(col, "width")) {
|
||||
if (
|
||||
this.allowResize &&
|
||||
this.internalColumns.length &&
|
||||
|
|
@ -315,7 +316,7 @@ export default {
|
|||
resizeCol(col, width) {
|
||||
if (!this.form) return
|
||||
const index = this.internalColumns.findIndex((c) => c.id === col.id)
|
||||
this.internalColumns[index].cell_width = width
|
||||
this.internalColumns[index].width = width
|
||||
this.setColumns(this.internalColumns)
|
||||
this.$nextTick(() => {
|
||||
this.$emit("resize")
|
||||
|
|
|
|||
|
|
@ -24,8 +24,14 @@ export default {
|
|||
components: { OpenTag },
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
type: [String, Object, Array],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
property: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
@ -34,10 +40,7 @@ export default {
|
|||
|
||||
computed: {
|
||||
valueIsObject() {
|
||||
if (typeof this.value === "object" && this.value !== null) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return Array.isArray(this.value) || (typeof this.value === "object" && this.value !== null)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ export default {
|
|||
components: {},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue