Skip to content
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
6 changes: 4 additions & 2 deletions apps/admin-x-framework/src/api/slugs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,23 @@ export interface GenerateSlugParams {
text: string;
/** The record being edited, so its own current slug is not counted as a collision */
id?: string;
/** False when the caller handles an expired session itself instead of leaving the page. */
sessionExpiryRedirect?: boolean;
}

export const useGenerateSlug = () => {
const fetchApi = useFetchApi();

return useCallback(
async ({ type, text, id }: GenerateSlugParams): Promise<string> => {
async ({ type, text, id, sessionExpiryRedirect }: GenerateSlugParams): Promise<string> => {
if (!text) {
return '';
}

// Slugified client-side first: raw reserved characters in the path (a newline as %0A) 404 at the CDN before reaching Ghost
const name = encodeURIComponent(slugify(text));
const path = id ? `/slugs/${type}/${name}/${id}/` : `/slugs/${type}/${name}/`;
const data = await fetchApi<SlugsResponseType>(apiUrl(path));
const data = await fetchApi<SlugsResponseType>(apiUrl(path), { sessionExpiryRedirect });

return data.slugs[0].slug;
},
Expand Down
7 changes: 7 additions & 0 deletions apps/admin-x-framework/src/api/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,13 @@ export const useBrowseSnippets = ({
searchParams: { limit: 'all', ...searchParams, formats },
});

// Snippet writes happen from inside the editor, which surfaces an expired
// session in place rather than navigating away from unsaved content.
const sessionExpiryRedirect = false;

export const useAddSnippet = createMutation<SnippetsResponseType, SnippetEditableData>({
method: 'POST',
sessionExpiryRedirect,
path: () => '/snippets/',
searchParams: () => ({ formats }),
body: (snippet) => ({ snippets: [snippet] }),
Expand All @@ -53,6 +58,7 @@ export const useEditSnippet = createMutation<
SnippetEditableData & { id: string }
>({
method: 'PUT',
sessionExpiryRedirect,
path: ({ id }) => `/snippets/${id}/`,
searchParams: () => ({ formats }),
body: ({ id: _id, ...snippet }) => ({ snippets: [snippet] }),
Expand All @@ -61,6 +67,7 @@ export const useEditSnippet = createMutation<

export const useDeleteSnippet = createMutation<void, string>({
method: 'DELETE',
sessionExpiryRedirect,
path: (id) => `/snippets/${id}/`,
invalidateQueries: { dataType },
});
16 changes: 12 additions & 4 deletions apps/admin-x-framework/src/hooks/use-koenig-fetch-embed.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { useCallback } from 'react';
import { getGhostPaths } from '../utils/helpers';
import { useFetchApi } from '../utils/api/fetch-api';
import { useFetchApi, type RequestOptions } from '../utils/api/fetch-api';

interface KoenigFetchEmbedOptions {
type?: string;
}

export const useKoenigFetchEmbed = () => {
type EmbedRequestOptions = Pick<RequestOptions, 'sessionExpiryRedirect'>;

// Shared so an omitted argument keeps the returned fetcher's identity stable.
const DEFAULT_REQUEST_OPTIONS: EmbedRequestOptions = {};

/** The session-expiry policy applies to every lookup this fetcher makes. */
export const useKoenigFetchEmbed = (
requestOptions: EmbedRequestOptions = DEFAULT_REQUEST_OPTIONS,
) => {
const fetchApi = useFetchApi();

return useCallback(
Expand All @@ -17,8 +25,8 @@ export const useKoenigFetchEmbed = () => {
oembedUrl.searchParams.set('type', type);
}

return await fetchApi(oembedUrl);
return await fetchApi(oembedUrl, requestOptions);
},
[fetchApi],
[fetchApi, requestOptions],
);
};
8 changes: 5 additions & 3 deletions apps/admin-x-framework/src/utils/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import useHandleError from '../../hooks/use-handle-error';
import { usePermission } from '../../hooks/use-permissions';
import { UserRoleType } from '../../api/roles';
import { useFramework } from '../../providers/framework-provider';
import { RequestOptions, apiUrl, useFetchApi } from './fetch-api';
import { apiUrl, useFetchApi, type RequestOptions } from './fetch-api';

export interface Meta {
capabilities?: {
Expand Down Expand Up @@ -47,11 +47,13 @@ type QueryHookOptions<ResponseData> = Omit<
> & {
searchParams?: Record<string, string>;
defaultErrorHandler?: boolean;
/** Whether this query leaves an expired session for its caller to handle in place. */
requestOptions?: Pick<RequestOptions, 'sessionExpiryRedirect'>;
};

export const createQuery =
<ResponseData>(options: QueryOptions<ResponseData>) =>
({ searchParams, ...query }: QueryHookOptions<ResponseData> = {}): Omit<
({ searchParams, requestOptions, ...query }: QueryHookOptions<ResponseData> = {}): Omit<
UseQueryResult<ResponseData>,
'data'
> & { data: ResponseData | undefined } => {
Expand All @@ -64,7 +66,7 @@ export const createQuery =
...query,
enabled: hasPermission && (query.enabled ?? true),
queryKey: [options.dataType, url],
queryFn: () => fetchApi(url, { ...options }),
queryFn: () => fetchApi(url, { ...options, ...requestOptions }),
});

const data = useMemo(
Expand Down
11 changes: 11 additions & 0 deletions apps/admin-x-framework/src/utils/recipient-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ export const PAID_SEGMENT = 'status:-free';
*/
export const EVERYONE_RECIPIENT_FILTER = `${FREE_SEGMENT},${PAID_SEGMENT}`;

/** Expands the API's legacy segment sentinels into the filters used by Admin. */
export function normalizeRecipientFilter(filter: string | null | undefined): string | null {
if (filter === 'all') {
return EVERYONE_RECIPIENT_FILTER;
}
if (!filter || filter === 'none') {
return null;
}
return filter;
}

const BASE_SEGMENTS: string[] = [FREE_SEGMENT, PAID_SEGMENT];

export interface RecipientFilterSegments {
Expand Down
14 changes: 14 additions & 0 deletions apps/admin-x-framework/test/unit/utils/recipient-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,24 @@ import {
getFullRecipientFilter,
getNewsletterRecipientFilter,
getRecipientType,
normalizeRecipientFilter,
parseRecipientFilter,
} from '../../../src/utils/recipient-filter';

describe('recipient-filter', () => {
describe('normalizeRecipientFilter', () => {
it('expands legacy all and none sentinels', () => {
expect(normalizeRecipientFilter('all')).toBe(EVERYONE_RECIPIENT_FILTER);
expect(normalizeRecipientFilter('none')).toBeNull();
});

it('preserves real filters and normalizes empty values', () => {
expect(normalizeRecipientFilter('label:vip')).toBe('label:vip');
expect(normalizeRecipientFilter(null)).toBeNull();
expect(normalizeRecipientFilter(undefined)).toBeNull();
});
});

describe('parseRecipientFilter', () => {
it('returns empty segments for null, undefined and empty filters', () => {
for (const filter of [null, undefined, '']) {
Expand Down
1 change: 1 addition & 0 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@tryghost/custom-field-types": "workspace:*",
"@tryghost/custom-fonts": "catalog:",
"@tryghost/i18n": "workspace:*",
"@tryghost/kg-clean-basic-html": "workspace:*",
"@tryghost/kg-unsplash-selector": "workspace:*",
"@tryghost/koenig-lexical": "workspace:*",
"@tryghost/nql": "catalog:",
Expand Down
Loading
Loading