Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes a bug in the flexible-search-menu component #512

Open
wants to merge 8 commits into
base: 0.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 1 addition & 298 deletions dist/css/field.css

Large diffs are not rendered by default.

29,862 changes: 2 additions & 29,860 deletions dist/js/field.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
"production": "mix --production"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"axios": "^0.21.2",
"cross-env": "^5.0.0",
"laravel-mix": "^6.0",
"laravel-nova": "^1.0",
"postcss": "^8.2",
"postcss": "^8.4.38",
"resolve-url-loader": "^3.1.2",
"sass": "^1.32.8",
"sass-loader": "10.*",
"tailwindcss": "^3.4.1",
"vue-loader": "^15.9.5",
"vue-template-compiler": "^2.5.0",
"webpack-dev-server": "^3.1.11"
Expand Down
6 changes: 6 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
2 changes: 1 addition & 1 deletion resources/js/components/SearchMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
}
},
availableLayouts() {
return this.layouts.filter(layout => this.limitPerLayoutCounter[layout.name] > 0);
return this.layouts.filter(layout => (this.limitPerLayoutCounter[layout.name] > 0) || this.limitPerLayoutCounter[layout.name] === undefined);
},
},

Expand Down
255 changes: 255 additions & 0 deletions resources/js/components/SelectMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<template>
<div>
<!-- Add Widget Button -->
<button
type="button"
class="btn btn-default btn-primary flex items-center justify-center px-4 py-2 rounded-lg font-semibold transition-colors duration-200 bg-primary-500 text-white hover:bg-primary-600"
@click.prevent="openModal"
>
Add Widget
</button>

<div class="fixed top-0 left-0 w-screen h-screen z-50 flex items-center justify-center bg-black/80 overflow-hidden"
v-if="showModal"
@modal-close="showModal = false"
>
<div class="bg-white rounded-lg shadow-lg overflow-hidden w-[1200px] max-h-[90vh] flex flex-col" >
<!-- Header - Fixed -->
<div class="p-6 border-b border-gray-200">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Select a Widget</h3>
<button
type="button"
class="text-gray-500 hover:text-gray-700"
@click="showModal = false"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>

<!-- Search and Category Filter -->
<div class="flex gap-4">
<!-- Search Bar -->
<div class="relative flex-1">
<input
type="text"
v-model="searchQuery"
placeholder="Search widgets..."
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<svg
class="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>

<!-- Category Filter -->
<select
v-model="selectedCategory"
class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
>
<option value="">All Categories</option>
<option v-for="category in categories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
</div>

<!-- Content - Scrollable -->
<div class="flex-1 overflow-y-auto p-6">
<div v-for="(widgets, category) in groupedAndFilteredLayouts"
:key="category"
class="mb-8"
>
<h3 class="text-lg font-semibold mb-4 text-gray-700">{{ category }}</h3>
<div class="widget-grid">
<div
v-for="layout in widgets"
:key="layout.name"
class="widget-item"
@click="selectLayout(layout)"
>
<div class="widget-item__preview">
<img
:src="layout.metadata['preview'] || '/images/widgets/default-preview.png'"
:alt="layout.title"
class="w-full h-full object-contain"
>
</div>
<div class="widget-item__content">
<h4 class="widget-item__title">{{ layout.title }}</h4>
<p v-if="layout.description" class="widget-item__description">
{{ layout.description }}
</p>
</div>
</div>
</div>
</div>

<!-- No Results Message -->
<div v-if="Object.keys(groupedAndFilteredLayouts).length === 0" class="text-center py-8 text-gray-500">
No widgets found matching your search.
</div>
</div>
</div>
</div>
</div>
</template>

