/** * WordPress dependencies */ import { store as CORE } from '@safe-wordpress/core-data'; import { useDispatch, useSelect } from '@safe-wordpress/data'; import { useEffect, useMemo } from '@safe-wordpress/element'; import { sprintf, _x, _nx } from '@safe-wordpress/i18n'; import type { User } from '@safe-wordpress/core-data'; /** * External dependencies */ import { castArray, chunk, every, isArray, keyBy, keys, map, mapValues, mergeWith, pick, reduce, toPairs, trim, uniq, values, zipObject, } from 'lodash'; import { EMPTY_ARRAY, EMPTY_OBJECT } from '@nelio-content/constants'; import { store as NC_DATA, usePostTypes } from '@nelio-content/data'; import { doesNetworkSupport, getTargetLabel } from '@nelio-content/networks'; import { listify, isDefined, isUniversalGroup, CAT, TAG, PROD_CAT, PROD_TAG, } from '@nelio-content/utils'; import type { AuthorId, AutomationGroup, AutomationGroupId, CustomField, CustomKey, CustomPlaceholder, Maybe, MetaKey, PostTypeName, RegularAutomationGroup, RegularAutomationGroupId, SocialTemplate, SocialTemplateAvailability, Taxonomy, TaxonomySlug, TermId, TimeInterval, TimeString, UniversalAutomationGroup, Uuid, } from '@nelio-content/types'; /** * Internal dependencies */ import { store as NC_AUTOMATION_SETTINGS } from './store'; import type { TemplateErrors } from './store/types'; export const useAreGroupsDirty = (): boolean => useSelect( ( select ) => { const { getAutomationGroup: getExistingGroup, getAutomationGroups: getExistingGroups, getSocialProfiles: getProfiles, } = select( NC_DATA ); const groups = getExistingGroups() .map( getExistingGroup ) .filter( isDefined ); const profiles = getProfiles(); return select( NC_AUTOMATION_SETTINGS ).isDirty( groups, profiles ); }, [] ); export const useAutomationGroups = (): { readonly data: ReadonlyArray< AutomationGroup >; readonly isLoading: boolean; } => { const groups = useSelect( ( select ) => select( NC_DATA ) .getAutomationGroups() .map( ( id ) => select( NC_DATA ).getAutomationGroup( id ) ) .filter( isDefined ), [] ); const isLoadingGroups = useSelect( ( select ) => ! select( NC_DATA ).hasFinishedResolution( 'getAutomationGroups' ), [] ); const isCachingAuthors = useCache( 'authors', isLoadingGroups ? [] : groups ); const isCachingTerms = useCache( 'terms', isLoadingGroups ? [] : groups ); return { data: groups, isLoading: isLoadingGroups || isCachingAuthors || isCachingTerms, }; }; export function useAutomationGroup( groupId: 'universal' ): UniversalAutomationGroup; export function useAutomationGroup( groupId: Maybe< RegularAutomationGroupId > ): Maybe< RegularAutomationGroup >; export function useAutomationGroup( groupId: Maybe< AutomationGroupId > ): Maybe< AutomationGroup >; export function useAutomationGroup( groupId: Maybe< AutomationGroupId > ): Maybe< AutomationGroup > { return useSelect( ( select ) => { const { getAutomationGroup } = select( NC_AUTOMATION_SETTINGS ); return groupId ? getAutomationGroup( groupId ) : undefined; }, [ groupId ] ); } export const useRegularGroupProperty = < P extends keyof RegularAutomationGroup, >( groupId: RegularAutomationGroupId, property: P ): [ Maybe< RegularAutomationGroup[ P ] >, ( value: RegularAutomationGroup[ P ] ) => void, ] => { const group = useAutomationGroup( groupId ); const { updateAutomationGroup } = useDispatch( NC_AUTOMATION_SETTINGS ); const value = ! group ? undefined : group[ property ]; const setValue = ( v: RegularAutomationGroup[ P ] ) => { if ( ! group ) { return; } void updateAutomationGroup( groupId, { [ property ]: v } ); }; return [ value, setValue ]; }; export const useGroupName = ( groupId: AutomationGroupId ): string => { const group = useAutomationGroup( groupId ); const defaultGroupName = usePostSelectionLabel( group ?? {} ); return isUniversalGroup( group ) ? _x( 'Universal Group', 'text', 'nelio-content' ) : group?.name || defaultGroupName; }; // NOTE. This function is equivalent to useHasResults in item select control export const useEntityRecordLoader = ( postType?: PostTypeName ): boolean => { const taxonomies = useTaxonomies( postType ); const statuses = useSelect( ( select ) => { const { hasFinishedResolution, hasResolutionFailed, getEntityRecords, } = select( CORE ); const isDone = ( x: string, y: unknown[] ) => hasFinishedResolution( x, y ) || hasResolutionFailed( x, y ); const searchTaxOpts = { per_page: 10, context: 'edit', search: '', page: 1, exclude: [], }; const searchUserOpts = { per_page: 10, context: 'view', search: '', page: 1, exclude: [], who: 'authors', }; taxonomies.forEach( ( t ) => getEntityRecords( 'taxonomy', t.slug, searchTaxOpts ) ); getEntityRecords( 'root', 'user', searchUserOpts ); return [ ...taxonomies.map( ( t ) => isDone( 'getEntityRecords', [ 'taxonomy', t.slug, searchTaxOpts, ] ) ), isDone( 'getEntityRecords', [ 'root', 'user', searchUserOpts, ] ), ]; }, [ taxonomies ] ); return statuses.every( Boolean ); }; export const useTaxonomies = ( postType?: PostTypeName ): ReadonlyArray< Taxonomy > => useTaxonomiesPerPostType()[ postType || 'nc_any' ] || EMPTY_ARRAY; type PostSelection = Partial< Pick< RegularAutomationGroup, 'postType' | 'taxonomies' | 'authors' | 'publication' > >; type TemplateSelection = { readonly postType?: PostTypeName; } & Partial< Pick< SocialTemplate, 'taxonomies' | 'author' | 'availability' | 'attachment' > >; export const usePostSelectionLabel = ( { authors, postType, publication, taxonomies, }: PostSelection ): string => { const postTypeName = usePostTypeName( postType ); const byline = useAuthorNames( authors ?? [] ); const intaxonomies = useInTaxonomyString( postType, taxonomies ?? {} ); const time = usePublicationString( publication ?? { type: 'always' } ); const name = sprintf( /* translators: %1$s: Plural post type name (like “Posts” or “Pages”) or “All Content”. %2$s: Byline. %3$s: In taxonomy. %4$s: Published time. */ _x( '%1$s %2$s %3$s %4$s', 'text', 'nelio-content' ), postTypeName, byline, intaxonomies, time ); return trim( name.replace( /\s+/g, ' ' ) ); }; export const useTemplateSelectionLabel = ( { attachment, author, postType, taxonomies, availability, }: TemplateSelection ): string => { const postTypeName = usePostTypeName( postType ); const byline = useAuthorNames( isDefined( author ) ? [ author ] : [] ); const intaxonomies = useInTaxonomyString( postType, mapValues( taxonomies, castArray ) ?? {} ); const time = useAvailabilityString( availability ); const contentType = useContentTypeString( attachment ); const name = sprintf( /* translators: %1$s: Plural post type name. %2$s: Byline. %3$s: In taxonomy. %4$s: Availability time. %5$s: Text / image template. */ _x( '%1$s %2$s %3$s %4$s %5$s', 'text', 'nelio-content' ), postTypeName, byline, intaxonomies, time, contentType ); return trim( name.replace( /\s+/g, ' ' ).replace( / , /g, ', ' ) ); }; export const useCustomFields = ( type?: Maybe< PostTypeName > ): ReadonlyArray< Omit< CustomField, 'value' > > => { const fields = useSelect( ( select ) => select( NC_AUTOMATION_SETTINGS ).getCustomFields(), [] ); return useMemo( () => { if ( ! type ) { return values( keyBy( values( fields ).flatMap( ( fs ) => fs ), 'key' ) ); } return fields[ type ] || EMPTY_ARRAY; }, [ fields, type ] ); }; export const useCustomPlaceholders = ( type?: Maybe< PostTypeName > ): ReadonlyArray< Omit< CustomPlaceholder, 'value' > > => { const placeholders = useSelect( ( select ) => select( NC_AUTOMATION_SETTINGS ).getCustomPlaceholders(), [] ); return useMemo( () => { if ( ! type ) { return values( keyBy( values( placeholders ).flatMap( ( ps ) => ps ), 'key' ) ); } return placeholders[ type ] || EMPTY_ARRAY; }, [ placeholders, type ] ); }; export const useSupportsAuthor = ( postType: Maybe< PostTypeName > ): boolean => !! useSupportsAuthorPerPostType()[ postType || 'nc_any' ]; export const useTemplateErrors = ( group: Maybe< AutomationGroup >, templates: ReadonlyArray< SocialTemplate > ): Record< Uuid, TemplateErrors > => { const taxonomiesPerPostType = useTaxonomiesPerPostType(); const supportsAuthor = useSupportsAuthorPerPostType(); const contentErrors = useTemplateContentErrors( templates ); const existingAuthors = useSelect( ( select ) => { const { getEntityRecord, hasFinishedResolution, hasResolutionFailed, } = select( CORE ); const isDone = ( fn: string, args: unknown[] ): boolean => hasFinishedResolution( fn, args ) || hasResolutionFailed( fn, args ); const exists = ( authorId: AuthorId ): boolean => !! getEntityRecord( 'root', 'user', authorId, { context: 'view', } ) || ! isDone( 'getEntityRecord', [ 'root', 'user', authorId, { context: 'view' }, ] ); return templates .map( ( t ) => t.author ) .filter( isDefined ) .filter( ( a ) => exists( a ) ); }, [ templates ] ); const existingTerms = useSelect( ( select ) => { const { getEntityRecord, hasFinishedResolution, hasResolutionFailed, } = select( CORE ); const isDone = ( fn: string, args: unknown[] ): boolean => hasFinishedResolution( fn, args ) || hasResolutionFailed( fn, args ); const exists = ( type: string, termId: TermId ): boolean => !! getEntityRecord( 'taxonomy', type, termId, { context: 'edit', } ) || ! isDone( 'getEntityRecord', [ 'taxonomy', type, termId, { context: 'edit' }, ] ); return templates.flatMap( ( t ) => toPairs( t.taxonomies ) .map( ( [ tax, termId ] ) => exists( tax, termId ) ? `tax:${ tax }-term:${ termId }` : undefined ) .filter( isDefined ) ); }, [ templates ] ); if ( ! group ) { return EMPTY_OBJECT; } const taxSlugs = mapValues( taxonomiesPerPostType, ( ts ) => map( ts, 'slug' ) ); return mapValues( keyBy( castArray( templates ), 'id' ), ( t ): TemplateErrors => ( { target: !! t.profileId && !! doesNetworkSupport( 'multi-target', t.network ) && ! t.targetName && getTargetLabel( 'selectTargetError', t.network ), content: contentErrors[ t.id ] ?? false, availability: !! t.availability && getAvailabilityErrors( t.availability ), author: getAuthorError( { doesAuthorExist: ! t.author || existingAuthors.includes( t.author ), isAuthorSupported: !! supportsAuthor[ t.postType || 'nc_any' ], isGroupAuthor: isUniversalGroup( group ) || inArray( group.authors, t.author ), } ), taxonomies: pick( mapValues( t.taxonomies, ( termId, tax ) => getTaxonomyError( { doesTermExist: ! termId || existingTerms.includes( `tax:${ tax }-term:${ termId }` ), isGroupTerm: isUniversalGroup( group ) || inArray( group.taxonomies[ tax ], termId ), } ) ), taxSlugs[ t.postType || 'nc_any' ] || [] ), } ) ); }; export const useIsSaving = (): boolean => useSelect( ( select ) => select( NC_AUTOMATION_SETTINGS ).isSaving(), [] ); export const useUnsavedExitEffect = ( isDirty: boolean, isLoading = false ): void => useEffect( () => { if ( isLoading ) { return; } if ( isDirty ) { window.addEventListener( 'beforeunload', onBeforeUnload, { capture: true, } ); } else { window.removeEventListener( 'beforeunload', onBeforeUnload, { capture: true, } ); } return () => { window.removeEventListener( 'beforeunload', onBeforeUnload, { capture: true, } ); }; }, [ isDirty, isLoading ] ); // ============ // HELPER HOOKS // ============ const usePostTypeName = ( postType: Maybe< PostTypeName > ): string => { const postTypes = usePostTypes( 'social' ); if ( ! postType ) { return _x( 'All Content', 'text', 'nelio-content' ); } const postTypeData = postTypes.find( ( p ) => p.name === postType ); return ( postTypeData?.labels.plural || sprintf( /* translators: %s: Post type name. */ _x( 'Post type %s', 'text', 'nelio-content' ), postType ) ); }; const useAuthorNames = ( authors: RegularAutomationGroup[ 'authors' ] ): string => { const usernames = useSelect( ( select ) => { const { getEntityRecord } = select( CORE ); return authors .map( ( id ): Maybe< User > => getEntityRecord( 'root', 'user', id, { context: 'view', } ) ) .map( ( author ) => author?.name ) .filter( isDefined ); }, [ authors ] ); if ( ! usernames.length ) { return ''; } return sprintf( /* translators: %s: Author name(s). */ _x( 'by %s', 'text', 'nelio-content' ), listify( 'or', usernames ) ); }; const useInTaxonomyString = ( postType: Maybe< PostTypeName >, taxonomies: RegularAutomationGroup[ 'taxonomies' ] ): string => { const postTaxonomies = useTaxonomies( postType ); const taxonomyNames: Record< TaxonomySlug, string | undefined > = useSelect( ( select ) => { const { getEntityRecord } = select( CORE ); const taxSlugs = keys( taxonomies ).filter( ( t ) => postTaxonomies.some( ( pt ) => pt.slug === t ) ); const taxNames = taxSlugs .map( ( t ): Maybe< Taxonomy > => getEntityRecord( 'root', 'taxonomy', t ) ) .map( ( t ) => t?.labels.singular_name ); return zipObject( taxSlugs, taxNames ); }, [ taxonomies, postTaxonomies ] ); const taxonomyTerms: Record< TaxonomySlug, ReadonlyArray< string > > = useSelect( ( select ) => { const { getEntityRecord } = select( CORE ); const relevantTaxonomies = pick( taxonomies, keys( taxonomyNames ) ); return mapValues( relevantTaxonomies, ( terms, taxonomy ) => terms .map( ( term ) => getEntityRecord( 'taxonomy', taxonomy, term, { context: 'edit', } ) as Maybe< { name: string } > ) .map( ( term ) => term?.name ) .filter( isDefined ) ); }, [ taxonomies, taxonomyNames ] ); const taxsWithTerms = keys( taxonomyNames ).filter( ( tax: TaxonomySlug ) => !! taxonomyTerms[ tax ]?.length ); const hasCats = !! taxonomyTerms[ CAT ]?.length || !! taxonomyTerms[ PROD_CAT ]?.length; const hasTags = !! taxonomyTerms[ TAG ]?.length || !! taxonomyTerms[ PROD_TAG ]?.length; const catsAndTags: string[] = []; if ( hasCats ) { catsAndTags.push( sprintf( /* translators: %s: Category names. */ _x( 'in %s', 'text', 'nelio-content' ), listify( 'or', taxonomyTerms[ CAT ] ?? taxonomyTerms[ PROD_CAT ] ?? [] ) ) ); } if ( hasTags ) { catsAndTags.push( sprintf( /* translators: %s: Tag names. */ _x( 'tagged as %s', 'text', 'nelio-content' ), listify( 'or', taxonomyTerms[ TAG ] ?? taxonomyTerms[ PROD_TAG ] ?? [] ) ) ); } const otherTaxs = taxsWithTerms .filter( ( tax ) => ! [ CAT, PROD_CAT, TAG, PROD_TAG ].includes( tax ) ) .map( ( tax, i ) => ! i ? sprintf( /* translators: %1$s: Taxonomy name. %2$s: Taxonomy terms. */ _x( 'with %2$s in taxonomy %1$s', 'text', 'nelio-content' ), taxonomyNames[ tax ], listify( 'or', taxonomyTerms[ tax ] ?? [] ) ) : sprintf( /* translators: %1$s: Taxonomy name. %2$s: Taxonomy terms. */ _x( '%2$s in taxonomy %1$s', 'text', 'nelio-content' ), taxonomyNames[ tax ], listify( 'or', taxonomyTerms[ tax ] ?? [] ) ) ); return listify( 'and', [ ...catsAndTags, ...otherTaxs ] ); }; const usePublicationString = ( publication: AutomationGroup[ 'publication' ] ): string => { if ( publication.type === 'always' ) { return ''; } const knownValues: Record< string, string > = { '30': _x( 'from last month', 'text', 'nelio-content' ), '60': _x( 'from last two months', 'text', 'nelio-content' ), '90': _x( 'from last three months', 'text', 'nelio-content' ), '180': _x( 'from last six months', 'text', 'nelio-content' ), '365': _x( 'from last year', 'text', 'nelio-content' ), }; return ( knownValues[ `${ publication.days }` ] || sprintf( /* translators: %d: Number of days. */ _x( 'from last %d days', 'text', 'nelio-content' ), publication.days ) ); }; const useAvailabilityString = ( availability: SocialTemplate[ 'availability' ] ): string => { if ( ! availability ) { return ''; } let time = ''; switch ( availability.type ) { case 'publication-day-offset': time = availability.hoursAfterPublication === 0 ? _x( 'on publication', 'text', 'nelio-content' ) : sprintf( /* translators: %d: Number of hours. */ _nx( '%d hour after publication', '%d hours after publication', availability.hoursAfterPublication, 'text', 'nelio-content' ), availability.hoursAfterPublication ); break; case 'publication-day-period': time = getTimeString( availability.time ); break; case 'after-publication': switch ( availability.daysAfterPublication ) { case 0: time = _x( 'on publication', 'text', 'nelio-content' ); break; case 1: time = _x( 'the day after publication', 'text', 'nelio-content' ); break; case 7: time = _x( 'one week after publication', 'text', 'nelio-content' ); break; case 28: case 29: case 30: case 31: time = _x( 'one month after publication', 'text', 'nelio-content' ); break; default: time = sprintf( /* translators: %d: Number of days. */ _x( '%d days after publication', 'text', 'nelio-content' ), availability.daysAfterPublication ); break; } time += ' ' + getTimeString( availability.time ); break; case 'reshare': const days = keys( availability.weekday ).filter( ( d ) => availability.weekday[ d ] !== false ); let weekdays = ''; if ( days.length === 7 ) { weekdays = _x( 'every day', 'text', 'nelio-content' ); } else if ( days.length === 2 && days.includes( 'sat' ) && days.includes( 'sun' ) ) { weekdays = _x( 'on weekends', 'text', 'nelio-content' ); } else if ( days.length === 5 && ! days.includes( 'sat' ) && ! days.includes( 'sun' ) ) { weekdays = _x( 'on weekdays', 'text', 'nelio-content' ); } else if ( days.length === 1 ) { weekdays = sprintf( /* translators: %s: Day of the week. */ _x( 'all week but %s,', 'text', 'nelio-content' ), expandShortDayNames( days )[ 0 ] ); } else { weekdays = sprintf( /* translators: %s: List of days. */ _x( 'on %s', 'text', 'nelio-content' ), listify( 'and', expandShortDayNames( days ) ) ); } time = weekdays + ' ' + getTimeString( availability.time ); break; } return sprintf( /* translators: %s: Time period. */ _x( ', used %s', 'text', 'nelio-content' ), time ); }; const useContentTypeString = ( attachment: SocialTemplate[ 'attachment' ] ): string => { if ( ! attachment ) { return ''; } switch ( attachment ) { case 'none': return _x( '(text only)', 'text', 'nelio-content' ); case 'image': return _x( '(image)', 'text', 'nelio-content' ); case 'video': return _x( '(video)', 'text', 'nelio-content' ); } }; const TAXONOMY_QUERY = { per_page: -1 } as const; const useAllTaxonomies = (): ReadonlyArray< Taxonomy > => { const records = useSelect( ( select ): ReadonlyArray< Taxonomy > => ( select( CORE ).getEntityRecords( 'root', 'taxonomy', TAXONOMY_QUERY ) as ReadonlyArray< Taxonomy > | null | undefined ) ?? EMPTY_ARRAY, [] ); return useMemo( () => records.filter( ( t ) => t.visibility.public ), [ records ] ); }; const useTemplateContentErrors = ( templates: ReadonlyArray< SocialTemplate > ): Record< Uuid, string | false > => { const customFields = mapValues( useCustomFieldsPerPostType(), ( fs ) => map( fs, 'key' ) ); const customPlaceholders = mapValues( useCustomPlaceholdersPerPostType(), ( ps ) => map( ps, 'key' ) ); const supportsAuthor = useSupportsAuthorPerPostType(); const taxSlugs = mapValues( useTaxonomiesPerPostType(), ( ts ) => map( ts, 'slug' ) ); const errors = map( templates, ( t ) => { if ( ! trim( t.text ).length ) { return _x( 'Please create a template with some content', 'user', 'nelio-content' ); } const unsupportedPlaceholder = /* translators: %s: Placeholder name, like author or tags. */ _x( 'Unsupported %s placeholder found in template', 'text', 'nelio-content' ); if ( ! supportsAuthor[ t.postType || 'nc_any' ] && t.text.includes( '{author}' ) ) { return sprintf( unsupportedPlaceholder, '{author}' ); } const tss = taxSlugs[ t.postType || 'nc_any' ] || []; if ( ! tss.includes( CAT ) && ! tss.includes( PROD_CAT ) && t.text.includes( '{categories}' ) ) { return sprintf( unsupportedPlaceholder, '{categories}' ); } if ( ! tss.includes( TAG ) && ! tss.includes( PROD_TAG ) && t.text.includes( '{tags}' ) ) { return sprintf( unsupportedPlaceholder, '{tags}' ); } const usedTaxonomies = map( Array.from( t.text.matchAll( /{taxonomy:([^}]+)}/g ) ), 1 ); for ( const usedTaxSlug of usedTaxonomies ) { if ( ! tss.includes( usedTaxSlug as TaxonomySlug ) ) { return sprintf( unsupportedPlaceholder, `{taxonomy:${ usedTaxSlug }}` ); } } const cfs = t.postType ? customFields[ t.postType ] || [] : values( customFields ).flatMap( ( i ) => i ); const usedCustomFields = map( Array.from( t.text.matchAll( /{field:([^}]+)}/g ) ), 1 ); for ( const usedCustomField of usedCustomFields ) { if ( ! cfs.includes( usedCustomField as MetaKey ) ) { return sprintf( unsupportedPlaceholder, `{field:${ usedCustomField }}` ); } } const cps = t.postType ? customPlaceholders[ t.postType ] || [] : values( customPlaceholders ).flatMap( ( i ) => i ); const usedCustomPlaceholders = map( Array.from( t.text.matchAll( /{custom:([^}]+)}/g ) ), 1 ); for ( const usedCustomPlaceholder of usedCustomPlaceholders ) { if ( ! cps.includes( usedCustomPlaceholder as CustomKey ) ) { return sprintf( unsupportedPlaceholder, `{custom:${ usedCustomPlaceholder }}` ); } } return false; } ); const ids = map( templates, 'id' ); return zipObject( ids, errors ); }; const useCustomFieldsPerPostType = () => useSelect( ( select ) => select( NC_AUTOMATION_SETTINGS ).getCustomFields(), [] ); const useCustomPlaceholdersPerPostType = () => useSelect( ( select ) => select( NC_AUTOMATION_SETTINGS ).getCustomPlaceholders(), [] ); const useTaxonomiesPerPostType = () => { const socialPostTypes = usePostTypes( 'social' ); const allPostTypes = useMemo( () => socialPostTypes.map( ( pt ) => pt.name ), [ socialPostTypes ] ); const taxonomies = useAllTaxonomies(); return useMemo( () => { const result = {} as Record< 'nc_any' | PostTypeName, ReadonlyArray< Taxonomy > >; result.nc_any = taxonomies.filter( ( tax ) => allPostTypes.some( ( pt ) => tax.types.includes( pt ) ) ); for ( const name of allPostTypes ) { result[ name ] = taxonomies.filter( ( tax ) => tax.types.includes( name ) ); } return result; }, [ taxonomies, allPostTypes ] ); }; const useSupportsAuthorPerPostType = () => { const postTypes = usePostTypes( 'social' ); return reduce( [ 'nc_any' as const, ...map( postTypes, 'name' ) ], ( result, name ) => { result[ name ] = postTypes .filter( ( pt ) => name === 'nc_any' || pt.name === name ) .some( ( pt ) => pt.supports.author ); return result; }, { nc_any: false } as Record< 'nc_any' | PostTypeName, boolean > ); }; // ======= // HELPERS // ======= const getAvailabilityErrors = ( availability: SocialTemplateAvailability ): string | false => { switch ( availability.type ) { case 'publication-day-offset': return 0 <= availability.hoursAfterPublication && availability.hoursAfterPublication <= 24 ? false : _x( 'Please select a number of hours after publication between 0 and 24 for the availability time.', 'user', 'nelio-content' ); case 'publication-day-period': return getTimeErrors( availability.time ); case 'after-publication': if ( availability.daysAfterPublication < 0 ) { return _x( 'Please select a valid positive number of days after publication for the availability date.', 'user', 'nelio-content' ); } return getTimeErrors( availability.time ); case 'reshare': if ( ! values( availability.weekday ).some( Boolean ) ) { return _x( 'Please select at least one day for the availability date or the template will not be used.', 'user', 'nelio-content' ); } return getTimeErrors( availability.time ); } }; const getTimeErrors = ( t: TimeString ): false | string => { return isValidTime( t ) ? false : _x( 'Please select a valid availability time.', 'user', 'nelio-content' ); }; const getTimeString = ( time: TimeString ): string => { switch ( time ) { case 'morning': return _x( 'in the morning', 'text', 'nelio-content' ); case 'noon': return _x( 'at noon', 'text', 'nelio-content' ); case 'afternoon': return _x( 'in the afternoon', 'text', 'nelio-content' ); case 'night': return _x( 'at night', 'text', 'nelio-content' ); default: return sprintf( /* translators: %s: Fixed time in the format HH:mm. */ _x( 'at %s', 'text', 'nelio-content' ), time ); } }; const getAuthorError = ( { doesAuthorExist, isAuthorSupported, isGroupAuthor, }: { readonly doesAuthorExist: boolean; readonly isAuthorSupported: boolean; readonly isGroupAuthor: boolean; } ): string | false => { if ( ! isAuthorSupported ) { return false; } if ( ! doesAuthorExist ) { return _x( 'Selected author doesn’t exist', 'text', 'nelio-content' ); } if ( ! isGroupAuthor ) { return _x( 'The author is not among those selected in the automation group', 'text', 'nelio-content' ); } return false; }; const getTaxonomyError = ( { doesTermExist, isGroupTerm, }: { readonly doesTermExist: boolean; readonly isGroupTerm: boolean; } ): string | false => { if ( ! doesTermExist ) { return _x( 'Selected term doesn’t exist', 'text', 'nelio-content' ); } if ( ! isGroupTerm ) { return _x( 'The term is not among those selected in the automation group', 'text', 'nelio-content' ); } return false; }; const isValidTime = ( t: TimeString ): boolean => { return ( isTimeInterval( t ) || /^(?:[01][0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9])?$/.test( t ) ); }; const isTimeInterval = ( t: TimeString ): t is TimeInterval => { return [ 'morning', 'noon', 'afternoon', 'night' ].includes( t ); }; function expandShortDayNames( days: string[] ): readonly string[] { return days.map( ( day ) => { switch ( day ) { case 'mon': return _x( 'Monday', 'text', 'nelio-content' ); case 'tue': return _x( 'Tuesday', 'text', 'nelio-content' ); case 'wed': return _x( 'Wednesday', 'text', 'nelio-content' ); case 'thu': return _x( 'Thursday', 'text', 'nelio-content' ); case 'fri': return _x( 'Friday', 'text', 'nelio-content' ); case 'sat': return _x( 'Saturday', 'text', 'nelio-content' ); case 'sun': return _x( 'Sunday', 'text', 'nelio-content' ); default: return ''; } } ); } const useCache = ( entityType: 'authors' | 'terms', groups: ReadonlyArray< AutomationGroup > ) => { const allEntities: Record< string, ReadonlyArray< number > > = 'authors' === entityType ? getAuthors( groups ) : getTermsByTax( groups ); const root = 'authors' === entityType ? 'root' : 'taxonomy'; const context = 'authors' === entityType ? 'view' : 'edit'; const hasFinishedResolution = useSelect( ( select ) => { select( CORE ); if ( ! groups.length ) { return false; } mapValues( allEntities, ( allIds, key ) => chunk( allIds, 100 ).flatMap( ( ids ) => map( select( CORE ).getEntityRecords( root, key, { include: ids.join( ',' ), per_page: ids.length, context, } ) as { id: number }[], ( es ) => es.id as TermId ) ) ); return every( values( mapValues( allEntities, ( allIds, key ) => every( chunk( allIds, 100 ).flatMap( ( ids ) => select( CORE ).hasFinishedResolution( 'getEntityRecords', [ root, key, { include: ids.join( ',' ), per_page: ids.length, context, }, ] ) ) ) ) ) ); }, [ allEntities, context, groups, root ] ); return ! hasFinishedResolution; }; const getAuthors = ( groups: ReadonlyArray< AutomationGroup > ): { user: ReadonlyArray< AuthorId > } => { const templateAuthors = groups .flatMap( ( g ) => [ ...values( g.profileSettings ), ...values( g.networkSettings ), ] ) .flatMap( ( s ) => [ ...s.publication.templates, ...s.reshare.templates, ] ) .map( ( t ) => t.author ) .filter( isDefined ); const groupAuthors = groups.flatMap( ( g ) => isUniversalGroup( g ) ? [] : g.authors ); return { user: uniq( [ ...templateAuthors, ...groupAuthors ] ) }; }; const getTermsByTax = ( groups: ReadonlyArray< AutomationGroup > ): Record< TaxonomySlug, ReadonlyArray< TermId > > => { const templateTaxonomies = groups .flatMap( ( g ) => [ ...values( g.profileSettings ), ...values( g.networkSettings ), ] ) .flatMap( ( s ) => [ ...s.publication.templates, ...s.reshare.templates, ] ) .map( ( t ) => mapValues( t.taxonomies, ( ts ) => [ ts ] ) ); const groupTaxonomies = groups.flatMap( ( g ) => isUniversalGroup( g ) ? [] : g.taxonomies ); const allTaxonomies = [ ...templateTaxonomies, ...groupTaxonomies ]; const loadedTermsByTax = reduce( allTaxonomies, ( result, record ) => mergeWith( result, record, ( a, b ) => // eslint-disable-next-line isArray( a ) && isArray( b ) ? [ ...a, ...b ] : undefined ), {} as Record< TaxonomySlug, ReadonlyArray< TermId > > ); return mapValues( loadedTermsByTax, uniq ); }; const onBeforeUnload = ( ev: BeforeUnloadEvent ) => { ev.preventDefault(); return ( ev.returnValue = '' ); }; const inArray = < T >( arr: Maybe< ReadonlyArray< T > >, v: Maybe< T > ): boolean => ! v || ! arr?.length || arr.includes( v );