From efe7e3a0fd5d4f991243530b180e74c7fc4bc3a1 Mon Sep 17 00:00:00 2001 From: Eric Teubert Date: Tue, 13 Feb 2024 21:20:47 +0100 Subject: [PATCH] feat: auto-generate file slug from episode-post-title Implements #1436 --- .../mediafiles/components/MediaSlug.vue | 3 ++ client/src/sagas/episode.sagas.ts | 9 ++++- client/src/sagas/mediafiles.sagas.ts | 27 +++++++++++++- client/src/sagas/wordpress.sagas.ts | 8 +++- client/src/store/mediafiles.store.ts | 15 ++++++++ client/src/store/selectors.ts | 4 ++ includes/api/episodes.php | 37 +++++++++++++++++++ js/src/admin/episode.js | 35 ------------------ lib/ajax/ajax.php | 8 ---- 9 files changed, 99 insertions(+), 47 deletions(-) diff --git a/client/src/modules/mediafiles/components/MediaSlug.vue b/client/src/modules/mediafiles/components/MediaSlug.vue index e4503d0c9..d41822dab 100644 --- a/client/src/modules/mediafiles/components/MediaSlug.vue +++ b/client/src/modules/mediafiles/components/MediaSlug.vue @@ -30,6 +30,7 @@ import { mapState, injectStore } from 'redux-vuex' import { selectors } from '@store' import { update as updateEpisode } from '@store/episode.store' +import { disableSlugAutogen } from '@store/mediafiles.store' export default defineComponent({ setup() { @@ -47,6 +48,8 @@ export default defineComponent({ this.dispatch( updateEpisode({ prop: 'slug', value: (event.target as HTMLInputElement).value }) ) + // disable slug generation on any manual input + this.dispatch(disableSlugAutogen()) }, }, diff --git a/client/src/sagas/episode.sagas.ts b/client/src/sagas/episode.sagas.ts index ee1643357..0fa09ac7a 100644 --- a/client/src/sagas/episode.sagas.ts +++ b/client/src/sagas/episode.sagas.ts @@ -6,6 +6,7 @@ import { debounce, fork, put, select, takeEvery } from 'redux-saga/effects' import { PodloveEpisode } from '../types/episode.types' import * as auphonic from '../store/auphonic.store' import * as episode from '../store/episode.store' +import * as mediafiles from '../store/mediafiles.store' import * as wordpress from '../store/wordpress.store' import { createApi } from './api' import { WebhookConfig } from './auphonic.sagas' @@ -34,9 +35,15 @@ function* updateAuphonicWebhookConfig() { function* initialize(api: PodloveApiClient) { const episodeId: string = yield select(selectors.episode.id) - const { result: episodesResult }: { result: PodloveEpisode } = yield api.get(`episodes/${episodeId}`) + const { result: episodesResult }: { result: PodloveEpisode } = yield api.get( + `episodes/${episodeId}` + ) if (episodesResult) { + if (episodesResult.slug === null) { + yield put(mediafiles.enableSlugAutogen()) + } + yield put(episode.set(episodesResult)) } } diff --git a/client/src/sagas/mediafiles.sagas.ts b/client/src/sagas/mediafiles.sagas.ts index b54abb159..f864a0e30 100644 --- a/client/src/sagas/mediafiles.sagas.ts +++ b/client/src/sagas/mediafiles.sagas.ts @@ -1,11 +1,11 @@ import { PodloveApiClient } from '@lib/api' import { selectors } from '@store' -import { all, call, delay, fork, put, select, takeEvery, throttle } from 'redux-saga/effects' +import { all, call, debounce, fork, put, select, takeEvery, throttle } from 'redux-saga/effects' import * as mediafiles from '@store/mediafiles.store' import * as episode from '@store/episode.store' import * as wordpress from '@store/wordpress.store' import { MediaFile } from '@store/mediafiles.store' -import { takeFirst } from './helper' +import { takeFirst, channel } from './helper' import { createApi } from './api' import { Action } from 'redux' import { get } from 'lodash' @@ -17,6 +17,7 @@ function* mediafilesSaga(): any { function* initialize(api: PodloveApiClient) { const episodeId: string = yield select(selectors.episode.id) + const episodeSlug: string = yield select(selectors.episode.slug) const { result: { results: files }, }: { result: { results: MediaFile[] } } = yield api.get(`episodes/${episodeId}/media`) @@ -29,6 +30,8 @@ function* initialize(api: PodloveApiClient) { yield takeEvery(mediafiles.DISABLE, handleDisable, api) yield takeEvery(mediafiles.VERIFY, handleVerify, api) yield takeEvery(episode.SAVED, maybeReverify, api) + yield debounce(2000, wordpress.UPDATE, maybeUpdateSlug, api) + yield throttle( 2000, [mediafiles.ENABLE, mediafiles.DISABLE, mediafiles.UPDATE], @@ -101,6 +104,26 @@ function* maybeReverify(api: PodloveApiClient, action: { type: string; payload: yield all(mediaFiles.map((file) => call(verifyEpisodeAsset, api, episodeId, file.asset_id))) } +function* maybeUpdateSlug( + api: PodloveApiClient, + action: { type: string; payload: { prop: string; value: any } } +) { + const episodeId: boolean = yield select(selectors.episode.id) + const oldSlug: boolean = yield select(selectors.episode.slug) + const enabled: boolean = yield select(selectors.mediafiles.slugAutogenerationEnabled) + + if (enabled && action.payload.prop == 'title' && action.payload.value) { + const newTitle = action.payload.value + + const { result } = yield api.get(`episodes/${episodeId}/build_slug`, { + query: { title: newTitle }, + }) + if (oldSlug != result.slug) { + yield put(episode.update({ prop: 'slug', value: result.slug })) + } + } +} + function* verifyEpisodeAsset(api: PodloveApiClient, episodeId: number, assetId: number) { const mediaFiles: MediaFile[] = yield select(selectors.mediafiles.files) const prevMediaFile: MediaFile | undefined = mediaFiles.find((mf) => mf.asset_id == assetId) diff --git a/client/src/sagas/wordpress.sagas.ts b/client/src/sagas/wordpress.sagas.ts index ba3ec7211..ecc4495ad 100644 --- a/client/src/sagas/wordpress.sagas.ts +++ b/client/src/sagas/wordpress.sagas.ts @@ -68,11 +68,17 @@ function* updatePostTitle() { const seasonNumber: string = '' const padding: number = yield select(selectors.settings.episodeNumberPadding) - wordpress.postTitleInput.value = template + const newTitle = template .replace('%mnemonic%', mnemonic || '') .replace('%episode_number%', (episodeNumber || '').padStart(padding || 0, '0')) .replace('%season_number%', seasonNumber || '') .replace('%episode_title%', title || '') + + if (wordpress.postTitleInput.value != newTitle) { + wordpress.postTitleInput.value = newTitle + + yield postTitleUpdate(newTitle) + } } function* selectMediaFromLibrary(action: { payload: { onSuccess: Action } }) { diff --git a/client/src/store/mediafiles.store.ts b/client/src/store/mediafiles.store.ts index 100559f7e..5d7388167 100644 --- a/client/src/store/mediafiles.store.ts +++ b/client/src/store/mediafiles.store.ts @@ -11,11 +11,13 @@ export type MediaFile = { export type State = { is_initializing: boolean + slug_autogeneration_enabled: boolean files: MediaFile[] } export const initialState: State = { is_initializing: true, + slug_autogeneration_enabled: false, files: [], } @@ -28,6 +30,8 @@ export const DISABLE = 'podlove/publisher/mediafiles/DISABLE' export const VERIFY = 'podlove/publisher/mediafiles/VERIFY' export const UPLOAD_INTENT = 'podlove/publisher/mediafiles/UPLOAD_INTENT' export const SET_UPLOAD_URL = 'podlove/publisher/mediafiles/SET_UPLOAD_URL' +export const ENABLE_SLUG_AUTOGEN = 'podlove/publisher/mediafiles/ENABLE_SLUG_AUTOGEN' +export const DISABLE_SLUG_AUTOGEN = 'podlove/publisher/mediafiles/DISABLE_SLUG_AUTOGEN' export const init = createAction(INIT) export const initDone = createAction(INIT_DONE) @@ -38,6 +42,8 @@ export const disable = createAction(DISABLE) export const verify = createAction(VERIFY) export const uploadIntent = createAction(UPLOAD_INTENT) export const setUploadUrl = createAction(SET_UPLOAD_URL) +export const enableSlugAutogen = createAction(ENABLE_SLUG_AUTOGEN) +export const disableSlugAutogen = createAction(DISABLE_SLUG_AUTOGEN) // TODO: enable revalidates I think? export const reducer = handleActions( @@ -80,11 +86,20 @@ export const reducer = handleActions( [] ), }), + [ENABLE_SLUG_AUTOGEN]: (state: State): State => ({ + ...state, + slug_autogeneration_enabled: true, + }), + [DISABLE_SLUG_AUTOGEN]: (state: State): State => ({ + ...state, + slug_autogeneration_enabled: false, + }), }, initialState ) export const selectors = { isInitializing: (state: State) => state.is_initializing, + slugAutogenerationEnabled: (state: State) => state.slug_autogeneration_enabled, files: (state: State) => state.files, } diff --git a/client/src/store/selectors.ts b/client/src/store/selectors.ts index 98b89996c..ee355beb2 100644 --- a/client/src/store/selectors.ts +++ b/client/src/store/selectors.ts @@ -113,6 +113,10 @@ const episode = { const mediafiles = { isInitializing: createSelector(root.mediafiles, mediafilesStore.selectors.isInitializing), files: createSelector(root.mediafiles, mediafilesStore.selectors.files), + slugAutogenerationEnabled: createSelector( + root.mediafiles, + mediafilesStore.selectors.slugAutogenerationEnabled + ), } const runtime = { diff --git a/includes/api/episodes.php b/includes/api/episodes.php index 257b775eb..6a131a9b7 100644 --- a/includes/api/episodes.php +++ b/includes/api/episodes.php @@ -197,6 +197,24 @@ public function register_routes() 'permission_callback' => [$this, 'create_item_permissions_check'], ] ]); + + register_rest_route($this->namespace, '/'.$this->rest_base.'/(?P[\d]+)/build_slug', [ + 'args' => [ + 'id' => [ + 'description' => __('Unique identifier for the episode.', 'podlove-podcasting-plugin-for-wordpress'), + 'type' => 'integer', + ], + 'title' => [ + 'type' => 'string' + ] + ], + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'build_slug'], + 'permission_callback' => [$this, 'create_item_permissions_check'], + ] + ]); + register_rest_route($this->namespace, '/'.$this->rest_base.'/(?P[\d]+)', [ 'args' => [ 'id' => [ @@ -665,6 +683,25 @@ public function create_item($request) return new \WP_REST_Response(null, 500); } + public function build_slug($request) + { + $id = $request->get_param('id'); + if (!$id) { + return; + } + + $episode = Episode::find_by_id($id); + if (!$episode) { + return new \Podlove\Api\Error\NotFound(); + } + + $title = $request->get_param('title') ?? get_the_title($episode->post_id); + + $slug = sanitize_title($title); + + return new \Podlove\Api\Response\CreateResponse(['slug' => $slug]); + } + public function update_item_permissions_check($request) { if (!current_user_can('edit_posts')) { diff --git a/js/src/admin/episode.js b/js/src/admin/episode.js index 0806d95b2..3959069ae 100644 --- a/js/src/admin/episode.js +++ b/js/src/admin/episode.js @@ -7,32 +7,6 @@ var PODLOVE = PODLOVE || {} PODLOVE.Episode = function (container) { var o = {} - // private - - function maybe_update_episode_slug(title) { - if (o.slug_field.data('auto-update')) { - update_episode_slug(title) - } - } - - // current ajax object to ensure only the latest one is active - var update_episode_slug_xhr - - function update_episode_slug(title) { - if (update_episode_slug_xhr) update_episode_slug_xhr.abort() - - update_episode_slug_xhr = $.ajax({ - url: ajaxurl, - data: { - action: 'podlove-episode-slug', - title: title, - }, - context: o.slug_field, - }).done(function (slug) { - $(this).val(slug).blur() - }) - } - o.slug_field = container.find('[name*=slug]') $('#_podlove_meta_subtitle').count_characters({ @@ -63,12 +37,6 @@ var PODLOVE = PODLOVE || {} } }) - o.slug_field - .data('auto-update', !Boolean(o.slug_field.val())) // only auto-update if it is empty - .on('keyup', function () { - o.slug_field.data('auto-update', false) // stop autoupdate on manual change - }) - var typewatch = (function () { var timer = 0 return function (callback, ms) { @@ -97,9 +65,6 @@ var PODLOVE = PODLOVE || {} // update episode title $('#_podlove_meta_title').attr('placeholder', title) - - // maybe update episode slug - maybe_update_episode_slug(title) }) .trigger('titleHasChanged') diff --git a/lib/ajax/ajax.php b/lib/ajax/ajax.php index 42f218df3..0ce5f72a9 100644 --- a/lib/ajax/ajax.php +++ b/lib/ajax/ajax.php @@ -39,7 +39,6 @@ public function __construct() 'analytics-global-total-downloads', 'analytics-global-total-downloads-by-show', 'analytics-csv-episodes-table', - 'episode-slug', 'admin-news', 'job-create', 'job-get', @@ -915,13 +914,6 @@ public function get_license_parameters_from_url() self::respond_with_json(\Podlove\Model\License::get_license_from_url($_REQUEST['url'])); } - public function episode_slug() - { - echo sanitize_title($_REQUEST['title']); - - exit; - } - private static function analytics_date_condition() { $from = filter_input(INPUT_GET, 'date_from');