PluginProbe ʕ •ᴥ•ʔ
Presto Player / 4.3.3
Presto Player v4.3.3
4.3.3 4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / src / admin / dashboard / pages / Settings.js
presto-player / src / admin / dashboard / pages Last commit date
learn 2 months ago settings 6 days ago Analytics.js 2 months ago AnalyticsDetail.js 2 months ago Dashboard.js 2 months ago Emails.js 2 months ago Learn.js 2 months ago MediaHub.js 1 month ago Onboarding.js 1 month ago Settings.js 6 days ago UserAnalyticsDetail.js 2 months ago
Settings.js
204 lines
1 import { useEffect, useRef, useState, useCallback } from 'react';
2 import { Sidebar, Menu } from '@bsf/force-ui';
3 import { useLocation, useHistory } from '../router/router';
4 import SettingsDataProvider, {
5 useSettingsData,
6 } from './settings/shared/SettingsDataProvider';
7 import UnsavedChangesDialog from './settings/shared/UnsavedChangesDialog';
8 import { registerNavGuard } from './settings/shared/navigationGuard';
9 import SettingsSkeleton from '../components/Skeletons/SettingsSkeleton';
10 import {
11 SETTINGS_PAGES,
12 GROUPS,
13 DEFAULT_SLUG,
14 findPage,
15 } from './settings/config';
16
17 // Registers core-data entities (preset, audio-preset, webhook) for
18 // `@wordpress/core-data` selectors used by Settings pages.
19 import './settings/entities';
20
21 const isProPluginActive = window.prestoPlayer?.isProPluginActive ?? false;
22 const canManageOptions = window.prestoPlayer?.canManageOptions ?? false;
23
24 const isPageAvailable = ( page ) => {
25 if ( ! page ) {
26 return false;
27 }
28 if ( page.requiresProPlugin && ! isProPluginActive ) {
29 return false;
30 }
31 if ( page.requireManageOptions && ! canManageOptions ) {
32 return false;
33 }
34 return true;
35 };
36
37 const SettingsInner = () => {
38 const location = useLocation();
39 const history = useHistory();
40 const { isLoading } = useSettingsData();
41 const slug = location.params?.section;
42
43 const activePage = findPage( slug );
44 const activePageAvailable = isPageAvailable( activePage );
45
46 useEffect( () => {
47 // replace() not push() — bad/deep-linked slugs shouldn't clutter
48 // back-button history with redirects the user can step through.
49 if ( ! slug || ! activePage ) {
50 history.replace( { tab: 'Settings', section: DEFAULT_SLUG } );
51 return;
52 }
53 if ( ! activePageAvailable ) {
54 history.replace( { tab: 'Settings', section: DEFAULT_SLUG } );
55 }
56 }, [ slug, activePage, activePageAvailable, history ] );
57
58 // Ref (not context) bridges the active page's dirty state to navigation
59 // handlers. A context would force the sidebar to re-render on every
60 // keystroke; navigation only needs to *read* isDirty at click time.
61 const activePageRef = useRef( { isDirty: false, save: null, discard: null } );
62 const [ pendingTarget, setPendingTarget ] = useState( null );
63 const [ modalOpen, setModalOpen ] = useState( false );
64
65 const registerActivePage = useCallback( ( next ) => {
66 activePageRef.current = next || {
67 isDirty: false,
68 save: null,
69 discard: null,
70 };
71 }, [] );
72
73 const tryNavigate = useCallback(
74 ( target ) => {
75 const { isDirty } = activePageRef.current;
76 if ( ! isDirty ) {
77 history.push( target );
78 return false;
79 }
80 setPendingTarget( target );
81 setModalOpen( true );
82 return true;
83 },
84 [ history ]
85 );
86
87 useEffect( () => {
88 return registerNavGuard( ( target ) => tryNavigate( target ) );
89 }, [ tryNavigate ] );
90
91 // Replace sidebar + main area with a full-page skeleton while the initial
92 // /wp/v2/settings fetch is in flight. Matches SureDash's layout-level
93 // pattern — leaf pages never mount in the loading state, so no per-page
94 // skeleton logic or flash-of-empty-sidebar. The target page's
95 // `skeletonCards` (falls back to 1) drives how many cards we stub so the
96 // skeleton matches the specific page the router is resolving to.
97 if ( isLoading ) {
98 return <SettingsSkeleton cards={ activePage?.skeletonCards ?? 1 } />;
99 }
100
101 const closeModal = () => {
102 setModalOpen( false );
103 setPendingTarget( null );
104 };
105
106 const handleModalSave = async () => {
107 const target = pendingTarget;
108 try {
109 await activePageRef.current.save?.();
110 closeModal();
111 if ( target ) {
112 history.push( target );
113 }
114 } catch ( err ) {
115 // Save failed — toast already surfaced inside the page; keep modal open.
116 }
117 };
118
119 const handleModalDiscard = () => {
120 const target = pendingTarget;
121 activePageRef.current.discard?.();
122 closeModal();
123 if ( target ) {
124 history.push( target );
125 }
126 };
127
128 const visiblePages = SETTINGS_PAGES.filter( isPageAvailable );
129 const groupedPages = GROUPS.map( ( group ) => ( {
130 ...group,
131 items: visiblePages.filter( ( p ) => p.group === group.key ),
132 } ) ).filter( ( g ) => g.items.length > 0 );
133 const standalonePages = visiblePages.filter( ( p ) => p.group === null );
134
135 const renderLeaf = ( page ) => {
136 const Icon = page.icon;
137 return (
138 <Menu.Item
139 key={ page.slug }
140 active={ slug === page.slug }
141 onClick={ () => tryNavigate( { tab: 'Settings', section: page.slug } ) }
142 >
143 <Icon />
144 <div className="flex-1">{ page.label }</div>
145 </Menu.Item>
146 );
147 };
148
149 const ActiveComponent = activePageAvailable ? activePage.component : null;
150
151 return (
152 <div className="flex min-h-[calc(100vh-var(--presto-admin-bar-h)-var(--presto-navbar-h))]">
153 <Sidebar
154 borderOn={ false }
155 collapsible={ false }
156 collapsed={ false }
157 className="!w-[240px] !min-w-[240px] shrink-0 !p-3 !h-auto sticky top-[calc(var(--presto-admin-bar-h)+var(--presto-navbar-h))] max-h-[calc(100vh-var(--presto-admin-bar-h)-var(--presto-navbar-h))] overflow-y-auto"
158 >
159 <Sidebar.Body className="grow">
160 <Menu size="md" className="p-0 w-full gap-2">
161 { groupedPages.map( ( group ) => (
162 <Menu.List key={ group.key } heading={ group.label } arrow open>
163 { group.items.map( renderLeaf ) }
164 </Menu.List>
165 ) ) }
166
167 { groupedPages.length > 0 && standalonePages.length > 0 && (
168 <Menu.Separator />
169 ) }
170
171 { standalonePages.length > 0 && (
172 <Menu.List open>{ standalonePages.map( renderLeaf ) }</Menu.List>
173 ) }
174 </Menu>
175 </Sidebar.Body>
176 </Sidebar>
177
178 <div className="flex-1 min-w-0 bg-background-secondary p-6">
179 { ActiveComponent && (
180 <ActiveComponent
181 key={ activePage.slug }
182 registerActivePage={ registerActivePage }
183 />
184 ) }
185 </div>
186
187 <UnsavedChangesDialog
188 open={ modalOpen }
189 onSave={ handleModalSave }
190 onDiscard={ handleModalDiscard }
191 onCancel={ closeModal }
192 />
193 </div>
194 );
195 };
196
197 const Settings = () => (
198 <SettingsDataProvider>
199 <SettingsInner />
200 </SettingsDataProvider>
201 );
202
203 export default Settings;
204