Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bd57f27
Updated newsletter sending status copy in post analytics (#30620)
luissazevedo Sep 9, 2026
b74ac8e
Fixed automation email analytics filtering (#30430)
cmraible Sep 9, 2026
c6aa1bf
Added the Code injection pane to the React editor's settings sidebar …
9larsons Sep 9, 2026
1eda410
Added tests and types to LLM discovery middleware (#30616)
EvanHahn Sep 9, 2026
13d1d36
Changed every member read to state who the extra fields are read for
rob-ghost Sep 9, 2026
3f3cd9f
Added per-field control over what a member may see and change
rob-ghost Sep 9, 2026
555b290
Added the control a publisher opens a custom field to members with
rob-ghost Sep 9, 2026
11d5eac
Added a browser test for who a custom field is for
rob-ghost Sep 9, 2026
ad9e9c9
Changed the browser tests to one timeout everywhere
rob-ghost Sep 9, 2026
bbc8e8d
Moved both chip pickers onto one shared shell (#30609)
9larsons Sep 9, 2026
c48b9d7
Added a host limit that can switch custom member fields off per site
rob-ghost Sep 9, 2026
c61a5a7
Fixed gift email analytics site filtering (#30624)
kevinansfield Sep 9, 2026
478a4bf
Added the Keyboard shortcuts pane to the React editor's settings side…
9larsons Sep 9, 2026
78561ea
Added the Facebook card pane to the React editor's settings sidebar (…
9larsons Sep 9, 2026
0237ab1
Added the X card pane to the React editor's settings sidebar (#30611)
9larsons Sep 9, 2026
4b14f58
Changed the React editor's save gates to validate the same fields (#3…
9larsons Sep 9, 2026
707373f
Removed the duplicated image-upload and social-card helpers in the po…
9larsons Sep 9, 2026
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
4 changes: 4 additions & 0 deletions apps/admin-x-framework/src/api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export type Config = {
disabled: boolean;
error?: string;
};
limitCustomFields?: {
disabled: boolean;
error?: string;
};
publicSiteAccess?: {
disabled: boolean;
// Copy shown in the pre-launch banner when public site access is disabled.
Expand Down
9 changes: 8 additions & 1 deletion apps/admin-x-framework/src/api/images.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { z } from 'zod';
import { createMutation } from '../utils/api/hooks';

export interface ImagesResponseType {
Expand All @@ -18,4 +19,10 @@ export const useUploadImage = createMutation<ImagesResponseType, { file: File }>
},
});

export const getImageUrl = (response: ImagesResponseType) => response.images[0].url;
const UploadedImageResponseSchema = z.object({
// Storage adapters may return relative paths as well as absolute URLs.
images: z.array(z.object({ url: z.string().min(1) })).min(1),
});

export const getImageUrl = (response: unknown): string =>
UploadedImageResponseSchema.parse(response).images[0].url;
57 changes: 51 additions & 6 deletions apps/admin-x-framework/src/api/member-custom-fields.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
FIELD_TYPES,
FIELD_TYPE_IDS,
type MemberAccess,
partTypesOf,
subFieldsOf,
type FieldKind,
Expand Down Expand Up @@ -37,10 +38,48 @@ export type MemberCustomField = {
// Browse hides archived fields by default (most surfaces only want active
// ones); Settings opts in via filter and splits on this.
status: 'active' | 'archived';
access: { member: MemberCustomFieldAccess };
created_at: string;
updated_at: string | null;
};

// The levels themselves are the shared vocabulary; what a publisher is told they mean
// is presentation, and stays below.
export type MemberCustomFieldAccess = MemberAccess;

export const MEMBER_CUSTOM_FIELD_ACCESS_OPTIONS: {
value: MemberCustomFieldAccess;
label: string;
description: string;
}[] = [
{
value: 'none',
label: 'Only staff',
description: 'Members never see this field or what you record in it',
},
{
value: 'read',
label: 'Members can view',
description: 'Shown in their account, but only staff can change it',
},
{
value: 'write',
label: 'Members can edit',
description: 'Members fill this in and keep it up to date themselves',
},
];

/**
* The words a publisher reads for a level.
*
* A level this build does not know shows as itself rather than falling back to the
* closed label: the fallback would tell a publisher a field is staff-only when the
* server may be treating it as open, and a label that reassures is worse than one
* that reads oddly. Reachable only from a Core newer than this Admin.
*/
export const memberAccessLabel = (access: MemberCustomFieldAccess): string =>
MEMBER_CUSTOM_FIELD_ACCESS_OPTIONS.find((option) => option.value === access)?.label ?? access;

/**
* The user-type catalog: the presentation layer over the shared field types.
*
Expand Down Expand Up @@ -323,10 +362,16 @@ export const useBrowseMemberCustomFieldsIncludingArchived = (
) =>
useBrowseMemberCustomFields({ ...options, searchParams: { filter: 'status:[active,archived]' } });

// The backend mints the key from the name, so create takes just a name and a type.
/** Everything a new field is created from. The backend mints the key from the name. */
export type NewMemberCustomField = Pick<MemberCustomField, 'name' | 'type' | 'access'>;

/** A change to one field, addressed by key. Anything omitted is left as it is. */
export type MemberCustomFieldEdit = Pick<MemberCustomField, 'key'> &
Partial<Pick<MemberCustomField, 'name' | 'status' | 'access'>>;

export const useCreateMemberCustomField = createMutation<
MemberCustomFieldsResponseType,
Pick<MemberCustomField, 'name' | 'type'>
NewMemberCustomField
>({
method: 'POST',
path: () => '/members/metafields/custom/',
Expand Down Expand Up @@ -357,12 +402,12 @@ export const useCreateMemberCustomField = createMutation<
},
});

// Keys are immutable after creation (the API rejects changes); `name` and
// `status` are the editable surface — a status flip to 'active' is how an
// archived field is reactivated.
// Keys are immutable after creation (the API rejects changes); `name`, `status` and
// `access` are the editable surface — a status flip to 'active' is how an archived
// field is reactivated, and an access change is what opens a field to members.
export const useEditMemberCustomField = createMutation<
MemberCustomFieldsResponseType,
Pick<MemberCustomField, 'key'> & Partial<Pick<MemberCustomField, 'name' | 'status'>>
MemberCustomFieldEdit
>({
method: 'PUT',
path: (field) => `/members/metafields/custom/${field.key}/`,
Expand Down
23 changes: 23 additions & 0 deletions apps/admin-x-framework/test/unit/api/images.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { getImageUrl } from '../../../src/api/images';

describe('getImageUrl', () => {
it.each(['https://example.com/image.png', '/content/images/image.png'])(
'accepts an uploaded image URL (%s)',
(url) => {
expect(getImageUrl({ images: [{ url, ref: null }] })).toBe(url);
},
);

it.each([
null,
{},
{ images: [] },
{ images: [{}] },
{ images: [{ url: 123 }] },
{ images: [{ url: true }] },
{ images: [{ url: '' }] },
])('rejects a malformed upload response (%j)', (response) => {
expect(() => getImageUrl(response)).toThrow();
});
});
50 changes: 50 additions & 0 deletions apps/admin/src/editor/card-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,56 @@ describe('buildPostCardConfig', () => {
expect(cardConfig.deleteSnippet).toBe(ports.deleteSnippet);
});

it('carries the site images the social cards fall back to', () => {
const cardConfig = buildPostCardConfig(
sources({
settings: settingsFrom({
...baseSettings,
og_image: 'site-og.png',
twitter_image: 'site-twitter.png',
cover_image: 'cover.png',
}),
}),
ports,
);

expect(cardConfig).toMatchObject({
siteOgImage: 'site-og.png',
siteTwitterImage: 'site-twitter.png',
siteCoverImage: 'cover.png',
});
});

it('reports a site image the settings do not carry as none', () => {
const cardConfig = buildPostCardConfig(sources(), ports);

expect(cardConfig).toMatchObject({
siteOgImage: null,
siteTwitterImage: null,
siteCoverImage: null,
});
});

it.each([123, true])('ignores non-string site images (%s)', (value) => {
const cardConfig = buildPostCardConfig(
sources({
settings: settingsFrom({
...baseSettings,
og_image: value,
twitter_image: value,
cover_image: value,
}),
}),
ports,
);

expect(cardConfig).toMatchObject({
siteOgImage: null,
siteTwitterImage: null,
siteCoverImage: null,
});
});

it('drops Unsplash when the integration is off', () => {
const cardConfig = buildPostCardConfig(
sources({ settings: settingsFrom({ ...baseSettings, unsplash: false }) }),
Expand Down
11 changes: 11 additions & 0 deletions apps/admin/src/editor/card-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export interface PostCardConfig extends PostCardConfigPorts {
membersEnabled: boolean;
siteTitle: string;
siteDescription: string;
siteOgImage: string | null;
siteTwitterImage: string | null;
siteCoverImage: string | null;
siteUrl: string;
siteUuid: string;
stripeEnabled: boolean;
Expand Down Expand Up @@ -99,6 +102,11 @@ export function getCardVisibilitySettings(
return isPage ? 'web only' : 'web and email';
}

function imageSetting(settings: Setting[], key: string): string | null {
const value = getSettingValue(settings, key);
return typeof value === 'string' ? value : null;
}

export function buildPostCardConfig(
sources: PostCardConfigSources,
ports: PostCardConfigPorts,
Expand All @@ -124,6 +132,9 @@ export function buildPostCardConfig(
searchLinks: ports.searchLinks,
siteTitle: getSettingValue<string>(settings, 'title') ?? '',
siteDescription: getSettingValue<string>(settings, 'description') ?? '',
siteOgImage: imageSetting(settings, 'og_image'),
siteTwitterImage: imageSetting(settings, 'twitter_image'),
siteCoverImage: imageSetting(settings, 'cover_image'),
siteUrl: getHomepageUrl(site),
siteUuid: site.site_uuid,
stripeEnabled: checkStripeEnabled(settings, config),
Expand Down
1 change: 1 addition & 0 deletions apps/admin/src/editor/editor-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ function EditorContent({
<PostSettingsSidebar
cardConfig={currentCardConfig}
currentUser={currentUser}
featureImage={featureImage.featureImage}
hasInlineExcerpt={showExcerpt}
postType={postType}
session={session}
Expand Down
Loading
Loading