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:
Julien Nahum 2025-02-17 11:27:58 +01:00
parent 0d6bd1bfde
commit 55b0f57741
8 changed files with 203 additions and 38 deletions

View File

@ -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.

View File

@ -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.

View File

@ -1,5 +1,8 @@
<template>
<UButtonGroup size="xs" orientation="horizontal">
<UButtonGroup
size="xs"
orientation="horizontal"
>
<UButton
v-track.delete_record_click
size="sm"

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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)
},
},
}

View File

@ -7,8 +7,9 @@ export default {
components: {},
props: {
value: {
type: String,
required: true,
type: [String, Number],
required: false,
default: null
},
},
}