From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do
---
JVBase.php | 5
inc/managers/CRUDManager.php | 6
inc/managers/CustomTable.php | 49 +++-
assets/js/concise/FrontendFavourites.js | 40 +++
inc/ui/CRUDSkeleton.php | 10
assets/css/dash.min.css | 2
assets/js/min/favourites.min.js | 2
inc/registrar/Fields.php | 12
inc/rest/routes/FavouritesRoutes.php | 325 +++++++-------------------------
inc/managers/FavouritesManager.php | 109 ++++++++++
inc/registrar/Registrar.php | 11
11 files changed, 278 insertions(+), 293 deletions(-)
diff --git a/JVBase.php b/JVBase.php
index 28efe56..bf0db8a 100644
--- a/JVBase.php
+++ b/JVBase.php
@@ -219,6 +219,11 @@
return array_merge(array_keys($this->content), array_keys($this->taxonomies));
}
+ public function favourites(): FavouritesManager|false
+ {
+ return $this->managers['favourites'] ?? false;
+ }
+
public function dashboard(): DashboardManager|false
{
return $this->managers['dash'] ?? false;
diff --git a/assets/css/dash.min.css b/assets/css/dash.min.css
index 3e6de63..069bff2 100644
--- a/assets/css/dash.min.css
+++ b/assets/css/dash.min.css
@@ -1 +1 @@
-.replace{margin-left:var(--btn_)!important}.dashboard aside.main.left{bottom:0}.dashboard .qtoggle{left:0;bottom:0;margin:0!important}nav.sidebar{--wrap:nowrap;--align:flex-start;position:fixed;bottom:0;top:var(--btn);z-index:var(--z-4);height:calc(100% - var(--btn));background-color:rgb(var(--base));box-shadow:rgba(var(--base),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto;margin-left:0!important;padding-bottom:var(--btn)}nav.sidebar.left{left:0}nav.sidebar.right{right:0}nav.sidebar .icon{--w:var(--chip_);width:var(--w);transition:var(--trans-size)}nav.sidebar.open{width:fit-content;max-width:85vw}nav.sidebar.open .icon{--w:var(--chip)}nav.sidebar ul{height:max-content;width:100%;--gap:0;--dir:column}nav.sidebar li{--justify:center;--wrap:nowrap;--align:flex-start;overflow:hidden;height:max-content}nav.sidebar ul ul{max-height:0;overflow:hidden;transform:scaleY(0);transform-origin:top;transition:var(--trans-base)}nav.sidebar li.open>ul{max-height:max-content;transform:scaleY(1)}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .title{display:none}nav.sidebar.open .title{display:block;white-space:nowrap}nav.sidebar a{--justify:flex-start}nav.sidebar .a{gap:.5rem;display:flex;width:100%;justify-content:flex-start;align-items:center;min-height:var(--btn);padding:var(--padding)}nav.sidebar .toggle:not(.main){display:none;width:var(--btn);height:var(--chipchip)}nav.sidebar.open .toggle{display:flex}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base),var(--op-45)) var(--shdw)}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));padding:0;border-radius:0 0 var(--radius-outer) var(--radius-outer)}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover::after{background-color:rgb(var(--action-contrast))}.all-filters summary{width:100%}.all-filters>.row.row{padding:0 .75rem;width:var(--content);position:relative}.all-filters>.row>.label,.all-filters>.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row>.label{width:20%}.all-filters>.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{width:var(--chipchip);min-height:var(--chipchip);padding:0}.all-filters .btn+label,.all-filters button{position:unset}.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label,.btn+label .label{position:absolute;top:2rem;left:1rem;width:max-content;white-space:nowrap;opacity:0;z-index:var(--z-4)}.all-filters .radio-options.order>.row{position:relative;padding-bottom:2rem}.all-filters .radio-options .row .btn+label .label{bottom:0;top:unset;left:0;height:max-content}.all-filters button:hover .label,.btn+label:hover .label,.btn:checked+label .label{opacity:1}.all-filters .radio-options.order>.row{max-width:49%}.all-filters .radio-options.order{margin-top:1rem}.all-filters .filters select,.all-filters .filters>.row{width:max-content}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}section.main-actions.main-actions{padding:0;position:absolute;left:var(--offScreen)}.dashboard main>footer{padding:0;margin:0;position:absolute;left:var(--offScreen)}.item .select-item-label{width:100%;height:100%;aspect-ratio:1}
\ No newline at end of file
+.replace{margin-left:var(--btn_)!important}.dashboard aside.main.left{bottom:0}.dashboard .qtoggle{left:0;bottom:0;margin:0!important}nav.sidebar{--wrap:nowrap;--align:flex-start;position:fixed;bottom:0;top:var(--btn);z-index:var(--z-4);height:calc(100% - var(--btn));background-color:rgb(var(--base));box-shadow:rgba(var(--base),var(--op-45)) var(--shdw);width:var(--btn);transition:var(--trans-size);overflow:hidden auto;margin-left:0!important;padding-bottom:var(--btn)}nav.sidebar.left{left:0}nav.sidebar.right{right:0}nav.sidebar .icon{--w:var(--chip_);width:var(--w);transition:var(--trans-size)}nav.sidebar.open{width:fit-content;max-width:85vw}nav.sidebar.open .icon{--w:var(--chip)}nav.sidebar ul{height:max-content;width:100%;--gap:0;--dir:column}nav.sidebar li{--justify:center;--wrap:nowrap;--align:flex-start;overflow:hidden;height:max-content}nav.sidebar ul ul{max-height:0;overflow:hidden;transform:scaleY(0);transform-origin:top;transition:var(--trans-base)}nav.sidebar li.open>ul{max-height:max-content;transform:scaleY(1)}nav.sidebar.open li>div{width:100%;padding-right:var(--btn)}nav.sidebar.open li.has-submenu>div{padding-right:0}nav.sidebar.open li.has-submenu>ul{padding-left:var(--chip)}nav.sidebar .title{display:none}nav.sidebar.open .title{display:block;white-space:nowrap}nav.sidebar a{--justify:flex-start}nav.sidebar .a{gap:.5rem;display:flex;width:100%;justify-content:flex-start;align-items:center;min-height:var(--btn);padding:var(--padding)}nav.sidebar .toggle:not(.main){display:none;width:var(--btn);height:var(--chipchip)}nav.sidebar.open .toggle{display:flex}nav.sidebar .toggle.main{position:fixed;left:unset;bottom:0;right:0;width:var(--btn);height:var(--btn);z-index:var(--z-8);box-shadow:rgba(var(--base),var(--op-45)) var(--shdw)}.all-filters{font-size:var(--txt-x-small)}.all-filters[open]{border:2px solid rgb(var(--action-0));padding:0;border-radius:0 0 var(--radius-outer) var(--radius-outer)}.all-filters summary:hover,.all-filters[open] summary{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}.all-filters summary:hover::after{background-color:rgb(var(--action-contrast))}.all-filters summary{width:100%}.all-filters>.row.row{padding:0 .75rem;width:var(--content);position:relative}.all-filters>.row>.label,.all-filters>.row>.row>.label{font-family:var(--heading);font-weight:var(--fw-h-bold);text-transform:uppercase}.all-filters>.row>.label{width:20%}.all-filters>.row>.row>.label{white-space:nowrap}.all-filters .btn+label,.all-filters button{width:var(--chipchip);min-height:var(--chipchip);padding:0}.all-filters .btn+label,.all-filters button{position:unset}.all-filters .row:has(>.btn:not(:checked)+label:hover) :checked+label .label,.all-filters button .label,.btn+label .label{position:absolute;top:2rem;left:1rem;width:max-content;white-space:nowrap;opacity:0;z-index:var(--z-4)}.all-filters .radio-options.order>.row{position:relative;padding-bottom:2rem}.all-filters .radio-options .row .btn+label .label{bottom:0;top:unset;left:0;height:max-content}.all-filters button:hover .label,.btn+label:hover .label,.btn:checked+label .label{opacity:1}.all-filters .radio-options.order>.row{max-width:49%}.all-filters .radio-options.order{margin-top:1rem}.all-filters .filters select,.all-filters .filters>.row{width:max-content}.search-container:not(.open) .clear-search,.search-container:not(.open) input[type=search]{transform:scaleX(0);transform-origin:left;width:0;padding:0;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.search-container button{padding:.5rem}.search-container .icon{--w:1.5rem}.search-container.open .clear-search,.search-container.open input[type=search]{transform:scaleX(1);transform-origin:left;transition:transform var(--trans-base),width var(--trans-base),padding var(--trans-base)}.all-filters>.search,.search-container,input[type=search]{width:100%}section.main-actions.main-actions{padding:0;position:absolute;left:var(--offScreen)}.dashboard main>footer{padding:0;margin:0;position:absolute;left:var(--offScreen)}.item .select-item-label{width:100%;height:100%;aspect-ratio:1}dialog form nav.tabs{--wrap:nowrap!important;--gap:0!important;overflow-x:auto;touch-action:pan-x;position:absolute;top:0;left:0;right:0;max-width:100%;height:var(--chipchip);padding:0;background-color:rgba(var(--base),var(--op-5))}dialog form nav.tabs button{min-height:var(--chipchip);padding:0 1rem}dialog nav.tabs button:hover{background-color:rgb(var(--action-0));color:rgb(var(--action-contrast))}dialog:has(nav.tabs) .wrap{padding-top:3rem}dialog{height:100%!important}
\ No newline at end of file
diff --git a/assets/js/concise/FrontendFavourites.js b/assets/js/concise/FrontendFavourites.js
index 661bbfc..47befbe 100644
--- a/assets/js/concise/FrontendFavourites.js
+++ b/assets/js/concise/FrontendFavourites.js
@@ -71,12 +71,40 @@
// Update button icon
button.innerHTML = jvbSettings.icons[button.classList.contains('favourited') ? 'heart-filled' : 'heart'];
- this.store.setItem(button.dataset.id, {
- target_id: button.dataset.id,
- action: action,
- type: button.dataset.type,
- artist: button.dataset.artist,
- });
+ window.debouncer.schedule(
+ `favourite-${button.dataset.id}`,
+ this.postFavourite({
+ user: window.auth.getUser(),
+ target_id: button.dataset.id,
+ action: action,
+ type: button.dataset.type
+ }),
+ 100
+ );
+
+ }
+
+ async postFavourite(args) {
+ await window.auth.fetch(
+ `${jvbSettings.api}favourites`,
+ {
+ method: 'POST',
+ headers: {
+ 'X-Action-Nonce': window.auth.getNonce('favourites')
+ },
+ body: args
+ }
+ );
+
+ if (args.action === 'add') {
+ await this.store.setItem(button.dataset.id, {
+ target_id: button.dataset.id,
+ action: action,
+ type: button.dataset.type,
+ }).then(()=>{});
+ } else {
+ await this.store.delete(button.dataset.id).then(()=>{});
+ }
}
// async toggleFavourite(itemType, itemId) {
diff --git a/assets/js/min/favourites.min.js b/assets/js/min/favourites.min.js
index 8369985..6fc4b95 100644
--- a/assets/js/min/favourites.min.js
+++ b/assets/js/min/favourites.min.js
@@ -1 +1 @@
-(()=>{class t{constructor(){let t=window.jvbStore.register("favourites",{storeName:"favourites",keyPath:"id",endpoint:"favourites",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:window.auth.getUser(),content:"all",order:"desc",orderby:"date",page:1,all:!0}});this.store=t.favourites,this.store.subscribe(((t,e)=>{t}))}toggleFavourite(t){if(!window.auth.getUser())return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],this.store.setItem(t.dataset.id,{target_id:t.dataset.id,action:e,type:t.dataset.type,artist:t.dataset.artist})}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbFavourites=new t)}))})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
+(()=>{class t{constructor(){let t=window.jvbStore.register("favourites",{storeName:"favourites",keyPath:"id",endpoint:"favourites",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},indexes:[{name:"content",keyPath:"content"},{name:"listId",keyPath:"listId"}],TTL:36e4,showLoading:!1,filters:{user:window.auth.getUser(),content:"all",order:"desc",orderby:"date",page:1,all:!0}});this.store=t.favourites,this.store.subscribe(((t,e)=>{t}))}toggleFavourite(t){if(!window.auth.getUser())return void(window.location.href=jvbSettings.redirect+"&action=register&type=favourites");t.classList.toggle("favourited");const e=t.classList.contains("favourited")?"add":"remove",o=t.classList.contains("favourited")?`Added ${t.dataset.type} to favourites.`:`Removed ${t.dataset.type} from favourites.`;window.jvbA11y.announce(o),t.innerHTML=jvbSettings.icons[t.classList.contains("favourited")?"heart-filled":"heart"],window.debouncer.schedule(`favourite-${t.dataset.id}`,this.postFavourite({user:window.auth.getUser(),target_id:t.dataset.id,action:e,type:t.dataset.type}),100)}async postFavourite(t){await window.auth.fetch(`${jvbSettings.api}favourites`,{method:"POST",headers:{"X-Action-Nonce":window.auth.getNonce("favourites")},body:t}),"add"===t.action?await this.store.setItem(button.dataset.id,{target_id:button.dataset.id,action,type:button.dataset.type}).then((()=>{})):await this.store.delete(button.dataset.id).then((()=>{}))}isFavourited(t,e){const o=`${this.userId}_${t}_${e}`;return void 0!==this.store.get(o)}}document.addEventListener("DOMContentLoaded",(function(){window.jvbFavourites=!1,window.auth.subscribe((e=>{"auth-loaded"===e&&(window.jvbFavourites=new t)}))})),window.toggleFavourite=function(t){window.jvbFavourites()?window.jvbFavourites.toggleFavourite(t):console.log("No Favourites Loaded")},window.isFavourited=function(t,e){if(window.jvbFavourites())return window.jvbFavourites.isFavourited(t,e);console.log("No Favourites Loaded")}})();
\ No newline at end of file
diff --git a/inc/managers/CRUDManager.php b/inc/managers/CRUDManager.php
index 58aa17d..1fedd00 100644
--- a/inc/managers/CRUDManager.php
+++ b/inc/managers/CRUDManager.php
@@ -64,12 +64,10 @@
// Fields and sections
$this->skeleton->setFields($this->registrar->getFields());
- jvbDump($this->registrar->getSections());
$sections = $this->registrar->getSections();
if (count($sections) > 1) {
foreach ($sections as $config) {
- jvbDump($config);
- $this->skeleton->addSection($config['id'], $config);
+ $this->skeleton->addSection($config['slug'], $config);
}
}
@@ -189,7 +187,7 @@
protected function addDateRanges():array
{
- return $this->cache->remember(
+ return $this->cache->user()->remember(
'dateRanges',
function() {
$postType = jvbCheckBase($this->content);
diff --git a/inc/managers/CustomTable.php b/inc/managers/CustomTable.php
index 572d8a2..1a6719c 100644
--- a/inc/managers/CustomTable.php
+++ b/inc/managers/CustomTable.php
@@ -319,16 +319,16 @@
}
/**
- * Set LIMIT
+ * Set pagination
*
- * @param int $limit Number of records
- * @param int $offset Optional offset
+ * @param int $perPage Number of records per page
+ * @param int $page Page number (1-based)
* @return self
*/
- public function limit(int $limit, int $offset = 0): self
+ public function limit(int $perPage, int $page = 1): self
{
- $this->builder['limit'] = $limit;
- $this->builder['offset'] = $offset;
+ $this->builder['per_page'] = $perPage;
+ $this->builder['page'] = $page;
return $this;
}
@@ -601,12 +601,12 @@
* 'where' => ['user_id' => 1],
* 'orderby' => 'date_added',
* 'order' => 'DESC',
- * 'limit' => 20
+ * 'per_page' => 20
* ]);
*/
- public function getMany(array $args = [], string $output = OBJECT): array
+ public function getMany(array $args = [], bool $itemsOnly = true, string $output = OBJECT): array
{
- return $this->cache->remember(
+ $items = $this->cache->remember(
$this->cache->generateKey(array_merge($args, ['output' => $output])),
function () use ($args, $output) {
$query = "SELECT * FROM {$this->fullTableName}";
@@ -628,10 +628,19 @@
}
// LIMIT
- if (!empty($args['limit'])) {
- $limit = absint($args['limit']);
- $offset = !empty($args['offset']) ? absint($args['offset']) : 0;
- $query .= " LIMIT {$offset}, {$limit}";
+ if (array_key_exists('limit', $args)) {
+ error_log('[CustomTable]::getMany deprecated key \'limit\' - use \'per_page\' instead. '.print_r($args, true));
+ $args['per_page'] = $args['limit'];
+ }
+ if (array_key_exists('offset', $args)) {
+ error_log('[CustomTable]::getMany deprecated key \'offset\' - use \'page\' instead. '.print_r($args, true));
+ $args['page'] = $args['offset'] / $args['limit'];
+ }
+ if (!empty($args['per_page'])) {
+ $perPage = absint($args['per_page']);
+ $page = !empty($args['page']) ? absint($args['page']) : 1;
+ $offset = ($page - 1) * $perPage;
+ $query .= " LIMIT {$offset}, {$perPage}";
}
if (empty($values)) {
@@ -642,8 +651,20 @@
}
);
+ if ($itemsOnly) {
+ return $items;
+ }
+ $page = max(1, $args['page'] ?? 1);
+ $perPage = $args['per_page']??false;
+ $total = $this->count($args['where']);
+ return [
+ 'items' => $items,
+ 'total' => $total,
+ 'has_more' => $perPage && ($page * $perPage) < $total,
+ ];
}
+
/**
* Get a specific column value from all matches
* @param string $column
@@ -669,7 +690,7 @@
$args['order'] = $order;
}
if ($limit) {
- $args['limit'] = $limit;
+ $args['per_page'] = $limit;
}
return array_column($this->getMany($args), $column);
diff --git a/inc/managers/FavouritesManager.php b/inc/managers/FavouritesManager.php
index 0ea911c..125c75a 100644
--- a/inc/managers/FavouritesManager.php
+++ b/inc/managers/FavouritesManager.php
@@ -461,6 +461,115 @@
'status' => $accept ? 'accepted' : 'rejected',
], $args);
}
+
+ public function getFavourites(array $args = []):array
+ {
+ return $this->favourites->getMany($args, false);
+ }
+ public function getLists(array $args = []):array
+ {
+ return $this->lists->getMany($args, false);
+ }
+ public function getAvailableLists(array $args = [], bool $include_shared = true):array
+ {
+ $lists = [];
+ $owned = $this->lists->getMany($args, false);
+ foreach ($owned['items'] as &$list) {
+ $list['item_count'] = $this->listItems->count(['list_id' => $list['id']]);
+ }
+
+ $lists['owned'] = $owned;
+
+ if ($include_shared) {
+ $args['where']['status'] = 'accepted';
+ $sharedLists = $this->listShares->getMany($args);
+ $shared = [];
+
+ foreach ($sharedLists as $share) {
+ $sharedList = $this->lists->get(['id' => $share->list_id]);
+ if ($sharedList) {
+ $sharedList['owner_name'] = jvbGetUsername($sharedList['user_id']);
+ $sharedList['item_count'] = $this->listItems->count(['list_id' => $sharedList['id']]);
+ $sharedList['permission_type'] = $sharedList->permission_type;
+ $shared[] = $sharedList;
+ }
+ }
+ $lists['shared'] = $shared;
+ }
+ return $lists;
+ }
+
+ public function userOwnsList(int $list_id, int $user_id):bool
+ {
+ return $this->lists->count(['id' => $list_id, 'user_id' => $user_id]) > 0;
+ }
+
+ public function getListDetails(int $list_id, int $user_id, int $page = 1):array
+ {
+ $list = JVB()->favourites()->getLists(['id' => $list_id, 'user_id' => $user_id]);
+ if (!$list) {
+ return [];
+ }
+ $items = $this->listItems->getMany([
+ 'where' => ['list_id' => $list_id],
+ 'order_by' => 'added_at',
+ 'order' => 'DESC',
+ 'per_page' => 25,
+ 'page' => $page,
+ ]);
+
+ $formatted = [];
+
+ foreach($items as $item) {
+ //See if we can get the actual favourite record first
+ if ($item->favourite_id) {
+ $fav = $this->favourites->get(['id' => $item->favourite_id]);
+ if ($fav) {
+ $formatted[] = $fav;
+ continue;
+ }
+ }
+
+ //Create a dummy favourite object otherwise
+ $formatted[] = (object) [
+ 'type' => $item->item_type,
+ 'target_id' => $item->item_id,
+ 'created_at'=> $item->added_at
+ ];
+ }
+
+ $sharedWith = [];
+ $is_owner = $this->userOwnsList($list_id, $user_id);
+ if ($is_owner) {
+ $shares = $this->listShares->getMany([
+ 'where' => ['list_id' => $list_id],
+ 'order_by' => 'created_at',
+ 'order' => 'DESC'
+ ]);
+ foreach ($shares as $share_item) {
+ $user = [
+ 'email' => $share_item->email,
+ 'status' => $share_item->status,
+ 'date_added'=> $share_item->created_at,
+ ];
+ if ($share_item->status === 'accepted' && $share_item->user_id) {
+ $user['name'] = jvbGetUsername($share_item->user_id);
+ $user['permission_type'] = $share_item->permission_type;
+ }
+ $sharedWith[] = $user;
+ }
+ }
+
+ return [
+ 'id' => (int)$list['id'],
+ 'name' => $list['name'],
+ 'description' => $list['description']??'',
+ 'created_at' => $list['created_at'],
+ 'is_owner' => $is_owner,
+ 'items' => $formatted,
+ 'shared_users' => $sharedWith
+ ];
+ }
/***************************************************************
* UTILITY METHODS
**************************************************************/
diff --git a/inc/registrar/Fields.php b/inc/registrar/Fields.php
index 5635c0c..8e37662 100644
--- a/inc/registrar/Fields.php
+++ b/inc/registrar/Fields.php
@@ -97,6 +97,12 @@
];
}
$fields = [
+ 'post_status' => [
+ 'type' => 'radio',
+ 'label' => 'Status',
+ 'default' => 'draft',
+ 'options' => $statuses
+ ],
'post_thumbnail' => [
'type' => 'upload',
'subtype' => 'image',
@@ -127,12 +133,6 @@
'label' => 'TLDR',
'maxLength' => 158,
],
- 'post_status' => [
- 'type' => 'radio',
- 'label' => 'Status',
- 'default' => 'draft',
- 'options' => $statuses
- ]
];
foreach ($fields as $name => $config) {
diff --git a/inc/registrar/Registrar.php b/inc/registrar/Registrar.php
index 6b5cf2c..8b4b57f 100644
--- a/inc/registrar/Registrar.php
+++ b/inc/registrar/Registrar.php
@@ -724,9 +724,10 @@
public function getSections():array
{
$allSections = array_map(function($section) {
- return $section->getConfig;
+ return $section->getConfig();
}, $this->sections);
+
if (!empty($this->sectionOrder)) {
$allSections['order'] = $this->sectionOrder;
}
@@ -759,13 +760,11 @@
foreach ($sections as $s) {
$section = new Section($s, $this);
$section->setTitle(ucwords(implode(' ', explode('-', $s))));
- $sectionFields = array_map(function ($f) {
- return $f['name'];
- }, array_filter($fields, function ($f) use ($s) {
+ $sectionFields = array_filter($fields, function ($f) use ($s) {
$tmp = array_key_exists('section', $f) && !is_null($f['section']) ? $f['section'] : 'main';
return $s === $tmp;
- }));
- $section->setFields($sectionFields);
+ });
+ $section->setFields(array_keys($sectionFields));
$this->sections[$s] = $section;
}
}
diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index 32d7866..9f38917 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/inc/rest/routes/FavouritesRoutes.php
@@ -16,9 +16,6 @@
exit;
}
-/**
- * TODO: Extract business logic into a Favourites.php manager class
- */
class FavouritesRoutes extends Rest
{
protected array $valid_types;
@@ -73,7 +70,6 @@
->post([$this, 'handleFavourite'])
->args([
'user' => 'integer|required',
- 'id' => 'string|required',
'action' => 'string|required|enum:add,remove,toggle,batch,note',
'type' => 'string',
'target_id' => 'integer',
@@ -130,149 +126,50 @@
if ($cache_check) {
return $cache_check;
}
-
- if (count($args) === 1 || ($request->get_param('include_all') === true)) {
- $result = $this->getAllFavourites($user_id);
- } else {
- $result = $this->cache->remember($key, function() use ($args) {
- return $this->getFilteredFavourites($args);
- });
- }
+ $result = JVB()->favourites()->getFavourites($args);
+ $result['items'] = $this->formatItems($result['items']);
return $this->addCacheHeaders(Response::success($result));
}
- /**
- * Get filtered favourites using CustomTable fluent interface
- */
- protected function getFilteredFavourites(array $args): array
- {
- try {
- // Build base query
- $query = $this->favourites->where(['user_id' => $args['user']]);
-
- // Add type filter if specified
- if (!empty($args['content']) && $args['content'] !== 'all') {
- $query = $this->favourites->where([
- 'user_id' => $args['user'],
- 'type' => BASE . $args['content']
- ]);
- }
-
- // Apply ordering and pagination
- $orderby = in_array($args['orderby'] ?? 'date_added', ['date_added', 'type'])
- ? $args['orderby']
- : 'date_added';
- $order = in_array(strtoupper($args['order'] ?? 'DESC'), ['ASC', 'DESC'])
- ? strtoupper($args['order'])
- : 'DESC';
-
- $favourites = $query
- ->orderBy($orderby, $order)
- ->limit(100, ($args['page'] - 1) * 100)
- ->getResults();
-
- // Get total count
- $count_query = $this->favourites->where(['user_id' => $args['user']]);
- if (!empty($args['content']) && $args['content'] !== 'all') {
- $count_query->where(['type' => BASE . $args['content']]);
- }
- $total_items = $count_query->countResults();
-
- return [
- 'items' => $this->formatItems($favourites),
- 'has_more' => ($args['page'] * 100) < $total_items,
- 'total' => $total_items,
- 'success' => true,
- ];
-
- } catch (Exception $e) {
- $this->logError('getFilteredFavourites', [
- 'error' => $e->getMessage(),
- 'args' => $args
- ]);
-
- return [
- 'success' => false,
- 'items' => [],
- 'total' => 0,
- 'has_more' => false
- ];
- }
- }
-
- /**
- * Get all user's favourites organized by content type
- */
- protected function getAllFavourites(int $user_id): array
- {
- return $this->cache->remember($user_id, function() use ($user_id) {
- try {
- $favourites = $this->favourites
- ->where(['user_id' => $user_id])
- ->getResults();
-
- $by_type = [];
- foreach ($favourites as $fav) {
- $type = str_replace(BASE, '', $fav->type);
- if (!isset($by_type[$type])) {
- $by_type[$type] = [];
- }
- $by_type[$type][] = (int)$fav->target_id;
- }
-
- return [
- 'success' => true,
- 'items' => $by_type,
- 'has_more' => false,
- ];
-
- } catch (Exception $e) {
- $this->logError('getAllFavourites', [
- 'error' => $e->getMessage(),
- 'user_id' => $user_id
- ]);
-
- return [
- 'success' => false,
- 'items' => [],
- ];
- }
- });
- }
/**
* Handle favourite operations
*/
public function handleFavourite(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
- $operation_id = sanitize_text_field($request->get_param('id'));
- $action = sanitize_text_field($request->get_param('action'));
+ $params = $request->get_params();
+ $user_id = absint($params['user']??0);
if (!$this->userCheck($user_id)) {
return $this->unauthorized();
}
+ $action = strtolower(sanitize_text_field($params['action']));
+ $action = in_array($action, ['add', 'remove']) ? $action : false;
+ if (!$action) {
+ return $this->error('Invalid favourite action');
+ }
+ $target_id = absint($params['target_id']??0);
+ if ($target_id === 0) {
+ return $this->error('Invalid target id');
+ }
- $data = [
- 'action' => $action,
- 'type' => sanitize_text_field($request->get_param('type') ?? ''),
- 'target_id' => absint($request->get_param('target_id') ?? 0),
- 'items' => $request->get_param('items') ?? [],
- 'notes' => sanitize_textarea_field($request->get_param('notes') ?? ''),
- ];
+ $type = sanitize_text_field($params['type']??'');
+ if (empty($type)) {
+ return $this->error('No type provided');
+ }
- JVB()->queue()->queueOperation(
- 'favourite_' . $action,
+ $result = JVB()->favourites()->toggleFavourite(
+ $action === 'add',
$user_id,
- $data,
- [
- 'operation_id' => $operation_id,
- 'priority' => 'high',
- ]
+ $target_id,
+ $type
);
- return $this->queued($operation_id);
+ if ($result) {
+ return $this->success();
+ }
+ return $this->error('Something went wrong');
}
/**
@@ -280,18 +177,21 @@
*/
public function getLists(WP_REST_Request $request): WP_REST_Response
{
- $user_id = absint($request->get_param('user'));
+ $params = $request->get_params();
+ $user_id = absint($params['user']);
if (!$this->userCheck($user_id)) {
return $this->unauthorized();
}
- $params = ['user' => $user_id];
- if ($request->get_param('id')) {
- $params['list'] = sanitize_text_field($request->get_param('id'));
+ $args = $this->buildParams($request);
+ $args['per_page'] = 20;
+ $listId = $request->get_param('id');
+ if (!empty($listId)) {
+ $args['where']['id'] = sanitize_text_field($listId);
}
- $key = $this->listsCache->generateKey($params);
+ $key = $this->listsCache->generateKey($args);
// Check cache headers
$cache_check = $this->checkHeaders($request, $key);
@@ -299,94 +199,18 @@
return $cache_check;
}
- $list_id = $request->get_param('id');
- $response = $list_id
- ? $this->getListDetails($list_id, $user_id)
- : $this->getAvailableLists($user_id);
+ $includeShares = !empty($request->get_param('include_shares'));
+
+ $response = !empty($listId)
+ ? JVB()->favourites()->getListDetails($listId, $user_id)
+ : JVB()->favourites()->getAvailableLists($args, $includeShares);
return $this->addCacheHeaders(Response::success($response));
}
- /**
- * Get lists available to a user using CustomTable
- */
- protected function getAvailableLists(int $user_id, bool $include_shared = true): array
- {
- if (!$this->checkUser($user_id)) {
- return [];
- }
-
- $cache = $include_shared ? $this->sharedListsCache : $this->listsCache;
-
- return $cache->remember($user_id, function() use ($user_id, $include_shared) {
- try {
- // Get owned lists
- $owned = $this->lists
- ->where(['user_id' => $user_id])
- ->orderBy('created_at', 'DESC')
- ->getResults(ARRAY_A);
-
- // Add item counts
- foreach ($owned as &$list) {
- $list['item_count'] = $this->listItems
- ->where(['list_id' => $list['id']])
- ->countResults();
- $list['is_owner'] = true;
- $list['is_shared'] = false;
- }
-
- if (!$include_shared) {
- return [
- 'success' => true,
- 'lists' => $owned
- ];
- }
-
- // Get shared lists
- $shares = $this->listShares
- ->where(['user_id' => $user_id, 'status' => 'accepted'])
- ->getResults();
-
- $shared_lists = [];
- foreach ($shares as $share) {
- $list = $this->lists
- ->where(['id' => $share->list_id])
- ->first(ARRAY_A);
-
- if ($list) {
- $owner = get_userdata($list['user_id']);
- $list['owner_name'] = $owner ? $owner->display_name : 'Unknown';
- $list['item_count'] = $this->listItems
- ->where(['list_id' => $list['id']])
- ->countResults();
- $list['permission_type'] = $share->permission_type;
- $list['is_owner'] = false;
- $list['is_shared'] = true;
-
- $shared_lists[] = $list;
- }
- }
-
- return [
- 'success' => true,
- 'lists' => [
- 'owned' => $owned,
- 'shared' => $shared_lists
- ]
- ];
-
- } catch (Exception $e) {
- $this->logError('getAvailableLists', [
- 'error' => $e->getMessage(),
- 'user_id' => $user_id
- ]);
-
- return [];
- }
- });
- }
/**
+ * TODO: Done until here
* Get favourite counts by type
*/
public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response
@@ -840,18 +664,22 @@
protected function buildParams(WP_REST_Request $request): array
{
$data = $request->get_params();
- $args = ['user' => absint($data['user'])];
- if (!array_key_exists('page', $data)) {
- return $args;
+ $where = ['user_id' => absint($data['user'])];
+ if (!empty($data['content']) && $data['content'] !== 'all') {
+ $where['type'] = BASE . $data['content'];
}
- $args = array_merge($args, [
- 'page' => max(1, absint($data['page'] ?? 1)),
- 'content' => Registrar::getInstance($data['content']) ? $data['content'] : 'all',
- ]);
+ $page = max(1, absint($data['page'] ?? 1));
+ $perPage = 250;
- return $this->applyOrderFilters($args, $data);
+ return [
+ 'where' => $where,
+ 'orderby' => sanitize_text_field($data['orderby'] ?? 'created_at'),
+ 'order' => sanitize_text_field($data['order'] ?? 'DESC'),
+ 'per_page' => $perPage,
+ 'page' => $page
+ ];
}
@@ -1034,40 +862,35 @@
*/
protected function getListDetails(int $list_id, int $user_id): array
{
+ // Check access - either owner or has share
+ $is_owner = JVB()->favourites()->userOwnsList($list_id, $user_id);
+ $is_shared = JVB()->favourites()->userCanViewList($list_id, $user_id);
+
+
+ if (!$is_owner && !$is_shared) {
+ return [
+ 'success' => false,
+ 'message' => 'You do not have access to this list.'
+ ];
+ }
+
+ $list = JVB()->favourites()->getListDetails($list_id, $user_id);
+
+ if (empty($list)) {
+ return [
+ 'success' => false,
+ 'message' => 'List not found'
+ ];
+ }
+
+
+
$key = "list_{$list_id}_user_{$user_id}";
return $this->listsCache->remember($key, function () use ($list_id, $user_id) {
try {
- // Check access - either owner or has share
- $is_owner = $this->lists->where([
- 'id' => $list_id,
- 'user_id' => $user_id
- ])->existsInQuery();
- $share = null;
- if (!$is_owner) {
- $share = $this->listShares->where([
- 'list_id' => $list_id,
- 'user_id' => $user_id,
- 'status' => 'accepted'
- ])->first();
- }
- if (!$is_owner && !$share) {
- return [
- 'success' => false,
- 'message' => 'You do not have access to this list'
- ];
- }
-
- // Get list details
- $list = $this->lists->where(['id' => $list_id])->first(ARRAY_A);
- if (!$list) {
- return [
- 'success' => false,
- 'message' => 'List not found'
- ];
- }
// Get list items
$items = $this->listItems
diff --git a/inc/ui/CRUDSkeleton.php b/inc/ui/CRUDSkeleton.php
index d1d4834..2e35ae0 100644
--- a/inc/ui/CRUDSkeleton.php
+++ b/inc/ui/CRUDSkeleton.php
@@ -1579,7 +1579,7 @@
<input type="hidden" name="content" value="<?=$this->dataType?>" />
<div class="fields">
<?php
- if (!empty($this->statuses)) {
+ if (empty($this->sections) && !empty($this->statuses)) {
echo Form::render('post_status', '', $this->getStatusFieldConfig('edit-'));
}
@@ -1593,8 +1593,9 @@
'icon' => $config['icon']
];
}
+
$tabs[$slug] = array_merge([
- 'title' => $config['label'],
+ 'title' => $config['title'],
'content' => '',
'description' => $config['description']??'',
], $section);
@@ -1615,7 +1616,7 @@
foreach ($first as $f) {
if (array_key_exists($f, $fields)) {
if ($tabs) {
- $tabs['basic']['content'] .= Form::render($f, '', $fields[$f]);
+ $tabs['main']['content'] .= Form::render($f, '', $fields[$f]);
} else {
echo Form::render($f, '', $fields[$f]);
}
@@ -1660,12 +1661,13 @@
$fields = $this->nonTimelineFields;
}
+
foreach ($fields as $n => $config) {
if (in_array($config['type'], ['taxonomy', 'selector'])) {
$config = array_merge($config, $this->taxConfig($config['taxonomy'], $config['label']));
}
if ($tabs) {
- $section = (array_key_exists('section', $config)) ? $config['section'] : 'basic';
+ $section = (array_key_exists('section', $config)) && !empty($config['section']) ? $config['section'] : 'main';
$tabs[$section]['content'] .= Form::render($n, '', $config);
} else {
echo Form::render($n, '', $config);
--
Gitblit v1.10.0