From 2d42389117c97e53db5e569fd8b220c9e2eec1d5 Mon Sep 17 00:00:00 2001 From: jvega190 Date: Wed, 8 Oct 2025 15:09:36 -0600 Subject: [PATCH 01/18] [7418] PageNavOrder control --- .../FormsEngine/controls/PageNavOrder.tsx | 188 ++++++++++++++++++ .../components/FormsEngine/lib/controlMap.ts | 2 +- ui/app/src/services/content.ts | 25 +++ 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx diff --git a/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx b/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx new file mode 100644 index 0000000000..6a6910c33a --- /dev/null +++ b/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2007-2025 Crafter Software Corporation. All Rights Reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as published by + * the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import type { ControlProps } from '../types'; +import FormsEngineField from '../components/FormsEngineField'; +import React, { useEffect, useId, useState } from 'react'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import { FormattedMessage, useIntl } from 'react-intl'; +import ChangeCircleOutlinedIcon from '@mui/icons-material/ChangeCircleOutlined'; +import Button from '@mui/material/Button'; +import Box from '@mui/material/Box'; +import { EnhancedDialog } from '../../EnhancedDialog'; +import useEnhancedDialogState from '../../../hooks/useEnhancedDialogState'; +import useActiveSiteId from '../../../hooks/useActiveSiteId'; +import { getNavItemsOrder, type PageNavItem, reorderNavItems } from '../../../services/content'; +import { DialogBody } from '../../DialogBody'; +import Typography from '@mui/material/Typography'; +import useSpreadState from '../../../hooks/useSpreadState'; +import { ApiResponse } from '../../../models'; +import { SortableList, type TItem } from '../components/SortableList'; +import { useItemContext } from '../lib/formsEngineContext'; +import { DialogFooter } from '../../DialogFooter'; +import SecondaryButton from '../../SecondaryButton'; +import PrimaryButton from '../../PrimaryButton'; +import { pushErrorDialog } from '../../../utils/system'; +import { useDispatch } from 'react-redux'; +import Paper from '@mui/material/Paper'; +import useUpdateRefs from '../../../hooks/useUpdateRefs'; +import { showSystemNotification } from '../../../state/actions/system'; + +export interface PageNavOrderProps extends ControlProps { + value: string; +} + +const createSortableItemList = (order: PageNavItem[]): TItem[] => { + return order.map((item) => ({ + key: item.id, + value: item.name, + data: item + })); +}; + +export function PageNavOrder(props: PageNavOrderProps) { + const { value, setValue, field, autoFocus, readonly } = props; + const [initialValue] = useState(value); + const htmlId = useId(); + const orderDialogState = useEnhancedDialogState(); + const { formatMessage } = useIntl(); + const [pagesOrderState, setPagesOrderState] = useSpreadState<{ + fetching: boolean; + error: ApiResponse; + order: TItem[] | null; + }>({ + fetching: false, + error: null, + order: null + }); + const contextItem = useItemContext(); + const currentPath = contextItem?.path; + const siteId = useActiveSiteId(); + const dispatch = useDispatch(); + const effectRefs = useUpdateRefs({ + initialValue, + contextItem + }); + + useEffect(() => { + if (currentPath) { + setPagesOrderState({ fetching: true, error: null }); + getNavItemsOrder(siteId, currentPath).subscribe({ + next: (order) => { + const newOrder = createSortableItemList(order); + // If the initialValue is false, then it means that we'll be adding this page to the navigation (since it won't + // be in the order response). + if (!effectRefs.current.initialValue) { + newOrder.push({ + key: currentPath, + value: effectRefs.current.contextItem?.label || currentPath + }); + } + setPagesOrderState({ fetching: false, order: newOrder }); + }, + error: ({ response }) => setPagesOrderState({ fetching: false, error: response.response }) + }); + } + }, [siteId, setPagesOrderState, currentPath, effectRefs]); + + const handleChange = (event: SelectChangeEvent) => { + setValue(event.target.value === 'true'); + }; + const handleUpdateOrder = () => { + orderDialogState.onClose(); + + const currentItemIndex = pagesOrderState.order?.findIndex((item) => item.key === currentPath); + const previewItemIndex = currentItemIndex - 1; + const nextItemIndex = currentItemIndex + 1; + const previewItemPath = previewItemIndex >= 0 ? pagesOrderState.order?.[previewItemIndex]?.key : null; + const nextItemPath = + nextItemIndex < (pagesOrderState.order?.length ?? 0) ? pagesOrderState.order?.[nextItemIndex]?.key : null; + reorderNavItems(siteId, currentPath, previewItemPath, nextItemPath).subscribe({ + next: () => { + dispatch(showSystemNotification({ message: formatMessage({ defaultMessage: 'Navigation items reordered.' }) })); + }, + error: ({ response }) => { + dispatch(pushErrorDialog({ props: { error: response.response } })); + } + }); + }; + + return ( + + + + {value && ( + + )} + + } + > + + + + + + []) => + setPagesOrderState({ + order: fields + }) + } + /> + + + + orderDialogState.onClose()}> + + + + + + + + + ); +} + +export default PageNavOrder; diff --git a/ui/app/src/components/FormsEngine/lib/controlMap.ts b/ui/app/src/components/FormsEngine/lib/controlMap.ts index 543ac59946..ecd0ed98ed 100644 --- a/ui/app/src/components/FormsEngine/lib/controlMap.ts +++ b/ui/app/src/components/FormsEngine/lib/controlMap.ts @@ -69,7 +69,7 @@ export const controlMap: Record = { 'locale-selector': null, 'node-selector': lazy(() => import('../controls/NodeSelector')), 'numeric-input': lazy(() => import('../controls/Numeric')), - 'page-nav-order': null, + 'page-nav-order': lazy(() => import('../controls/PageNavOrder')), repeat: lazy(() => import('../controls/Repeat')), rte: lazy(() => import('../controls/RichTextEditor')), textarea: lazy(() => import('../controls/Textarea')), diff --git a/ui/app/src/services/content.ts b/ui/app/src/services/content.ts index 4203d94de0..22237b6514 100644 --- a/ui/app/src/services/content.ts +++ b/ui/app/src/services/content.ts @@ -1498,3 +1498,28 @@ export function fetchContentByCommitId(site: string, path: string, commitId: str }) ); } + +export interface PageNavItem { + order: number; + name: string; + id: string; + disabled: string; + placeInNav: string; +} + +export function getNavItemsOrder(site: string, path: string, order: string = 'default'): Observable { + const qs = toQueryString({ site, path, order }); + return get(`/studio/api/1/services/api/1/content/get-item-orders.json${qs}`).pipe( + map((response) => response?.response?.order), + catchError(errorSelectorApi1) + ); +} + +export function reorderNavItems(site: string, path: string, before: string, after: string) { + const qs = toQueryString({ site, path, before, after }); + return get(`/studio/api/1/services/api/1/content/reorder-items.json${qs}`).pipe( + tap((response) => console.log('response', response)), + map((response) => response?.response?.orderValue), + catchError(errorSelectorApi1) + ); +} From dbb06496e22ca60fb15f0c3405c583306f2266b3 Mon Sep 17 00:00:00 2001 From: jvega190 Date: Wed, 8 Oct 2025 15:22:56 -0600 Subject: [PATCH 02/18] [7418] Update PageNavOrder to use RadioButtons --- .../FormsEngine/controls/PageNavOrder.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx b/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx index 6a6910c33a..8ef3d19c78 100644 --- a/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx +++ b/ui/app/src/components/FormsEngine/controls/PageNavOrder.tsx @@ -17,8 +17,7 @@ import type { ControlProps } from '../types'; import FormsEngineField from '../components/FormsEngineField'; import React, { useEffect, useId, useState } from 'react'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import MenuItem from '@mui/material/MenuItem'; +import { SelectChangeEvent } from '@mui/material/Select'; import { FormattedMessage, useIntl } from 'react-intl'; import ChangeCircleOutlinedIcon from '@mui/icons-material/ChangeCircleOutlined'; import Button from '@mui/material/Button'; @@ -41,6 +40,10 @@ import { useDispatch } from 'react-redux'; import Paper from '@mui/material/Paper'; import useUpdateRefs from '../../../hooks/useUpdateRefs'; import { showSystemNotification } from '../../../state/actions/system'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; +import Alert from '@mui/material/Alert'; export interface PageNavOrderProps extends ControlProps { value: string; @@ -111,6 +114,8 @@ export function PageNavOrder(props: PageNavOrderProps) { const previewItemPath = previewItemIndex >= 0 ? pagesOrderState.order?.[previewItemIndex]?.key : null; const nextItemPath = nextItemIndex < (pagesOrderState.order?.length ?? 0) ? pagesOrderState.order?.[nextItemIndex]?.key : null; + // TODO: Waiting for the new v2 API to handle reordering the full items list. Currently, this only reorders + // the current item, and no other items can be re-arranged. reorderNavItems(siteId, currentPath, previewItemPath, nextItemPath).subscribe({ next: () => { dispatch(showSystemNotification({ message: formatMessage({ defaultMessage: 'Navigation items reordered.' }) })); @@ -123,18 +128,15 @@ export function PageNavOrder(props: PageNavOrderProps) { return ( - - + + + } label={} /> + } label={} /> + + {value && (