PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / QuickEdit / components / modals / NavItemModal.jsx

NavItemModal.jsx in Extendify 3.2.1, at src/QuickEdit/components/modals/NavItemModal.jsx

205 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { track } from '@shared/lib/track';
2 import { __experimentalLinkControl as LinkControl } from '@wordpress/block-editor';
3 import { Button, Modal, Notice, TextControl } from '@wordpress/components';
4 import { useState } from '@wordpress/element';
5 import { __ } from '@wordpress/i18n';
6 import { save, saveWpNavigationItem } from '../../lib/api';
7 import { useCmdEnterSave } from '../../lib/cmd-enter-save';
8 import { friendlyMessage } from '../../lib/errors';
9 import { normalizeText } from '../../lib/fingerprint';
10 import { closeModal, QE_MODAL_BODY_OPEN_CLASS } from '../../lib/modal-root';
11 import { pushUndo } from '../../state/undo';
12 import { ModalCloseButton } from './ModalCloseButton';
13
14 const readNavAttrs = (liveEl) => {
15 const a = liveEl.querySelector('a[href]');
16 const url = a?.getAttribute('href') || '';
17 const labelEl =
18 liveEl.querySelector('.wp-block-navigation-item__label') ||
19 liveEl.querySelector('a');
20 const label = (labelEl?.textContent || '').trim();
21 return { label, url };
22 };
23
24 export const NavItemModal = ({ selected, onAfterSave }) => {
25 const initial = readNavAttrs(selected.el);
26 const [label, setLabel] = useState(initial.label);
27 const [url, setUrl] = useState('');
28 const [saving, setSaving] = useState(false);
29 const [error, setError] = useState(null);
30
31 const handleSave = async () => {
32 if (saving) return;
33 setSaving(true);
34 setError(null);
35 // Empty URL keeps the existing link — typo on label alone shouldn't break it.
36 const finalUrl = url || initial.url;
37 const patches = [];
38 if (label !== initial.label) {
39 patches.push({ fieldKey: 'label', value: label });
40 }
41 if (finalUrl !== initial.url) {
42 patches.push({ fieldKey: 'url', value: finalUrl });
43 }
44 if (patches.length === 0) {
45 onAfterSave(false);
46 return;
47 }
48 // The clicked item's label is its render-time identity; the server
49 // refuses (409) when itemIndex / blockId resolves to a different item.
50 const fingerprint = initial.label
51 ? { text: normalizeText(initial.label) }
52 : null;
53 try {
54 // Two save paths share the same `attrs.label` / `attrs.url`
55 // patch shape (handled by `Schemas\NavigationLink`):
56 //
57 // - INLINE items (a navigation block whose items are real
58 // innerBlocks of the host post/template-part) →
59 // `/quick-edit/save` (resolved by SaveController's
60 // findBlock walk).
61 //
62 // - REF items (a navigation block with a `ref` attr →
63 // items live in a separate `wp_navigation` CPT post) →
64 // `/quick-edit/wp-navigation` (this is what the user
65 // hit when About/Contact failed: the host tree skips
66 // past the navigation block because `innerBlocks` is
67 // empty for ref-based navs, so findBlock can't reach
68 // the items).
69 //
70 // resolveTarget hands us `selected.source.kind = 'wp-
71 // navigation'` for the ref case, with navPostId + itemIndex
72 // already populated from `NavRefTagger` data attributes.
73 if (selected.source?.kind === 'wp-navigation') {
74 await saveWpNavigationItem({
75 navPostId: selected.navPostId,
76 itemIndex: selected.itemIndex,
77 blockType: selected.blockType,
78 fingerprint,
79 patches,
80 });
81 } else {
82 await save({
83 source: selected.source,
84 blockId: selected.blockId,
85 blockType: selected.blockType,
86 fingerprint,
87 patches,
88 });
89 }
90 // Push the undo entry shaped to match the FORWARD-save
91 // endpoint we just used — `performUndo` dispatches on
92 // the replay flag, so wp-navigation undos must carry
93 // navPostId + itemIndex + blockType + patches; the
94 // regular nav-item undo carries source/blockId/etc.
95 const beforePatches = [
96 { fieldKey: 'label', value: initial.label },
97 { fieldKey: 'url', value: initial.url },
98 ];
99 if (selected.source?.kind === 'wp-navigation') {
100 pushUndo({
101 kind: 'nav-item',
102 navReplay: true,
103 navPostId: selected.navPostId,
104 itemIndex: selected.itemIndex,
105 blockType: selected.blockType,
106 patches: beforePatches,
107 });
108 } else {
109 pushUndo({
110 kind: 'nav-item',
111 source: selected.source,
112 blockId: selected.blockId,
113 blockType: selected.blockType,
114 patches: beforePatches,
115 });
116 }
117 track('save', { kind: 'nav_item' });
118 onAfterSave(true);
119 } catch (err) {
120 track('save_failed', { kind: 'nav_item' });
121 setError(friendlyMessage(err));
122 setSaving(false);
123 }
124 };
125 useCmdEnterSave(handleSave, !saving);
126
127 const linkField = LinkControl ? (
128 <div className="extendify-quick-edit-link-field">
129 <div className="extendify-quick-edit-modal-label">
130 {__('Pick a new destination', 'extendify-local')}
131 </div>
132 {initial.url ? (
133 <div className="extendify-quick-edit-link-current">
134 <span>{__('Currently linked to:', 'extendify-local')}</span>{' '}
135 <code>{initial.url}</code>
136 </div>
137 ) : null}
138 {/* LinkControl reads the global core/block-editor settings, not
139 its own props — fetchSearchSuggestions wires up at boot. */}
140 <LinkControl
141 value={{ url }}
142 onChange={(v) => setUrl(v?.url || '')}
143 forceIsEditingLink
144 hasTextControl={false}
145 showInitialSuggestions
146 settings={[]}
147 suggestionsQuery={{ type: 'post', subtype: 'page' }}
148 />
149 </div>
150 ) : (
151 <TextControl
152 __nextHasNoMarginBottom
153 autoFocus
154 label={__('URL', 'extendify-local')}
155 value={url}
156 onChange={setUrl}
157 placeholder={initial.url}
158 />
159 );
160
161 return (
162 <Modal
163 title={__('Edit navigation link', 'extendify-local')}
164 onRequestClose={() => onAfterSave(false)}
165 isDismissible={false}
166 headerActions={<ModalCloseButton onClick={() => onAfterSave(false)} />}
167 className="extendify-quick-edit-modal extendify-quick-edit-modal-nav"
168 overlayClassName="extendify-quick-edit"
169 bodyOpenClassName={QE_MODAL_BODY_OPEN_CLASS}
170 size="medium"
171 >
172 {error ? (
173 <Notice status="error" isDismissible={false}>
174 {error}
175 </Notice>
176 ) : null}
177 <TextControl
178 __nextHasNoMarginBottom
179 label={__('Label', 'extendify-local')}
180 value={label}
181 onChange={setLabel}
182 />
183 {linkField}
184 <div className="extendify-quick-edit-modal-actions">
185 <Button variant="tertiary" onClick={() => onAfterSave(false)}>
186 {__('Cancel', 'extendify-local')}
187 </Button>
188 <Button
189 variant="primary"
190 isBusy={saving}
191 disabled={saving}
192 onClick={handleSave}
193 >
194 {__('Save', 'extendify-local')}
195 </Button>
196 </div>
197 </Modal>
198 );
199 };
200
201 export const openNavItemModal = (selected) => {
202 const handleClose = (didSave) => closeModal(didSave);
203 return <NavItemModal selected={selected} onAfterSave={handleClose} />;
204 };
205