PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Hooks/Handlers/Menu.php +228 -120 1.10.3 → 2.4.5 View file →
@@ -3,34 +3,30 @@
3 3 namespace FluentSupport\App\Hooks\Handlers;
4 4
5 5 use FluentSupport\App\App;
6 6 use FluentSupport\App\Models\Agent;
7 +use FluentSupport\App\Models\AgentGroup;
7 8 use FluentSupport\App\Models\MailBox;
8 9 use FluentSupport\App\Models\Product;
10 +use FluentSupport\App\Models\TagPivot;
9 11 use FluentSupport\App\Models\TicketTag;
10 12 use FluentSupport\App\Modules\PermissionManager;
11 13 use FluentSupport\App\Services\Helper;
12 -use FluentSupport\App\Services\TransStrings;
14 +use FluentSupport\App\Services\Notifications\NotificationSettings;
15 +use FluentSupport\App\Services\TranslationStrings;
16 +use FluentSupport\App\Services\Integrations\FluentBooking\FluentBookingService;
17 +use FluentSupport\App\Vite;
13 18
14 19 class Menu
15 20 {
16 21 public function add()
17 22 {
18 - $currentUserPermissions = PermissionManager::currentUserPermissions();
23 + $capability = PermissionManager::getMenuPermission();
19 24
20 - if (!$currentUserPermissions) {
25 + if (!$capability) {
21 26 return;
22 27 }
23 28
24 - $permission = 'fst_view_dashboard';
25 -
26 - $isAdmin = false;
27 -
28 - if (current_user_can('manage_options')) {
29 - $permission = 'manage_options';
30 - $isAdmin = true;
31 - }
32 -
33 29 $menuPosition = 25;
34 30 if (defined('FLUENTCRM')) {
35 31 $menuPosition = 4;
36 32 }
@@ -43,9 +39,9 @@
43 39
44 40 add_menu_page(
45 41 __('Fluent Support', 'fluent-support'),
46 42 __('Fluent Support', 'fluent-support'),
47 - $permission,
43 + $capability,
48 44 'fluent-support',
49 45 array($this, 'renderApp'),
50 46 $this->getMenuIcon(),
51 47 $menuPosition
@@ -50,14 +46,13 @@
50 46 $this->getMenuIcon(),
51 47 $menuPosition
52 48 );
53 49
54 -
55 50 add_submenu_page(
56 51 'fluent-support',
57 52 __('Dashboard', 'fluent-support'),
58 53 __('Dashboard', 'fluent-support'),
59 - $permission,
54 + $capability,
60 55 'fluent-support',
61 56 array($this, 'renderApp')
62 57 );
63 58
@@ -64,161 +59,210 @@
64 59 add_submenu_page(
65 60 'fluent-support',
66 61 __('Tickets', 'fluent-support'),
67 62 __('Tickets', 'fluent-support'),
68 - ($isAdmin) ? 'manage_options' : 'fst_manage_own_tickets',
63 + $capability,
69 64 'fluent-support#/tickets',
70 65 array($this, 'renderApp')
71 66 );
72 67
73 - add_submenu_page(
74 - 'fluent-support',
75 - __('Workflows', 'fluent-support'),
76 - __('Workflows', 'fluent-support'),
77 - ($isAdmin) ? 'manage_options' : 'fst_manage_workflows',
78 - 'fluent-support#/workflows',
79 - array($this, 'renderApp')
80 - );
68 + if (PermissionManager::currentUserCan('fst_view_all_reports')) {
69 + add_submenu_page(
70 + 'fluent-support',
71 + __('Reports', 'fluent-support'),
72 + __('Reports', 'fluent-support'),
73 + $capability,
74 + 'fluent-support#/reports',
75 + array($this, 'renderApp')
76 + );
77 + }
81 78
82 - add_submenu_page(
83 - 'fluent-support',
84 - __('Activities', 'fluent-support'),
85 - __('Activities', 'fluent-support'),
86 - ($isAdmin) ? 'manage_options' : 'fst_view_activity_logs',
87 - 'fluent-support#/activity',
88 - array($this, 'renderApp')
89 - );
79 + if (PermissionManager::currentUserCan('fst_sensitive_data')) {
80 + add_submenu_page(
81 + 'fluent-support',
82 + __('Customers', 'fluent-support'),
83 + __('Customers', 'fluent-support'),
84 + $capability,
85 + 'fluent-support#/customers',
86 + array($this, 'renderApp')
87 + );
88 + }
90 89
91 - add_submenu_page(
92 - 'fluent-support',
93 - __('Settings', 'fluent-support'),
94 - __('Settings', 'fluent-support'),
95 - ($isAdmin) ? 'manage_options' : 'fst_manage_settings',
96 - 'fluent-support#/settings',
97 - array($this, 'renderApp')
98 - );
90 + if (PermissionManager::currentUserCan('fst_view_activity_logs')) {
91 + add_submenu_page(
92 + 'fluent-support',
93 + __('Activities', 'fluent-support'),
94 + __('Activities', 'fluent-support'),
95 + $capability,
96 + 'fluent-support#/activity',
97 + array($this, 'renderApp')
98 + );
99 + }
99 100
100 - add_submenu_page(
101 - 'fluent-support',
102 - __('Reports', 'fluent-support'),
103 - __('Reports', 'fluent-support'),
104 - ($isAdmin) ? 'manage_options' : 'fst_sensitive_data',
105 - 'fluent-support#/reports',
106 - array($this, 'renderApp')
107 - );
101 + if (PermissionManager::currentUserCan('fst_manage_settings')) {
102 + add_submenu_page(
103 + 'fluent-support',
104 + __('Business Inboxes', 'fluent-support'),
105 + __('Business Inboxes', 'fluent-support'),
106 + $capability,
107 + 'fluent-support#/mailboxes',
108 + array($this, 'renderApp')
109 + );
110 + }
108 111
112 + if (PermissionManager::currentUserCan('fst_manage_workflows')) {
113 + add_submenu_page(
114 + 'fluent-support',
115 + __('Workflows', 'fluent-support'),
116 + __('Workflows', 'fluent-support'),
117 + $capability,
118 + 'fluent-support#/workflows',
119 + array($this, 'renderApp')
120 + );
121 + }
122 +
123 + if (PermissionManager::currentUserCan('fst_manage_saved_replies')) {
124 + add_submenu_page(
125 + 'fluent-support',
126 + __('Saved Replies', 'fluent-support'),
127 + __('Saved Replies', 'fluent-support'),
128 + $capability,
129 + 'fluent-support#/saved-replies',
130 + array($this, 'renderApp')
131 + );
132 + }
133 +
134 + if (PermissionManager::currentUserCan('fst_manage_settings')) {
135 + add_submenu_page(
136 + 'fluent-support',
137 + __('Settings', 'fluent-support'),
138 + __('Settings', 'fluent-support'),
139 + $capability,
140 + 'fluent-support#/settings',
141 + array($this, 'renderApp')
142 + );
143 + }
109 144 }
110 145
111 - public function renderApp()
146 + public function getMenuItems()
112 147 {
113 - $app = App::getInstance();
114 -
115 - $assets = $app['url.assets'];
116 -
117 148 $baseUrl = apply_filters('fluent_support/base_url', admin_url('admin.php?page=fluent-support#/'));
118 149
119 150 $menuItems = [
120 - [
151 + 'dashboard' => [
121 152 'key' => 'dashboard',
122 153 'label' => __('Dashboard', 'fluent-support'),
123 - 'permalink' => $baseUrl
154 + 'permalink' => $baseUrl,
124 155 ],
125 - [
156 + 'tickets' => [
126 157 'key' => 'tickets',
127 158 'label' => __('Tickets', 'fluent-support'),
128 159 'permalink' => $baseUrl . 'tickets',
129 160 ],
130 - [
161 + 'reports' => [
131 162 'key' => 'reports',
132 163 'label' => __('Reports', 'fluent-support'),
133 - 'permalink' => $baseUrl . 'reports'
164 + 'permalink' => $baseUrl . 'reports',
134 165 ],
135 166 ];
136 167
137 - $hasSensitiveAccess = PermissionManager::currentUserCan('fst_sensitive_data');
138 - if ($hasSensitiveAccess) {
139 - $menuItems[] = [
140 - 'key' => 'customers',
141 - 'label' => __('Customers', 'fluent-support'),
142 - 'permalink' => $baseUrl . 'customers'
143 - ];
144 - }
168 + $canManageSettings = PermissionManager::currentUserCan('fst_manage_settings');
145 169
146 - $secondaryItems = [];
147 -
148 - if (PermissionManager::currentUserCan('fst_manage_saved_replies')) {
149 - $secondaryItems = [
150 - [
151 - 'key' => 'saved_replies',
152 - 'label' => __('Saved Replies', 'fluent-support'),
153 - 'permalink' => $baseUrl . 'saved-replies'
154 - ]
170 + if ($canManageSettings) {
171 + $menuItems['mailboxes'] = [
172 + 'key' => 'mailboxes',
173 + 'label' => __('Business Inboxes', 'fluent-support'),
174 + 'permalink' => $baseUrl . 'mailboxes',
155 175 ];
156 176 }
157 177
158 178 if (PermissionManager::currentUserCan('fst_view_activity_logs')) {
159 - $secondaryItems[] = [
179 + $menuItems['activity'] = [
160 180 'key' => 'activity',
161 181 'label' => __('Activities', 'fluent-support'),
162 - 'permalink' => $baseUrl . 'activity'
182 + 'permalink' => $baseUrl . 'activity',
163 183 ];
164 184 }
165 185
166 - $canManageSettings = PermissionManager::currentUserCan('fst_manage_settings');
186 + if (PermissionManager::currentUserCan('fst_sensitive_data')) {
187 + $menuItems['customers'] = [
188 + 'key' => 'customers',
189 + 'label' => __('Customers', 'fluent-support'),
190 + 'permalink' => $baseUrl . 'customers',
191 + ];
192 + }
167 193
168 - if ($canManageSettings) {
169 - $secondaryItems[] = [
170 - 'key' => 'mailboxes',
171 - 'label' => __('Business Inboxes', 'fluent-support'),
172 - 'permalink' => $baseUrl . 'mailboxes'
194 + $moreChildren = [];
195 +
196 + if (PermissionManager::currentUserCan('fst_manage_saved_replies')) {
197 + $moreChildren['saved_replies'] = [
198 + 'key' => 'saved_replies',
199 + 'label' => __('Saved Replies', 'fluent-support'),
200 + 'permalink' => $baseUrl . 'saved-replies',
173 201 ];
174 202 }
175 203
176 204 if (PermissionManager::currentUserCan('fst_manage_workflows')) {
177 - $secondaryItems[] = [
205 + $moreChildren['workflows'] = [
178 206 'key' => 'workflows',
179 207 'label' => __('Workflows', 'fluent-support'),
180 - 'permalink' => $baseUrl . 'workflows'
208 + 'permalink' => $baseUrl . 'workflows',
181 209 ];
182 210 }
183 211
212 + if (!empty($moreChildren)) {
213 + $menuItems['more'] = [
214 + 'key' => 'more',
215 + 'label' => __('More', 'fluent-support'),
216 + 'permalink' => '#',
217 + 'children' => $moreChildren,
218 + ];
219 + }
220 +
184 221 if ($canManageSettings) {
185 - $secondaryItems[] = [
222 + $menuItems['settings'] = [
186 223 'key' => 'settings',
187 224 'label' => __('Global Settings', 'fluent-support'),
188 - 'permalink' => $baseUrl . 'settings'
225 + 'permalink' => $baseUrl . 'settings',
189 226 ];
190 227 }
191 228
192 - /*
193 - * Filter Fluent Support dashboard top-left menu items
194 - *
195 - * @since v1.0.0
196 - *
197 - * @param array $menuItems
198 - */
199 - $menuItems = apply_filters('fluent_support/primary_menu_items', $menuItems);
229 + return apply_filters('fluent_support/menu_items', $menuItems);
230 + }
200 231
201 - /*
202 - * Filter Fluent Support dashboard top-right menu items
203 - *
204 - * @since v1.0.0
205 - *
206 - * @param array $secondaryItems
207 - */
208 - $secondaryItems = apply_filters('fluent_support/secondary_menu_items', $secondaryItems);
232 + public function renderApp()
233 + {
234 + $app = App::getInstance();
209 235
236 + $assets = $app['url.assets'];
210 237
238 + $allItems = $this->getMenuItems();
211 239
240 + $secondaryKeys = ['settings'];
241 + $menuItems = [];
242 + $secondaryItems = [];
243 +
244 + foreach ($allItems as $key => $item) {
245 + if (in_array($key, $secondaryKeys, true)) {
246 + $secondaryItems[] = $item;
247 + } else {
248 + $menuItems[] = $item;
249 + }
250 + }
251 +
252 + $menuItems = apply_filters('fluent_support/primary_menu_items', $menuItems);
253 + $secondaryItems = apply_filters('fluent_support/secondary_menu_items', $secondaryItems);
254 +
212 255 if (!defined('FLUENT_SUPPORT_PRO_DIR_FILE')) {
213 256 $secondaryItems[] = [
214 257 'key' => 'upgrade_to_pro',
215 - 'label' => 'Upgrade to Pro',
216 - 'permalink' => 'https://fluentsupport.com'
258 + 'label' => __('Upgrade to Pro', 'fluent-support'),
259 + 'permalink' => Helper::getUpgradeUrl('sidebar_cta')
217 260 ];
218 261 }
219 262
220 - $app = App::getInstance();
263 + $baseUrl = apply_filters('fluent_support/base_url', admin_url('admin.php?page=fluent-support#/'));
264 +
221 265 $this->enqueueAssets();
222 266
223 267 do_action('fluent_support/admin_app_loaded', $app);
224 268 $app->view->render('admin.menu', [
@@ -223,8 +267,11 @@
223 267 do_action('fluent_support/admin_app_loaded', $app);
224 268 $app->view->render('admin.menu', [
225 269 'base_url' => $baseUrl,
226 270 'logo' => $assets . 'images/logo.svg',
271 + 'settingsLogo' => $assets . 'images/gear.svg',
272 + 'upgradeLogo' => $assets . 'images/crown.svg',
273 + 'assets' => $assets,
227 274 'menuItems' => $menuItems,
228 275 'secondaryItems' => isset($secondaryItems) ? $secondaryItems : [],
229 276 ]);
230 277 }
@@ -230,13 +277,44 @@
230 277 }
231 278
232 279 public function maybeEnqueueAssets()
233 280 {
234 - if (isset($_GET['page']) && $_GET['page'] == 'fluent-support') {
281 + if (isset($_GET['page']) && sanitize_text_field(wp_unslash($_GET['page'])) === 'fluent-support') {
282 + add_action('admin_head', [$this, 'printDarkModeInit'], 1);
235 283 $this->enqueueAssets();
236 284 }
237 285 }
238 286
287 + public function printDarkModeInit()
288 + {
289 + ?>
290 + <script>
291 + (function () {
292 + var savedTheme = localStorage.getItem('fs-theme');
293 +
294 + // Check if dark mode should be active
295 + // localStorage stores: 'dark', 'light', or 'system:dark' / 'system:light'
296 + var isDark = savedTheme
297 + ? savedTheme.split(':').pop() === 'dark'
298 + : window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
299 +
300 + if (isDark) {
301 + // Apply immediately to <html> to prevent white flash
302 + document.documentElement.classList.add('fs-dark-mode');
303 +
304 + // Watch for <body> to appear and apply class as soon as it exists
305 + new MutationObserver(function (mutations, observer) {
306 + if (document.body) {
307 + document.body.classList.add('fs-dark-mode');
308 + observer.disconnect();
309 + }
310 + }).observe(document.documentElement, { childList: true });
311 + }
312 + })();
313 + </script>
314 + <?php
315 + }
316 +
239 317 public function enqueueAssets()
240 318 {
241 319 $app = App::getInstance();
242 320
@@ -241,17 +319,20 @@
241 319 $app = App::getInstance();
242 320
243 321 $assets = $app['url.assets'];
244 322
245 - add_filter('admin_footer_text', function ($text) {
246 - return '<span id="footer-thankyou">We value your feedback! If the plugin is helpful, please rate Fluent Support with <a target="_blank" rel="nofollow" href="https://wordpress.org/support/plugin/fluent-support/reviews/#new-post">★★★★★</a> on WordPress.org. For assistance, check out the <a target="_blank" rel="nofollow" href="https://fluentsupport.com/docs/navigate-with-the-keyboard-shortcut">keyboard shortcuts</a> and <a target="_blank" rel="nofollow" href="https://fluentsupport.com/docs/">documentation</a>.</span>';
247 - });
323 + // Inject Vite HMR client for dev mode
324 + add_action('admin_head', function () {
325 + Vite::injectViteClient();
326 + }, 1);
248 327
249 - wp_enqueue_script('dompurify', $assets . 'libs/purify/purify.min.js', [], '2.4.3');
328 + wp_enqueue_script('dompurify', $assets . 'libs/purify/purify.min.js', [], '3.4.16');
250 329
251 - wp_enqueue_style(
252 - 'fluent_support_admin_app', $assets . 'admin/css/alpha-admin.css', [], FLUENT_SUPPORT_VERSION
253 - );
330 + if (is_rtl()) {
331 + wp_enqueue_style('fluent_support_admin_app_rtl', $assets . 'admin/css/alpha-admin-rtl.css', [], FLUENT_SUPPORT_VERSION);
332 + } else {
333 + wp_enqueue_style('fluent_support_admin_app', Vite::getEnqueuePath('admin/css/alpha-admin.css'), [], FLUENT_SUPPORT_VERSION);
334 + }
254 335
255 336 $agents = Agent::select(['id', 'first_name', 'last_name'])
256 337 ->where('person_type', 'agent')
257 338 ->get()->toArray();
@@ -259,8 +340,18 @@
259 340 foreach ($agents as $index => $agent) {
260 341 $agents[$index]['id'] = strval($agent['id']);
261 342 }
262 343
344 + $agentGroups = AgentGroup::select(['id', 'title', 'settings'])->get();
345 + $groupPivots = TagPivot::where('source_type', 'agent_group')->get();
346 + foreach ($agentGroups as $group) {
347 + $group->agent_ids = $groupPivots->where('tag_id', $group->id)
348 + ->pluck('source_id')
349 + ->map(function ($id) { return strval($id); })
350 + ->values()
351 + ->toArray();
352 + }
353 +
263 354 $me = Helper::getAgentByUserId(get_current_user_id());
264 355
265 356 if (!$me && current_user_can('manage_options')) {
266 357 // we should create the agent
@@ -284,9 +375,9 @@
284 375 wp_enqueue_media();
285 376
286 377 wp_enqueue_script(
287 378 'fluent_support_admin_app_start',
288 - $assets . 'admin/js/start.js',
379 + Vite::getEnqueuePath('admin/js/start.js'),
289 380 array('jquery'),
290 381 FLUENT_SUPPORT_VERSION,
291 382 true
292 383 );
@@ -292,9 +383,9 @@
292 383 );
293 384
294 385 wp_enqueue_script(
295 386 'fluent_support_global_admin',
296 - $assets . 'admin/js/global_admin.js',
387 + Vite::getEnqueuePath('admin/js/global_admin.js'),
297 388 array('jquery'),
298 389 FLUENT_SUPPORT_VERSION,
299 390 true
300 391 );
@@ -331,10 +422,12 @@
331 422 $tag['id'] = strval($tag['id']);
332 423 return $tag;
333 424 }, $tags);
334 425
335 - $i18ns = TransStrings::getTransStrings();
426 + $i18ns = TranslationStrings::getAdminStrings();
336 427 $i18ns['allowed_files_and_size'] = Helper::getFileUploadMessage();
428 + $i18ns['open'] = __('Open', 'fluent-support');
429 + $i18ns['all'] = __('All', 'fluent-support');
337 430
338 431 /*
339 432 * Filter agent portal localize javascript data
340 433 *
@@ -350,21 +443,31 @@
350 443 'firstEntry' => '',
351 444 'lastEntry' => '',
352 445 'asset_url' => $assets,
353 446 'support_agents' => $agents,
354 - 'support_products' => Product::select(['id', 'title'])->get(),
447 + 'agent_groups' => $agentGroups,
448 + 'support_products' => Product::select(['id', 'title'])->orderedByTitle()->get(),
355 449 'client_priorities' => Helper::customerTicketPriorities(),
356 450 'ticket_statuses' => Helper::ticketStatuses(),
357 451 'ticket_statuses_group' => Helper::ticketStatusGroups(),
358 452 'changeable_ticket_statuses' => Helper::changeableTicketStatuses(),
359 453 'admin_priorities' => Helper::adminTicketPriorities(),
360 - 'mailboxes' => MailBox::select(['id', 'name', 'settings'])->get(),
454 + 'mailboxes' => MailBox::getAccessibleBoxes(),
455 + // Whether the site has any inbox at all, regardless of what this agent may see.
456 + // `mailboxes` is restriction-filtered and can legitimately be empty, so it cannot
457 + // double as the "is Fluent Support set up yet?" signal the setup wizard keys off.
458 + 'has_mailboxes' => MailBox::count() > 0,
361 459 'me' => $me,
460 + // Deleting a customer needs fst_sensitive_data plus administrator rights unless
461 + // the site turns the administrator part off.
462 + 'customer_delete_requires_admin' => (bool) apply_filters('fluent_support/customer_delete_requires_admin', true),
362 463 'pref' => [
363 464 'go_back_after_reply' => 'yes'
364 465 ],
365 466 'notification_integrations' => $integrationDrivers,
366 - 'server_time' => gmdate('Y-m-d\TH:i:sP'),
467 + 'internal_notification_settings' => (new NotificationSettings())->get(false),
468 + // Site time, like every date the API sends; relative times compare the two.
469 + 'server_time' => current_time('mysql'),
367 470 'has_email_parser' => defined('FLUENTSUPPORTPRO_PLUGIN_VERSION'),
368 471 'ticket_tags' => $tags,
369 472 'i18n' => $i18ns,
370 473 'custom_fields' => apply_filters('fluent_support/ticket_custom_fields', []),
@@ -378,8 +481,10 @@
378 481 'auth_provider' => Helper::getAuthProvider(),
379 482 'fluent_bot_integration' => Helper::fluentBotIntegrationStatus(),
380 483 ));
381 484
485 + $appVars['fluent_booking'] = (new FluentBookingService())->getStatus();
486 +
382 487 if (defined('FLUENTCRM')) {
383 488 $appVars['fluentcrm_config'] = Helper::getFluentCRMTagConfig();
384 489 }
385 490
@@ -387,8 +492,11 @@
387 492 $appVars['fluent_boards'] = true;
388 493 }
389 494
390 495 $appVars['has_pro'] = defined('FLUENTSUPPORTPRO_PLUGIN_VERSION');
496 + $appVars['version'] = FLUENT_SUPPORT_VERSION;
497 + // Single source of truth for upgrade CTAs; the SPA only re-points utm_content on this.
498 + $appVars['upgrade_url'] = Helper::getUpgradeUrl();
391 499 if ($appVars['has_pro']) {
392 500 $appVars['agent_feedback_rating'] = Helper::getBusinessSettings('agent_feedback_rating', 'no');
393 501 $appVars['open_ai_integration'] = Helper::openAIIntegrationStatus();
394 502 }