Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-Generate File Slug from Episode-Post-Title #1438

Merged
merged 4 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/src/modules/mediafiles/components/MediaSlug.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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())
},
},

Expand Down
9 changes: 8 additions & 1 deletion client/src/sagas/episode.sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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))
}
}
Expand Down
27 changes: 25 additions & 2 deletions client/src/sagas/mediafiles.sagas.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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`)
Expand All @@ -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],
Expand Down Expand Up @@ -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)
Expand Down
8 changes: 7 additions & 1 deletion client/src/sagas/wordpress.sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }) {
Expand Down
15 changes: 15 additions & 0 deletions client/src/store/mediafiles.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
}

Expand All @@ -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<void>(INIT)
export const initDone = createAction<void>(INIT_DONE)
Expand All @@ -38,6 +42,8 @@ export const disable = createAction<number>(DISABLE)
export const verify = createAction<number>(VERIFY)
export const uploadIntent = createAction<void>(UPLOAD_INTENT)
export const setUploadUrl = createAction<string>(SET_UPLOAD_URL)
export const enableSlugAutogen = createAction<void>(ENABLE_SLUG_AUTOGEN)
export const disableSlugAutogen = createAction<void>(DISABLE_SLUG_AUTOGEN)

// TODO: enable revalidates I think?
export const reducer = handleActions(
Expand Down Expand Up @@ -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,
}
4 changes: 4 additions & 0 deletions client/src/store/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
37 changes: 37 additions & 0 deletions includes/api/episodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,24 @@ public function register_routes()
'permission_callback' => [$this, 'create_item_permissions_check'],
]
]);

register_rest_route($this->namespace, '/'.$this->rest_base.'/(?P<id>[\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<id>[\d]+)', [
'args' => [
'id' => [
Expand Down Expand Up @@ -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')) {
Expand Down
35 changes: 0 additions & 35 deletions js/src/admin/episode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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')

Expand Down
8 changes: 0 additions & 8 deletions lib/ajax/ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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');
Expand Down