<script>
export default {
props: {
layouts: {
type: Array,
required: true
}
},

data() {
return {
showModal: false,
searchQuery: '',
selectedCategory: ''
}
},

computed: {
// Get unique categories from layouts
categories() {
return [...new Set(this.layouts.map(layout => layout.metadata['category'] || 'Uncategorized'))].sort();
},

// Group and filter layouts by category
groupedAndFilteredLayouts() {
const filtered = this.layouts.filter(layout => {
const matchesSearch = !this.searchQuery ||
layout.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
(layout.description && layout.description.toLowerCase().includes(this.searchQuery.toLowerCase()));

const category = layout.metadata['category'] || 'Uncategorized';
const matchesCategory = !this.selectedCategory || category === this.selectedCategory;

return matchesSearch && matchesCategory;
});

return filtered.reduce((groups, layout) => {
const category = layout.metadata['category'] || 'Uncategorized';
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(layout);
return groups;
}, {});
}
},

methods: {
openModal() {
console.log('Opening modal');
this.showModal = true;
this.searchQuery = ''; // Reset search when opening modal
this.selectedCategory = ''; // Reset category filter
},

selectLayout(layout) {
if (!layout) return;
this.$emit('addGroup', layout);
this.showModal = false;
this.searchQuery = ''; // Reset search when closing modal
this.selectedCategory = ''; // Reset category filter
}
}
}
</script>

<style scoped>
@tailwind base;
@tailwind components;
@tailwind utilities;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}

.btn-primary {
background-color: var(--primary);
color: white;
border: none;
}

.btn-primary:hover {
background-color: var(--primary-dark);
}

.widget-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
transition: all 0.3s ease;
}

.widget-item {
border: 2px solid #e5e7eb;
border-radius: 0.5rem;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
background: white;
}

.widget-item:hover {
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.widget-item__preview {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
}

.widget-item__preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}

.widget-item__content {
padding: 1rem;
}

.widget-item__title {
font-size: 1rem;
font-weight: 600;
color: #111827;
margin-bottom: 0.5rem;
}

.widget-item__description {
font-size: 0.875rem;
color: #6b7280;
line-height: 1.4;
}

/* Add styles for search input focus state */
input:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2);
}
</style>
1 change: 1 addition & 0 deletions resources/js/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Nova.booting((Vue, router, store) => {
Vue.component('form-nova-flexible-content-group', require('./components/FormGroup.vue').default)
Vue.component('flexible-drop-menu', require('./components/OriginalDropMenu.vue').default)
Vue.component('flexible-search-menu', require('./components/SearchMenu.vue').default)
Vue.component('flexible-select-menu', require('./components/SelectMenu.vue').default)
Vue.component('delete-flexible-content-group-modal', require('./components/DeleteGroupModal.vue').default)
Vue.component('icon-arrow-down', require('./components/icons/ArrowDown.vue').default)
Vue.component('icon-arrow-up', require('./components/icons/ArrowUp.vue').default)
Expand Down
16 changes: 15 additions & 1 deletion src/Layouts/Layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class Layout implements LayoutInterface, JsonSerializable, ArrayAccess, Arrayabl
*/
protected $title;

/**
* The layout's preview image url
*
* @var string
*/
protected $metadata;

/**
* The layout's registered fields
*
Expand Down Expand Up @@ -130,10 +137,11 @@ class Layout implements LayoutInterface, JsonSerializable, ArrayAccess, Arrayabl
* @param int|null $limit
* @return void
*/
public function __construct($title = null, $name = null, $fields = null, $key = null, $attributes = [], callable $removeCallbackMethod = null)
public function __construct($title = null, $name = null, $fields = null, $key = null, $attributes = [], callable $removeCallbackMethod = null, $metadata = [])
{
$this->title = $title ?? $this->title();
$this->name = $name ?? $this->name();
$this->metadata = $metadata ?? $this->metadata();
$this->fields = new FieldCollection($fields ?? $this->fields());
$this->key = is_null($key) ? null : $this->getProcessedKey($key);
$this->removeCallbackMethod = $removeCallbackMethod;
Expand Down Expand Up @@ -647,6 +655,7 @@ public function jsonSerialize()
'title' => $this->title,
'fields' => $this->fields->jsonSerialize(),
'limit' => $this->limit,
'metadata' => $this->metadata
];
}

Expand Down Expand Up @@ -689,4 +698,9 @@ public function toArray()
return $this->attributesToArray();
}

public function metadata()
{
return $this->metadata;
}

}
1 change: 1 addition & 0 deletions src/Layouts/LayoutInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface LayoutInterface
{
public function name();
public function title();
public function metadata();
public function fields();
public function key();
public function getResolved();
Expand Down
11 changes: 11 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./resources/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

Loading