diff --git a/admin/tracking/class-tracking-settings-data.php b/admin/tracking/class-tracking-settings-data.php
index 29e1a9e6d30..559200edc37 100644
--- a/admin/tracking/class-tracking-settings-data.php
+++ b/admin/tracking/class-tracking-settings-data.php
@@ -61,6 +61,13 @@ class WPSEO_Tracking_Settings_Data implements WPSEO_Collection {
'most_linked_ignore_list',
'least_linked_ignore_list',
'indexables_page_reading_list',
+ 'publishing_principles_id',
+ 'ownership_funding_info_id',
+ 'actionable_feedback_policy_id',
+ 'corrections_policy_id',
+ 'ethics_policy_id',
+ 'diversity_policy_id',
+ 'diversity_staffing_report_id',
];
/**
diff --git a/inc/options/class-wpseo-option-titles.php b/inc/options/class-wpseo-option-titles.php
index 067db4b7b99..9b7661aa15c 100644
--- a/inc/options/class-wpseo-option-titles.php
+++ b/inc/options/class-wpseo-option-titles.php
@@ -93,6 +93,14 @@ class WPSEO_Option_Titles extends WPSEO_Option {
'open_graph_frontpage_image' => '', // Text field.
'open_graph_frontpage_image_id' => 0,
+ 'publishing_principles_id' => false,
+ 'ownership_funding_info_id' => false,
+ 'actionable_feedback_policy_id' => false,
+ 'corrections_policy_id' => false,
+ 'ethics_policy_id' => false,
+ 'diversity_policy_id' => false,
+ 'diversity_staffing_report_id' => false,
+
/*
* Uses enrich_defaults to add more along the lines of:
* - 'title-' . $pt->name => ''; // Text field.
@@ -579,6 +587,13 @@ protected function validate_option( $dirty, $clean, $old ) {
case 'person_logo_id':
case 'social-image-id-':
case 'open_graph_frontpage_image_id':
+ case 'publishing_principles_id':
+ case 'ownership_funding_info_id':
+ case 'actionable_feedback_policy_id':
+ case 'corrections_policy_id':
+ case 'ethics_policy_id':
+ case 'diversity_policy_id':
+ case 'diversity_staffing_report_id':
if ( isset( $dirty[ $key ] ) ) {
$int = WPSEO_Utils::validate_int( $dirty[ $key ] );
if ( $int !== false && $int >= 0 ) {
@@ -592,7 +607,6 @@ protected function validate_option( $dirty, $clean, $old ) {
}
}
break;
-
/* Separator field - Radio. */
case 'separator':
if ( isset( $dirty[ $key ] ) && $dirty[ $key ] !== '' ) {
diff --git a/packages/js/src/settings/components/formik-page-select-field.js b/packages/js/src/settings/components/formik-page-select-field.js
new file mode 100644
index 00000000000..5a794f769b4
--- /dev/null
+++ b/packages/js/src/settings/components/formik-page-select-field.js
@@ -0,0 +1,148 @@
+/* eslint-disable complexity */
+import { DocumentAddIcon } from "@heroicons/react/outline";
+import { useCallback, useMemo, useState } from "@wordpress/element";
+import { __ } from "@wordpress/i18n";
+import { AutocompleteField, Spinner } from "@yoast/ui-library";
+import classNames from "classnames";
+import { useField } from "formik";
+import { debounce, find, isEmpty, map, values } from "lodash";
+import PropTypes from "prop-types";
+import { ASYNC_ACTION_STATUS } from "../constants";
+import { useDispatchSettings, useSelectSettings } from "../hooks";
+
+/**
+ * @param {JSX.node} children The children.
+ * @param {string} [className] The className.
+ * @returns {JSX.Element} The page select options content decorator component.
+ */
+const PageSelectOptionsContent = ( { children, className = "" } ) => (
+
+ { children }
+
+);
+
+PageSelectOptionsContent.propTypes = {
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+};
+
+/**
+ * @param {Object} props The props object.
+ * @param {string} props.name The field name.
+ * @param {string} props.id The field id.
+ * @param {string} props.className The className.
+ * @returns {JSX.Element} The page select component.
+ */
+const FormikPageSelectField = ( { name, id, className = "", ...props } ) => {
+ const siteBasicsPolicies = useSelectSettings( "selectPreference", [], "siteBasicsPolicies", {} );
+ const pages = useSelectSettings( "selectPagesWith", [ siteBasicsPolicies ], values( siteBasicsPolicies ) );
+ const { fetchPages } = useDispatchSettings();
+ const [ { value, ...field }, , { setTouched, setValue } ] = useField( { type: "select", name, id, ...props } );
+ const [ status, setStatus ] = useState( ASYNC_ACTION_STATUS.idle );
+ const [ queriedPageIds, setQueriedPageIds ] = useState( [] );
+ const canCreatePages = useSelectSettings( "selectPreference", [], "canCreatePages", false );
+ const createPageUrl = useSelectSettings( "selectPreference", [], "createPageUrl", "" );
+
+ const selectedPage = useMemo( () => {
+ const pageObjects = values( pages );
+ return find( pageObjects, [ "id", value ] );
+ }, [ value, pages ] );
+
+ const debouncedFetchPages = useCallback( debounce( async search => {
+ try {
+ setStatus( ASYNC_ACTION_STATUS.loading );
+ // eslint-disable-next-line camelcase
+ const response = await fetchPages( { search } );
+
+ setQueriedPageIds( map( response.payload, "id" ) );
+ setStatus( ASYNC_ACTION_STATUS.success );
+ } catch ( error ) {
+ if ( error instanceof DOMException && error.name === "AbortError" ) {
+ // Expected abort errors can be ignored.
+ return;
+ }
+ setQueriedPageIds( [] );
+ setStatus( ASYNC_ACTION_STATUS.error );
+ }
+ }, 200 ), [ setQueriedPageIds, setStatus, fetchPages ] );
+
+ const handleChange = useCallback( newValue => {
+ setTouched( true, false );
+ setValue( newValue );
+ }, [ setValue, setTouched ] );
+ const handleQueryChange = useCallback( event => debouncedFetchPages( event.target.value ), [ debouncedFetchPages ] );
+ const selectablePages = useMemo( () => isEmpty( queriedPageIds ) ? map( pages, "id" ) : queriedPageIds, [ queriedPageIds, pages ] );
+ const hasNoPages = useMemo( () => ( status === ASYNC_ACTION_STATUS.success && isEmpty( queriedPageIds ) ), [ queriedPageIds, status ] );
+
+ return (
+
+ <>
+ { ( status === ASYNC_ACTION_STATUS.idle || status === ASYNC_ACTION_STATUS.success ) && (
+ <>
+ { hasNoPages ? (
+
+ { __( "No pages found.", "wordpress-seo" ) }
+
+ ) : map( selectablePages, pageId => {
+ const page = pages?.[ pageId ];
+ return page ? (
+
+ { page?.name }
+
+ ) : null;
+ } ) }
+ { canCreatePages && (
+
+
+
+ { __( "Add new page...", "wordpress-seo" ) }
+
+
+ ) }
+ >
+ ) }
+ { status === ASYNC_ACTION_STATUS.loading && (
+
+
+ { __( "Searching pages...", "wordpress-seo" ) }
+
+ ) }
+ { status === ASYNC_ACTION_STATUS.error && (
+
+ { __( "Failed to retrieve pages.", "wordpress-seo" ) }
+
+ ) }
+ >
+
+ );
+};
+
+FormikPageSelectField.propTypes = {
+ name: PropTypes.string.isRequired,
+ id: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ disabled: PropTypes.bool,
+};
+
+export default FormikPageSelectField;
diff --git a/packages/js/src/settings/components/formik-user-select-field.js b/packages/js/src/settings/components/formik-user-select-field.js
index 93a0f821afa..b00dc80a87b 100644
--- a/packages/js/src/settings/components/formik-user-select-field.js
+++ b/packages/js/src/settings/components/formik-user-select-field.js
@@ -108,7 +108,7 @@ const FormikUserSelectField = ( { name, id, className = "", ...props } ) => {
{ ...props }
>
<>
- { status === ASYNC_ACTION_STATUS.idle || status === ASYNC_ACTION_STATUS.success && (
+ { ( status === ASYNC_ACTION_STATUS.idle || status === ASYNC_ACTION_STATUS.success ) && (
<>
{ isEmpty( queriedUserIds ) ? (
diff --git a/packages/js/src/settings/components/index.js b/packages/js/src/settings/components/index.js
index fe638356b1f..099edcbc259 100644
--- a/packages/js/src/settings/components/index.js
+++ b/packages/js/src/settings/components/index.js
@@ -5,6 +5,7 @@ export { default as FormikMediaSelectField } from "./formik-media-select-field";
export { default as FormikReplacementVariableEditorField } from "./formik-replacement-variable-editor-field";
export { default as FormikTagField } from "./formik-tag-field";
export { default as FormikUserSelectField } from "./formik-user-select-field";
+export { default as FormikPageSelectField } from "./formik-page-select-field";
export { default as FormikValueChangeField } from "./formik-value-change-field";
export { default as FormikWithErrorField } from "./formik-with-error-field";
export { default as Notifications } from "./notifications";
diff --git a/packages/js/src/settings/helpers/search.js b/packages/js/src/settings/helpers/search.js
index f518f79699c..d5b28e771bc 100644
--- a/packages/js/src/settings/helpers/search.js
+++ b/packages/js/src/settings/helpers/search.js
@@ -350,6 +350,70 @@ export const createSearchIndex = ( postTypes, taxonomies, { userLocale } = {} )
fieldLabel: __( "Usage tracking", "wordpress-seo" ),
keywords: [],
},
+ publishing_principles_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-publishing_principles_id",
+ fieldLabel: __( "Publishing principles", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+ ],
+ },
+ ownership_funding_info_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-ownership_funding_info_id",
+ fieldLabel: __( "Ownership / Funding info", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+
+ ],
+ },
+ actionable_feedback_policy_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-actionable_feedback_policy_id",
+ fieldLabel: __( "Actionable feedback policy", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+ ],
+ },
+ corrections_policy_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-corrections_policy_id",
+ fieldLabel: __( "Corrections policy", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+ ],
+ },
+ ethics_policy_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-ethics_policy_id",
+ fieldLabel: __( "Ethics policy", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+ ],
+ },
+ diversity_policy_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-diversity_policy_id",
+ fieldLabel: __( "Diversity policy", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+ ],
+ },
+ diversity_staffing_report_id: {
+ route: "/site-basics",
+ routeLabel: __( "Site basics", "wordpress-seo" ),
+ fieldId: "input-wpseo_titles-diversity_staffing_report_id",
+ fieldLabel: __( "Diversity staffing report", "wordpress-seo" ),
+ keywords: [
+ __( "Publishing policies", "wordpress-seo" ),
+ ],
+ },
baiduverify: {
route: "/site-connections",
routeLabel: __( "Site connections", "wordpress-seo" ),
diff --git a/packages/js/src/settings/hocs/index.js b/packages/js/src/settings/hocs/index.js
index 13013b13b79..bd9a462eece 100644
--- a/packages/js/src/settings/hocs/index.js
+++ b/packages/js/src/settings/hocs/index.js
@@ -1,3 +1,4 @@
export { default as withDisabledMessageSupport } from "./with-disabled-message-support";
export { default as withFormikDummyField } from "./with-formik-dummy-field";
+export { default as withFormikDummySelectField } from "./with-formik-dummy-select-field";
export { default as withFormikError } from "./with-formik-error";
diff --git a/packages/js/src/settings/hocs/with-formik-dummy-select-field.js b/packages/js/src/settings/hocs/with-formik-dummy-select-field.js
new file mode 100644
index 00000000000..d2bd601ed49
--- /dev/null
+++ b/packages/js/src/settings/hocs/with-formik-dummy-select-field.js
@@ -0,0 +1,41 @@
+import { noop } from "lodash";
+import PropTypes from "prop-types";
+import { useSelectSettings } from "../hooks";
+
+/**
+ * @param {JSX.ElementClass} Component The component to wrap.
+ * @returns {JSX.ElementClass} The wrapped component.
+ */
+const withFormikDummySelectField = Component => {
+ /**
+ * @param {string} name The name.
+ * @param {boolean} isDummy Whether this is a dummy field.
+ * @param {Object} [props] Any extra props.
+ * @returns {JSX.Element} The element.
+ */
+ const ComponentWithFormikDummySelectField = ( { name, isDummy = false, ...props } ) => {
+ const defaultValue = useSelectSettings( "selectDefaultSettingValue", [ name ], name );
+
+ if ( isDummy ) {
+ return ;
+ }
+
+ return ;
+ };
+
+ ComponentWithFormikDummySelectField.propTypes = {
+ name: PropTypes.string.isRequired,
+ isDummy: PropTypes.bool,
+ };
+
+ return ComponentWithFormikDummySelectField;
+};
+
+export default withFormikDummySelectField;
diff --git a/packages/js/src/settings/routes/site-basics.js b/packages/js/src/settings/routes/site-basics.js
index a44247c20e9..b8717c81391 100644
--- a/packages/js/src/settings/routes/site-basics.js
+++ b/packages/js/src/settings/routes/site-basics.js
@@ -1,21 +1,23 @@
-import { createInterpolateElement, useMemo } from "@wordpress/element";
+import { createInterpolateElement, useEffect, useMemo } from "@wordpress/element";
import { __, sprintf } from "@wordpress/i18n";
-import { Alert, Radio, RadioGroup, TextField, ToggleField } from "@yoast/ui-library";
+import { Alert, FeatureUpsell, Radio, RadioGroup, TextField, ToggleField } from "@yoast/ui-library";
import { Field, useFormikContext } from "formik";
import { get, map } from "lodash";
import {
FieldsetLayout,
FormikMediaSelectField,
FormikValueChangeField,
+ FormikPageSelectField,
FormikWithErrorField,
FormLayout,
OpenGraphDisabledAlert,
RouteLayout,
} from "../components";
-import { withDisabledMessageSupport } from "../hocs";
-import { useSelectSettings } from "../hooks";
+import { withDisabledMessageSupport, withFormikDummySelectField } from "../hocs";
+import { useDispatchSettings, useSelectSettings } from "../hooks";
const ToggleFieldWithDisabledMessageSupport = withDisabledMessageSupport( ToggleField );
+const FormikSelectPageWithDummy = withFormikDummySelectField( FormikPageSelectField );
/**
* @returns {JSX.Element} The site defaults route.
@@ -27,7 +29,12 @@ const SiteBasics = () => {
const showForceRewriteTitlesSetting = useSelectSettings( "selectPreference", [], "showForceRewriteTitlesSetting", false );
const replacementVariablesLink = useSelectSettings( "selectLink", [], "https://yoa.st/site-basics-replacement-variables" );
const usageTrackingLink = useSelectSettings( "selectLink", [], "https://yoa.st/usage-tracking-2" );
+ const sitePoliciesLink = useSelectSettings( "selectLink", [], "https://yoa.st/site-policies-learn-more" );
const siteTitle = useSelectSettings( "selectPreference", [], "siteTitle", "" );
+ const publishingPremiumLink = useSelectSettings( "selectLink", [], "https://yoa.st/site-policies-upsell" );
+ const isPremium = useSelectSettings( "selectPreference", [], "isPremium" );
+ const premiumUpsellConfig = useSelectSettings( "selectUpsellSettingsAsProps" );
+ const { fetchPages } = useDispatchSettings();
const usageTrackingDescription = useMemo( () => createInterpolateElement(
sprintf(
@@ -42,6 +49,18 @@ const SiteBasics = () => {
}
), [] );
+ const sitePoliciesDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /* translators: %1$s expands to an opening tag. %2$s expands to a closing tag. */
+ __( "Select the pages on your website which contain information about your organizational and publishing policies. Some of these might not apply to your site, and you can select the same page for multiple policies. %1$sLearn more about why setting your site policies is important%2$s.", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-no-target-blank
+ a: ,
+ }
+ ), [ sitePoliciesLink ] );
const siteInfoDescription = useMemo( () => createInterpolateElement(
sprintf(
/* translators: %1$s and %2$s expand to an opening and closing emphasis tag. %3$s and %4$s expand to an opening and closing anchor tag. */
@@ -54,7 +73,10 @@ const SiteBasics = () => {
{
em: ,
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-no-target-blank
- a: ,
+ a: ,
}
), [] );
const canNotManageOptionsAlertText = useMemo( () => createInterpolateElement(
@@ -98,9 +120,115 @@ const SiteBasics = () => {
}
), [] );
+ const publishingPrinciplesDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which describes the editorial principles of your organization. %1$sWhat%2$s do you write about, %1$swho%2$s do you write for, and %1$swhy%2$s?", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
+ const ownershipDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which describes the ownership structure of your organization. It should include information about %1$sfunding%2$s and %1$sgrants%2$s.", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
+
+ const actionableFeedbackDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which describes how your organization collects and responds to %1$sfeedback%2$s, engages with the %1$spublic%2$s, and prioritizes %1$stransparency%2$s.", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
+
+ const correctionsDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which outlines your procedure for addressing %1$serrors%2$s (e.g., publishing retractions or corrections).", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
+
+ const ethicsDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which describes the personal, organizational, and corporate %1$sstandards%2$s of %1$sbehavior%2$s expected by your organization.", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
+
+ const diversityDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which provides information on your diversity policies for %1$seditorial%2$s content.", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
+ const diversityStaffingDescription = useMemo( () => createInterpolateElement(
+ sprintf(
+ /**
+ * translators: %1$s expands to an opening italics tag.
+ * %2$s expands to a closing italics tag.
+ */
+ __( "Select a page which provides information about your diversity policies for %1$sstaffing%2$s, %1$shiring%2$s and %1$semployment%2$s.", "wordpress-seo" ),
+ "",
+ ""
+ ),
+ {
+ i: ,
+ }
+ ), [] );
const { values } = useFormikContext();
const { opengraph } = values.wpseo_social;
+ useEffect( () => {
+ // Get initial options.
+ fetchPages();
+ }, [ fetchPages ] );
return (
{
className="yst-max-w-sm"
/>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/js/src/settings/store/index.js b/packages/js/src/settings/store/index.js
index 47feeeb0c39..c7727afd08b 100644
--- a/packages/js/src/settings/store/index.js
+++ b/packages/js/src/settings/store/index.js
@@ -13,6 +13,7 @@ import linkParams, { createInitialLinkParamsState, linkParamsActions, linkParams
import media, { createInitialMediaState, mediaActions, mediaControls, mediaSelectors } from "./media";
import notifications, { createInitialNotificationsState, notificationsActions, notificationsSelectors } from "./notifications";
import postTypes, { createInitialPostTypesState, postTypesActions, postTypesSelectors } from "./post-types";
+import pageReducer, { getPageInitialState, PAGE_NAME, pageActions, pageControls, pageSelectors } from "./pages";
import preferences, { createInitialPreferencesState, preferencesActions, preferencesSelectors } from "./preferences";
import replacementVariables, {
createInitialReplacementVariablesState,
@@ -38,6 +39,7 @@ const createStore = ( { initialState } ) => {
...linkParamsActions,
...mediaActions,
...notificationsActions,
+ ...pageActions,
...postTypesActions,
...preferencesActions,
...replacementVariablesActions,
@@ -53,6 +55,7 @@ const createStore = ( { initialState } ) => {
...linkParamsSelectors,
...mediaSelectors,
...notificationsSelectors,
+ ...pageSelectors,
...postTypesSelectors,
...preferencesSelectors,
...replacementVariablesSelectors,
@@ -69,6 +72,7 @@ const createStore = ( { initialState } ) => {
linkParams: createInitialLinkParamsState(),
media: createInitialMediaState(),
notifications: createInitialNotificationsState(),
+ [ PAGE_NAME ]: getPageInitialState(),
postTypes: createInitialPostTypesState(),
preferences: createInitialPreferencesState(),
replacementVariables: createInitialReplacementVariablesState(),
@@ -85,6 +89,7 @@ const createStore = ( { initialState } ) => {
linkParams,
media,
notifications,
+ [ PAGE_NAME ]: pageReducer,
postTypes,
preferences,
replacementVariables,
@@ -96,6 +101,7 @@ const createStore = ( { initialState } ) => {
controls: {
...mediaControls,
...usersControls,
+ ...pageControls,
},
} );
};
diff --git a/packages/js/src/settings/store/pages.js b/packages/js/src/settings/store/pages.js
new file mode 100644
index 00000000000..52142cb609e
--- /dev/null
+++ b/packages/js/src/settings/store/pages.js
@@ -0,0 +1,120 @@
+/* eslint-disable camelcase, complexity */
+import { createEntityAdapter, createSelector, createSlice } from "@reduxjs/toolkit";
+import apiFetch from "@wordpress/api-fetch";
+import { buildQueryString } from "@wordpress/url";
+import { map, trim } from "lodash";
+import { ASYNC_ACTION_NAMES, ASYNC_ACTION_STATUS } from "../constants";
+
+const pagesAdapter = createEntityAdapter();
+
+export const FETCH_PAGES_ACTION_NAME = "fetchPages";
+export const PAGE_NAME = "pages";
+// Global abort controller for this reducer to abort requests made by multiple selects.
+let abortController;
+
+/**
+ * @param {Object} queryData The query data.
+ * @returns {Object} Success or error action object.
+ */
+export function* fetchPages( queryData ) {
+ yield{ type: `${ FETCH_PAGES_ACTION_NAME }/${ ASYNC_ACTION_NAMES.request }` };
+ try {
+ // Trigger the fetch pages control flow.
+ const pages = yield{
+ type: FETCH_PAGES_ACTION_NAME,
+ payload: { ...queryData },
+ };
+ return { type: `${ FETCH_PAGES_ACTION_NAME }/${ ASYNC_ACTION_NAMES.success }`, payload: pages };
+ } catch ( error ) {
+ return { type: `${ FETCH_PAGES_ACTION_NAME }/${ ASYNC_ACTION_NAMES.error }`, payload: error };
+ }
+}
+
+/**
+ * @param {Object} page The page.
+ * @returns {Object} The prepared and predictable user.
+ */
+const preparePage = page => (
+ {
+ id: page?.id,
+ // Fallbacks for page title, because we always need something to show.
+ name: trim( page?.title.rendered ) || page?.slug || page.id,
+ slug: page?.slug,
+ } );
+
+const pagesSlice = createSlice( {
+ name: "pages",
+ initialState: pagesAdapter.getInitialState( {
+ status: ASYNC_ACTION_STATUS.idle,
+ error: "",
+ } ),
+ reducers: {
+ addOnePage: {
+ reducer: pagesAdapter.addOne,
+ prepare: page => ( { payload: preparePage( page ) } ),
+ },
+ addManyPages: {
+ reducer: pagesAdapter.addMany,
+ prepare: pages => ( { payload: map( pages, preparePage ) } ),
+ },
+ },
+ extraReducers: ( builder ) => {
+ builder.addCase( `${ FETCH_PAGES_ACTION_NAME }/${ ASYNC_ACTION_NAMES.request }`, ( state ) => {
+ state.status = ASYNC_ACTION_STATUS.loading;
+ } );
+ builder.addCase( `${ FETCH_PAGES_ACTION_NAME }/${ ASYNC_ACTION_NAMES.success }`, ( state, action ) => {
+ state.status = ASYNC_ACTION_STATUS.success;
+ pagesAdapter.addMany( state, map( action.payload, preparePage ) );
+ } );
+ builder.addCase( `${ FETCH_PAGES_ACTION_NAME }/${ ASYNC_ACTION_NAMES.error }`, ( state, action ) => {
+ state.status = ASYNC_ACTION_STATUS.error;
+ state.error = action.payload;
+ } );
+ },
+} );
+
+export const getPageInitialState = pagesSlice.getInitialState;
+// Prefix selectors
+const pageAdapterSelectors = pagesAdapter.getSelectors( state => state.pages );
+
+export const pageSelectors = {
+ selectPageIds: pageAdapterSelectors.selectIds,
+ selectPageById: pageAdapterSelectors.selectById,
+ selectPages: pageAdapterSelectors.selectEntities,
+};
+pageSelectors.selectPagesWith = createSelector(
+ [
+ pageSelectors.selectPages,
+ ( state, additionalPage = {} ) => additionalPage,
+ ],
+ ( pages, additionalPage ) => {
+ const additionalPages = {};
+ additionalPage.forEach( page => {
+ if ( page?.id && ! pages[ page.id ] ) {
+ // Add the additional page.
+ additionalPages[ page.id ] = { ...page };
+ }
+ } );
+
+ return { ...additionalPages, ...pages };
+ }
+);
+export const pageActions = {
+ ...pagesSlice.actions,
+ fetchPages,
+};
+
+export const pageControls = {
+ [ FETCH_PAGES_ACTION_NAME ]: async( { payload } ) => {
+ abortController?.abort();
+
+
+ abortController = new AbortController();
+ return apiFetch( {
+ path: `/wp/v2/pages?${ buildQueryString( payload ) }`,
+ signal: abortController.signal,
+ } );
+ },
+};
+
+export default pagesSlice.reducer;
diff --git a/packages/ui-library/src/elements/autocomplete/style.css b/packages/ui-library/src/elements/autocomplete/style.css
index 07beaaf904b..c82d44cffdd 100644
--- a/packages/ui-library/src/elements/autocomplete/style.css
+++ b/packages/ui-library/src/elements/autocomplete/style.css
@@ -19,6 +19,14 @@
}
}
+ .yst-autocomplete--disabled {
+
+ @apply
+ yst-opacity-50
+ yst-cursor-not-allowed
+ focus:yst-ring-0;
+ }
+
.yst-autocomplete__button {
@apply
yst-w-full
@@ -55,7 +63,7 @@
.yst-autocomplete__options {
@apply
yst-absolute
- yst-z-10
+ yst-z-20
yst-w-full
yst-mt-1
yst-overflow-auto
diff --git a/src/integrations/settings-integration.php b/src/integrations/settings-integration.php
index 6ae604392c5..2870c2fdaa9 100644
--- a/src/integrations/settings-integration.php
+++ b/src/integrations/settings-integration.php
@@ -441,12 +441,14 @@ protected function get_preferences( $settings ) {
'homepagePageEditUrl' => \get_edit_post_link( $page_on_front, 'js' ),
'homepagePostsEditUrl' => \get_edit_post_link( $page_for_posts, 'js' ),
'createUserUrl' => \admin_url( 'user-new.php' ),
+ 'createPageUrl' => \admin_url( 'post-new.php?post_type=page' ),
'editUserUrl' => \admin_url( 'user-edit.php' ),
'editTaxonomyUrl' => \admin_url( 'edit-tags.php' ),
'generalSettingsUrl' => \admin_url( 'options-general.php' ),
'companyOrPersonMessage' => \apply_filters( 'wpseo_knowledge_graph_setting_msg', '' ),
'currentUserId' => \get_current_user_id(),
'canCreateUsers' => \current_user_can( 'create_users' ),
+ 'canCreatePages' => \current_user_can( 'edit_pages' ),
'canEditUsers' => \current_user_can( 'edit_users' ),
'canManageOptions' => \current_user_can( 'manage_options' ),
'userLocale' => \str_replace( '_', '-', \get_user_locale() ),
@@ -454,6 +456,7 @@ protected function get_preferences( $settings ) {
'showForceRewriteTitlesSetting' => ! \current_theme_supports( 'title-tag' ) && ! ( \function_exists( 'wp_is_block_theme' ) && \wp_is_block_theme() ),
'upsellSettings' => $this->get_upsell_settings(),
'siteRepresentsPerson' => $this->get_site_represents_person( $settings ),
+ 'siteBasicsPolicies' => $this->get_site_basics_policies( $settings ),
];
}
@@ -481,6 +484,55 @@ protected function get_site_represents_person( $settings ) {
return $person;
}
+ /**
+ * Get site policy data.
+ *
+ * @param array $settings The settings.
+ *
+ * @return array The policy data.
+ */
+ private function get_site_basics_policies( $settings ) {
+ $policies = [];
+
+
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['publishing_principles_id'], 'publishing_principles_id' );
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ownership_funding_info_id'], 'ownership_funding_info_id' );
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['actionable_feedback_policy_id'], 'actionable_feedback_policy_id' );
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['corrections_policy_id'], 'corrections_policy_id' );
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['ethics_policy_id'], 'ethics_policy_id' );
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_policy_id'], 'diversity_policy_id' );
+ $policies = $this->maybe_add_policy( $policies, $settings['wpseo_titles']['diversity_staffing_report_id'], 'diversity_staffing_report_id' );
+
+ return $policies;
+ }
+
+ /**
+ * Adds policy data if it is present.
+ *
+ * @param array $policies The existing policy data.
+ * @param int $policy The policy id to check.
+ * @param string $key The option key name.
+ *
+ * @return array The policy data.
+ */
+ private function maybe_add_policy( $policies, $policy, $key ) {
+ $policy_array = [
+ 'id' => false,
+ 'name' => '',
+ ];
+
+ if ( isset( $policy ) && \is_int( $policy ) ) {
+ $policy_array['id'] = $policy;
+ $post = \get_post( $policy );
+ if ( $post instanceof \WP_Post ) {
+ $policy_array['name'] = $post->post_title;
+ }
+ }
+ $policies[ $key ] = $policy_array;
+
+ return $policies;
+ }
+
/**
* Returns settings for the Call to Buy (CTB) buttons.
*