diff --git a/.distignore b/.distignore index b69d2af9c..9a5cba0e4 100644 --- a/.distignore +++ b/.distignore @@ -10,6 +10,7 @@ .env.example .gitignore .gitattributes +.php-cs-fixer.dist.php composer.json composer.lock package.json diff --git a/.gitignore b/.gitignore index 44fa2850b..83c571da2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .phpunit.result.cache node_modules .DS_Store +.php-cs-fixer.cache # Playwright test-results/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 000000000..7989348bd --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,19 @@ +in( __DIR__ ) + ->exclude( + [ + 'tests/', + 'vendor/', + 'node_modules/', + ] + ); + +return ( new PhpCsFixer\Config() )->setRules( + [ + 'native_function_invocation' => [ + 'include' => [ '@all' ], + ], + ] +)->setFinder( $finder ); \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d63e9e1b..1cd1497c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ += 1.6.0 = + +Enhancements: + +* Allow users to collect extra points for previous months' badges. +* Added WP-CLI commands to manage recommendations. + +Under the hood: + +* Ravi's Recommendations are now a custom post type. + = 1.5.0 = Added these recommendations from Ravi: diff --git a/assets/css/admin.css b/assets/css/admin.css index 164d4e5b6..ce430cbbb 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -137,8 +137,6 @@ } } } - - } .prpl-hidden { @@ -281,7 +279,6 @@ button.prpl-info-icon { } } - /*------------------------------------*\ Buttons \*------------------------------------*/ @@ -481,3 +478,33 @@ button.prpl-info-icon { grid-template-columns: repeat(2, 1fr); gap: var(--prpl-padding); } + +/*------------------------------------*\ + Loader. + See https://cssloaders.github.io/ for more. +\*------------------------------------*/ +.prpl-loader { + width: 48px; + height: 48px; + border: 5px solid #fff; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; + z-index: 20; + position: absolute; + top: calc(50% - 24px); + left: calc(50% - 24px); +} + +@keyframes rotation { + + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/assets/css/dashboard-widgets/score.css b/assets/css/dashboard-widgets/score.css index fbebb3769..fca269f99 100644 --- a/assets/css/dashboard-widgets/score.css +++ b/assets/css/dashboard-widgets/score.css @@ -3,7 +3,7 @@ /** * Admin widget. * - * Dependencies: progress-planner/web-components/prpl-suggested-task, progress-planner/web-components/prpl-badge + * Dependencies: progress-planner/suggested-task, progress-planner/web-components/prpl-badge */ #progress_planner_dashboard_widget_score { diff --git a/assets/css/page-widgets/suggested-tasks.css b/assets/css/page-widgets/suggested-tasks.css index dfd884dac..d0b1e4666 100644 --- a/assets/css/page-widgets/suggested-tasks.css +++ b/assets/css/page-widgets/suggested-tasks.css @@ -3,7 +3,7 @@ /** * Suggested tasks widget. * - * Dependencies: progress-planner/web-components/prpl-suggested-task, progress-planner/web-components/prpl-badge + * Dependencies: progress-planner/suggested-task, progress-planner/web-components/prpl-badge */ .prpl-widget-wrapper.prpl-suggested-tasks { @@ -96,16 +96,19 @@ .prpl-dashboard-widget-suggested-tasks { + &:not(:has(.prpl-suggested-tasks-loading)):not(:has(.prpl-suggested-tasks-list li)) { + + .prpl-no-suggested-tasks { + display: block; + } + } + &:has(.prpl-suggested-tasks-list li) { .prpl-widget-title { display: flex; } - .prpl-no-suggested-tasks { - display: none; - } - hr { display: block; } @@ -116,11 +119,16 @@ display: none; } - .prpl-no-suggested-tasks { - display: block; + .prpl-no-suggested-tasks, + .prpl-suggested-tasks-loading { + display: none; background-color: var(--prpl-background-green); padding: calc(var(--prpl-padding) / 2); } + + .prpl-suggested-tasks-loading { + display: block; + } } .prpl-suggested-tasks-list { @@ -132,11 +140,8 @@ border-bottom: none; } - prpl-suggested-task:nth-child(odd) { - - .prpl-suggested-task { - background-color: #f9fafb; - } + .prpl-suggested-task:nth-child(odd) { + background-color: #f9fafb; } /* If task has disabled checkbox it's title should be italic. */ @@ -223,7 +228,6 @@ flex-direction: column; justify-content: space-between; - .progress-label { display: inline-block; } @@ -544,7 +548,6 @@ } } - /* Checkmark */ .prpl-custom-control::after { content: ""; diff --git a/assets/css/page-widgets/todo.css b/assets/css/page-widgets/todo.css index 296baff86..6c72876a9 100644 --- a/assets/css/page-widgets/todo.css +++ b/assets/css/page-widgets/todo.css @@ -1,7 +1,7 @@ /** * TODOs widget. * - * Dependencies: progress-planner/web-components/prpl-suggested-task + * Dependencies: progress-planner/suggested-task */ .prpl-widget-wrapper.prpl-todo { @@ -145,7 +145,6 @@ } } - #create-todo-item { padding: 0 16px; } @@ -192,3 +191,22 @@ display: none; } } + +#todo-list { + + &:has(.prpl-loader) { + position: relative; + + &::before { + content: ""; + display: block; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: 0; + left: 0; + z-index: 10; + } + } +} diff --git a/assets/css/page-widgets/whats-new.css b/assets/css/page-widgets/whats-new.css index 56a9ea72d..46d3f4e1e 100644 --- a/assets/css/page-widgets/whats-new.css +++ b/assets/css/page-widgets/whats-new.css @@ -25,7 +25,6 @@ } } - img { width: 100%; } diff --git a/assets/css/settings-page.css b/assets/css/settings-page.css index 8f4c68a13..638834814 100644 --- a/assets/css/settings-page.css +++ b/assets/css/settings-page.css @@ -48,8 +48,6 @@ padding: var(--prpl-settings-page-gap) var(--prpl-settings-page-gap) 2rem var(--prpl-settings-page-gap); } } - - } .prpl-settings-section-title { @@ -108,7 +106,6 @@ } } - .item-actions, .prpl-select-page { display: flex; @@ -251,7 +248,6 @@ } - /* License */ .prpl-column-license { @@ -298,7 +294,6 @@ } - /* Grid layout for wrapper for: - Valuable post types - Default login destination diff --git a/assets/css/web-components/prpl-suggested-task.css b/assets/css/suggested-task.css similarity index 99% rename from assets/css/web-components/prpl-suggested-task.css rename to assets/css/suggested-task.css index 92d8986de..d9263d3f9 100644 --- a/assets/css/web-components/prpl-suggested-task.css +++ b/assets/css/suggested-task.css @@ -211,8 +211,6 @@ } } - - } &[data-task-action="celebrate"] { diff --git a/assets/css/upgrade-tasks.css b/assets/css/upgrade-tasks.css index c7a3f158b..4e497ed42 100644 --- a/assets/css/upgrade-tasks.css +++ b/assets/css/upgrade-tasks.css @@ -40,7 +40,6 @@ } } - .prpl-onboarding-task-status { display: block; width: 1.5rem; diff --git a/assets/js/celebrate.js b/assets/js/celebrate.js index 7e37d501e..05479ac62 100644 --- a/assets/js/celebrate.js +++ b/assets/js/celebrate.js @@ -4,7 +4,7 @@ * * A script that triggers confetti on the container element. * - * Dependencies: particles-confetti + * Dependencies: particles-confetti, progress-planner/suggested-task */ /* eslint-disable camelcase */ @@ -13,7 +13,7 @@ document.addEventListener( 'prpl/celebrateTasks', ( event ) => { /** * Trigger the confetti on the container element. */ - const containerElement = event.detail?.element + const containerEl = event.detail?.element ? event.detail.element.closest( '.prpl-suggested-tasks-list' ) : document.querySelector( '.prpl-widget-wrapper.prpl-suggested-tasks .prpl-suggested-tasks-list' @@ -30,14 +30,14 @@ document.addEventListener( 'prpl/celebrateTasks', ( event ) => { const prplRenderAttemptshoot = () => { // Get the tasks list position - const origin = containerElement + const origin = containerEl ? { x: - ( containerElement.getBoundingClientRect().left + - containerElement.offsetWidth / 2 ) / + ( containerEl.getBoundingClientRect().left + + containerEl.offsetWidth / 2 ) / window.innerWidth, y: - ( containerElement.getBoundingClientRect().top + 50 ) / + ( containerEl.getBoundingClientRect().top + 50 ) / window.innerHeight, } : { x: 0.5, y: 0.3 }; // fallback if list not found @@ -83,75 +83,23 @@ document.addEventListener( 'prpl/celebrateTasks', ( event ) => { setTimeout( prplRenderAttemptshoot, 0 ); setTimeout( prplRenderAttemptshoot, 100 ); setTimeout( prplRenderAttemptshoot, 200 ); - - /** - * Strike completed tasks. - */ - document.dispatchEvent( new CustomEvent( 'prpl/strikeCelebratedTasks' ) ); - - // Remove celebrated tasks and add them to the completed tasks. - setTimeout( () => { - document.dispatchEvent( - new CustomEvent( 'prpl/markTasksAsCompleted' ) - ); - }, 2000 ); -} ); - -/** - * Mark tasks as completed. - */ -document.addEventListener( 'prpl/markTasksAsCompleted', ( event ) => { - const taskList = event.detail?.taskList || 'prplSuggestedTasks'; - document - .querySelectorAll( '.prpl-suggested-task-celebrated' ) - .forEach( ( item ) => { - const task_id = item.getAttribute( 'data-task-id' ); - const providerID = item.getAttribute( 'data-task-provider-id' ); - const category = item.getAttribute( 'data-task-category' ); - const el = document.querySelector( - `.prpl-suggested-task[data-task-id="${ task_id }"]` - ); - - if ( el ) { - el.parentElement.remove(); - } - - // Get the task index. - let taskIndex = false; - window[ taskList ].tasks.forEach( ( taskItem, index ) => { - if ( taskItem.task_id === task_id ) { - taskIndex = index; - } - } ); - - // Mark the task as completed. - if ( false !== taskIndex ) { - window[ taskList ].tasks[ taskIndex ].status = 'completed'; - } - - // Refresh the list. - document.dispatchEvent( - new CustomEvent( 'prpl/suggestedTask/maybeInjectItem', { - detail: { - task_id, - providerID, - category, - }, - } ) - ); - } ); } ); /** - * Strike completed tasks. + * Remove tasks from the DOM. + * The task will be striked through, before removed, if it has points. */ -document.addEventListener( 'prpl/strikeCelebratedTasks', () => { +document.addEventListener( 'prpl/removeCelebratedTasks', () => { document .querySelectorAll( '.prpl-suggested-task[data-task-action="celebrate"]' ) .forEach( ( item ) => { + // Triggers the strikethrough animation. item.classList.add( 'prpl-suggested-task-celebrated' ); + + // Remove the item from the DOM. + setTimeout( () => item.remove(), 2000 ); } ); } ); @@ -163,9 +111,7 @@ document.addEventListener( 'prpl/celebrateTasks', () => { '#adminmenu #toplevel_page_progress-planner .update-plugins' ); if ( points ) { - points.forEach( ( point ) => { - point.remove(); - } ); + points.forEach( ( point ) => point.remove() ); } } ); diff --git a/assets/js/external-link-accessibility-helper.js b/assets/js/external-link-accessibility-helper.js index 81f3e98f0..91241173c 100644 --- a/assets/js/external-link-accessibility-helper.js +++ b/assets/js/external-link-accessibility-helper.js @@ -118,9 +118,9 @@ prplDocumentReady( () => { } ); // Recheck the accessibility of the page when a new task is injected. -document.addEventListener( 'prpl/suggestedTask/injectItem', () => { +document.addEventListener( 'prpl/suggestedTask/itemInjected', () => { // Wait for the new task to be added to the DOM. setTimeout( () => { externalLinkHelper.applyAccessibility(); - }, 500 ); + }, 10 ); } ); diff --git a/assets/js/suggested-task-terms.js b/assets/js/suggested-task-terms.js new file mode 100644 index 000000000..9aa71e191 --- /dev/null +++ b/assets/js/suggested-task-terms.js @@ -0,0 +1,122 @@ +/* global prplDocumentReady */ +/* + * Populate prplSuggestedTasksTerms with the terms for the taxonomies we use. + * + * Dependencies: wp-api, progress-planner/document-ready + */ + +const prplSuggestedTasksTerms = {}; + +const prplTerms = { + category: 'prpl_recommendations_category', + provider: 'prpl_recommendations_provider', + + /** + * Get the terms for a given taxonomy. + * + * @param {string} taxonomy The taxonomy. + * @return {Object} The terms. + */ + // eslint-disable-next-line no-unused-vars + get: ( taxonomy ) => { + if ( 'category' === taxonomy ) { + taxonomy = prplTerms.category; + } else if ( 'provider' === taxonomy ) { + taxonomy = prplTerms.provider; + } + return prplSuggestedTasksTerms[ taxonomy ] || {}; + }, + + /** + * Get a promise for the terms collection for a given taxonomy. + * + * @param {string} taxonomy The taxonomy. + * @return {Promise} A promise for the terms collection. + */ + getCollectionPromise: ( taxonomy ) => { + return new Promise( ( resolve ) => { + if ( prplSuggestedTasksTerms[ taxonomy ] ) { + console.info( + `Terms already fetched for taxonomy: ${ taxonomy }` + ); + resolve( prplSuggestedTasksTerms[ taxonomy ] ); + } + wp.api.loadPromise.done( () => { + console.info( `Fetching terms for taxonomy: ${ taxonomy }...` ); + + const typeName = taxonomy.replace( 'prpl_', 'Prpl_' ); + prplSuggestedTasksTerms[ taxonomy ] = + prplSuggestedTasksTerms[ taxonomy ] || {}; + const TermsCollection = new wp.api.collections[ typeName ](); + TermsCollection.fetch( { data: { per_page: 100 } } ).done( + ( data ) => { + let userTermFound = false; + // 100 is the maximum number of terms that can be fetched in one request. + data.forEach( ( term ) => { + prplSuggestedTasksTerms[ taxonomy ][ term.slug ] = + term; + if ( 'user' === term.slug ) { + userTermFound = true; + } + } ); + + if ( userTermFound ) { + resolve( prplSuggestedTasksTerms[ taxonomy ] ); + } else { + // If the `user` term doesn't exist, create it. + const newTermModel = new wp.api.models[ typeName ]( + { + slug: 'user', + name: 'user', + } + ); + newTermModel + .save() + .then( ( response ) => { + prplSuggestedTasksTerms[ taxonomy ].user = + response; + return prplSuggestedTasksTerms[ taxonomy ]; + } ) + .then( resolve ); // Resolve the promise after all requests are complete. + } + } + ); + } ); + } ); + }, + + /** + * Get promises for the terms collections for the taxonomies we use. + * + * @return {Promise} A promise for the terms collections. + */ + getCollectionsPromises: () => { + return new Promise( ( resolve ) => { + prplDocumentReady( () => { + Promise.all( [ + prplTerms.getCollectionPromise( prplTerms.category ), + prplTerms.getCollectionPromise( prplTerms.provider ), + ] ).then( () => resolve( prplSuggestedTasksTerms ) ); + } ); + } ); + }, + + /** + * Get a term object from the terms array. + * + * @param {number} termId The term ID. + * @param {string} taxonomy The taxonomy. + * @return {Object} The term object. + */ + getTerm: ( termId, taxonomy ) => { + let termObject = {}; + Object.values( prplSuggestedTasksTerms[ taxonomy ] ).forEach( + ( term ) => { + if ( parseInt( term.id ) === parseInt( termId ) ) { + termObject = term; + } + } + ); + return termObject; + }, +}; diff --git a/assets/js/suggested-task.js b/assets/js/suggested-task.js new file mode 100644 index 000000000..c7d25cdb5 --- /dev/null +++ b/assets/js/suggested-task.js @@ -0,0 +1,646 @@ +/* global HTMLElement, prplSuggestedTask, prplL10n, prplUpdateRaviGauge, prplTerms, prplSuggestedTasksWidget */ +/* + * Suggested Task scripts & helpers. + * + * Dependencies: wp-api, progress-planner/l10n, progress-planner/suggested-task-terms, progress-planner/web-components/prpl-gauge, progress-planner/widgets/suggested-tasks + */ +/* eslint-disable camelcase, jsdoc/require-param-type, jsdoc/require-param, jsdoc/check-param-names */ + +prplSuggestedTask = { + ...prplSuggestedTask, + injectedItemIds: [], + l10n: { + info: prplL10n( 'info' ), + moveUp: prplL10n( 'moveUp' ), + moveDown: prplL10n( 'moveDown' ), + snooze: prplL10n( 'snooze' ), + snoozeThisTask: prplL10n( 'snoozeThisTask' ), + howLong: prplL10n( 'howLong' ), + snoozeDurationOneWeek: prplL10n( 'snoozeDurationOneWeek' ), + snoozeDurationOneMonth: prplL10n( 'snoozeDurationOneMonth' ), + snoozeDurationThreeMonths: prplL10n( 'snoozeDurationThreeMonths' ), + snoozeDurationSixMonths: prplL10n( 'snoozeDurationSixMonths' ), + snoozeDurationOneYear: prplL10n( 'snoozeDurationOneYear' ), + snoozeDurationForever: prplL10n( 'snoozeDurationForever' ), + disabledRRCheckboxTooltip: prplL10n( 'disabledRRCheckboxTooltip' ), + markAsComplete: prplL10n( 'markAsComplete' ), + }, + + /** + * Fetch items for arguments. + * + * @param {Object} args The arguments to pass to the injectItems method. + * @return {Promise} A promise that resolves with the collection of posts. + */ + fetchItems: ( args ) => { + console.info( + `Fetching recommendations with args: ${ JSON.stringify( args ) }...` + ); + + const fetchData = { + status: args.status, + per_page: args.per_page || 1, + _embed: true, + exclude: prplSuggestedTask.injectedItemIds, + filter: { + orderby: 'menu_order', + order: 'ASC', + }, + }; + if ( args.category ) { + fetchData[ prplTerms.category ] = + prplTerms.get( 'category' )[ args.category ].id; + } + + return prplSuggestedTask + .getPostsCollectionPromise( { data: fetchData } ) + .then( ( response ) => response.data ); + }, + + /** + * Inject items from a category. + * + * @param {string} taskCategorySlug The task category slug. + * @param {string[]} taskStatus The task status. + */ + injectItemsFromCategory: ( args ) => + prplSuggestedTask + .fetchItems( { + category: args.category, + status: args.status || [ 'publish' ], + per_page: args.per_page || 1, + } ) + .then( ( data ) => { + if ( data.length ) { + // Inject the items into the DOM. + data.forEach( ( item ) => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item, + listId: 'prpl-suggested-tasks-list', + insertPosition: 'beforeend', + }, + } ) + ); + prplSuggestedTask.injectedItemIds.push( item.id ); + } ); + } + + return data; + } ) + .then( ( data ) => { + // Toggle the "Loading..." text. + prplSuggestedTasksWidget.removeLoadingItems(); + + // Trigger the grid resize event. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + + return data; + } ), + + /** + * Inject items. + * + * @param {Object[]} items The items to inject. + */ + injectItems: ( items ) => { + if ( items.length ) { + // Inject the items into the DOM. + items.forEach( ( item ) => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item, + listId: 'prpl-suggested-tasks-list', + insertPosition: 'beforeend', + }, + } ) + ); + prplSuggestedTask.injectedItemIds.push( item.id ); + } ); + } + + // Trigger the grid resize event. + window.dispatchEvent( new CustomEvent( 'prpl/grid/resize' ) ); + }, + + /** + * Get a collection of posts. + * + * @param {Object} fetchArgs The arguments to pass to the fetch method. + * @return {Promise} A promise that resolves with the collection of posts. + */ + getPostsCollectionPromise: ( fetchArgs ) => { + const collectionsPromise = new Promise( ( resolve ) => { + const postsCollection = + new wp.api.collections.Prpl_recommendations(); + postsCollection + .fetch( fetchArgs ) + .done( ( data ) => resolve( { data, postsCollection } ) ); + } ); + + return collectionsPromise; + }, + + /** + * Render a new item. + * + * @param {Object} post The post object. + * @param {boolean} useCheckbox Whether to use a checkbox. + */ + getNewItemTemplatePromise: ( { + post = {}, + useCheckbox = true, + listId = '', + } ) => + new Promise( ( resolve ) => { + const { + prpl_recommendations_provider, + prpl_recommendations_category, + } = post; + const terms = { + prpl_recommendations_provider, + prpl_recommendations_category, + }; + + Object.values( prplTerms.get( 'provider' ) ).forEach( ( term ) => { + if ( term.id === terms[ prplTerms.provider ][ 0 ] ) { + terms[ prplTerms.provider ] = term; + } + } ); + + Object.values( prplTerms.get( 'category' ) ).forEach( ( term ) => { + if ( term.id === terms[ prplTerms.category ][ 0 ] ) { + terms[ prplTerms.category ] = term; + } + } ); + + const template = wp.template( 'prpl-suggested-task' ); + const data = { + post, + terms, + useCheckbox, + listId, + assets: prplSuggestedTask.assets, + action: 'pending' === post.status ? 'celebrate' : '', + l10n: prplSuggestedTask.l10n, + }; + + resolve( template( data ) ); + } ), + + /** + * Run a task action. + * + * @param {number} postId The post ID. + * @param {string} actionType The action type. + * @return {Promise} A promise that resolves with the response from the server. + */ + runTaskAction: ( postId, actionType ) => + wp.ajax.post( 'progress_planner_suggested_task_action', { + post_id: postId, + nonce: prplSuggestedTask.nonce, + action_type: actionType, + } ), + + /** + * Trash (delete) a task. + * Only user tasks can be trashed. + * + * @param {number} postId The post ID. + */ + trash: ( postId ) => { + const post = new wp.api.models.Prpl_recommendations( { + id: postId, + } ); + post.fetch().then( () => { + // Handle the case when plain URL structure is used, it used to result in invalid URL (404): http://localhost:8080/index.php?rest_route=/wp/v2/prpl_recommendations/35?force=true + const url = post.url().includes( 'rest_route=' ) + ? post.url() + '&force=true' + : post.url() + '?force=true'; + + post.destroy( { url } ).then( () => { + // Remove the task from the todo list. + prplSuggestedTask.removeTaskElement( postId ); + setTimeout( + () => + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ), + 500 + ); + + prplSuggestedTask.runTaskAction( postId, 'delete' ); + } ); + } ); + }, + + /** + * Maybe complete a task. + * + * @param {number} postId The post ID. + */ + maybeComplete: ( postId ) => { + // Get the task. + const post = new wp.api.models.Prpl_recommendations( { id: postId } ); + post.fetch().then( ( postData ) => { + const taskProviderId = prplTerms.getTerm( + postData?.[ prplTerms.provider ], + prplTerms.provider + ).slug; + const taskCategorySlug = prplTerms.getTerm( + postData?.[ prplTerms.category ], + prplTerms.category + ).slug; + + const el = prplSuggestedTask.getTaskElement( postId ); + + // Dismissable tasks don't have pending status, it's either publish or trash. + const newStatus = + 'publish' === postData.status ? 'trash' : 'publish'; + + // Disable the checkbox for RR tasks, to prevent multiple clicks. + el.querySelector( '.prpl-suggested-task-checkbox' ).setAttribute( + 'disabled', + 'disabled' + ); + + post.set( 'status', newStatus ) + .save() + .then( () => { + prplSuggestedTask.runTaskAction( + postId, + 'trash' === newStatus ? 'complete' : 'pending' + ); + const eventPoints = parseInt( postData?.meta?.prpl_points ); + + // Task is trashed, check if we need to celebrate. + if ( 'trash' === newStatus ) { + el.setAttribute( 'data-task-action', 'celebrate' ); + if ( 'user' === taskProviderId ) { + // Set class to trigger strike through animation. + el.classList.add( + 'prpl-suggested-task-celebrated' + ); + + setTimeout( () => { + // Move task from published to trash. + document + .getElementById( 'todo-list-completed' ) + .insertAdjacentElement( 'beforeend', el ); + + // Remove the class to trigger the strike through animation. + el.classList.remove( + 'prpl-suggested-task-celebrated' + ); + + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + + // Remove the disabled attribute for user tasks, so they can be clicked again. + el.querySelector( + '.prpl-suggested-task-checkbox' + ).removeAttribute( 'disabled' ); + }, 2000 ); + } else { + /** + * Strike completed tasks and remove them from the DOM. + */ + document.dispatchEvent( + new CustomEvent( 'prpl/removeCelebratedTasks' ) + ); + + // Inject more tasks from the same category. + prplSuggestedTask.injectItemsFromCategory( { + category: taskCategorySlug, + status: [ 'publish' ], + } ); + } + + // We trigger celebration only if the task has points. + if ( 0 < eventPoints ) { + prplUpdateRaviGauge( eventPoints ); + + // Trigger the celebration event (confetti). + document.dispatchEvent( + new CustomEvent( 'prpl/celebrateTasks', { + detail: { element: el }, + } ) + ); + } + } else if ( + 'publish' === newStatus && + 'user' === taskProviderId + ) { + // This is only possible for user tasks. + // Set the task action to publish. + el.setAttribute( 'data-task-action', 'publish' ); + + // Update the Ravi gauge. + prplUpdateRaviGauge( 0 - eventPoints ); + + // Move task from trash to published. + document + .getElementById( 'todo-list' ) + .insertAdjacentElement( 'beforeend', el ); + + window.dispatchEvent( + new CustomEvent( 'prpl/grid/resize' ) + ); + + // Remove the disabled attribute for user tasks, so they can be clicked again. + el.querySelector( + '.prpl-suggested-task-checkbox' + ).removeAttribute( 'disabled' ); + } + } ); + } ); + }, + + /** + * Snooze a task. + * + * @param {number} postId The post ID. + * @param {string} snoozeDuration The snooze duration. + */ + snooze: ( postId, snoozeDuration ) => { + const snoozeDurationMap = { + '1-week': 7, + '2-weeks': 14, + '1-month': 30, + '3-months': 90, + '6-months': 180, + '1-year': 365, + forever: 3650, + }; + + const snoozeDurationDays = snoozeDurationMap[ snoozeDuration ]; + const date = new Date( + Date.now() + snoozeDurationDays * 24 * 60 * 60 * 1000 + ) + .toISOString() + .split( '.' )[ 0 ]; + const postModelToSave = new wp.api.models.Prpl_recommendations( { + id: postId, + status: 'future', + date, + date_gmt: date, + } ); + postModelToSave.save().then( ( postData ) => { + const taskCategorySlug = prplTerms.getTerm( + postData?.[ prplTerms.category ], + prplTerms.category + ).slug; + + prplSuggestedTask.removeTaskElement( postId ); + + // Inject more tasks from the same category. + prplSuggestedTask.injectItemsFromCategory( { + category: taskCategorySlug, + status: [ 'publish' ], + } ); + } ); + }, + + /** + * Run a tooltip action. + * + * @param {HTMLElement} button The button that was clicked. + */ + runButtonAction: ( button ) => { + let action = button.getAttribute( 'data-action' ); + const target = button.getAttribute( 'data-target' ); + const item = button.closest( 'li.prpl-suggested-task' ); + const tooltipActions = item.querySelector( '.tooltip-actions' ); + const elClass = '.prpl-suggested-task-' + target; + + // If the tooltip was already open, close it. + if ( + !! tooltipActions.querySelector( + `${ elClass }[data-tooltip-visible]` + ) + ) { + action = 'close-' + target; + } else { + const closestTaskListVisible = item + .closest( '.prpl-suggested-tasks-list' ) + .querySelector( `[data-tooltip-visible]` ); + // Close the any opened radio group. + closestTaskListVisible?.classList.remove( + 'prpl-toggle-radio-group-open' + ); + // Remove any existing tooltip visible attribute, in the entire list. + closestTaskListVisible?.removeAttribute( 'data-tooltip-visible' ); + } + + switch ( action ) { + case 'snooze': + tooltipActions + .querySelector( elClass ) + ?.setAttribute( 'data-tooltip-visible', 'true' ); + break; + + case 'close-snooze': + // Close the radio group. + tooltipActions + .querySelector( + `${ elClass }.prpl-toggle-radio-group-open` + ) + ?.classList.remove( 'prpl-toggle-radio-group-open' ); + // Close the tooltip. + tooltipActions + .querySelector( `${ elClass }[data-tooltip-visible]` ) + ?.removeAttribute( 'data-tooltip-visible' ); + break; + + case 'info': + tooltipActions + .querySelector( elClass ) + ?.setAttribute( 'data-tooltip-visible', 'true' ); + break; + + case 'close-info': + tooltipActions + .querySelector( elClass ) + .removeAttribute( 'data-tooltip-visible' ); + break; + + case 'move-up': + case 'move-down': + if ( 'move-up' === action && item.previousElementSibling ) { + item.parentNode.insertBefore( + item, + item.previousElementSibling + ); + } else if ( + 'move-down' === action && + item.nextElementSibling + ) { + item.parentNode.insertBefore( + item.nextElementSibling, + item + ); + } + // Trigger a custom event. + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/move', { + detail: { node: item }, + } ) + ); + break; + } + }, + + /** + * Update the task title. + * + * @param {HTMLElement} el The element that was edited. + */ + updateTaskTitle: ( el ) => { + // Add debounce to the input event. + clearTimeout( this.debounceTimeout ); + this.debounceTimeout = setTimeout( () => { + // Update an existing post. + const title = el.textContent.replace( /\n/g, '' ); + const postModel = new wp.api.models.Prpl_recommendations( { + id: parseInt( el.getAttribute( 'data-post-id' ) ), + title, + } ); + postModel.save().then( () => + // Update the task title. + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/update', { + detail: { + node: el.closest( 'li.prpl-suggested-task' ), + }, + } ) + ) + ); + el + .closest( 'li.prpl-suggested-task' ) + .querySelector( + 'label:has(.prpl-suggested-task-checkbox) .screen-reader-text' + ).innerHTML = `${ title }: ${ prplL10n( 'markAsComplete' ) }`; + }, 300 ); + }, + + /** + * Get the task element. + * + * @param {number} postId The post ID. + * @return {HTMLElement} The task element. + */ + getTaskElement: ( postId ) => + document.querySelector( + `.prpl-suggested-task[data-post-id="${ postId }"]` + ), + + /** + * Remove the task element. + * + * @param {number} postId The post ID. + */ + removeTaskElement: ( postId ) => + prplSuggestedTask.getTaskElement( postId )?.remove(), +}; + +/** + * Inject an item. + */ +document.addEventListener( 'prpl/suggestedTask/injectItem', ( event ) => { + prplSuggestedTask + .getNewItemTemplatePromise( { + post: event.detail.item, + listId: event.detail.listId, + } ) + .then( ( itemHTML ) => { + /** + * @todo Implement the parent task functionality. + * Use this code: `const parent = event.detail.item.parent && '' !== event.detail.item.parent ? event.detail.item.parent : null; + */ + const parent = false; + + if ( ! parent ) { + // Inject the item into the list. + document + .getElementById( event.detail.listId ) + .insertAdjacentHTML( + event.detail.insertPosition, + itemHTML + ); + + return; + } + + // If we could not find the parent item, try again after 500ms. + window.prplRenderAttempts = window.prplRenderAttempts || 0; + if ( window.prplRenderAttempts > 20 ) { + return; + } + const parentItem = document.querySelector( + `.prpl-suggested-task[data-task-id="${ parent }"]` + ); + if ( ! parentItem ) { + setTimeout( () => { + document.dispatchEvent( + new CustomEvent( 'prpl/suggestedTask/injectItem', { + detail: { + item: event.detail.item, + listId: event.detail.listId, + insertPosition: event.detail.insertPosition, + }, + } ) + ); + window.prplRenderAttempts++; + }, 100 ); + return; + } + + // If the child list does not exist, create it. + if ( + ! parentItem.querySelector( '.prpl-suggested-task-children' ) + ) { + const childListElement = document.createElement( 'ul' ); + childListElement.classList.add( + 'prpl-suggested-task-children' + ); + parentItem.appendChild( childListElement ); + } + + // Inject the item into the child list. + parentItem + .querySelector( '.prpl-suggested-task-children' ) + .insertAdjacentHTML( 'beforeend', itemHTML ); + } ); +} ); + +// When the 'prpl/suggestedTask/move' event is triggered, +// update the menu_order of the todo items. +document.addEventListener( 'prpl/suggestedTask/move', ( event ) => { + const listUl = event.detail.node.closest( 'ul' ); + const todoItemsIDs = []; + // Get all the todo items. + const todoItems = listUl.querySelectorAll( '.prpl-suggested-task' ); + let menuOrder = 0; + todoItems.forEach( ( todoItem ) => { + const itemID = parseInt( todoItem.getAttribute( 'data-post-id' ) ); + todoItemsIDs.push( itemID ); + todoItem.setAttribute( 'data-task-order', menuOrder ); + + listUl + .querySelector( `.prpl-suggested-task[data-post-id="${ itemID }"]` ) + .setAttribute( 'data-task-order', menuOrder ); + + // Update an existing post. + const post = new wp.api.models.Prpl_recommendations( { + id: itemID, + menu_order: menuOrder, + } ); + post.save(); + menuOrder++; + } ); +} ); + +/* eslint-enable camelcase, jsdoc/require-param-type, jsdoc/require-param, jsdoc/check-param-names */ diff --git a/assets/js/tour.js b/assets/js/tour.js index 6878a3500..754f87b91 100644 --- a/assets/js/tour.js +++ b/assets/js/tour.js @@ -115,10 +115,8 @@ function prplStartTour() { // Start the tour if the URL contains the query parameter. if ( window.location.href.includes( 'content-scan-finished=true' ) ) { - // If there are pending celebration tasks, delay the tour until celebration is done. + // If there are pending tasks, delay the tour until celebration is done. const delay = window.location.href.includes( 'delay-tour=true' ) ? 5000 : 0; - setTimeout( () => { - prplStartTour(); - }, delay ); + setTimeout( prplStartTour, delay ); } diff --git a/assets/js/upgrade-tasks.js b/assets/js/upgrade-tasks.js index de56557d2..340dbb107 100644 --- a/assets/js/upgrade-tasks.js +++ b/assets/js/upgrade-tasks.js @@ -12,8 +12,8 @@ * * @return {Promise} The promise of the tasks. */ -async function prplOnboardTasks() { - return new Promise( ( resolve ) => { +const prplOnboardTasks = async () => + new Promise( ( resolve ) => { ( async () => { const tasksElement = document.getElementById( 'prpl-onboarding-tasks' @@ -79,7 +79,6 @@ async function prplOnboardTasks() { resolve(); } )(); } ); -} /** * Redirect user to the stats page after onboarding or plugin upgrade. diff --git a/assets/js/web-components/prpl-badge-progress-bar.js b/assets/js/web-components/prpl-badge-progress-bar.js new file mode 100644 index 000000000..132fa4f8d --- /dev/null +++ b/assets/js/web-components/prpl-badge-progress-bar.js @@ -0,0 +1,164 @@ +/* global customElements, HTMLElement */ +/* + * Badge Progress Bar + * + * A web component to display a badge progress bar. + * + * Dependencies: progress-planner/l10n, progress-planner/web-components/prpl-badge + */ + +/** + * Register the custom web component. + */ +customElements.define( + 'prpl-badge-progress-bar', + class extends HTMLElement { + constructor( badgeId, points, maxPoints ) { + // Get parent class properties + super(); + badgeId = badgeId || this.getAttribute( 'data-badge-id' ); + points = points || this.getAttribute( 'data-points' ); + maxPoints = maxPoints || this.getAttribute( 'data-max-points' ); + const progress = ( points / maxPoints ) * 100; + + this.innerHTML = ` +
+ `; + } + } +); + +/** + * Update the previous month badge progress bar. + * + * @param {number} pointsDiff The points difference. + * + * @return {void} + */ +// eslint-disable-next-line no-unused-vars +const prplUpdatePreviousMonthBadgeProgressBar = ( pointsDiff ) => { + const progressBars = document.querySelectorAll( + '.prpl-previous-month-badge-progress-bar-wrapper prpl-badge-progress-bar' + ); + + // Bail early if no badge progress bars are found. + if ( ! progressBars.length ) { + return; + } + + // Get the 1st incomplete badge progress bar. + const progressBar = + parseInt( progressBars[ 0 ]?.getAttribute( 'data-points' ) ) >= + parseInt( progressBars[ 0 ]?.getAttribute( 'data-max-points' ) ) + ? progressBars[ 1 ] + : progressBars[ 0 ]; + + // Bail early if no badge progress bar is found. + if ( ! progressBar ) { + return; + } + + // Get the badge progress bar properties. + const badgeId = progressBar.getAttribute( 'data-badge-id' ); + const badgePoints = progressBar.getAttribute( 'data-points' ); + const badgeMaxPoints = progressBar.getAttribute( 'data-max-points' ); + const badgeProgress = customElements.get( 'prpl-badge-progress-bar' ); + const badgeNewPoints = parseInt( badgePoints ) + pointsDiff; + + // Create a new badge progress bar. + const newProgressBar = new badgeProgress( + badgeId, + badgeNewPoints, + badgeMaxPoints + ); + newProgressBar.setAttribute( 'data-badge-id', badgeId ); + newProgressBar.setAttribute( 'data-points', badgeNewPoints ); + newProgressBar.setAttribute( 'data-max-points', badgeMaxPoints ); + + // Replace the old badge progress bar with the new one. + progressBar.replaceWith( newProgressBar ); + + // Update the remaining points. + const remainingPointsEl = document.querySelector( + `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"] .prpl-previous-month-badge-progress-bar-remaining` + ); + + if ( remainingPointsEl ) { + remainingPointsEl.textContent = remainingPointsEl.textContent.replace( + remainingPointsEl.getAttribute( 'data-remaining' ), + badgeMaxPoints - badgeNewPoints + ); + remainingPointsEl.setAttribute( + 'data-remaining', + badgeMaxPoints - badgeNewPoints + ); + } + + // Update the previous month badge points number. + const badgePointsNumberEl = document.querySelector( + `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"] .prpl-widget-previous-ravi-points-number` + ); + if ( badgePointsNumberEl ) { + badgePointsNumberEl.textContent = badgeNewPoints + 'pt'; + } + + // If the previous month badge is completed, update badge elements. + if ( badgeNewPoints >= parseInt( badgeMaxPoints ) ) { + document + .querySelectorAll( + `.prpl-badge-row-wrapper-inner .prpl-badge prpl-badge[complete="false"][badge-id="${ badgeId }"]` + ) + ?.forEach( ( badge ) => { + badge.setAttribute( 'complete', 'true' ); + } ); + + // Remove the previous month badge progress bar. + document + .querySelector( + `.prpl-previous-month-badge-progress-bar-wrapper[data-badge-id="${ badgeId }"]` + ) + ?.remove(); + + // If there are no more progress bars, remove the previous month badge progress bar wrapper. + if ( + ! document.querySelector( + '.prpl-previous-month-badge-progress-bar-wrapper' + ) + ) { + document + .querySelector( + '.prpl-previous-month-badge-progress-bars-wrapper' + ) + ?.remove(); + } + } +}; diff --git a/assets/js/web-components/prpl-gauge.js b/assets/js/web-components/prpl-gauge.js index 1f4d84461..e740967f8 100644 --- a/assets/js/web-components/prpl-gauge.js +++ b/assets/js/web-components/prpl-gauge.js @@ -1,10 +1,10 @@ -/* global customElements, HTMLElement */ +/* global customElements, HTMLElement, prplUpdatePreviousMonthBadgeProgressBar */ /* * Web Component: prpl-gauge * * A web component that displays a gauge. * - * Dependencies: progress-planner/web-components/prpl-badge + * Dependencies: progress-planner/web-components/prpl-badge, progress-planner/web-components/prpl-badge-progress-bar */ /** @@ -97,3 +97,93 @@ customElements.define( } } ); + +/** + * Update the Ravi gauge. + * + * @param {number} pointsDiff The points difference. + * + * @return {void} + */ +// eslint-disable-next-line no-unused-vars +const prplUpdateRaviGauge = ( pointsDiff ) => { + if ( ! pointsDiff ) { + return; + } + + const gaugeElement = document.getElementById( 'prpl-gauge-ravi' ); + if ( ! gaugeElement ) { + return; + } + + const gaugeProps = { + id: gaugeElement.id, + background: gaugeElement.getAttribute( 'background' ), + color: gaugeElement.getAttribute( 'color' ), + max: gaugeElement.getAttribute( 'data-max' ), + value: gaugeElement.getAttribute( 'data-value' ), + badgeId: gaugeElement.getAttribute( 'data-badge-id' ), + }; + + if ( ! gaugeProps ) { + return; + } + + let newValue = parseInt( gaugeProps.value ) + pointsDiff; + newValue = Math.round( newValue ); + newValue = Math.max( 0, newValue ); + newValue = Math.min( newValue, parseInt( gaugeProps.max ) ); + + const Gauge = customElements.get( 'prpl-gauge' ); + const gauge = new Gauge( + { + max: parseInt( gaugeProps.max ), + value: parseFloat( newValue / parseInt( gaugeProps.max ) ), + background: gaugeProps.background, + color: gaugeProps.color, + maxDeg: '180deg', + start: '270deg', + cutout: '57%', + contentFontSize: 'var(--prpl-font-size-6xl)', + contentPadding: + 'var(--prpl-padding) var(--prpl-padding) calc(var(--prpl-padding) * 2) var(--prpl-padding)', + marginBottom: 'var(--prpl-padding)', + }, + `' . sprintf( + return '
' . \sprintf( /* translators: %1$s Review link, %2$s: The post title, %3$s: The number of months. */ \esc_html__( '%1$s the post "%2$s" as it was last updated more than %3$s months ago.', 'progress-planner' ), '' . \esc_html__( 'Review', 'progress-planner' ) . '', \esc_html( $post->post_title ), // @phpstan-ignore-line property.nonObject \esc_html( $months ) - ) . '
' . ( $this->capability_required() ? '' . \esc_html__( 'Edit the post', 'progress-planner' ) . '.
' : '' ); // @phpstan-ignore-line property.nonObject + ) . ''; } /** * Get the task URL. * - * @param string $task_id The task ID. + * @param array $task_data The task data. * * @return string */ - public function get_url( $task_id = '' ) { - $post = $this->get_post_from_task_id( $task_id ); + protected function get_url_with_data( $task_data = [] ) { + if ( ! isset( $task_data['target_post_id'] ) ) { + return ''; + } + + $post = \get_post( $task_data['target_post_id'] ); - return $post && $this->capability_required() - ? \esc_url( (string) \get_edit_post_link( $post->ID ) ) - : ''; + if ( ! $post ) { + return ''; + } + + // We don't use the edit_post_link() function because we need to bypass it's current_user_can() check. + return \esc_url( + \add_query_arg( + [ + 'post' => $post->ID, + 'action' => 'edit', + ], + \admin_url( 'post.php' ) + ) + ); } /** @@ -177,108 +209,106 @@ public function get_url( $task_id = '' ) { * @return bool */ public function should_add_task() { - if ( null === $this->task_post_mappings ) { - $this->task_post_mappings = []; + if ( null !== $this->task_post_mappings ) { + return 0 < \count( $this->task_post_mappings ); + } - $number_of_posts_to_inject = static::ITEMS_TO_INJECT; - $last_updated_posts = []; + $this->task_post_mappings = []; - // Check if there are any important pages to update. - $important_page_ids = []; - foreach ( \progress_planner()->get_admin__page_settings()->get_settings() as $important_page ) { - if ( 0 !== (int) $important_page['value'] ) { - $important_page_ids[] = (int) $important_page['value']; - } - } + $number_of_posts_to_inject = static::ITEMS_TO_INJECT; + $last_updated_posts = []; - // Add the privacy policy page ID if it exists. Not 'publish' page will not be fetched by get_posts(). - $privacy_policy_page_id = \get_option( 'wp_page_for_privacy_policy' ); - if ( $privacy_policy_page_id ) { - $important_page_ids[] = (int) $privacy_policy_page_id; + // Check if there are any important pages to update. + $important_page_ids = []; + foreach ( \progress_planner()->get_admin__page_settings()->get_settings() as $important_page ) { + if ( 0 !== (int) $important_page['value'] ) { + $important_page_ids[] = (int) $important_page['value']; } + } - /** - * Filters the pages we deem more important for content updates. - * - * @param int[] $important_page_ids Post & page IDs of the important pages. - */ - $important_page_ids = \apply_filters( 'progress_planner_update_posts_important_page_ids', $important_page_ids ); - - if ( ! empty( $important_page_ids ) ) { - $last_updated_posts = $this->get_old_posts( - [ - 'post__in' => $important_page_ids, - 'post_type' => 'any', - 'date_query' => [ - [ - 'column' => 'post_modified', - 'before' => '-6 months', // Important pages are updated more often. - ], - ], - ] - ); - } + // Add the privacy policy page ID if it exists. Not 'publish' page will not be fetched by get_posts(). + $privacy_policy_page_id = \get_option( 'wp_page_for_privacy_policy' ); + if ( $privacy_policy_page_id ) { + $important_page_ids[] = (int) $privacy_policy_page_id; + } - // Lets check for other posts to update. - $number_of_posts_to_inject = $number_of_posts_to_inject - count( $last_updated_posts ); + /** + * Filters the pages we deem more important for content updates. + * + * @param int[] $important_page_ids Post & page IDs of the important pages. + */ + $important_page_ids = \apply_filters( 'progress_planner_update_posts_important_page_ids', $important_page_ids ); - if ( 0 < $number_of_posts_to_inject ) { - // Get the post that was updated last. - $last_updated_posts = array_merge( - $last_updated_posts, - $this->get_old_posts( + if ( ! empty( $important_page_ids ) ) { + $last_updated_posts = $this->get_old_posts( + [ + 'post__in' => $important_page_ids, + 'post_type' => 'any', + 'date_query' => [ [ - 'post__not_in' => $important_page_ids, // This can be an empty array. - 'post_type' => $this->include_post_types, - ] - ) - ); - } + 'column' => 'post_modified', + 'before' => '-6 months', // Important pages are updated more often. + ], + ], + ] + ); + } - if ( ! $last_updated_posts ) { - return false; - } + // Lets check for other posts to update. + $number_of_posts_to_inject = $number_of_posts_to_inject - \count( $last_updated_posts ); - foreach ( $last_updated_posts as $post ) { - $task_data = [ - 'post_id' => $post->ID, - 'provider_id' => $this->get_provider_id(), - ]; + if ( 0 < $number_of_posts_to_inject ) { + // Get the post that was updated last. + $last_updated_posts = \array_merge( + $last_updated_posts, + $this->get_old_posts( + [ + 'post__not_in' => $important_page_ids, // This can be an empty array. + 'post_type' => $this->include_post_types, + ] + ) + ); + } - // Skip if the task has been dismissed. - if ( $this->is_task_dismissed( $task_data ) ) { - continue; - } + if ( ! $last_updated_posts ) { + return false; + } - $task_id = $this->get_task_id( [ 'post_id' => $post->ID ] ); + foreach ( $last_updated_posts as $post ) { + // Skip if the task has been dismissed. + if ( $this->is_task_dismissed( + [ + 'target_post_id' => $post->ID, + 'provider_id' => $this->get_provider_id(), + ] + ) ) { + continue; + } - // Don't add the task if it was completed. - if ( true === \progress_planner()->get_suggested_tasks()->was_task_completed( $task_id ) ) { - continue; - } + $task_id = $this->get_task_id( [ 'target_post_id' => $post->ID ] ); - $this->task_post_mappings[ $task_id ] = [ - 'task_id' => $task_id, - 'post_id' => $post->ID, - 'post_type' => $post->post_type, - ]; + // Don't add the task if it was completed. + if ( true === \progress_planner()->get_suggested_tasks()->was_task_completed( $task_id ) ) { + continue; } + + $this->task_post_mappings[ $task_id ] = [ + 'task_id' => $task_id, + 'target_post_id' => $post->ID, + 'target_post_type' => $post->post_type, + ]; } - return 0 < count( $this->task_post_mappings ); + return 0 < \count( $this->task_post_mappings ); } - /** * Get an array of tasks to inject. * * @return array */ public function get_tasks_to_inject() { - - if ( - ! $this->should_add_task() // No need to add the task. - ) { + if ( ! $this->should_add_task() ) { return []; } @@ -290,47 +320,35 @@ public function get_tasks_to_inject() { } $task_to_inject[] = [ - 'task_id' => $this->get_task_id( [ 'post_id' => $task_data['post_id'] ] ), - 'provider_id' => $this->get_provider_id(), - 'category' => $this->get_provider_category(), - 'post_id' => $task_data['post_id'], - 'post_type' => $task_data['post_type'], - 'date' => \gmdate( 'YW' ), + 'task_id' => $this->get_task_id( [ 'target_post_id' => $task_data['target_post_id'] ] ), + 'provider_id' => $this->get_provider_id(), + 'category' => $this->get_provider_category(), + 'target_post_id' => $task_data['target_post_id'], + 'target_post_type' => $task_data['target_post_type'], + 'date' => \gmdate( 'YW' ), + 'post_title' => $this->get_title_with_data( $task_data ), + 'description' => $this->get_description_with_data( $task_data ), + 'url' => $this->get_url_with_data( $task_data ), + 'url_target' => $this->get_url_target(), + 'dismissable' => $this->is_dismissable(), + 'snoozable' => $this->is_snoozable, + 'points' => $this->get_points(), ]; } } - return $task_to_inject; - } + $added_tasks = []; - /** - * Get the task details. - * - * @param string $task_id The task ID. - * - * @return array - */ - public function get_task_details( $task_id = '' ) { + foreach ( $task_to_inject as $task_data ) { + // Skip the task if it was already injected. + if ( \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] ) ) { + continue; + } - if ( ! $task_id ) { - return []; + $added_tasks[] = \progress_planner()->get_suggested_tasks_db()->add( $task_data ); } - $task_details = [ - 'task_id' => $task_id, - 'provider_id' => $this->get_provider_id(), - 'title' => $this->get_title( $task_id ), - 'parent' => $this->get_parent(), - 'priority' => $this->get_priority(), - 'category' => $this->get_provider_category(), - 'points' => $this->get_points(), - 'dismissable' => $this->is_dismissable(), - 'url' => $this->get_url( $task_id ), - 'url_target' => $this->get_url_target(), - 'description' => $this->get_description( $task_id ), - ]; - - return $task_details; + return $added_tasks; } /** @@ -341,15 +359,15 @@ public function get_task_details( $task_id = '' ) { * @return \WP_Post|null */ public function get_post_from_task_id( $task_id ) { - $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id ); + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] ); if ( empty( $tasks ) ) { return null; } - $data = $tasks[0]; - - return isset( $data['post_id'] ) && $data['post_id'] ? \get_post( $data['post_id'] ) : null; + return isset( $tasks[0]->target_post_id ) && $tasks[0]->target_post_id + ? \get_post( $tasks[0]->target_post_id ) + : null; } /** @@ -374,7 +392,7 @@ public function get_old_posts( $args = [] ) { $posts = []; // Parse default args. - $args = wp_parse_args( + $args = \wp_parse_args( $args, [ 'posts_per_page' => static::ITEMS_TO_INJECT, @@ -396,7 +414,7 @@ public function get_old_posts( $args = [] ) { * * @param array $args The get_posts args. */ - $args = apply_filters( 'progress_planner_update_posts_tasks_args', $args ); + $args = \apply_filters( 'progress_planner_update_posts_tasks_args', $args ); // Get the post that was updated last. $posts = \get_posts( $args ); @@ -416,7 +434,7 @@ public function filter_update_posts_args( $args ) { ? $args['post__not_in'] : []; - $args['post__not_in'] = array_merge( + $args['post__not_in'] = \array_merge( $args['post__not_in'], // Add the snoozed post IDs to the post__not_in array. $this->get_snoozed_post_ids(), @@ -425,10 +443,10 @@ public function filter_update_posts_args( $args ) { $dismissed_post_ids = $this->get_dismissed_post_ids(); if ( ! empty( $dismissed_post_ids ) ) { - $args['post__not_in'] = array_merge( $args['post__not_in'], $dismissed_post_ids ); + $args['post__not_in'] = \array_merge( $args['post__not_in'], $dismissed_post_ids ); } - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { // Handle the case when the meta key doesn't exist. $args['meta_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'OR', @@ -455,18 +473,22 @@ public function filter_update_posts_args( $args ) { * @return array */ protected function get_snoozed_post_ids() { - if ( null !== $this->snoozed_post_ids ) { return $this->snoozed_post_ids; } $this->snoozed_post_ids = []; - $snoozed = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'snoozed' ); + $snoozed = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'future' ] ); if ( ! empty( $snoozed ) ) { foreach ( $snoozed as $task ) { - if ( isset( $task['provider_id'] ) && 'review-post' === $task['provider_id'] ) { - $this->snoozed_post_ids[] = $task['post_id']; + /** + * The task object. + * + * @var \Progress_Planner\Suggested_Tasks\Task $task + */ + if ( isset( $task->provider->slug ) && 'review-post' === $task->provider->slug ) { + $this->snoozed_post_ids[] = $task->target_post_id; } } } @@ -480,7 +502,6 @@ protected function get_snoozed_post_ids() { * @return array */ protected function get_dismissed_post_ids() { - if ( null !== $this->dismissed_post_ids ) { return $this->dismissed_post_ids; } @@ -489,7 +510,7 @@ protected function get_dismissed_post_ids() { $dismissed = $this->get_dismissed_tasks(); if ( ! empty( $dismissed ) ) { - $this->dismissed_post_ids = array_values( wp_list_pluck( $dismissed, 'post_id' ) ); + $this->dismissed_post_ids = \array_values( \wp_list_pluck( $dismissed, 'post_id' ) ); } return $this->dismissed_post_ids; @@ -504,7 +525,7 @@ protected function get_dismissed_post_ids() { * @return string|false The task identifier or false if not applicable. */ protected function get_task_identifier( $task_data ) { - return $this->get_provider_id() . '-' . $task_data['post_id']; + return $this->get_provider_id() . '-' . $task_data['target_post_id']; } /** @@ -531,15 +552,16 @@ protected function get_saved_page_types() { * @return bool */ protected function is_specific_task_completed( $task_id ) { + $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id ); - $task = Task_Factory::create_task_from( 'id', $task_id ); - $data = $task->get_data(); - - if ( isset( $data['post_id'] ) && (int) \get_post_modified_time( 'U', false, (int) $data['post_id'] ) > strtotime( '-12 months' ) ) { - return true; + if ( ! $task ) { + return false; } - return false; + $data = $task->get_data(); + + return $data && isset( $data['target_post_id'] ) + && (int) \get_post_modified_time( 'U', false, (int) $data['target_post_id'] ) > \strtotime( '-12 months' ); } /** @@ -549,18 +571,19 @@ protected function is_specific_task_completed( $task_id ) { * @return int[] */ public function add_yoast_cornerstone_pages( $important_page_ids ) { - if ( function_exists( 'YoastSEO' ) ) { - $cornerstone_page_ids = \get_posts( - [ - 'post_type' => 'any', - 'meta_key' => '_yoast_wpseo_is_cornerstone', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key - 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value - 'fields' => 'ids', - ] - ); - if ( ! empty( $cornerstone_page_ids ) ) { - $important_page_ids = array_merge( $important_page_ids, $cornerstone_page_ids ); - } + if ( ! \function_exists( 'YoastSEO' ) ) { + return $important_page_ids; + } + $cornerstone_page_ids = \get_posts( + [ + 'post_type' => 'any', + 'meta_key' => '_yoast_wpseo_is_cornerstone', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + 'fields' => 'ids', + ] + ); + if ( ! empty( $cornerstone_page_ids ) ) { + $important_page_ids = \array_merge( $important_page_ids, $cornerstone_page_ids ); } return $important_page_ids; } @@ -584,7 +607,7 @@ protected function get_expiration_period( $dismissal_data ) { } // Check if it his cornerstone content. - if ( function_exists( 'YoastSEO' ) ) { + if ( \function_exists( 'YoastSEO' ) ) { $is_cornerstone = \get_post_meta( $dismissal_data['post_id'], '_yoast_wpseo_is_cornerstone', true ); if ( '1' === $is_cornerstone ) { return 6 * MONTH_IN_SECONDS; diff --git a/classes/suggested-tasks/providers/class-core-update.php b/classes/suggested-tasks/providers/class-core-update.php index 8bf12ad5b..475646ffc 100644 --- a/classes/suggested-tasks/providers/class-core-update.php +++ b/classes/suggested-tasks/providers/class-core-update.php @@ -43,18 +43,17 @@ class Core_Update extends Tasks { /** * The task priority. * - * @var string + * @var int */ - protected $priority = 'high'; - + protected $priority = 0; /** - * Constructor. + * Get the task URL. * - * @return void + * @return string */ - public function __construct() { - $this->url = \admin_url( 'update-core.php' ); + protected function get_url() { + return \admin_url( 'update-core.php' ); } /** @@ -73,7 +72,7 @@ public function init() { * * @return string */ - public function get_title() { + protected function get_title() { return \esc_html__( 'Perform all updates', 'progress-planner' ); } @@ -82,8 +81,8 @@ public function get_title() { * * @return string */ - public function get_description() { - return sprintf( + protected function get_description() { + return \sprintf( /* translators: %s:See why we recommend this link */ \esc_html__( 'Regular updates improve security and performance. %s.', 'progress-planner' ), '' . \esc_html__( 'See why we recommend this', 'progress-planner' ) . '' @@ -98,14 +97,14 @@ public function get_description() { * @return array */ public function add_core_update_link( $update_actions ) { - $pending_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'pending' ); + $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'publish' ] ); - // All updates are completed and there is a 'update-core' task in the pending tasks. + // All updates are completed and there is a 'update-core' task in the published tasks. if ( $pending_tasks && $this->is_task_completed() ) { foreach ( $pending_tasks as $task ) { - if ( $this->get_task_id() === $task['task_id'] ) { + if ( $this->get_task_id() === $task->task_id ) { $update_actions['prpl_core_update'] = - 'WP_DEBUG_DISPLAY
',
@@ -55,6 +56,6 @@ public function get_description() {
* @return bool
*/
public function should_add_task() {
- return defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY;
+ return \defined( 'WP_DEBUG' ) && WP_DEBUG && \defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY;
}
}
diff --git a/classes/suggested-tasks/providers/class-disable-comments.php b/classes/suggested-tasks/providers/class-disable-comments.php
index eedfd7476..715f8d90e 100644
--- a/classes/suggested-tasks/providers/class-disable-comments.php
+++ b/classes/suggested-tasks/providers/class-disable-comments.php
@@ -27,32 +27,42 @@ class Disable_Comments extends Tasks {
protected const PROVIDER_ID = 'disable-comments';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
+ */
+ protected function get_url() {
+ return \admin_url( 'options-discussion.php' );
+ }
+
+ /**
+ * Get the link setting.
+ *
+ * @return array
*/
- public function __construct() {
- $this->url = \admin_url( 'options-discussion.php' );
- $this->link_setting = [
+ public function get_link_setting() {
+ return [
'hook' => 'options-discussion.php',
'iconEl' => 'label[for="default_comment_status"]',
];
}
/**
- * Get the title.
+ * Get the task title.
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Disable comments', 'progress-planner' );
}
/**
- * Get the title.
+ * Get the task description.
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
\esc_html(
// translators: %d is the number of approved comments, %s is the disabling them link.
\_n(
diff --git a/classes/suggested-tasks/providers/class-fewer-tags.php b/classes/suggested-tasks/providers/class-fewer-tags.php
index fe479ffbc..0bdf17cf2 100644
--- a/classes/suggested-tasks/providers/class-fewer-tags.php
+++ b/classes/suggested-tasks/providers/class-fewer-tags.php
@@ -40,9 +40,9 @@ class Fewer_Tags extends Tasks {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'high';
+ protected $priority = 10;
/**
* The plugin active state.
@@ -72,6 +72,13 @@ class Fewer_Tags extends Tasks {
*/
private $plugin_path = 'fewer-tags/fewer-tags.php';
+ /**
+ * Whether the task is dismissable.
+ *
+ * @var bool
+ */
+ protected $is_dismissable = true;
+
/**
* Constructor.
*/
@@ -79,9 +86,15 @@ public function __construct() {
// Data collectors.
$this->post_tag_count_data_collector = new Post_Tag_Count();
$this->published_post_count_data_collector = new Published_Post_Count();
+ }
- $this->url = \admin_url( '/plugin-install.php?tab=search&s=fewer+tags' );
- $this->is_dismissable = true;
+ /**
+ * Get the task URL.
+ *
+ * @return string
+ */
+ protected function get_url() {
+ return \admin_url( '/plugin-install.php?tab=search&s=fewer+tags' );
}
/**
@@ -89,7 +102,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Install Fewer Tags and clean up your tags', 'progress-planner' );
}
@@ -98,8 +111,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
// translators: %1$s is the number of tags, %2$s is the number of published posts, %3$s Read more link.
\esc_html__( 'We detected that you have %1$s tags and %2$s published posts. Consider installing the "Fewer Tags" plugin. %3$s', 'progress-planner' ),
$this->post_tag_count_data_collector->collect(),
@@ -116,11 +129,7 @@ public function get_description() {
*/
public function should_add_task() {
// If the plugin is active, we don't need to add the task.
- if ( $this->is_plugin_active() ) {
- return false;
- }
-
- return $this->is_task_relevant();
+ return $this->is_plugin_active() ? false : $this->is_task_relevant();
}
/**
@@ -149,14 +158,13 @@ public function is_task_completed( $task_id = '' ) {
* @return bool
*/
protected function is_plugin_active() {
-
if ( null === $this->is_plugin_active ) {
- if ( ! function_exists( 'get_plugins' ) ) {
+ if ( ! \function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php'; // @phpstan-ignore requireOnce.fileNotFound
}
- $plugins = get_plugins();
- $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && is_plugin_active( $this->plugin_path );
+ $plugins = \get_plugins();
+ $this->is_plugin_active = isset( $plugins[ $this->plugin_path ] ) && \is_plugin_active( $this->plugin_path );
}
return $this->is_plugin_active;
diff --git a/classes/suggested-tasks/providers/class-hello-world.php b/classes/suggested-tasks/providers/class-hello-world.php
index 772cb7d67..8f2a46180 100644
--- a/classes/suggested-tasks/providers/class-hello-world.php
+++ b/classes/suggested-tasks/providers/class-hello-world.php
@@ -36,23 +36,35 @@ class Hello_World extends Tasks {
protected const CAPABILITY = 'edit_posts';
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Hello_World
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Hello_World_Data_Collector::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Hello_World_Data_Collector();
+ protected function get_url() {
+ $hello_world_post_id = $this->get_data_collector()->collect();
- $hello_world_post_id = $this->data_collector->collect();
-
- if ( 0 !== $hello_world_post_id ) {
- $this->url = (string) \get_edit_post_link( $hello_world_post_id );
+ if ( 0 === $hello_world_post_id ) {
+ return '';
}
+ // We don't use the edit_post_link() function because we need to bypass it's current_user_can() check.
+ $this->url = \esc_url(
+ \add_query_arg(
+ [
+ 'post' => $hello_world_post_id,
+ 'action' => 'edit',
+ ],
+ \admin_url( 'post.php' )
+ )
+ );
+
+ return $this->url;
}
/**
@@ -60,7 +72,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Delete the "Hello World!" post.', 'progress-planner' );
}
@@ -69,8 +81,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s:Hello World! link */
\esc_html__( 'On install, WordPress creates a %s post. This post is not needed and should be deleted.', 'progress-planner' ),
'' . \esc_html__( '"Hello World!"', 'progress-planner' ) . ''
@@ -83,6 +95,6 @@ public function get_description() {
* @return bool
*/
public function should_add_task() {
- return 0 !== $this->data_collector->collect();
+ return 0 !== $this->get_data_collector()->collect();
}
}
diff --git a/classes/suggested-tasks/providers/class-interactive.php b/classes/suggested-tasks/providers/class-interactive.php
index 81a8de173..2885486ef 100644
--- a/classes/suggested-tasks/providers/class-interactive.php
+++ b/classes/suggested-tasks/providers/class-interactive.php
@@ -25,7 +25,7 @@ abstract class Interactive extends Tasks {
* @return void
*/
public function __construct() {
- add_action( 'progress_planner_admin_page_after_widgets', [ $this, 'add_popover' ] );
+ \add_action( 'progress_planner_admin_page_after_widgets', [ $this, 'add_popover' ] );
}
/**
diff --git a/classes/suggested-tasks/providers/class-permalink-structure.php b/classes/suggested-tasks/providers/class-permalink-structure.php
index 6749b0ba6..f0993387d 100644
--- a/classes/suggested-tasks/providers/class-permalink-structure.php
+++ b/classes/suggested-tasks/providers/class-permalink-structure.php
@@ -27,11 +27,20 @@ class Permalink_Structure extends Tasks {
protected const PROVIDER_ID = 'core-permalink-structure';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'options-permalink.php' );
+ protected function get_url() {
+ return \admin_url( 'options-permalink.php' );
+ }
+ /**
+ * Get the link setting.
+ *
+ * @return array
+ */
+ public function get_link_setting() {
$icon_el = 'label[for="permalink-input-month-name"], label[for="permalink-input-post-name"]';
// If the task is completed, we want to add icon element only to the selected option (not both).
@@ -47,7 +56,7 @@ public function __construct() {
}
}
- $this->link_setting = [
+ return [
'hook' => 'options-permalink.php',
'iconEl' => $icon_el,
];
@@ -58,7 +67,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Set permalink structure', 'progress-planner' );
}
@@ -67,8 +76,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %1$s We recommend link */
\esc_html__( 'On install, WordPress sets the permalink structure to a format that is not SEO-friendly. %1$s changing it.', 'progress-planner' ),
'' . \esc_html__( 'We recommend', 'progress-planner' ) . '',
diff --git a/classes/suggested-tasks/providers/class-php-version.php b/classes/suggested-tasks/providers/class-php-version.php
index 8428d2a4a..8a97dc06f 100644
--- a/classes/suggested-tasks/providers/class-php-version.php
+++ b/classes/suggested-tasks/providers/class-php-version.php
@@ -26,26 +26,35 @@ class Php_Version extends Tasks {
*/
protected const PROVIDER_ID = 'php-version';
+ /**
+ * The minimum PHP version.
+ *
+ * @var string
+ */
+ protected const RECOMMENDED_PHP_VERSION = '8.2';
+
/**
* Get the title.
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Update PHP version', 'progress-planner' );
}
/**
* Get the description.
*
+ * @param array $task_data Optional data to include in the task.
* @return string
*/
- public function get_description() {
- return sprintf(
- /* translators: %1$s: php version, %2$s: We recommend link */
- \esc_html__( 'Your site is running on PHP version %1$s. %2$s updating to PHP version 8.0 or higher.', 'progress-planner' ),
- phpversion(),
+ protected function get_description( $task_data = [] ) {
+ return \sprintf(
+ /* translators: %1$s: php version, %2$s: We recommend link. %3$s: minimum PHP version recommended. */
+ \esc_html__( 'Your site is running on PHP version %1$s. %2$s updating to PHP version %3$s or higher.', 'progress-planner' ),
+ \phpversion(),
'' . \esc_html__( 'We recommend', 'progress-planner' ) . '',
+ \esc_html( self::RECOMMENDED_PHP_VERSION )
);
}
@@ -55,6 +64,6 @@ public function get_description() {
* @return bool
*/
public function should_add_task() {
- return version_compare( phpversion(), '8.0', '<' );
+ return \version_compare( \phpversion(), self::RECOMMENDED_PHP_VERSION, '<' );
}
}
diff --git a/classes/suggested-tasks/providers/class-remove-inactive-plugins.php b/classes/suggested-tasks/providers/class-remove-inactive-plugins.php
index 24ac53504..5145b0de1 100644
--- a/classes/suggested-tasks/providers/class-remove-inactive-plugins.php
+++ b/classes/suggested-tasks/providers/class-remove-inactive-plugins.php
@@ -29,18 +29,19 @@ class Remove_Inactive_Plugins extends Tasks {
protected const IS_ONBOARDING_TASK = false;
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Inactive_Plugins
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Inactive_Plugins_Data_Collector::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Inactive_Plugins_Data_Collector();
- $this->url = \admin_url( 'plugins.php' );
+ protected function get_url() {
+ return \admin_url( 'plugins.php' );
}
/**
@@ -48,7 +49,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Remove inactive plugins', 'progress-planner' );
}
@@ -57,8 +58,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %1$s removing any plugins link */
\esc_html__( 'You have inactive plugins. Consider %1$s that are not activated to free up resources, and improve security.', 'progress-planner' ),
'' . \esc_html__( 'removing any plugins', 'progress-planner' ) . '',
@@ -71,6 +72,6 @@ public function get_description() {
* @return bool
*/
public function should_add_task() {
- return $this->data_collector->collect() > 0;
+ return $this->get_data_collector()->collect() > 0;
}
}
diff --git a/classes/suggested-tasks/providers/class-remove-terms-without-posts.php b/classes/suggested-tasks/providers/class-remove-terms-without-posts.php
index 5e4ab6e0f..65e7c42fc 100644
--- a/classes/suggested-tasks/providers/class-remove-terms-without-posts.php
+++ b/classes/suggested-tasks/providers/class-remove-terms-without-posts.php
@@ -51,18 +51,18 @@ class Remove_Terms_Without_Posts extends Tasks {
protected $is_dismissable = true;
/**
- * The task priority.
+ * The task URL target.
*
* @var string
*/
- protected $priority = 'medium';
+ protected $url_target = '_blank';
/**
- * The data collector.
+ * The task priority.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Terms_Without_Posts
+ * @var int
*/
- protected $data_collector;
+ protected $priority = 60;
/**
* The minimum number of posts.
@@ -71,6 +71,13 @@ class Remove_Terms_Without_Posts extends Tasks {
*/
protected const MIN_POSTS = 1;
+ /**
+ * The data collector class name.
+ *
+ * @var string
+ */
+ protected const DATA_COLLECTOR_CLASS = Terms_Without_Posts_Data_Collector::class;
+
/**
* The completed term IDs.
*
@@ -82,8 +89,6 @@ class Remove_Terms_Without_Posts extends Tasks {
* Constructor.
*/
public function __construct() {
- $this->data_collector = new Terms_Without_Posts_Data_Collector();
-
\add_filter( 'progress_planner_terms_without_posts_exclude_term_ids', [ $this, 'exclude_completed_terms' ] );
}
@@ -107,107 +112,77 @@ public function init() {
* @return void
*/
public function maybe_remove_irrelevant_tasks( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
- $pending_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() );
+ $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] );
if ( ! $pending_tasks ) {
return;
}
foreach ( $pending_tasks as $task ) {
- if ( isset( $task['term_id'] ) && isset( $task['taxonomy'] ) ) {
- $term = \get_term( $task['term_id'], $task['taxonomy'] );
+ /**
+ * The task post object.
+ *
+ * @var \Progress_Planner\Suggested_Tasks\Task $task
+ */
+ if ( $task->target_term_id && $task->target_taxonomy ) {
+ $term = \get_term( $task->target_term_id, $task->target_taxonomy );
if ( \is_wp_error( $term ) || ! $term || $term->count > self::MIN_POSTS ) {
- \progress_planner()->get_suggested_tasks()->delete_task( $task['task_id'] );
+ \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID );
}
}
}
}
- /**
- * Get the task ID.
- *
- * @param array $data Optional data to include in the task ID.
- * @return string
- */
- public function get_task_id( $data = [] ) {
- $parts = [ $this->get_provider_id() ];
-
- // Add optional data parts if provided.
- if ( ! empty( $data ) ) {
- foreach ( $data as $value ) {
- $parts[] = $value;
- }
- }
-
- return implode( '-', $parts );
- }
-
/**
* Get the title.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_title( $task_id = '' ) {
- if ( ! $task_id ) {
- return '';
- }
-
- // Get the task data.
- $task_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id );
-
- // We don't want to link if the term was deleted.
- if ( empty( $task_data ) || ! $task_data[0] ) {
- return '';
- }
-
- return \sprintf(
- /* translators: %s: The term name */
- \esc_html__( 'Remove term named "%s"', 'progress-planner' ),
- \esc_html( $task_data[0]['term_name'] )
- );
+ protected function get_title_with_data( $task_data = [] ) {
+ $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] );
+ return ( $term && ! \is_wp_error( $term ) )
+ ? \sprintf(
+ /* translators: %s: The term name */
+ \esc_html__( 'Remove term named "%s"', 'progress-planner' ),
+ \esc_html( $term->name )
+ )
+ : '';
}
/**
* Get the description.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_description( $task_id = '' ) {
- $term = $this->get_term_from_task_id( $task_id );
-
- if ( ! $term ) {
- return '';
- }
-
- return sprintf(
- /* translators: %1$s: The term name, %2$s Read more link */
- \esc_html__( 'The "%1$s" term has one or less posts associated with it, we recommend removing it. %2$s', 'progress-planner' ),
- $term->name,
- '' . \esc_html__( 'Read more', 'progress-planner' ) . ''
- );
+ protected function get_description_with_data( $task_data = [] ) {
+ $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] );
+ return ( $term && ! \is_wp_error( $term ) )
+ ? \sprintf(
+ /* translators: %1$s: The term name, %2$s Read more link */
+ \esc_html__( 'The "%1$s" term has one or less posts associated with it, we recommend removing it. %2$s', 'progress-planner' ),
+ $term->name,
+ '' . \esc_html__( 'Read more', 'progress-planner' ) . ''
+ )
+ : '';
}
/**
* Get the URL.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_url( $task_id = '' ) {
- $term = $this->get_term_from_task_id( $task_id );
-
- // We don't want to link if the term was deleted.
- if ( ! $term ) {
- return '';
- }
-
- return \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id );
+ protected function get_url_with_data( $task_data = [] ) {
+ $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] );
+ return ( $term && ! \is_wp_error( $term ) )
+ ? \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id )
+ : '';
}
/**
@@ -216,7 +191,7 @@ public function get_url( $task_id = '' ) {
* @return bool
*/
public function should_add_task() {
- return ! empty( $this->data_collector->collect() );
+ return ! empty( $this->get_data_collector()->collect() );
}
/**
@@ -228,13 +203,24 @@ public function should_add_task() {
*/
protected function is_specific_task_completed( $task_id ) {
$term = $this->get_term_from_task_id( $task_id );
+ return $term ? self::MIN_POSTS < $term->count : true;
+ }
- // Terms was deleted.
- if ( ! $term ) {
- return true;
- }
-
- return self::MIN_POSTS < $term->count;
+ /**
+ * Transform data collector data into task data format.
+ *
+ * @param array $data The data from data collector.
+ * @return array The transformed data with original data merged.
+ */
+ protected function transform_collector_data( array $data ): array {
+ return \array_merge(
+ $data,
+ [
+ 'target_term_id' => $data['term_id'],
+ 'target_taxonomy' => $data['taxonomy'],
+ 'target_term_name' => $data['name'],
+ ]
+ );
}
/**
@@ -243,7 +229,6 @@ protected function is_specific_task_completed( $task_id ) {
* @return array
*/
public function get_tasks_to_inject() {
-
if (
true === $this->is_task_snoozed() ||
! $this->should_add_task() // No need to add the task.
@@ -251,7 +236,7 @@ public function get_tasks_to_inject() {
return [];
}
- $data = $this->data_collector->collect();
+ $data = $this->get_data_collector()->collect();
$task_id = $this->get_task_id(
[
'term_id' => $data['term_id'],
@@ -263,47 +248,36 @@ public function get_tasks_to_inject() {
return [];
}
- return [
- [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'category' => $this->get_provider_category(),
- 'term_id' => $data['term_id'],
- 'taxonomy' => $data['taxonomy'],
- 'term_name' => $data['name'],
- 'date' => \gmdate( 'YW' ),
- ],
- ];
+ // Transform the data to match the task data structure.
+ $task_data = $this->modify_injection_task_data(
+ $this->get_task_details(
+ $this->transform_collector_data( $data )
+ )
+ );
+
+ // Get the task post.
+ $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] );
+
+ // Skip the task if it was already injected.
+ return $task_post ? [] : [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ];
}
/**
- * Get the task details.
+ * Modify task data before injecting it.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return array
*/
- public function get_task_details( $task_id = '' ) {
+ protected function modify_injection_task_data( $task_data ) {
+ // Transform the data to match the task data structure.
+ $data = $this->transform_collector_data( $this->get_data_collector()->collect() );
- if ( ! $task_id ) {
- return [];
- }
+ $task_data['target_term_id'] = $data['target_term_id'];
+ $task_data['target_taxonomy'] = $data['target_taxonomy'];
+ $task_data['target_term_name'] = $data['target_term_name'];
- $task_details = [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'title' => $this->get_title( $task_id ),
- 'parent' => $this->get_parent(),
- 'priority' => $this->get_priority(),
- 'category' => $this->get_provider_category(),
- 'points' => $this->get_points(),
- 'dismissable' => $this->is_dismissable(),
- 'url' => $this->get_url( $task_id ),
- 'url_target' => $this->get_url_target(),
- 'description' => $this->get_description( $task_id ),
- ];
-
- return $task_details;
+ return $task_data;
}
/**
@@ -314,25 +288,20 @@ public function get_task_details( $task_id = '' ) {
* @return \WP_Term|null
*/
public function get_term_from_task_id( $task_id ) {
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id );
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] );
if ( empty( $tasks ) ) {
return null;
}
- $data = $tasks[0];
+ $task = $tasks[0];
- if ( ! isset( $data['term_id'] ) || ! $data['term_id'] || ! isset( $data['taxonomy'] ) || ! $data['taxonomy'] ) {
+ if ( ! $task->target_term_id || ! $task->target_taxonomy ) {
return null;
}
- $term = \get_term( $data['term_id'], $data['taxonomy'] );
-
- if ( is_wp_error( $term ) ) {
- return null;
- }
-
- return $term;
+ $term = \get_term( $task->target_term_id, $task->target_taxonomy );
+ return $term && ! \is_wp_error( $term ) ? $term : null;
}
/**
@@ -341,18 +310,17 @@ public function get_term_from_task_id( $task_id ) {
* @return array
*/
protected function get_completed_term_ids() {
-
if ( null !== $this->completed_term_ids ) {
return $this->completed_term_ids;
}
$this->completed_term_ids = [];
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() );
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] );
if ( ! empty( $tasks ) ) {
foreach ( $tasks as $task ) {
- if ( isset( $task['status'] ) && 'completed' === $task['status'] ) {
- $this->completed_term_ids[] = $task['term_id'];
+ if ( 'trash' === $task->post_status ) {
+ $this->completed_term_ids[] = $task->target_term_id;
}
}
}
@@ -367,8 +335,6 @@ protected function get_completed_term_ids() {
* @return array
*/
public function exclude_completed_terms( $exclude_term_ids ) {
- $exclude_term_ids = array_merge( $exclude_term_ids, $this->get_completed_term_ids() );
-
- return $exclude_term_ids;
+ return \array_merge( $exclude_term_ids, $this->get_completed_term_ids() );
}
}
diff --git a/classes/suggested-tasks/providers/class-rename-uncategorized-category.php b/classes/suggested-tasks/providers/class-rename-uncategorized-category.php
index 44c61b796..d8516b1db 100644
--- a/classes/suggested-tasks/providers/class-rename-uncategorized-category.php
+++ b/classes/suggested-tasks/providers/class-rename-uncategorized-category.php
@@ -36,18 +36,19 @@ class Rename_Uncategorized_Category extends Tasks {
protected const CAPABILITY = 'manage_categories';
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Uncategorized_Category
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Uncategorized_Category_Data_Collector::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Uncategorized_Category_Data_Collector();
- $this->url = \admin_url( 'edit-tags.php?taxonomy=category&post_type=post' );
+ protected function get_url() {
+ return \admin_url( 'edit-tags.php?taxonomy=category&post_type=post' );
}
/**
@@ -55,7 +56,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Rename Uncategorized category', 'progress-planner' );
}
@@ -64,8 +65,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %1$s We recommend link */
\esc_html__( 'The Uncategorized category is used for posts that don\'t have a category. %1$s renaming it to something that fits your site better.', 'progress-planner' ),
'' . \esc_html__( 'We recommend', 'progress-planner' ) . '',
@@ -78,7 +79,7 @@ public function get_description() {
* @return bool
*/
public function should_add_task() {
- return 0 !== $this->data_collector->collect();
+ return 0 !== $this->get_data_collector()->collect();
}
/**
@@ -87,6 +88,6 @@ public function should_add_task() {
* @return void
*/
public function update_uncategorized_category_cache() {
- $this->data_collector->update_uncategorized_category_cache();
+ $this->get_data_collector()->update_uncategorized_category_cache(); // @phpstan-ignore-line method.notFound
}
}
diff --git a/classes/suggested-tasks/providers/class-sample-page.php b/classes/suggested-tasks/providers/class-sample-page.php
index c1382f5c7..5991ccd5d 100644
--- a/classes/suggested-tasks/providers/class-sample-page.php
+++ b/classes/suggested-tasks/providers/class-sample-page.php
@@ -36,23 +36,34 @@ class Sample_Page extends Tasks {
protected const CAPABILITY = 'edit_pages';
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Sample_Page
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Sample_Page_Data_Collector::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Sample_Page_Data_Collector();
-
- $sample_page_id = $this->data_collector->collect();
+ protected function get_url() {
+ $sample_page_id = $this->get_data_collector()->collect();
if ( 0 !== $sample_page_id ) {
- $this->url = (string) \get_edit_post_link( $sample_page_id );
+ // We don't use the edit_post_link() function because we need to bypass it's current_user_can() check.
+ $this->url = \esc_url(
+ \add_query_arg(
+ [
+ 'post' => $sample_page_id,
+ 'action' => 'edit',
+ ],
+ \admin_url( 'post.php' )
+ )
+ );
}
+
+ return $this->url;
}
/**
@@ -60,7 +71,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Delete "Sample Page"', 'progress-planner' );
}
@@ -69,8 +80,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s:Sample Page link */
\esc_html__( 'On install, WordPress creates a %s page. This page is not needed and should be deleted.', 'progress-planner' ),
'' . \esc_html__( '"Sample Page"', 'progress-planner' ) . ''
@@ -83,6 +94,6 @@ public function get_description() {
* @return bool
*/
public function should_add_task() {
- return 0 !== $this->data_collector->collect();
+ return 0 !== $this->get_data_collector()->collect();
}
}
diff --git a/classes/suggested-tasks/providers/class-search-engine-visibility.php b/classes/suggested-tasks/providers/class-search-engine-visibility.php
index 12e2dafe0..d94ff4bde 100644
--- a/classes/suggested-tasks/providers/class-search-engine-visibility.php
+++ b/classes/suggested-tasks/providers/class-search-engine-visibility.php
@@ -27,32 +27,42 @@ class Search_Engine_Visibility extends Tasks {
protected const PROVIDER_ID = 'search-engine-visibility';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
+ */
+ protected function get_url() {
+ return \admin_url( 'options-reading.php' );
+ }
+
+ /**
+ * Get the link setting.
+ *
+ * @return array
*/
- public function __construct() {
- $this->url = \admin_url( 'options-reading.php' );
- $this->link_setting = [
+ public function get_link_setting() {
+ return [
'hook' => 'options-reading.php',
'iconEl' => 'label[for="blog_public"]',
];
}
/**
- * Get the title.
+ * Get the task title.
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Allow your site to be indexed by search engines', 'progress-planner' );
}
/**
- * Get the description.
+ * Get the task description.
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %1$s allowing search engines link */
\esc_html__( 'Your site is not currently visible to search engines. Consider %1$s to index your site.', 'progress-planner' ),
'' . \esc_html__( 'allowing search engines', 'progress-planner' ) . '',
diff --git a/classes/suggested-tasks/providers/class-set-valuable-post-types.php b/classes/suggested-tasks/providers/class-set-valuable-post-types.php
index 1c96aac43..35d0db935 100644
--- a/classes/suggested-tasks/providers/class-set-valuable-post-types.php
+++ b/classes/suggested-tasks/providers/class-set-valuable-post-types.php
@@ -29,15 +29,17 @@ class Set_Valuable_Post_Types extends Tasks {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'low';
+ protected $priority = 70;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'admin.php?page=progress-planner-settings' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=progress-planner-settings' );
}
/**
@@ -65,7 +67,7 @@ public function remove_upgrade_option() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Set valuable content types', 'progress-planner' );
}
@@ -74,8 +76,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s:Read more link */
\esc_html__( 'Tell us which post types matter most for your site. Go to your settings and select your valuable content types. %s', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -84,21 +86,21 @@ public function get_description() {
/**
* Check if the task should be added.
- * We add tasks only to users who have have completed "Fill the settings page" task and have upgraded from v1.2 or have 'include_post_types' option empty.
- * Reason being that this option was migrated, but it could be missed, and post type selection should be revisited.
+ * We add tasks only to users who have have completed "Fill the settings page" task
+ * and have upgraded from v1.2 or have 'include_post_types' option empty.
+ * Reason being that this option was migrated,
+ * but it could be missed, and post type selection should be revisited.
*
* @return bool
*/
public function should_add_task() {
-
- // Check the "Settings saved" task, if the has not been added as 'pending' don't add the task.
- $settings_saved_task = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', 'settings-saved' );
- if ( empty( $settings_saved_task ) ) {
+ $saved_posts = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => 'settings-saved' ] );
+ if ( empty( $saved_posts ) ) {
return false;
}
- // Save settings task completed?
- $save_settings_task_completed = 'completed' === $settings_saved_task[0]['status'];
+ // Is the task trashed?
+ $post_trashed = 'trash' === $saved_posts[0]->post_status;
// Upgraded from <= 1.2?
$upgraded = (bool) \get_option( 'progress_planner_set_valuable_post_types', false );
@@ -107,7 +109,7 @@ public function should_add_task() {
$include_post_types = \progress_planner()->get_settings()->get( 'include_post_types', [] );
// Add the task only to users who have completed the "Settings saved" task and have upgraded from v1.2 or have 'include_post_types' option empty.
- return $save_settings_task_completed && ( true === $upgraded || empty( $include_post_types ) );
+ return $post_trashed && ( true === $upgraded || empty( $include_post_types ) );
}
/**
diff --git a/classes/suggested-tasks/providers/class-settings-saved.php b/classes/suggested-tasks/providers/class-settings-saved.php
index 72357f69f..3112a49c4 100644
--- a/classes/suggested-tasks/providers/class-settings-saved.php
+++ b/classes/suggested-tasks/providers/class-settings-saved.php
@@ -22,9 +22,9 @@ class Settings_Saved extends Tasks {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'high';
+ protected $priority = 1;
/**
* Whether the task is an onboarding task.
@@ -34,10 +34,12 @@ class Settings_Saved extends Tasks {
protected const IS_ONBOARDING_TASK = false;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'admin.php?page=progress-planner-settings' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=progress-planner-settings' );
}
/**
@@ -45,7 +47,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Fill settings page', 'progress-planner' );
}
@@ -54,7 +56,7 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
+ protected function get_description() {
return \esc_html__( 'Head over to the settings page and fill in the required information.', 'progress-planner' );
}
diff --git a/classes/suggested-tasks/providers/class-site-icon.php b/classes/suggested-tasks/providers/class-site-icon.php
index def8df8ae..619ac9371 100644
--- a/classes/suggested-tasks/providers/class-site-icon.php
+++ b/classes/suggested-tasks/providers/class-site-icon.php
@@ -27,32 +27,42 @@ class Site_Icon extends Tasks {
protected const PROVIDER_ID = 'core-siteicon';
/**
- * Constructor.
+ * Get the link setting.
+ *
+ * @return array
*/
- public function __construct() {
- $this->url = \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() );
- $this->link_setting = [
+ public function get_link_setting() {
+ return [
'hook' => 'options-general.php',
'iconEl' => '.site-icon-section th',
];
}
/**
- * Get the title.
+ * Get the task URL.
+ *
+ * @return string
+ */
+ protected function get_url() {
+ return \admin_url( 'options-general.php?pp-focus-el=' . $this->get_task_id() );
+ }
+
+ /**
+ * Get the task title.
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Set site icon', 'progress-planner' );
}
/**
- * Get the description.
+ * Get the task description.
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s:site icon link */
\esc_html__( 'Set the %s to make your website look more professional.', 'progress-planner' ),
'' . \esc_html__( 'site icon', 'progress-planner' ) . ''
diff --git a/classes/suggested-tasks/providers/class-tasks.php b/classes/suggested-tasks/providers/class-tasks.php
index 673d64754..e54a2bde0 100644
--- a/classes/suggested-tasks/providers/class-tasks.php
+++ b/classes/suggested-tasks/providers/class-tasks.php
@@ -8,7 +8,6 @@
namespace Progress_Planner\Suggested_Tasks\Providers;
use Progress_Planner\Suggested_Tasks\Tasks_Interface;
-use Progress_Planner\Suggested_Tasks\Task_Factory;
/**
* Add tasks for content updates.
@@ -43,6 +42,13 @@ abstract class Tasks implements Tasks_Interface {
*/
protected const IS_ONBOARDING_TASK = false;
+ /**
+ * The data collector class name.
+ *
+ * @var string
+ */
+ protected const DATA_COLLECTOR_CLASS = \Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector::class;
+
/**
* Whether the task is repetitive.
*
@@ -67,9 +73,9 @@ abstract class Tasks implements Tasks_Interface {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'medium';
+ protected $priority = 50;
/**
* Whether the task is dismissable.
@@ -78,6 +84,13 @@ abstract class Tasks implements Tasks_Interface {
*/
protected $is_dismissable = false;
+ /**
+ * Whether the task is snoozable.
+ *
+ * @var bool
+ */
+ protected $is_snoozable = true;
+
/**
* The task URL.
*
@@ -99,6 +112,13 @@ abstract class Tasks implements Tasks_Interface {
*/
protected $link_setting;
+ /**
+ * The data collector.
+ *
+ * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector|null
+ */
+ protected $data_collector = null;
+
/**
* Initialize the task provider.
*
@@ -112,7 +132,7 @@ public function init() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return '';
}
@@ -121,7 +141,7 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
+ protected function get_description() {
return '';
}
@@ -146,10 +166,10 @@ public function get_parent() {
/**
* Get the task priority.
*
- * @return string
+ * @return int
*/
public function get_priority() {
- return $this->priority;
+ return (int) $this->priority;
}
/**
@@ -161,17 +181,22 @@ public function is_dismissable() {
return $this->is_dismissable;
}
+ /**
+ * Get whether the task is snoozable.
+ *
+ * @return bool
+ */
+ public function is_snoozable() {
+ return $this->is_snoozable;
+ }
+
/**
* Get the task URL.
*
* @return string
*/
- public function get_url() {
- if ( $this->url ) {
- return \esc_url( $this->url );
- }
-
- return '';
+ protected function get_url() {
+ return $this->url ? \esc_url( $this->url ) : '';
}
/**
@@ -179,7 +204,7 @@ public function get_url() {
*
* @return string
*/
- public function get_url_target() {
+ protected function get_url_target() {
return $this->url_target ? $this->url_target : '_self';
}
@@ -198,7 +223,7 @@ public function get_link_setting() {
* @return string
*/
public function get_provider_type() {
- _deprecated_function( 'Progress_Planner\Suggested_Tasks\Providers\Tasks::get_provider_type()', '1.1.1', 'get_provider_category' );
+ \_deprecated_function( 'Progress_Planner\Suggested_Tasks\Providers\Tasks::get_provider_type()', '1.1.1', 'get_provider_category' );
return $this->get_provider_category();
}
@@ -223,25 +248,75 @@ public function get_provider_id() {
/**
* Get the task ID.
*
- * @param array $data Optional data to include in the task ID.
+ * @param array $task_data Optional data to include in the task ID.
* @return string
*/
- public function get_task_id( $data = [] ) {
- if ( ! $this->is_repetitive() ) {
- return $this->get_provider_id();
+ public function get_task_id( $task_data = [] ) {
+ $parts = [ $this->get_provider_id() ];
+
+ // Order is important here, new parameters should be added at the end.
+ if ( isset( $task_data['target_post_id'] ) ) {
+ $parts[] = $task_data['target_post_id'];
}
- $parts = [ $this->get_provider_id() ];
+ if ( isset( $task_data['target_term_id'] ) ) {
+ $parts[] = $task_data['target_term_id'];
+ }
+
+ if ( isset( $task_data['target_taxonomy'] ) ) {
+ $parts[] = $task_data['target_taxonomy'];
+ }
- // Add optional data parts if provided.
- if ( ! empty( $data['post_id'] ) ) {
- $parts[] = $data['post_id'];
+ // If the task is repetitive, add the date as the last part.
+ if ( $this->is_repetitive() ) {
+ $parts[] = \gmdate( 'YW' );
}
- // Always add the date as the last part.
- $parts[] = \gmdate( 'YW' );
+ return \implode( '-', $parts );
+ }
+
+ /**
+ * Get the data collector.
+ *
+ * @return \Progress_Planner\Suggested_Tasks\Data_Collector\Base_Data_Collector
+ */
+ public function get_data_collector() {
+ if ( ! $this->data_collector ) {
+ $class_name = static::DATA_COLLECTOR_CLASS;
+ $this->data_collector = new $class_name(); // @phpstan-ignore-line assign.propertyType
+ }
- return implode( '-', $parts );
+ return $this->data_collector; // @phpstan-ignore-line return.type
+ }
+
+ /**
+ * Get the title with data.
+ *
+ * @param array $task_data Optional data to include in the task.
+ * @return string
+ */
+ protected function get_title_with_data( $task_data = [] ) {
+ return $this->get_title();
+ }
+
+ /**
+ * Get the description with data.
+ *
+ * @param array $task_data Optional data to include in the task.
+ * @return string
+ */
+ protected function get_description_with_data( $task_data = [] ) {
+ return $this->get_description();
+ }
+
+ /**
+ * Get the URL with data.
+ *
+ * @param array $task_data Optional data to include in the task.
+ * @return string
+ */
+ protected function get_url_with_data( $task_data = [] ) {
+ return $this->get_url();
}
/**
@@ -273,36 +348,20 @@ public function is_onboarding_task() {
return static::IS_ONBOARDING_TASK;
}
- /**
- * Get the data from a task-ID.
- *
- * @param string $task_id The task ID (unused here).
- *
- * @return array The data.
- */
- public function get_data_from_task_id( $task_id ) {
- $data = [
- 'provider_id' => $this->get_provider_id(),
- 'id' => $task_id,
- ];
-
- return $data;
- }
-
/**
* Check if a task category is snoozed.
*
* @return bool
*/
public function is_task_snoozed() {
- $snoozed = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'status', 'snoozed' );
+ $snoozed = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => 'future' ] );
if ( empty( $snoozed ) ) {
return false;
}
foreach ( $snoozed as $task ) {
- $task_object = Task_Factory::create_task_from( 'id', $task['task_id'] );
- $provider_id = $task_object->get_provider_id();
+ $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task->task_id );
+ $provider_id = $task ? $task->get_provider_id() : '';
if ( $provider_id === $this->get_provider_id() ) {
return true;
@@ -328,7 +387,7 @@ public function is_task_relevant() {
*
* @param string $task_id The task ID.
*
- * @return false|\Progress_Planner\Suggested_Tasks\Task The task data or false if the task is not completed.
+ * @return \Progress_Planner\Suggested_Tasks\Task|false The task data or false if the task is not completed.
*/
public function evaluate_task( $task_id ) {
// Early bail if the user does not have the capability to manage options.
@@ -336,22 +395,32 @@ public function evaluate_task( $task_id ) {
return false;
}
+ $task = \progress_planner()->get_suggested_tasks_db()->get_post( $task_id );
+
+ if ( ! $task ) {
+ return false;
+ }
+
if ( ! $this->is_repetitive() ) {
- if ( 0 !== strpos( $task_id, $this->get_task_id() ) ) {
+ // Collaborator tasks have custom task_ids, so strpos check does not work for them.
+ if ( ! $task->task_id || ( 0 !== \strpos( $task->task_id, $this->get_task_id() ) && 'collaborator' !== $this->get_provider_id() ) ) {
return false;
}
- return $this->is_task_completed( $task_id ) ? Task_Factory::create_task_from( 'id', $task_id ) : false;
+ return $this->is_task_completed( $task->task_id ) ? $task : false;
}
- $task_object = Task_Factory::create_task_from( 'id', $task_id );
- $task_data = $task_object->get_data();
-
- if ( $task_data['provider_id'] === $this->get_provider_id() && \gmdate( 'YW' ) === $task_data['date'] && $this->is_task_completed( $task_id ) ) {
+ if (
+ $task->provider &&
+ $task->provider->slug === $this->get_provider_id() &&
+ \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date ) &&
+ \gmdate( 'YW' ) === \gmdate( 'YW', \DateTime::createFromFormat( 'Y-m-d H:i:s', $task->post_date )->getTimestamp() ) && // @phpstan-ignore-line
+ $this->is_task_completed( $task->task_id )
+ ) {
// Allow adding more data, for example in case of 'create-post' tasks we are adding the post_id.
- $task_data = $this->modify_evaluated_task_data( $task_data );
- $task_object->set_data( $task_data );
+ $task_data = $this->modify_evaluated_task_data( $task->get_data() );
+ $task->update( $task_data );
- return $task_object;
+ return $task;
}
return false;
@@ -359,9 +428,8 @@ public function evaluate_task( $task_id ) {
/**
* Check if the task condition is satisfied.
- * (bool) true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed.
*
- * @return bool
+ * @return bool true means that the task condition is satisfied, meaning that we don't need to add the task or task was completed.
*/
abstract protected function should_add_task();
@@ -404,7 +472,6 @@ protected function check_task_condition() {
* @return array
*/
public function get_tasks_to_inject() {
-
$task_id = $this->get_task_id();
if (
@@ -415,18 +482,13 @@ public function get_tasks_to_inject() {
return [];
}
- $task_data = [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'category' => $this->get_provider_category(),
- 'date' => \gmdate( 'YW' ),
- ];
+ $task_data = $this->modify_injection_task_data( $this->get_task_details() );
- $task_data = $this->modify_injection_task_data( $task_data );
+ // Get the task post.
+ $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] );
- return [
- $task_data,
- ];
+ // Skip the task if it was already injected.
+ return $task_post ? [] : [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ];
}
/**
@@ -456,24 +518,26 @@ protected function modify_evaluated_task_data( $task_data ) {
/**
* Get the task details.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return array
*/
- public function get_task_details( $task_id = '' ) {
-
+ public function get_task_details( $task_data = [] ) {
return [
- 'task_id' => $this->get_task_id(),
+ 'task_id' => $this->get_task_id( $task_data ),
'provider_id' => $this->get_provider_id(),
- 'title' => $this->get_title(),
+ 'post_title' => $this->get_title_with_data( $task_data ),
+ 'description' => $this->get_description_with_data( $task_data ),
'parent' => $this->get_parent(),
'priority' => $this->get_priority(),
'category' => $this->get_provider_category(),
'points' => $this->get_points(),
- 'url' => $this->capability_required() ? \esc_url( $this->get_url() ) : '',
- 'description' => $this->get_description(),
+ 'date' => \gmdate( 'YW' ),
+ 'url' => $this->get_url_with_data( $task_data ),
+ 'url_target' => $this->get_url_target(),
'link_setting' => $this->get_link_setting(),
'dismissable' => $this->is_dismissable(),
+ 'snoozable' => $this->is_snoozable(),
];
}
}
diff --git a/classes/suggested-tasks/providers/class-update-term-description.php b/classes/suggested-tasks/providers/class-update-term-description.php
index 931f9aac5..bebeb1a22 100644
--- a/classes/suggested-tasks/providers/class-update-term-description.php
+++ b/classes/suggested-tasks/providers/class-update-term-description.php
@@ -43,6 +43,13 @@ class Update_Term_Description extends Tasks {
*/
protected const CAPABILITY = 'edit_others_posts';
+ /**
+ * The data collector class name.
+ *
+ * @var string
+ */
+ protected const DATA_COLLECTOR_CLASS = Terms_Without_Description_Data_Collector::class;
+
/**
* Whether the task is dismissable.
*
@@ -51,18 +58,18 @@ class Update_Term_Description extends Tasks {
protected $is_dismissable = true;
/**
- * The task priority.
+ * The task URL target.
*
* @var string
*/
- protected $priority = 'low';
+ protected $url_target = '_blank';
/**
- * The data collector.
+ * The task priority.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Terms_Without_Description
+ * @var int
*/
- protected $data_collector;
+ protected $priority = 80;
/**
* The completed term IDs.
@@ -71,13 +78,6 @@ class Update_Term_Description extends Tasks {
*/
protected $completed_term_ids = null;
- /**
- * Constructor.
- */
- public function __construct() {
- $this->data_collector = new Terms_Without_Description_Data_Collector();
- }
-
/**
* Initialize the task.
*/
@@ -99,106 +99,65 @@ public function init() {
* @return void
*/
public function maybe_remove_irrelevant_tasks( $term, $tt_id, $taxonomy, $deleted_term, $object_ids ) {
- $pending_tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() );
+ $pending_tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] );
if ( ! $pending_tasks ) {
return;
}
foreach ( $pending_tasks as $task ) {
- if ( isset( $task['term_id'] ) && isset( $task['taxonomy'] ) ) {
-
- if ( (int) $task['term_id'] === (int) $deleted_term->term_id ) {
- \progress_planner()->get_suggested_tasks()->delete_task( $task['task_id'] );
- }
- }
- }
- }
-
- /**
- * Get the task ID.
- *
- * @param array $data Optional data to include in the task ID.
- * @return string
- */
- public function get_task_id( $data = [] ) {
- $parts = [ $this->get_provider_id() ];
-
- // Add optional data parts if provided.
- if ( ! empty( $data ) ) {
- foreach ( $data as $value ) {
- $parts[] = $value;
+ if ( $task->target_term_id && $task->target_taxonomy && (int) $task->target_term_id === (int) $deleted_term->term_id ) {
+ \progress_planner()->get_suggested_tasks_db()->delete_recommendation( $task->ID );
}
}
-
- return implode( '-', $parts );
}
/**
* Get the title.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_title( $task_id = '' ) {
- if ( ! $task_id ) {
- return '';
- }
-
- // Get the task data.
- $task_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id );
-
- // We don't want to link if the term was deleted.
- if ( empty( $task_data ) || ! $task_data[0] ) {
- return '';
- }
-
- return \sprintf(
+ protected function get_title_with_data( $task_data = [] ) {
+ $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] );
+ return $term && ! \is_wp_error( $term ) ? \sprintf(
/* translators: %s: The term name */
\esc_html__( 'Write a description for term named "%s"', 'progress-planner' ),
- \esc_html( $task_data[0]['term_name'] )
- );
+ \esc_html( $term->name )
+ ) : '';
}
/**
* Get the description.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_description( $task_id = '' ) {
- $term = $this->get_term_from_task_id( $task_id );
+ public function get_description_with_data( $task_data = [] ) {
+ $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] );
- if ( ! $term ) {
- return '';
- }
-
- return sprintf(
+ return $term && ! \is_wp_error( $term ) ? \sprintf(
/* translators: %1$s: The term name, %2$s Read more link */
\esc_html__( 'Your "%1$s" archives probably show the description of that specific term. %2$s', 'progress-planner' ),
$term->name,
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
- );
+ ) : '';
}
/**
* Get the URL.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_url( $task_id = '' ) {
- $term = $this->get_term_from_task_id( $task_id );
-
- // We don't want to link if the term was deleted.
- if ( ! $term ) {
- return '';
- }
-
- return \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id );
+ protected function get_url_with_data( $task_data = [] ) {
+ $term = \get_term( $task_data['target_term_id'], $task_data['target_taxonomy'] );
+ return $term && ! \is_wp_error( $term )
+ ? \admin_url( 'term.php?taxonomy=' . $term->taxonomy . '&tag_ID=' . $term->term_id )
+ : '';
}
/**
@@ -207,7 +166,7 @@ public function get_url( $task_id = '' ) {
* @return bool
*/
public function should_add_task() {
- return ! empty( $this->data_collector->collect() );
+ return ! empty( $this->get_data_collector()->collect() );
}
/**
@@ -225,26 +184,39 @@ protected function is_specific_task_completed( $task_id ) {
return true;
}
- $term_description = trim( $term->description );
+ $term_description = \trim( $term->description );
return '' !== $term_description && ' ' !== $term_description;
}
+ /**
+ * Transform data collector data into task data format.
+ *
+ * @param array $data The data from data collector.
+ * @return array The transformed data with original data merged.
+ */
+ protected function transform_collector_data( array $data ): array {
+ return \array_merge(
+ $data,
+ [
+ 'target_term_id' => $data['term_id'],
+ 'target_taxonomy' => $data['taxonomy'],
+ 'target_term_name' => $data['name'],
+ ]
+ );
+ }
+
/**
* Get an array of tasks to inject.
*
* @return array
*/
public function get_tasks_to_inject() {
-
- if (
- true === $this->is_task_snoozed() ||
- ! $this->should_add_task() // No need to add the task.
- ) {
+ if ( true === $this->is_task_snoozed() || ! $this->should_add_task() ) {
return [];
}
- $data = $this->data_collector->collect();
+ $data = $this->get_data_collector()->collect();
$task_id = $this->get_task_id(
[
'term_id' => $data['term_id'],
@@ -256,47 +228,40 @@ public function get_tasks_to_inject() {
return [];
}
- return [
- [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'category' => $this->get_provider_category(),
- 'term_id' => $data['term_id'],
- 'taxonomy' => $data['taxonomy'],
- 'term_name' => $data['name'],
- 'date' => \gmdate( 'YW' ),
- ],
- ];
+ // Transform the data to match the task data structure.
+ $task_data = $this->modify_injection_task_data(
+ $this->get_task_details(
+ $this->transform_collector_data( $data )
+ )
+ );
+
+ // Get the task post.
+ $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] );
+
+ // Skip the task if it was already injected.
+ if ( $task_post ) {
+ return [];
+ }
+
+ return [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ];
}
/**
- * Get the task details.
+ * Modify task data before injecting it.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return array
*/
- public function get_task_details( $task_id = '' ) {
+ protected function modify_injection_task_data( $task_data ) {
+ // Transform the data to match the task data structure.
+ $data = $this->transform_collector_data( $this->get_data_collector()->collect() );
- if ( ! $task_id ) {
- return [];
- }
+ $task_data['target_term_id'] = $data['target_term_id'];
+ $task_data['target_taxonomy'] = $data['target_taxonomy'];
+ $task_data['target_term_name'] = $data['target_term_name'];
- $task_details = [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'title' => $this->get_title( $task_id ),
- 'parent' => $this->get_parent(),
- 'priority' => $this->get_priority(),
- 'category' => $this->get_provider_category(),
- 'points' => $this->get_points(),
- 'dismissable' => $this->is_dismissable(),
- 'url' => $this->get_url( $task_id ),
- 'url_target' => $this->get_url_target(),
- 'description' => $this->get_description( $task_id ),
- ];
-
- return $task_details;
+ return $task_data;
}
/**
@@ -307,25 +272,20 @@ public function get_task_details( $task_id = '' ) {
* @return \WP_Term|null
*/
public function get_term_from_task_id( $task_id ) {
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id );
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] );
if ( empty( $tasks ) ) {
return null;
}
- $data = $tasks[0];
-
- if ( ! isset( $data['term_id'] ) || ! $data['term_id'] || ! isset( $data['taxonomy'] ) || ! $data['taxonomy'] ) {
- return null;
- }
-
- $term = \get_term( $data['term_id'], $data['taxonomy'] );
+ $task = $tasks[0];
- if ( is_wp_error( $term ) ) {
+ if ( ! $task->target_term_id || ! $task->target_taxonomy ) {
return null;
}
- return $term;
+ $term = \get_term( $task->target_term_id, $task->target_taxonomy );
+ return $term && ! \is_wp_error( $term ) ? $term : null;
}
/**
@@ -334,18 +294,17 @@ public function get_term_from_task_id( $task_id ) {
* @return array
*/
protected function get_completed_term_ids() {
-
if ( null !== $this->completed_term_ids ) {
return $this->completed_term_ids;
}
$this->completed_term_ids = [];
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() );
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] );
if ( ! empty( $tasks ) ) {
foreach ( $tasks as $task ) {
- if ( isset( $task['status'] ) && 'completed' === $task['status'] ) {
- $this->completed_term_ids[] = $task['term_id'];
+ if ( 'trash' === $task->post_status ) {
+ $this->completed_term_ids[] = $task->target_term_id;
}
}
}
@@ -360,8 +319,6 @@ protected function get_completed_term_ids() {
* @return array
*/
public function exclude_completed_terms( $exclude_term_ids ) {
- $exclude_term_ids = array_merge( $exclude_term_ids, $this->get_completed_term_ids() );
-
- return $exclude_term_ids;
+ return \array_merge( $exclude_term_ids, $this->get_completed_term_ids() );
}
}
diff --git a/classes/suggested-tasks/providers/class-user.php b/classes/suggested-tasks/providers/class-user.php
index b59bfe3e2..fad97673b 100644
--- a/classes/suggested-tasks/providers/class-user.php
+++ b/classes/suggested-tasks/providers/class-user.php
@@ -12,6 +12,20 @@
*/
class User extends Tasks {
+ /**
+ * Whether the task is dismissable.
+ *
+ * @var bool
+ */
+ protected $is_dismissable = true;
+
+ /**
+ * Whether the task is snoozable.
+ *
+ * @var bool
+ */
+ protected $is_snoozable = false;
+
/**
* Whether the task is an onboarding task.
*
@@ -48,59 +62,19 @@ public function should_add_task() {
* @return array
*/
public function get_tasks_to_inject() {
-
- $tasks = [];
- $saved_tasks = \progress_planner()->get_settings()->get( 'tasks', [] );
- foreach ( $saved_tasks as $task_data ) {
- if ( isset( $task_data['provider_id'] ) && self::PROVIDER_ID === $task_data['provider_id'] ) {
- $tasks[] = [
- 'task_id' => $task_data['task_id'],
- 'provider_id' => $this->get_provider_id(),
- 'category' => $this->get_provider_category(),
- 'points' => 0,
- ];
- }
- }
-
- return $tasks;
+ return [];
}
/**
* Get the task details.
*
- * @param string $task_id The task ID.
+ * @param array $task_data Optional data to include in the task.
*
* @return array
*/
- public function get_task_details( $task_id = '' ) {
+ public function get_task_details( $task_data = [] ) {
// Get the user tasks from the database.
- $tasks = \progress_planner()->get_settings()->get( 'tasks', [] );
-
- foreach ( $tasks as $task ) {
- if ( $task['task_id'] !== $task_id ) {
- continue;
- }
-
- return wp_parse_args(
- $task,
- [
- 'task_id' => '',
- 'title' => '',
- 'parent' => 0,
- 'provider_id' => 'user',
- 'category' => 'user',
- 'priority' => 'medium',
- 'points' => 0,
- 'url' => '',
- 'url_target' => '_self',
- 'description' => '',
- 'link_setting' => [],
- 'dismissable' => true,
- 'snoozable' => false,
- ]
- );
- }
-
- return [];
+ $task_post = \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] );
+ return $task_post ? $task_post->get_data() : [];
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php
index 01f04e0b2..bf85f0a14 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-add-yoast-providers.php
@@ -23,9 +23,8 @@ class Add_Yoast_Providers {
* Constructor.
*/
public function __construct() {
- if ( function_exists( 'YoastSEO' ) ) {
+ if ( \function_exists( 'YoastSEO' ) ) {
\add_filter( 'progress_planner_suggested_tasks_providers', [ $this, 'add_providers' ], 11, 1 );
-
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
}
@@ -45,14 +44,13 @@ public function enqueue_assets( $hook ) {
$focus_tasks = [];
foreach ( $this->providers as $provider ) {
-
- // Add Ravi icon if the task is pending or is completed.
+ // Add Ravi icon if the task is published or is completed.
if ( $provider->is_task_relevant() || \progress_planner()->get_suggested_tasks()->was_task_completed( $provider->get_task_id() ) ) {
- if ( method_exists( $provider, 'get_focus_tasks' ) ) {
+ if ( \method_exists( $provider, 'get_focus_tasks' ) ) {
$focus_task = $provider->get_focus_tasks();
if ( $focus_task ) {
- $focus_tasks = array_merge( $focus_tasks, $focus_task );
+ $focus_tasks = \array_merge( $focus_tasks, $focus_task );
}
}
}
@@ -65,7 +63,7 @@ public function enqueue_assets( $hook ) {
'name' => 'progressPlannerYoastFocusElement',
'data' => [
'tasks' => $focus_tasks,
- 'base_url' => constant( 'PROGRESS_PLANNER_URL' ),
+ 'base_url' => \constant( 'PROGRESS_PLANNER_URL' ),
],
]
);
@@ -80,7 +78,6 @@ public function enqueue_assets( $hook ) {
* @return array
*/
public function add_providers( $providers ) {
-
$this->providers = [
new Archive_Author(),
new Archive_Date(),
@@ -94,12 +91,12 @@ public function add_providers( $providers ) {
];
// Yoast SEO Premium.
- if ( defined( 'WPSEO_PREMIUM_VERSION' ) ) {
+ if ( \defined( 'WPSEO_PREMIUM_VERSION' ) ) {
$this->providers[] = new Cornerstone_Workout();
$this->providers[] = new Orphaned_Content_Workout();
}
- return array_merge(
+ return \array_merge(
$providers,
$this->providers
);
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php b/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php
index 644a52932..02cde70a7 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-archive-author.php
@@ -29,18 +29,19 @@ class Archive_Author extends Yoast_Provider {
protected const PROVIDER_ID = 'yoast-author-archive';
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Post_Author
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Post_Author::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Post_Author();
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/author-archives' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/author-archives' );
}
/**
@@ -48,7 +49,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: disable the author archive', 'progress-planner' );
}
@@ -57,8 +58,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Yoast SEO can disable the author archive when you have only one author, as it is the same as the homepage. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -90,13 +91,12 @@ public function get_focus_tasks() {
* @return bool
*/
public function should_add_task() {
-
if ( ! $this->is_task_relevant() ) {
return false;
}
// If the author archive is already disabled, we don't need to add the task.
- if ( YoastSEO()->helpers->options->get( 'disable-author' ) === true ) {
+ if ( \YoastSEO()->helpers->options->get( 'disable-author' ) === true ) {
return false;
}
@@ -112,10 +112,6 @@ public function should_add_task() {
*/
public function is_task_relevant() {
// If there is more than one author, we don't need to add the task.
- if ( $this->data_collector->collect() > self::MINIMUM_AUTHOR_WITH_POSTS ) {
- return false;
- }
-
- return true;
+ return $this->get_data_collector()->collect() <= self::MINIMUM_AUTHOR_WITH_POSTS;
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php b/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php
index 9612fc173..e5f0eba52 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-archive-date.php
@@ -20,10 +20,12 @@ class Archive_Date extends Yoast_Provider {
protected const PROVIDER_ID = 'yoast-date-archive';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/date-archives' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/date-archives' );
}
/**
@@ -31,7 +33,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: disable the date archive', 'progress-planner' );
}
@@ -40,8 +42,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Yoast SEO can disable the date archive, which is really only useful for news sites and blogs. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -73,13 +75,8 @@ public function get_focus_tasks() {
* @return bool
*/
public function should_add_task() {
-
- if ( ! $this->is_task_relevant() ) {
- return false;
- }
-
// If the date archive is already disabled, we don't need to add the task.
- return YoastSEO()->helpers->options->get( 'disable-date' ) !== true;
+ return $this->is_task_relevant() && \YoastSEO()->helpers->options->get( 'disable-date' ) !== true;
}
/**
@@ -92,10 +89,8 @@ public function should_add_task() {
public function is_task_relevant() {
// If the permalink structure includes %year%, %monthnum%, or %day%, we don't need to add the task.
$permalink_structure = \get_option( 'permalink_structure' );
- if ( strpos( $permalink_structure, '%year%' ) !== false || strpos( $permalink_structure, '%monthnum%' ) !== false || strpos( $permalink_structure, '%day%' ) !== false ) {
- return false;
- }
-
- return true;
+ return \strpos( $permalink_structure, '%year%' ) === false
+ && \strpos( $permalink_structure, '%monthnum%' ) === false
+ && \strpos( $permalink_structure, '%day%' ) === false;
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php b/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php
index fe92ed42c..69bcc55a0 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-archive-format.php
@@ -29,18 +29,19 @@ class Archive_Format extends Yoast_Provider {
protected const MINIMUM_POSTS_WITH_FORMAT = 3;
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Archive_Format
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Archive_Format_Data_Collector::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Archive_Format_Data_Collector();
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/format-archives' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/format-archives' );
}
/**
@@ -48,7 +49,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: disable the format archives', 'progress-planner' );
}
@@ -57,8 +58,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'WordPress creates an archive for each post format. This is not useful and can be disabled in the Yoast SEO settings. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -90,16 +91,8 @@ public function get_focus_tasks() {
* @return bool
*/
public function should_add_task() {
- if ( ! $this->is_task_relevant() ) {
- return false;
- }
-
- // If the post format archive is already disabled, we don't need to add the task.
- if ( YoastSEO()->helpers->options->get( 'disable-post_format' ) === true ) {
- return false;
- }
-
- return true;
+ return $this->is_task_relevant()
+ && \YoastSEO()->helpers->options->get( 'disable-post_format' ) !== true;
}
/**
@@ -110,13 +103,7 @@ public function should_add_task() {
* @return bool
*/
public function is_task_relevant() {
- $archive_format_count = $this->data_collector->collect();
-
// If there are more than X posts with a post format, we don't need to add the task. X is set in the class.
- if ( $archive_format_count > static::MINIMUM_POSTS_WITH_FORMAT ) {
- return false;
- }
-
- return true;
+ return $this->get_data_collector()->collect() <= static::MINIMUM_POSTS_WITH_FORMAT;
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php b/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php
index 808e44671..dee134a0e 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-cornerstone-workout.php
@@ -33,9 +33,9 @@ class Cornerstone_Workout extends Yoast_Provider {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'low';
+ protected $priority = 90;
/**
* Whether the task is dismissable.
@@ -84,22 +84,22 @@ public function maybe_update_workout_status( $old_value, $value, $option ) {
return;
}
- // Check if there is pending task.
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $this->get_task_id() );
+ // Check if there is a published task.
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $this->get_task_id() ] );
- // If there is no pending task, return.
- if ( empty( $tasks ) || 'pending' !== $tasks[0]['status'] ) {
+ // If there is no published task, return.
+ if ( empty( $tasks ) || 'publish' !== $tasks[0]->post_status ) {
return;
}
// For this type of task only the provider ID is needed, but just in case.
- if ( $this->is_task_dismissed( $tasks[0] ) ) {
+ if ( $this->is_task_dismissed( $tasks[0]->get_data() ) ) {
return;
}
// There should be 3 steps in the workout.
- $workout_was_completed = 3 === count( $old_value['workouts']['cornerstone']['finishedSteps'] );
- $workout_completed = 3 === count( $value['workouts']['cornerstone']['finishedSteps'] );
+ $workout_was_completed = 3 === \count( $old_value['workouts']['cornerstone']['finishedSteps'] );
+ $workout_completed = 3 === \count( $value['workouts']['cornerstone']['finishedSteps'] );
// Dismiss the task if workout wasn't completed before and now is.
if ( ! $workout_was_completed && $workout_completed ) {
@@ -110,23 +110,19 @@ public function maybe_update_workout_status( $old_value, $value, $option ) {
/**
* Get the task title.
*
- * @param string $task_id The task ID.
- *
* @return string
*/
- public function get_title( $task_id = '' ) {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: do Yoast SEO\'s Cornerstone Content Workout', 'progress-planner' );
}
/**
* Get the task description.
*
- * @param string $task_id The task ID.
- *
* @return string
*/
- public function get_description( $task_id = '' ) {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Improve your most important pages with Yoast SEO\'s Cornerstone Content Workout. %s.', 'progress-planner' ),
'' . \esc_html__( 'Learn more', 'progress-planner' ) . ''
@@ -136,12 +132,10 @@ public function get_description( $task_id = '' ) {
/**
* Get the task URL.
*
- * @param string $task_id The task ID.
- *
* @return string
*/
- public function get_url( $task_id = '' ) {
- return $this->capability_required() ? \esc_url( admin_url( 'admin.php?page=wpseo_workouts#cornerstone' ) ) : '';
+ protected function get_url() {
+ return \esc_url( \admin_url( 'admin.php?page=wpseo_workouts#cornerstone' ) );
}
/**
@@ -150,46 +144,14 @@ public function get_url( $task_id = '' ) {
* @return bool
*/
public function should_add_task() {
- if ( ! defined( 'WPSEO_PREMIUM_VERSION' ) ) {
- return false;
- }
-
- $task_data = [
- 'provider_id' => $this->get_provider_id(),
- ];
-
- // Skip if the task has been dismissed.
- if ( $this->is_task_dismissed( $task_data ) ) {
+ if ( ! \defined( 'WPSEO_PREMIUM_VERSION' ) ) {
return false;
}
- return true;
- }
-
- /**
- * Get the task details.
- *
- * @param string $task_id The task ID.
- *
- * @return array
- */
- public function get_task_details( $task_id = '' ) {
- if ( ! $task_id ) {
- return [];
- }
-
- return [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'title' => $this->get_title( $task_id ),
- 'parent' => $this->get_parent(),
- 'priority' => $this->get_priority(),
- 'category' => $this->get_provider_category(),
- 'points' => $this->get_points(),
- 'dismissable' => $this->is_dismissable,
- 'url' => $this->get_url( $task_id ),
- 'url_target' => $this->get_url_target(),
- 'description' => $this->get_description( $task_id ),
- ];
+ return ! $this->is_task_dismissed(
+ [
+ 'provider_id' => $this->get_provider_id(),
+ ]
+ );
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php
index b270341fd..4c1675d62 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-emoji-scripts.php
@@ -20,28 +20,30 @@ class Crawl_Settings_Emoji_Scripts extends Yoast_Provider {
protected const PROVIDER_ID = 'yoast-crawl-settings-emoji-scripts';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_emoji_scripts' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_emoji_scripts' );
}
/**
- * Get the title.
+ * Get the task title.
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: remove emoji scripts', 'progress-planner' );
}
/**
- * Get the description.
+ * Get the task description.
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Remove JavaScript used for converting emoji characters in older browsers. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php
index b5b4dcba8..abcad1074 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-authors.php
@@ -29,36 +29,37 @@ class Crawl_Settings_Feed_Authors extends Yoast_Provider {
protected const PROVIDER_ID = 'yoast-crawl-settings-feed-authors';
/**
- * The data collector.
+ * The data collector class name.
*
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Post_Author
+ * @var string
*/
- protected $data_collector;
+ protected const DATA_COLLECTOR_CLASS = Post_Author::class;
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->data_collector = new Post_Author();
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_authors' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_authors' );
}
/**
- * Get the title.
+ * Get the task title.
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: remove post authors feeds', 'progress-planner' );
}
/**
- * Get the description.
+ * Get the task description.
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Remove URLs which provide information about recent posts by specific authors. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -90,7 +91,6 @@ public function get_focus_tasks() {
* @return bool
*/
public function should_add_task() {
-
if ( ! $this->is_task_relevant() ) {
return false;
}
@@ -115,10 +115,6 @@ public function should_add_task() {
*/
public function is_task_relevant() {
// If there is more than one author, we don't need to add the task.
- if ( $this->data_collector->collect() > self::MINIMUM_AUTHOR_WITH_POSTS ) {
- return false;
- }
-
- return true;
+ return $this->get_data_collector()->collect() <= self::MINIMUM_AUTHOR_WITH_POSTS;
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php
index 0e71abc81..a4d512ad5 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-crawl-settings-feed-global-comments.php
@@ -20,10 +20,12 @@ class Crawl_Settings_Feed_Global_Comments extends Yoast_Provider {
protected const PROVIDER_ID = 'yoast-crawl-settings-feed-global-comments';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_global_comments' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/crawl-optimization#input-wpseo-remove_feed_global_comments' );
}
/**
@@ -31,7 +33,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: remove global comment feeds', 'progress-planner' );
}
@@ -40,8 +42,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Remove URLs which provide an overview of recent comments on your site. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php b/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php
index 0dd818787..2ea2b4ede 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-fix-orphaned-content.php
@@ -36,13 +36,6 @@ class Fix_Orphaned_Content extends Yoast_Provider {
*/
protected const PROVIDER_ID = 'yoast-fix-orphaned-content';
- /**
- * The data collector.
- *
- * @var \Progress_Planner\Suggested_Tasks\Data_Collector\Yoast_Orphaned_Content
- */
- protected $data_collector;
-
/**
* Whether the task is dismissable.
*
@@ -58,11 +51,11 @@ class Fix_Orphaned_Content extends Yoast_Provider {
protected $completed_post_ids = null;
/**
- * Constructor.
+ * The data collector class name.
+ *
+ * @var string
*/
- public function __construct() {
- $this->data_collector = new Yoast_Orphaned_Content();
- }
+ protected const DATA_COLLECTOR_CLASS = Yoast_Orphaned_Content::class;
/**
* Initialize the task provider.
@@ -74,55 +67,27 @@ public function init() {
}
/**
- * Get the task ID.
- *
- * @param array $data Optional data to include in the task ID.
- * @return string
- */
- public function get_task_id( $data = [] ) {
- $parts = [ $this->get_provider_id() ];
-
- // Add optional data parts if provided.
- if ( ! empty( $data ) ) {
- foreach ( $data as $value ) {
- $parts[] = $value;
- }
- }
-
- return implode( '-', $parts );
- }
-
- /**
- * Get the title.
+ * Get the title with data.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_title( $task_id = '' ) {
- // Get the task data.
- $task_data = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id );
-
- // We don't want to link if the term was deleted.
- if ( empty( $task_data ) || ! $task_data[0] ) {
- return '';
- }
-
- return sprintf(
+ protected function get_title_with_data( $task_data = [] ) {
+ return \sprintf(
/* translators: %s: Post title. */
\esc_html__( 'Yoast SEO: add internal links to article "%s"!', 'progress-planner' ),
- \esc_html( $task_data[0]['post_title'] )
+ \esc_html( $task_data['target_post_title'] )
);
}
/**
* Get the description.
*
- * @param string $task_id The task ID.
* @return string
*/
- public function get_description( $task_id = '' ) {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Yoast SEO detected that this article has no links pointing to it. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -132,19 +97,12 @@ public function get_description( $task_id = '' ) {
/**
* Get the URL.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return string
*/
- public function get_url( $task_id = '' ) {
- $post = $this->get_post_from_task_id( $task_id );
-
- // We don't want to link if the post was deleted.
- if ( ! $post ) {
- return '';
- }
-
- return 'https://prpl.fyi/fix-orphaned-content';
+ protected function get_url_with_data( $task_data = [] ) {
+ return \get_post( $task_data['target_post_id'] ) ? 'https://prpl.fyi/fix-orphaned-content' : '';
}
/**
@@ -153,7 +111,7 @@ public function get_url( $task_id = '' ) {
* @return bool
*/
public function should_add_task() {
- return ! empty( $this->data_collector->collect() );
+ return ! empty( $this->get_data_collector()->collect() );
}
/**
@@ -189,21 +147,33 @@ protected function is_specific_task_completed( $task_id ) {
return 0 !== (int) $linked_count;
}
+ /**
+ * Transform data collector data into task data format.
+ *
+ * @param array $data The data from data collector.
+ * @return array The transformed data with original data merged.
+ */
+ protected function transform_collector_data( array $data ): array {
+ return \array_merge(
+ $data,
+ [
+ 'target_post_id' => $data['post_id'],
+ 'target_post_title' => $data['post_title'],
+ ]
+ );
+ }
+
/**
* Get an array of tasks to inject.
*
* @return array
*/
public function get_tasks_to_inject() {
-
- if (
- true === $this->is_task_snoozed() ||
- ! $this->should_add_task() // No need to add the task.
- ) {
+ if ( true === $this->is_task_snoozed() || ! $this->should_add_task() ) {
return [];
}
- $data = $this->data_collector->collect();
+ $data = $this->get_data_collector()->collect();
$task_id = $this->get_task_id(
[
'post_id' => $data['post_id'],
@@ -215,45 +185,28 @@ public function get_tasks_to_inject() {
return [];
}
- return [
- [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'category' => $this->get_provider_category(),
- 'post_id' => $data['post_id'],
- 'post_title' => $data['post_title'],
- ],
- ];
+ // Transform the data to match the task data structure.
+ $task_data = $this->modify_injection_task_data(
+ $this->get_task_details(
+ $this->transform_collector_data( $data )
+ )
+ );
+
+ return \progress_planner()->get_suggested_tasks_db()->get_post( $task_data['task_id'] )
+ ? []
+ : [ \progress_planner()->get_suggested_tasks_db()->add( $task_data ) ];
}
/**
- * Get the task details.
+ * Modify task data before injecting it.
*
- * @param string $task_id The task ID.
+ * @param array $task_data The task data.
*
* @return array
*/
- public function get_task_details( $task_id = '' ) {
-
- if ( ! $task_id ) {
- return [];
- }
-
- $task_details = [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'title' => $this->get_title( $task_id ),
- 'parent' => $this->get_parent(),
- 'priority' => $this->get_priority(),
- 'category' => $this->get_provider_category(),
- 'points' => $this->get_points(),
- 'dismissable' => $this->is_dismissable(),
- 'url' => $this->get_url( $task_id ),
- 'url_target' => $this->get_url_target(),
- 'description' => $this->get_description( $task_id ),
- ];
-
- return $task_details;
+ protected function modify_injection_task_data( $task_data ) {
+ $task_data['target_post_id'] = $this->transform_collector_data( $this->get_data_collector()->collect() )['target_post_id'];
+ return $task_data;
}
/**
@@ -264,15 +217,15 @@ public function get_task_details( $task_id = '' ) {
* @return \WP_Post|null
*/
public function get_post_from_task_id( $task_id ) {
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $task_id );
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $task_id ] );
if ( empty( $tasks ) ) {
return null;
}
- $data = $tasks[0];
+ $task = $tasks[0];
- return isset( $data['post_id'] ) && $data['post_id'] ? \get_post( $data['post_id'] ) : null;
+ return $task->target_post_id ? \get_post( $task->target_post_id ) : null;
}
/**
@@ -281,18 +234,17 @@ public function get_post_from_task_id( $task_id ) {
* @return array
*/
protected function get_completed_post_ids() {
-
if ( null !== $this->completed_post_ids ) {
return $this->completed_post_ids;
}
$this->completed_post_ids = [];
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'provider_id', $this->get_provider_id() );
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'provider_id' => $this->get_provider_id() ] );
if ( ! empty( $tasks ) ) {
foreach ( $tasks as $task ) {
- if ( isset( $task['status'] ) && 'completed' === $task['status'] ) {
- $this->completed_post_ids[] = $task['post_id'];
+ if ( 'trash' === $task->post_status ) {
+ $this->completed_post_ids[] = $task->target_post_id;
}
}
}
@@ -307,6 +259,6 @@ protected function get_completed_post_ids() {
* @return array
*/
public function exclude_completed_posts( $exclude_post_ids ) {
- return array_merge( $exclude_post_ids, $this->get_completed_post_ids() );
+ return \array_merge( $exclude_post_ids, $this->get_completed_post_ids() );
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php b/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php
index 0b645bae8..c97bf383f 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-media-pages.php
@@ -20,10 +20,12 @@ class Media_Pages extends Yoast_Provider {
protected const PROVIDER_ID = 'yoast-media-pages';
/**
- * Constructor.
+ * Get the task URL.
+ *
+ * @return string
*/
- public function __construct() {
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/media-pages' );
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/media-pages' );
}
/**
@@ -31,7 +33,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: disable the media pages', 'progress-planner' );
}
@@ -40,8 +42,8 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Yoast SEO can disable the media / attachment pages, which are the pages that show the media files. You really don\'t need them, except when you are displaying photos or art on your site through them. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -74,6 +76,6 @@ public function get_focus_tasks() {
*/
public function should_add_task() {
// If the media pages are already disabled, we don't need to add the task.
- return YoastSEO()->helpers->options->get( 'disable-attachment' ) !== true;
+ return \YoastSEO()->helpers->options->get( 'disable-attachment' ) !== true;
}
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php b/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php
index c9edb45be..90c7e22f3 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-organization-logo.php
@@ -30,8 +30,16 @@ class Organization_Logo extends Yoast_Provider {
* Constructor.
*/
public function __construct() {
- $this->yoast_seo = YoastSEO();
- $this->url = \admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' );
+ $this->yoast_seo = \YoastSEO();
+ }
+
+ /**
+ * Get the task URL.
+ *
+ * @return string
+ */
+ protected function get_url() {
+ return \admin_url( 'admin.php?page=wpseo_page_settings#/site-representation' );
}
/**
@@ -39,7 +47,7 @@ public function __construct() {
*
* @return string
*/
- public function get_title() {
+ protected function get_title() {
return $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) !== 'person'
? \esc_html__( 'Yoast SEO: set your organization logo', 'progress-planner' )
: \esc_html__( 'Yoast SEO: set your person logo', 'progress-planner' );
@@ -50,13 +58,13 @@ public function get_title() {
*
* @return string
*/
- public function get_description() {
+ protected function get_description() {
return $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) !== 'person'
- ? sprintf(
+ ? \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'To make Yoast SEO output the correct Schema, you need to set your organization logo in the Yoast SEO settings. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
- ) : sprintf(
+ ) : \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'To make Yoast SEO output the correct Schema, you need to set your person logo in the Yoast SEO settings. %s.', 'progress-planner' ),
'' . \esc_html__( 'Read more', 'progress-planner' ) . ''
@@ -97,14 +105,17 @@ public function get_focus_tasks() {
* @return bool
*/
public function should_add_task() {
-
// If the site is for a person, and the person logo is already set, we don't need to add the task.
- if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'company' && $this->yoast_seo->helpers->options->get( 'company_logo' ) ) {
+ if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'company'
+ && $this->yoast_seo->helpers->options->get( 'company_logo' )
+ ) {
return false;
}
// If the site is for a person, and the organization logo is already set, we don't need to add the task.
- if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'person' && $this->yoast_seo->helpers->options->get( 'person_logo' ) ) {
+ if ( $this->yoast_seo->helpers->options->get( 'company_or_person', 'company' ) === 'person'
+ && $this->yoast_seo->helpers->options->get( 'person_logo' )
+ ) {
return false;
}
diff --git a/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php b/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php
index 6479a77f9..c12cb6be2 100644
--- a/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php
+++ b/classes/suggested-tasks/providers/integrations/yoast/class-orphaned-content-workout.php
@@ -33,9 +33,9 @@ class Orphaned_Content_Workout extends Yoast_Provider {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'low';
+ protected $priority = 90;
/**
* Whether the task is dismissable.
@@ -80,26 +80,29 @@ public function init() {
* @return void
*/
public function maybe_update_workout_status( $old_value, $value, $option ) {
- if ( 'wpseo_premium' !== $option || ! isset( $value['workouts']['orphaned'] ) || ! isset( $old_value['workouts']['orphaned'] ) ) {
+ if ( 'wpseo_premium' !== $option
+ || ! isset( $value['workouts']['orphaned'] )
+ || ! isset( $old_value['workouts']['orphaned'] )
+ ) {
return;
}
- // Check if there is pending task.
- $tasks = \progress_planner()->get_suggested_tasks()->get_tasks_by( 'task_id', $this->get_task_id() );
+ // Check if there is a published task.
+ $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'task_id' => $this->get_task_id() ] );
- // If there is no pending task, return.
- if ( empty( $tasks ) || 'pending' !== $tasks[0]['status'] ) {
+ // If there is no published task, return.
+ if ( empty( $tasks ) || 'publish' !== $tasks[0]->post_status ) {
return;
}
// For this type of task only the provider ID is needed, but just in case.
- if ( $this->is_task_dismissed( $tasks[0] ) ) {
+ if ( $this->is_task_dismissed( $tasks[0]->get_data() ) ) {
return;
}
// There should be 3 steps in the workout.
- $workout_was_completed = 3 === count( $old_value['workouts']['orphaned']['finishedSteps'] );
- $workout_completed = 3 === count( $value['workouts']['orphaned']['finishedSteps'] );
+ $workout_was_completed = 3 === \count( $old_value['workouts']['orphaned']['finishedSteps'] );
+ $workout_completed = 3 === \count( $value['workouts']['orphaned']['finishedSteps'] );
// Dismiss the task if workout wasn't completed before and now is.
if ( ! $workout_was_completed && $workout_completed ) {
@@ -110,23 +113,19 @@ public function maybe_update_workout_status( $old_value, $value, $option ) {
/**
* Get the task title.
*
- * @param string $task_id The task ID.
- *
* @return string
*/
- public function get_title( $task_id = '' ) {
+ protected function get_title() {
return \esc_html__( 'Yoast SEO: do Yoast SEO\'s Orphaned Content Workout', 'progress-planner' );
}
/**
* Get the task description.
*
- * @param string $task_id The task ID.
- *
* @return string
*/
- public function get_description( $task_id = '' ) {
- return sprintf(
+ protected function get_description() {
+ return \sprintf(
/* translators: %s: "Read more" link. */
\esc_html__( 'Improve your internal linking structure with Yoast SEO\'s Orphaned Content Workout. %s.', 'progress-planner' ),
'' . \esc_html__( 'Lean more', 'progress-planner' ) . ''
@@ -136,12 +135,10 @@ public function get_description( $task_id = '' ) {
/**
* Get the task URL.
*
- * @param string $task_id The task ID.
- *
* @return string
*/
- public function get_url( $task_id = '' ) {
- return $this->capability_required() ? \esc_url( admin_url( 'admin.php?page=wpseo_workouts#orphaned' ) ) : '';
+ protected function get_url() {
+ return \esc_url( \admin_url( 'admin.php?page=wpseo_workouts#orphaned' ) );
}
/**
@@ -150,46 +147,14 @@ public function get_url( $task_id = '' ) {
* @return bool
*/
public function should_add_task() {
- if ( ! defined( 'WPSEO_PREMIUM_VERSION' ) ) {
- return false;
- }
-
- $task_data = [
- 'provider_id' => $this->get_provider_id(),
- ];
-
- // Skip if the task has been dismissed.
- if ( $this->is_task_dismissed( $task_data ) ) {
+ if ( ! \defined( 'WPSEO_PREMIUM_VERSION' ) ) {
return false;
}
- return true;
- }
-
- /**
- * Get the task details.
- *
- * @param string $task_id The task ID.
- *
- * @return array
- */
- public function get_task_details( $task_id = '' ) {
- if ( ! $task_id ) {
- return [];
- }
-
- return [
- 'task_id' => $task_id,
- 'provider_id' => $this->get_provider_id(),
- 'title' => $this->get_title( $task_id ),
- 'parent' => $this->get_parent(),
- 'priority' => $this->get_priority(),
- 'category' => $this->get_provider_category(),
- 'points' => $this->get_points(),
- 'dismissable' => $this->is_dismissable,
- 'url' => $this->get_url( $task_id ),
- 'url_target' => $this->get_url_target(),
- 'description' => $this->get_description( $task_id ),
- ];
+ return ! $this->is_task_dismissed(
+ [
+ 'provider_id' => $this->get_provider_id(),
+ ]
+ );
}
}
diff --git a/classes/suggested-tasks/providers/interactive/class-email-sending.php b/classes/suggested-tasks/providers/interactive/class-email-sending.php
index baedb4730..0bb195b0c 100644
--- a/classes/suggested-tasks/providers/interactive/class-email-sending.php
+++ b/classes/suggested-tasks/providers/interactive/class-email-sending.php
@@ -45,9 +45,9 @@ class Email_Sending extends Interactive {
/**
* The task priority.
*
- * @var string
+ * @var int
*/
- protected $priority = 'high';
+ protected $priority = 1;
/**
* The popover ID.
@@ -106,21 +106,24 @@ class Email_Sending extends Interactive {
public function init() {
// Enqueue the scripts.
- add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
+ \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
// Add the AJAX action.
- add_action( 'wp_ajax_prpl_test_email_sending', [ $this, 'ajax_test_email_sending' ] );
+ \add_action( 'wp_ajax_prpl_test_email_sending', [ $this, 'ajax_test_email_sending' ] );
// Set the email error message.
- add_action( 'wp_mail_failed', [ $this, 'set_email_error' ] );
+ \add_action( 'wp_mail_failed', [ $this, 'set_email_error' ] );
// By now all plugins should be loaded and hopefully add actions registered, so we can check if phpmailer is filtered.
\add_action( 'init', [ $this, 'check_if_wp_mail_is_filtered' ], PHP_INT_MAX );
\add_action( 'init', [ $this, 'check_if_wp_mail_has_override' ], PHP_INT_MAX );
$this->email_subject = \esc_html__( 'Your Progress Planner test message!', 'progress-planner' );
- // translators: %1$s