From ae1d637c308f2b0fbb310aaa2128fe78b550a26e Mon Sep 17 00:00:00 2001 From: Tofandel Date: Tue, 12 Mar 2024 19:01:45 +0100 Subject: [PATCH] Fix bucket morph map Improve buckets doc --- docs/content/1_docs/9_buckets/1_index.md | 8 +- frontend/js/components/buckets/Bucket.vue | 50 ++++--- frontend/js/components/buckets/BucketItem.vue | 39 +++--- frontend/js/mixins/buckets.js | 23 ++-- frontend/js/store/modules/buckets.js | 4 +- src/Helpers/modules_helpers.php | 9 +- .../Controllers/Admin/FeaturedController.php | 129 ++++++++---------- 7 files changed, 135 insertions(+), 127 deletions(-) diff --git a/docs/content/1_docs/9_buckets/1_index.md b/docs/content/1_docs/9_buckets/1_index.md index 497f081d6..534a603f5 100644 --- a/docs/content/1_docs/9_buckets/1_index.md +++ b/docs/content/1_docs/9_buckets/1_index.md @@ -21,6 +21,7 @@ Then, define your buckets' configuration: 'name' => 'Home primary feature', 'bucketables' => [ [ + 'repository' => GuidesRepository::class, 'module' => 'guides', 'name' => 'Guides', 'scopes' => ['published' => true], @@ -32,6 +33,7 @@ Then, define your buckets' configuration: 'name' => 'Home secondary features', 'bucketables' => [ [ + 'repository' => GuidesRepository::class, 'module' => 'guides', 'name' => 'Guides', 'scopes' => ['published' => true], @@ -45,7 +47,7 @@ Then, define your buckets' configuration: ``` You can allow mixing modules in a single bucket by adding more modules to the `bucketables` array. -Each `bucketable` should have its [model morph map](https://laravel.com/docs/10.x/eloquent-relationships#polymorphic-relationships) defined because features are stored in a polymorphic table. +We recommend that each `bucketable` model be in the [morph map](https://laravel.com/docs/10.x/eloquent-relationships#polymorphic-relationships) because features are stored in a polymorphic table. In your AppServiceProvider, you can do it like the following: @@ -55,7 +57,7 @@ use Illuminate\Database\Eloquent\Relations\Relation; public function boot() { Relation::morphMap([ - 'guides' => 'App\Models\Guide', + 'guides' => App\Models\Guide::class, ]); } ``` @@ -66,7 +68,7 @@ Finally, add a link to your buckets page in your CMS navigation: return [ 'featured' => [ 'title' => 'Features', - 'route' => 'admin.featured.homepage', + 'route' => 'twill.featured.homepage', 'primary_navigation' => [ 'homepage' => [ 'title' => 'Homepage', diff --git a/frontend/js/components/buckets/Bucket.vue b/frontend/js/components/buckets/Bucket.vue index 99e761b72..25d851010 100644 --- a/frontend/js/components/buckets/Bucket.vue +++ b/frontend/js/components/buckets/Bucket.vue @@ -50,6 +50,7 @@ { + labels[source.type] = source.label; + }) + return labels; + }, + singleBucket() { return this.buckets.length === 1 }, - singleSource: function () { + singleSource() { return this.dataSources.length === 1 }, - overrideBucketText: function () { + overrideBucketText() { const bucket = this.buckets.find(b => b.id === this.currentBucketID) let bucketName = '' let bucketSize = '' @@ -170,7 +178,7 @@ } }, methods: { - addToBucket: function (item, bucket) { + addToBucket(item, bucket) { const index = this.buckets.findIndex(b => b.id === bucket) if (!item && index === -1) return @@ -188,10 +196,10 @@ if (count > -1 && count < this.buckets[index].max) { // Commit before dispatch to prevent ui visual effect timeout - this.checkRestriced(item) + this.checkRestricted(item) this.$store.commit(BUCKETS.ADD_TO_BUCKET, data) } else if (this.overridableMax || this.overrideItem) { - this.checkRestriced(item) + this.checkRestricted(item) this.$store.commit(BUCKETS.ADD_TO_BUCKET, data) this.$store.commit(BUCKETS.DELETE_FROM_BUCKET, { index, itemIndex: 0 }) this.overrideItem = false @@ -199,11 +207,11 @@ this.$refs.overrideBucket.open() } }, - deleteFromBucket: function (item, bucket) { + deleteFromBucket(item, bucket) { const bucketIndex = this.buckets.findIndex(b => b.id === bucket) if (bucketIndex === -1) return - const itemIndex = this.buckets[bucketIndex].children.findIndex(c => c.id === item.id && c.content_type.value === item.content_type.value) + const itemIndex = this.buckets[bucketIndex].children.findIndex(c => c.id === item.id && c.type === item.type) if (itemIndex === -1) return @@ -213,11 +221,11 @@ } this.$store.commit(BUCKETS.DELETE_FROM_BUCKET, data) }, - toggleFeaturedInBucket: function (item, bucket) { + toggleFeaturedInBucket(item, bucket) { const bucketIndex = this.buckets.findIndex(b => b.id === bucket) if (bucketIndex === -1) return - const itemIndex = this.buckets[bucketIndex].children.findIndex(c => c.id === item.id && c.content_type.value === item.content_type.value) + const itemIndex = this.buckets[bucketIndex].children.findIndex(c => c.id === item.id && c.type === item.type) if (itemIndex === -1) return @@ -228,19 +236,19 @@ this.$store.commit(BUCKETS.TOGGLE_FEATURED_IN_BUCKET, data) }, - checkRestriced: function (item) { + checkRestricted(item) { // Remove item from each bucket if option restricted to one bucket is active if (this.restricted) { this.buckets.forEach((bucket) => { bucket.children.forEach((child) => { - if (child.id === item.id && child.content_type.value === item.content_type.value) { + if (child.id === item.id && child.type === item.type) { this.deleteFromBucket(item, bucket.id) } }) }) } }, - sortBucket: function (evt, index) { + sortBucket(evt, index) { const data = { bucketIndex: index, oldIndex: evt.moved.oldIndex, @@ -248,35 +256,35 @@ } this.$store.commit(BUCKETS.REORDER_BUCKET_LIST, data) }, - changeDataSource: function (value) { + changeDataSource(value) { this.$store.commit(BUCKETS.UPDATE_BUCKETS_DATASOURCE, value) this.$store.commit(BUCKETS.UPDATE_BUCKETS_DATA_PAGE, 1) this.$store.dispatch(ACTIONS.GET_BUCKETS) }, - filterBucketsData: function (formData) { + filterBucketsData(formData) { this.$store.commit(BUCKETS.UPDATE_BUCKETS_DATA_PAGE, 1) this.$store.commit(BUCKETS.UPDATE_BUCKETS_FILTER, formData || { search: '' }) // reload datas this.$store.dispatch(ACTIONS.GET_BUCKETS) }, - updateOffset: function (value) { + updateOffset(value) { this.$store.commit(BUCKETS.UPDATE_BUCKETS_DATA_PAGE, 1) this.$store.commit(BUCKETS.UPDATE_BUCKETS_DATA_OFFSET, value) // reload datas this.$store.dispatch(ACTIONS.GET_BUCKETS) }, - updatePage: function (value) { + updatePage(value) { this.$store.commit(BUCKETS.UPDATE_BUCKETS_DATA_PAGE, value) // reload datas this.$store.dispatch(ACTIONS.GET_BUCKETS) }, - override: function () { + override() { this.overrideItem = true this.addToBucket(this.currentItem, this.currentBucketID) this.$refs.overrideBucket.close() }, - save: function () { + save() { this.$store.dispatch(ACTIONS.SAVE_BUCKETS) } } diff --git a/frontend/js/components/buckets/BucketItem.vue b/frontend/js/components/buckets/BucketItem.vue index c5166dfa0..1ebc42459 100644 --- a/frontend/js/components/buckets/BucketItem.vue +++ b/frontend/js/components/buckets/BucketItem.vue @@ -18,8 +18,8 @@ {{ item.name }} - - {{ item.content_type.label }} + + {{ sourceLabels[item.type] }} @@ -75,29 +75,26 @@ toggleFeaturedLabels: { type: Array, default: () => [] - } + }, + sourceLabels: Object, }, mixins: [bucketMixin], computed: { - inBuckets: function () { - const self = this - let find = false - self.buckets.forEach(function (bucket) { - if (bucket.children.find(function (b) { - return b.id === self.item.id && b.content_type.value === self.item.content_type.value - })) { - find = true + inBuckets() { + for(const bucket in this.buckets) { + if (bucket.children.some((b) => b.id === self.item.id && b.type === self.item.type)) { + return true } - }) - return find + } + return false }, - customClasses: function () { + customClasses() { return { ...this.bucketClasses, draggable: this.draggable } }, - dropDownBuckets: function () { + dropDownBuckets() { const checkboxes = [] const self = this let index = 1 @@ -116,13 +113,13 @@ } }, methods: { - removeFromBucket: function (bucketId = this.bucket) { + removeFromBucket(bucketId = this.bucket) { this.$emit('remove-from-bucket', this.item, bucketId) }, - toggleFeatured: function () { + toggleFeatured() { this.$emit('toggle-featured-in-bucket', this.item, this.bucket) }, - selectedBuckets: function () { + selectedBuckets() { const selected = [] const self = this if (this.buckets.length > 0) { @@ -135,11 +132,11 @@ } return [] }, - slug: function (id) { + slug(id) { // Current Bucket ID + item id + bucket id - return 'bucket-' + this.bucket + '_item-' + this.item.id + '_type-' + this.item.content_type.value + '_inb-' + id + return 'bucket-' + this.bucket + '_item-' + this.item.id + '_type-' + this.item.type + '_inb-' + id }, - updateBucket: function (value) { + updateBucket(value) { const pattern = 'inb-' const self = this diff --git a/frontend/js/mixins/buckets.js b/frontend/js/mixins/buckets.js index 115585960..0f5c0bc0b 100644 --- a/frontend/js/mixins/buckets.js +++ b/frontend/js/mixins/buckets.js @@ -1,3 +1,5 @@ +import { mapState } from 'vuex' + export default { props: { buckets: { @@ -13,7 +15,10 @@ export default { } }, computed: { - bucketClasses: function () { + ...mapState({ + dataSources: state => state.buckets.dataSources.content_types + }), + bucketClasses() { return { selected: this.type !== 'bucket' && this.inBuckets, single: this.singleBucket @@ -21,21 +26,19 @@ export default { } }, methods: { - addToBucket: function (bucketId = this.bucket) { + addToBucket(bucketId = this.bucket) { this.$emit('add-to-bucket', this.item, bucketId) }, - inBucketById: function (id) { + inBucketById(id) { const index = this.buckets.findIndex(b => b.id === id) if (index === -1) return - const find = this.buckets[index].children.find((c) => { - return c.id === this.item.id && c.content_type.value === this.item.content_type.value + return this.buckets[index].children.some((c) => { + return c.id === this.item.id && c.type === this.item.type }) - - return !!find }, - restrictedBySource: function (id) { + restrictedBySource(id) { const bucket = this.buckets.find((b) => b.id === id) if (!bucket) return false @@ -43,8 +46,8 @@ export default { if (!bucket.hasOwnProperty('acceptedSources')) return true if (bucket.acceptedSources.length === 0) return true - const currentSource = this.item.content_type.value - return bucket.acceptedSources.findIndex((source) => source === currentSource) !== -1 + const currentSource = this.dataSources.find((source) => source.type === this.item.type); + return bucket.acceptedSources.findIndex((source) => source === currentSource.value) !== -1 } } } diff --git a/frontend/js/store/modules/buckets.js b/frontend/js/store/modules/buckets.js index 4277bc953..9d065f550 100644 --- a/frontend/js/store/modules/buckets.js +++ b/frontend/js/store/modules/buckets.js @@ -21,7 +21,7 @@ const state = { } const getters = { - currentSource: state => state.source.content_type + currentSource: state => state.dataSources.selected } const mutations = { @@ -80,7 +80,7 @@ const actions = { bucket.children.forEach((child) => { children.push({ id: child.id, - type: child.content_type.value, + type: child.type, starred: child.starred }) }) diff --git a/src/Helpers/modules_helpers.php b/src/Helpers/modules_helpers.php index a3f119bf7..c2a157ff4 100644 --- a/src/Helpers/modules_helpers.php +++ b/src/Helpers/modules_helpers.php @@ -65,11 +65,18 @@ function getRepositoryByModuleName($moduleName) if (!function_exists('getModelRepository')) { function getModelRepository($relation, $model = null) { + if ($relation instanceof TwillModelContract) { + $model = get_class($relation); + } if (!$model) { $model = ucfirst(Str::singular($relation)); } + if ($model instanceof TwillModelContract) { + $model = get_class($model); + } + $model = class_basename($model); - $repository = config('twill.namespace') . '\\Repositories\\' . ucfirst($model) . 'Repository'; + $repository = config('twill.namespace') . '\\Repositories\\' . $model . 'Repository'; if (!class_exists($repository)) { try { diff --git a/src/Http/Controllers/Admin/FeaturedController.php b/src/Http/Controllers/Admin/FeaturedController.php index 7c98401a8..3ab93d31d 100644 --- a/src/Http/Controllers/Admin/FeaturedController.php +++ b/src/Http/Controllers/Admin/FeaturedController.php @@ -40,31 +40,30 @@ public function index(Request $request, ViewFactory $viewFactory, UrlGenerator $ $featuredSection = $this->config->get("twill.buckets.$featuredSectionKey"); $filters = json_decode($request->get('filter'), true) ?? []; - $featuredSources = $this->getFeaturedSources($request, $featuredSection, $filters['search'] ?? ''); + $featuredSources = $this->getFeaturedSources($request, $featuredSection, $filters['search'] ?? '', $request->get('content_type')); $contentTypes = Collection::make($featuredSources)->map(function ($source, $sourceKey) { return [ 'label' => $source['name'], 'value' => $sourceKey, + 'type' => $source['type'], ]; - })->values()->toArray(); + })->values()->all(); + + $firstSource = Arr::first($featuredSources); if ($request->has('content_type')) { - $source = $featuredSources[$request->get('content_type')] ?? null; + $source = $firstSource; return [ 'source' => [ - 'content_type' => Arr::first($contentTypes, function ($contentTypeItem) use ($request) { - return $contentTypeItem['value'] == $request->get('content_type'); - }), 'items' => $source['items'], ], 'maxPage' => $source['maxPage'], ]; } - $buckets = $this->getFeaturedItemsByBucket($featuredSection, $featuredSectionKey); - $firstSource = Arr::first($featuredSources); + $buckets = $this->getFeaturedItemsByBucket($featuredSection); $routePrefix = 'featured'; @@ -79,7 +78,6 @@ public function index(Request $request, ViewFactory $viewFactory, UrlGenerator $ ], 'items' => $buckets, 'source' => [ - 'content_type' => Arr::first($contentTypes), 'items' => $firstSource['items'], ], 'maxPage' => $firstSource['maxPage'], @@ -94,14 +92,11 @@ public function index(Request $request, ViewFactory $viewFactory, UrlGenerator $ /** * @param array $featuredSection - * @param string $featuredSectionKey * @return array */ - private function getFeaturedItemsByBucket($featuredSection, $featuredSectionKey) + private function getFeaturedItemsByBucket($featuredSection) { - $bucketRouteConfig = $this->config->get('twill.bucketsRoutes') ?? [$featuredSectionKey => 'featured']; - return Collection::make($featuredSection['buckets'])->map(function ($bucket, $bucketKey) use ($featuredSectionKey, $bucketRouteConfig) { - $routePrefix = $bucketRouteConfig[$featuredSectionKey]; + return collect($featuredSection['buckets'])->map(function ($bucket, $bucketKey) { return [ 'id' => $bucketKey, 'name' => $bucket['name'], @@ -109,24 +104,20 @@ private function getFeaturedItemsByBucket($featuredSection, $featuredSectionKey) 'acceptedSources' => Collection::make($bucket['bucketables'])->pluck('module'), 'withToggleFeatured' => $bucket['with_starred_items'] ?? false, 'toggleFeaturedLabels' => $bucket['starred_items_labels'] ?? [], - 'children' => Feature::where('bucket_key', $bucketKey)->with('featured')->get()->map(function ($feature) use ($bucket) { + 'children' => Feature::where('bucket_key', $bucketKey)->with('featured')->get()->map(function ($feature) { if (($item = $feature->featured) != null) { - $forModuleRepository = collect($bucket['bucketables'])->where('module', $feature->featured_type)->first()['repository'] ?? null; - $repository = $this->getRepository($feature->featured_type, $forModuleRepository); + $repository = getModelRepository($item); $withImage = classHasTrait($repository, HandleMedias::class); return [ - 'id' => $item->id, - 'name' => $item->titleInBucket ?? $item->title, - 'edit' => $item->adminEditUrl ?? '', - 'starred' => $feature->starred ?? false, - 'content_type' => [ - 'label' => ucfirst($feature->featured_type), - 'value' => $feature->featured_type, - ], - ] + ($withImage ? [ - 'thumbnail' => $item->defaultCmsImage(['w' => 100, 'h' => 100]), - ] : []); + 'id' => $item->id, + 'name' => $item->titleInBucket ?? $item->title, + 'edit' => $item->adminEditUrl ?? '', + 'starred' => $feature->starred ?? false, + 'type' => $feature->featured_type, + ] + ($withImage ? [ + 'thumbnail' => $item->defaultCmsImage(['w' => 100, 'h' => 100]), + ] : []); } })->reject(function ($item) { return is_null($item); @@ -141,15 +132,18 @@ private function getFeaturedItemsByBucket($featuredSection, $featuredSectionKey) * @param string|null $search * @return array */ - private function getFeaturedSources(Request $request, $featuredSection, $search = null) + private function getFeaturedSources(Request $request, $featuredSection, string $search = null, string $contentType = null) { - $fetchedModules = []; $featuredSources = []; - Collection::make($featuredSection['buckets'])->map(function ($bucket, $bucketKey) use (&$fetchedModules, $search, $request) { - return Collection::make($bucket['bucketables'])->mapWithKeys(function ($bucketable) use (&$fetchedModules, $search, $request) { - + foreach ($featuredSection['buckets'] as $bucket) { + foreach ($bucket['bucketables'] as $bucketable) { $module = $bucketable['module']; + + if ((!empty($contentType) && $module !== $contentType) || isset($featuredSources[$module])) { + continue; + } + $repository = $this->getRepository($module, $bucketable['repository'] ?? null); $translated = classHasTrait($repository, HandleTranslations::class); $withImage = classHasTrait($repository, HandleMedias::class); @@ -159,45 +153,42 @@ private function getFeaturedSources(Request $request, $featuredSection, $search $scopes[$searchField] = $search; } - $items = $fetchedModules[$module] ?? $repository->get( - $bucketable['with'] ?? [], - ($bucketable['scopes'] ?? []) + ($scopes ?? []), - $bucketable['orders'] ?? [], - $bucketable['per_page'] ?? $request->get('offset') ?? 10, - true - )->appends('bucketable', $module); + $items = null; + if (!empty($contentType) || empty($featuredSources)) { + $items = $repository->get( + $bucketable['with'] ?? [], + ($bucketable['scopes'] ?? []) + ($scopes ?? []), + $bucketable['orders'] ?? [], + $bucketable['per_page'] ?? $request->get('offset') ?? 10, + true + )->appends('bucketable', $module); + } - $fetchedModules[$module] = $items; + $morphClass = $repository->getBaseModel()->getMorphClass(); - return [$module => [ + $featuredSources[$module] = [ 'name' => $bucketable['name'] ?? ucfirst($module), - 'items' => $items, - 'translated' => $translated, - 'withImage' => $withImage, - ]]; - }); - })->each(function ($bucketables, $bucket) use (&$featuredSources) { - $bucketables->each(function ($bucketableData, $bucketable) use (&$featuredSources) { - $featuredSources[$bucketable]['name'] = $bucketableData['name']; - $featuredSources[$bucketable]['maxPage'] = $bucketableData['items']->lastPage(); - $featuredSources[$bucketable]['offset'] = $bucketableData['items']->perPage(); - $featuredSources[$bucketable]['items'] = $bucketableData['items']->map(function ($item) use ($bucketableData, $bucketable) { - return [ - 'id' => $item->id, - 'name' => $item->titleInBucket ?? $item->title, - 'edit' => $item->adminEditUrl ?? '', - 'content_type' => [ - 'label' => $bucketableData['name'], - 'value' => $bucketable, - ], - ] + ($bucketableData['translated'] ? [ - 'languages' => $item->getActiveLanguages(), - ] : []) + ($bucketableData['withImage'] ? [ - 'thumbnail' => $item->defaultCmsImage(['w' => 100, 'h' => 100]), - ] : []); - })->toArray(); - }); - }); + 'type' => $morphClass, + 'maxPage' => $items?->lastPage(), + 'offset' => $items?->perPage(), + 'items' => $items?->map(function ($item) use ($morphClass, $translated, $withImage) { + return [ + 'id' => $item->id, + 'name' => $item->titleInBucket ?? $item->title, + 'edit' => $item->adminEditUrl ?? '', + 'type' => $morphClass, + ] + ($translated ? [ + 'languages' => $item->getActiveLanguages(), + ] : []) + ($withImage ? [ + 'thumbnail' => $item->defaultCmsImage(['w' => 100, 'h' => 100]), + ] : []); + })->all(), + ]; + if ($contentType === $module) { + break 2; + } + } + } return $featuredSources; }