mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-26 04:08:01 +03:00
Added feature flagging system for Comments UI (#20984)
ref PLG-229 - Previously we had no way of using Ghost labs flags in Comments UI. - With this change, we now get Labs data from the existing content settings endpoint. - Additionally, we have a `useLabs` hook that can be accessed from anywhere in the App to put those awesome new features behind a flag for staging - And we can pass labs params to the initialiser for testing. For more details: https://ghost.slack.com/archives/C06TQR9SHSM/p1726133527960489
This commit is contained in:
parent
6704705c86
commit
6d3317fcfc
@ -26,7 +26,8 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
|
||||
pagination: null,
|
||||
commentCount: 0,
|
||||
secundaryFormCount: 0,
|
||||
popup: null
|
||||
popup: null,
|
||||
labs: null
|
||||
});
|
||||
|
||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||
@ -135,15 +136,15 @@ const App: React.FC<AppProps> = ({scriptTag}) => {
|
||||
const initSetup = async () => {
|
||||
try {
|
||||
// Fetch data from API, links, preview, dev sources
|
||||
const {member} = await api.init();
|
||||
const {member, labs} = await api.init();
|
||||
const {comments, pagination, count} = await fetchComments();
|
||||
|
||||
const state = {
|
||||
member,
|
||||
initStatus: 'success',
|
||||
comments,
|
||||
pagination,
|
||||
commentCount: count
|
||||
commentCount: count,
|
||||
labs: labs
|
||||
};
|
||||
|
||||
setState(state);
|
||||
|
@ -33,6 +33,10 @@ export type AddComment = {
|
||||
html: string
|
||||
}
|
||||
|
||||
export type LabsContextType = {
|
||||
[key: string]: boolean
|
||||
}
|
||||
|
||||
export type CommentsOptions = {
|
||||
locale: string,
|
||||
siteUrl: string,
|
||||
@ -40,7 +44,7 @@ export type CommentsOptions = {
|
||||
apiUrl: string | undefined,
|
||||
postId: string,
|
||||
adminUrl: string | undefined,
|
||||
colorScheme: string| undefined,
|
||||
colorScheme: string | undefined,
|
||||
avatarSaturation: number | undefined,
|
||||
accentColor: string,
|
||||
commentsEnabled: string | undefined,
|
||||
@ -63,15 +67,16 @@ export type EditableAppContext = {
|
||||
commentCount: number,
|
||||
secundaryFormCount: number,
|
||||
popup: Page | null,
|
||||
labs: LabsContextType | null
|
||||
}
|
||||
|
||||
export type TranslationFunction = (key: string, replacements?: Record<string, string|number>) => string;
|
||||
export type TranslationFunction = (key: string, replacements?: Record<string, string | number>) => string;
|
||||
|
||||
export type AppContextType = EditableAppContext & CommentsOptions & {
|
||||
// This part makes sure we can add automatic data and return types to the actions when using context.dispatchAction('actionName', data)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
t: TranslationFunction,
|
||||
dispatchAction: <T extends ActionType | SyncActionType>(action: T, data: Parameters<(typeof Actions & typeof SyncActions)[T]>[0] extends {data: any} ? Parameters<(typeof Actions & typeof SyncActions)[T]>[0]['data'] : any) => T extends ActionType ? Promise<void> : void
|
||||
dispatchAction: <T extends ActionType | SyncActionType>(action: T, data: Parameters<(typeof Actions & typeof SyncActions)[T]>[0] extends { data: any } ? Parameters<(typeof Actions & typeof SyncActions)[T]>[0]['data'] : any) => T extends ActionType ? Promise<void> : void
|
||||
}
|
||||
|
||||
// Copy time from AppContextType
|
||||
@ -81,3 +86,14 @@ export const AppContext = React.createContext<AppContextType>({} as any);
|
||||
export const AppContextProvider = AppContext.Provider;
|
||||
|
||||
export const useAppContext = () => useContext(AppContext);
|
||||
|
||||
// create a hook that will only get labs data from the context
|
||||
|
||||
export const useLabs = () => {
|
||||
try {
|
||||
const context = useAppContext();
|
||||
return context.labs;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
@ -4,12 +4,13 @@ import ContentTitle from './ContentTitle';
|
||||
import MainForm from './forms/MainForm';
|
||||
import Pagination from './Pagination';
|
||||
import {ROOT_DIV_ID} from '../../utils/constants';
|
||||
import {useAppContext} from '../../AppContext';
|
||||
import {useAppContext, useLabs} from '../../AppContext';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
const Content = () => {
|
||||
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount, secundaryFormCount} = useAppContext();
|
||||
const commentsElements = comments.slice().reverse().map(comment => <Comment key={comment.id} comment={comment} />);
|
||||
const labs = useLabs();
|
||||
|
||||
const paidOnly = commentsEnabled === 'paid';
|
||||
const isPaidMember = member && !!member.paid;
|
||||
@ -47,6 +48,9 @@ const Content = () => {
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
labs?.testFlag ? <div data-testid="this-comes-from-a-flag" style={{display: 'none'}}></div> : null
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {AddComment, Comment} from '../AppContext';
|
||||
import {AddComment, Comment, LabsContextType} from '../AppContext';
|
||||
|
||||
function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}: {siteUrl: string, apiUrl: string, apiKey: string}) {
|
||||
const apiPath = 'members/api';
|
||||
@ -286,7 +286,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}: {site
|
||||
});
|
||||
}
|
||||
},
|
||||
init: (() => {}) as () => Promise<{ member: any; }>
|
||||
init: (() => {}) as () => Promise<{ member: any; labs: any}>
|
||||
};
|
||||
|
||||
api.init = async () => {
|
||||
@ -294,7 +294,18 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}: {site
|
||||
api.member.sessionData()
|
||||
]);
|
||||
|
||||
return {member};
|
||||
let labs = {};
|
||||
|
||||
try {
|
||||
const settings = await api.site.settings();
|
||||
if (settings.settings.labs) {
|
||||
Object.assign(labs, settings.settings.labs);
|
||||
}
|
||||
} catch (e) {
|
||||
labs = {};
|
||||
}
|
||||
|
||||
return {member, labs};
|
||||
};
|
||||
|
||||
return api;
|
||||
@ -302,3 +313,4 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}: {site
|
||||
|
||||
export default setupGhostApi;
|
||||
export type GhostApi = ReturnType<typeof setupGhostApi>;
|
||||
export type LabsType = LabsContextType;
|
||||
|
44
apps/comments-ui/test/e2e/labs.test.ts
Normal file
44
apps/comments-ui/test/e2e/labs.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {MockedApi, initialize} from '../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
|
||||
test.describe('Labs', async () => {
|
||||
test('Can toggle content based on Lab settings', async ({page}) => {
|
||||
const mockedApi = new MockedApi({});
|
||||
mockedApi.setMember({});
|
||||
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>'
|
||||
});
|
||||
|
||||
const {frame} = await initialize({
|
||||
mockedApi,
|
||||
page,
|
||||
publication: 'Publisher Weekly',
|
||||
labs: {
|
||||
testFlag: true
|
||||
}
|
||||
});
|
||||
|
||||
await expect(frame.getByTestId('this-comes-from-a-flag')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('test div is hidden if flag is not set', async ({page}) => {
|
||||
const mockedApi = new MockedApi({});
|
||||
mockedApi.setMember({});
|
||||
|
||||
mockedApi.addComment({
|
||||
html: '<p>This is comment 1</p>'
|
||||
});
|
||||
|
||||
const {frame} = await initialize({
|
||||
mockedApi,
|
||||
page,
|
||||
publication: 'Publisher Weekly',
|
||||
labs: {
|
||||
testFlag: false
|
||||
}
|
||||
});
|
||||
|
||||
await expect(frame.getByTestId('this-comes-from-a-flag')).not.toBeVisible();
|
||||
});
|
||||
});
|
@ -355,7 +355,6 @@ test.describe('Options', async () => {
|
||||
});
|
||||
expect(titleColor).toBe('rgb(0, 0, 0)');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,17 +1,19 @@
|
||||
import nql from '@tryghost/nql';
|
||||
import {buildComment, buildMember, buildReply} from './fixtures';
|
||||
import {buildComment, buildMember, buildReply, buildSettings} from './fixtures';
|
||||
|
||||
export class MockedApi {
|
||||
comments: any[];
|
||||
postId: string;
|
||||
member: any;
|
||||
settings: any;
|
||||
|
||||
#lastCommentDate = new Date('2021-01-01T00:00:00.000Z');
|
||||
|
||||
constructor({postId = 'ABC', comments = [], member = undefined}: {postId?: string, comments?: any[], member?: any}) {
|
||||
constructor({postId = 'ABC', comments = [], member = undefined, settings = {}}: {postId?: string, comments?: any[], member?: any, settings?: any}) {
|
||||
this.postId = postId;
|
||||
this.comments = comments;
|
||||
this.member = member;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
addComment(overrides: any = {}) {
|
||||
@ -49,6 +51,10 @@ export class MockedApi {
|
||||
this.member = buildMember(overrides);
|
||||
}
|
||||
|
||||
setSettings(overrides) {
|
||||
this.settings = buildSettings(overrides);
|
||||
}
|
||||
|
||||
commentsCounts() {
|
||||
return {
|
||||
[this.postId]: this.comments.length
|
||||
@ -281,5 +287,14 @@ export class MockedApi {
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
// get settings from content api
|
||||
|
||||
await page.route(`${path}/settings/*`, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(this.settings)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {E2E_PORT} from '../../playwright.config';
|
||||
import {LabsType, MockedApi} from './MockedApi';
|
||||
import {Locator, Page} from '@playwright/test';
|
||||
import {MockedApi} from './MockedApi';
|
||||
import {expect} from '@playwright/test';
|
||||
|
||||
export const MOCKED_SITE_URL = 'https://localhost:1234';
|
||||
@ -84,7 +84,7 @@ export async function mockAdminAuthFrame204({admin, page}) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function initialize({mockedApi, page, bodyStyle, ...options}: {
|
||||
export async function initialize({mockedApi, page, bodyStyle, labs = {}, key = '12345678', api = MOCKED_SITE_URL, ...options}: {
|
||||
mockedApi: MockedApi,
|
||||
page: Page,
|
||||
path?: string;
|
||||
@ -101,8 +101,18 @@ export async function initialize({mockedApi, page, bodyStyle, ...options}: {
|
||||
publication?: string,
|
||||
postId?: string,
|
||||
bodyStyle?: string,
|
||||
labs?: LabsType
|
||||
}) {
|
||||
const sitePath = MOCKED_SITE_URL;
|
||||
|
||||
mockedApi.setSettings({
|
||||
settings: {
|
||||
labs: {
|
||||
...labs
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await page.route(sitePath, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
@ -124,6 +134,14 @@ export async function initialize({mockedApi, page, bodyStyle, ...options}: {
|
||||
options.postId = mockedApi.postId;
|
||||
}
|
||||
|
||||
if (!options.key) {
|
||||
options.key = key;
|
||||
}
|
||||
|
||||
if (!options.api) {
|
||||
options.api = api;
|
||||
}
|
||||
|
||||
await page.evaluate((data) => {
|
||||
const scriptTag = document.createElement('script');
|
||||
scriptTag.src = data.url;
|
||||
@ -194,7 +212,7 @@ export async function setClipboard(page, text) {
|
||||
}
|
||||
|
||||
export function getModifierKey() {
|
||||
const os = require('os');
|
||||
const os = require('os'); // eslint-disable-line @typescript-eslint/no-var-requires
|
||||
const platform = os.platform();
|
||||
if (platform === 'darwin') {
|
||||
return 'Meta';
|
||||
|
@ -16,6 +16,14 @@ export function buildMember(override: any = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSettings(override: any = {}) {
|
||||
return {
|
||||
meta: {},
|
||||
settings: {},
|
||||
...override
|
||||
};
|
||||
}
|
||||
|
||||
export function buildComment(override: any = {}) {
|
||||
return {
|
||||
id: ObjectId().toString(),
|
||||
@ -31,7 +39,7 @@ export function buildComment(override: any = {}) {
|
||||
replies: 0,
|
||||
likes: 0,
|
||||
...override.count
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user