HEX
Server: LiteSpeed
System: Linux server342.web-hosting.com 4.18.0-553.124.4.lve.el8.x86_64 #1 SMP Fri May 15 13:02:13 UTC 2026 x86_64
User: ksonpoau (1099)
PHP: 8.2.31
Disabled: NONE
Upload Files
File: //home/ksonpoau/www/wp-content/plugins/extendify/src/QuickEdit/components/modals/NavItemModal.jsx
import { __experimentalLinkControl as LinkControl } from '@wordpress/block-editor';
import { Button, Modal, Notice, TextControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { save, saveWpNavigationItem } from '../../lib/api';
import { useCmdEnterSave } from '../../lib/cmd-enter-save';
import { friendlyMessage } from '../../lib/errors';
import { normalizeText } from '../../lib/fingerprint';
import { track } from '../../lib/insights';
import { closeModal, QE_MODAL_BODY_OPEN_CLASS } from '../../lib/modal-root';
import { pushUndo } from '../../state/undo';
import { ModalCloseButton } from './ModalCloseButton';

const readNavAttrs = (liveEl) => {
	const a = liveEl.querySelector('a[href]');
	const url = a?.getAttribute('href') || '';
	const labelEl =
		liveEl.querySelector('.wp-block-navigation-item__label') ||
		liveEl.querySelector('a');
	const label = (labelEl?.textContent || '').trim();
	return { label, url };
};

export const NavItemModal = ({ selected, onAfterSave }) => {
	const initial = readNavAttrs(selected.el);
	const [label, setLabel] = useState(initial.label);
	const [url, setUrl] = useState('');
	const [saving, setSaving] = useState(false);
	const [error, setError] = useState(null);

	const handleSave = async () => {
		if (saving) return;
		setSaving(true);
		setError(null);
		// Empty URL keeps the existing link — typo on label alone shouldn't break it.
		const finalUrl = url || initial.url;
		const patches = [];
		if (label !== initial.label) {
			patches.push({ fieldKey: 'label', value: label });
		}
		if (finalUrl !== initial.url) {
			patches.push({ fieldKey: 'url', value: finalUrl });
		}
		if (patches.length === 0) {
			onAfterSave(false);
			return;
		}
		// The clicked item's label is its render-time identity; the server
		// refuses (409) when itemIndex / blockId resolves to a different item.
		const fingerprint = initial.label
			? { text: normalizeText(initial.label) }
			: null;
		try {
			// Two save paths share the same `attrs.label` / `attrs.url`
			// patch shape (handled by `Schemas\NavigationLink`):
			//
			//   - INLINE items (a navigation block whose items are real
			//     innerBlocks of the host post/template-part) →
			//     `/quick-edit/save` (resolved by SaveController's
			//     findBlock walk).
			//
			//   - REF items (a navigation block with a `ref` attr →
			//     items live in a separate `wp_navigation` CPT post) →
			//     `/quick-edit/wp-navigation` (this is what the user
			//     hit when About/Contact failed: the host tree skips
			//     past the navigation block because `innerBlocks` is
			//     empty for ref-based navs, so findBlock can't reach
			//     the items).
			//
			// resolveTarget hands us `selected.source.kind = 'wp-
			// navigation'` for the ref case, with navPostId + itemIndex
			// already populated from `NavRefTagger` data attributes.
			if (selected.source?.kind === 'wp-navigation') {
				await saveWpNavigationItem({
					navPostId: selected.navPostId,
					itemIndex: selected.itemIndex,
					blockType: selected.blockType,
					fingerprint,
					patches,
				});
			} else {
				await save({
					source: selected.source,
					blockId: selected.blockId,
					blockType: selected.blockType,
					fingerprint,
					patches,
				});
			}
			// Push the undo entry shaped to match the FORWARD-save
			// endpoint we just used — `performUndo` dispatches on
			// the replay flag, so wp-navigation undos must carry
			// navPostId + itemIndex + blockType + patches; the
			// regular nav-item undo carries source/blockId/etc.
			const beforePatches = [
				{ fieldKey: 'label', value: initial.label },
				{ fieldKey: 'url', value: initial.url },
			];
			if (selected.source?.kind === 'wp-navigation') {
				pushUndo({
					kind: 'nav-item',
					navReplay: true,
					navPostId: selected.navPostId,
					itemIndex: selected.itemIndex,
					blockType: selected.blockType,
					patches: beforePatches,
				});
			} else {
				pushUndo({
					kind: 'nav-item',
					source: selected.source,
					blockId: selected.blockId,
					blockType: selected.blockType,
					patches: beforePatches,
				});
			}
			track('save', { kind: 'nav_item' });
			onAfterSave(true);
		} catch (err) {
			track('save_failed', { kind: 'nav_item' });
			setError(friendlyMessage(err));
			setSaving(false);
		}
	};
	useCmdEnterSave(handleSave, !saving);

	const linkField = LinkControl ? (
		<div className="extendify-quick-edit-link-field">
			<div className="extendify-quick-edit-modal-label">
				{__('Pick a new destination', 'extendify-local')}
			</div>
			{initial.url ? (
				<div className="extendify-quick-edit-link-current">
					<span>{__('Currently linked to:', 'extendify-local')}</span>{' '}
					<code>{initial.url}</code>
				</div>
			) : null}
			{/* LinkControl reads the global core/block-editor settings, not
			    its own props — fetchSearchSuggestions wires up at boot. */}
			<LinkControl
				value={{ url }}
				onChange={(v) => setUrl(v?.url || '')}
				forceIsEditingLink
				hasTextControl={false}
				showInitialSuggestions
				settings={[]}
				suggestionsQuery={{ type: 'post', subtype: 'page' }}
			/>
		</div>
	) : (
		<TextControl
			__nextHasNoMarginBottom
			autoFocus
			label={__('URL', 'extendify-local')}
			value={url}
			onChange={setUrl}
			placeholder={initial.url}
		/>
	);

	return (
		<Modal
			title={__('Edit navigation link', 'extendify-local')}
			onRequestClose={() => onAfterSave(false)}
			isDismissible={false}
			headerActions={<ModalCloseButton onClick={() => onAfterSave(false)} />}
			className="extendify-quick-edit-modal extendify-quick-edit-modal-nav"
			overlayClassName="extendify-quick-edit"
			bodyOpenClassName={QE_MODAL_BODY_OPEN_CLASS}
			size="medium"
		>
			{error ? (
				<Notice status="error" isDismissible={false}>
					{error}
				</Notice>
			) : null}
			<TextControl
				__nextHasNoMarginBottom
				label={__('Label', 'extendify-local')}
				value={label}
				onChange={setLabel}
			/>
			{linkField}
			<div className="extendify-quick-edit-modal-actions">
				<Button variant="tertiary" onClick={() => onAfterSave(false)}>
					{__('Cancel', 'extendify-local')}
				</Button>
				<Button
					variant="primary"
					isBusy={saving}
					disabled={saving}
					onClick={handleSave}
				>
					{__('Save', 'extendify-local')}
				</Button>
			</div>
		</Modal>
	);
};

export const openNavItemModal = (selected) => {
	const handleClose = (didSave) => closeModal(didSave);
	return <NavItemModal selected={selected} onAfterSave={handleClose} />;
};