PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
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 1 month ago settings 1 month ago Analytics.js 1 month ago AnalyticsDetail.js 1 month ago Dashboard.js 1 month ago Emails.js 1 month ago Learn.js 1 month ago MediaHub.js 3 weeks ago Onboarding.js 3 weeks ago Settings.js 1 month ago UserAnalyticsDetail.js 1 month ago
Settings.js
200 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
23 const isPageAvailable = ( page ) => {
24 if ( ! page ) {
25 return false;
26 }
27 if ( page.requiresProPlugin && ! isProPluginActive ) {
28 return false;
29 }
30 return true;
31 };
32
33 const SettingsInner = () => {
34 const location = useLocation();
35 const history = useHistory();
36 const { isLoading } = useSettingsData();
37 const slug = location.params?.section;
38
39 const activePage = findPage( slug );
40 const activePageAvailable = isPageAvailable( activePage );
41
42 useEffect( () => {
43 // replace() not push() — bad/deep-linked slugs shouldn't clutter
44 // back-button history with redirects the user can step through.
45 if ( ! slug || ! activePage ) {
46 history.replace( { tab: 'Settings', section: DEFAULT_SLUG } );
47 return;
48 }
49 if ( ! activePageAvailable ) {
50 history.replace( { tab: 'Settings', section: DEFAULT_SLUG } );
51 }
52 }, [ slug, activePage, activePageAvailable, history ] );
53
54 // Ref (not context) bridges the active page's dirty state to navigation
55 // handlers. A context would force the sidebar to re-render on every
56 // keystroke; navigation only needs to *read* isDirty at click time.
57 const activePageRef = useRef( { isDirty: false, save: null, discard: null } );
58 const [ pendingTarget, setPendingTarget ] = useState( null );
59 const [ modalOpen, setModalOpen ] = useState( false );
60
61 const registerActivePage = useCallback( ( next ) => {
62 activePageRef.current = next || {
63 isDirty: false,
64 save: null,
65 discard: null,
66 };
67 }, [] );
68
69 const tryNavigate = useCallback(
70 ( target ) => {
71 const { isDirty } = activePageRef.current;
72 if ( ! isDirty ) {
73 history.push( target );
74 return false;
75 }
76 setPendingTarget( target );
77 setModalOpen( true );
78 return true;
79 },
80 [ history ]
81 );
82
83 useEffect( () => {
84 return registerNavGuard( ( target ) => tryNavigate( target ) );
85 }, [ tryNavigate ] );
86
87 // Replace sidebar + main area with a full-page skeleton while the initial
88 // /wp/v2/settings fetch is in flight. Matches SureDash's layout-level
89 // pattern — leaf pages never mount in the loading state, so no per-page
90 // skeleton logic or flash-of-empty-sidebar. The target page's
91 // `skeletonCards` (falls back to 1) drives how many cards we stub so the
92 // skeleton matches the specific page the router is resolving to.
93 if ( isLoading ) {
94 return <SettingsSkeleton cards={ activePage?.skeletonCards ?? 1 } />;
95 }
96
97 const closeModal = () => {
98 setModalOpen( false );
99 setPendingTarget( null );
100 };
101
102 const handleModalSave = async () => {
103 const target = pendingTarget;
104 try {
105 await activePageRef.current.save?.();
106 closeModal();
107 if ( target ) {
108 history.push( target );
109 }
110 } catch ( err ) {
111 // Save failed — toast already surfaced inside the page; keep modal open.
112 }
113 };
114
115 const handleModalDiscard = () => {
116 const target = pendingTarget;
117 activePageRef.current.discard?.();
118 closeModal();
119 if ( target ) {
120 history.push( target );
121 }
122 };
123
124 const visiblePages = SETTINGS_PAGES.filter( isPageAvailable );
125 const groupedPages = GROUPS.map( ( group ) => ( {
126 ...group,
127 items: visiblePages.filter( ( p ) => p.group === group.key ),
128 } ) ).filter( ( g ) => g.items.length > 0 );
129 const standalonePages = visiblePages.filter( ( p ) => p.group === null );
130
131 const renderLeaf = ( page ) => {
132 const Icon = page.icon;
133 return (
134 <Menu.Item
135 key={ page.slug }
136 active={ slug === page.slug }
137 onClick={ () => tryNavigate( { tab: 'Settings', section: page.slug } ) }
138 >
139 <Icon />
140 <div className="flex-1">{ page.label }</div>
141 </Menu.Item>
142 );
143 };
144
145 const ActiveComponent = activePageAvailable ? activePage.component : null;
146
147 return (
148 <div className="flex min-h-[calc(100vh-var(--presto-admin-bar-h)-var(--presto-navbar-h))]">
149 <Sidebar
150 borderOn={ false }
151 collapsible={ false }
152 collapsed={ false }
153 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"
154 >
155 <Sidebar.Body className="grow">
156 <Menu size="md" className="p-0 w-full gap-2">
157 { groupedPages.map( ( group ) => (
158 <Menu.List key={ group.key } heading={ group.label } arrow open>
159 { group.items.map( renderLeaf ) }
160 </Menu.List>
161 ) ) }
162
163 { groupedPages.length > 0 && standalonePages.length > 0 && (
164 <Menu.Separator />
165 ) }
166
167 { standalonePages.length > 0 && (
168 <Menu.List open>{ standalonePages.map( renderLeaf ) }</Menu.List>
169 ) }
170 </Menu>
171 </Sidebar.Body>
172 </Sidebar>
173
174 <div className="flex-1 min-w-0 bg-background-secondary p-6">
175 { ActiveComponent && (
176 <ActiveComponent
177 key={ activePage.slug }
178 registerActivePage={ registerActivePage }
179 />
180 ) }
181 </div>
182
183 <UnsavedChangesDialog
184 open={ modalOpen }
185 onSave={ handleModalSave }
186 onDiscard={ handleModalDiscard }
187 onCancel={ closeModal }
188 />
189 </div>
190 );
191 };
192
193 const Settings = () => (
194 <SettingsDataProvider>
195 <SettingsInner />
196 </SettingsDataProvider>
197 );
198
199 export default Settings;
200