0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Added Recommendations browse API to admin-x-settings (#17870)

refs https://github.com/TryGhost/Product/issues/3786
This commit is contained in:
Simon Backx 2023-08-30 12:25:31 +02:00 committed by GitHub
parent b54191dbe4
commit 78ae776c5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 127 additions and 10 deletions

View file

@ -0,0 +1,27 @@
import {Meta, createQuery} from '../utils/apiRequests';
export type Recommendation = {
id: string
title: string
reason: string|null
excerpt: string|null // Fetched from the site meta data
featured_image: string|null // Fetched from the site meta data
favicon: string|null // Fetched from the site meta data
url: string
one_click_subscribe: boolean
created_at: string,
updated_at: string|null
}
export interface RecommendationResponseType {
meta?: Meta
recommendations: Recommendation[]
}
const dataType = 'RecommendationResponseType';
export const useBrowseRecommendations = createQuery<RecommendationResponseType>({
dataType,
path: '/recommendations/',
defaultSearchParams: {}
});

View file

@ -1,15 +1,39 @@
import React from 'react';
import Button from '../../../admin-x-ds/global/Button';
import React, {useState} from 'react';
import RecommendationList from './recommendations/RecommendationList';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import TabView from '../../../admin-x-ds/global/TabView';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {useBrowseRecommendations} from '../../../api/recommendations';
const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {
saveState,
handleSave
} = useSettingGroup();
const {data: {recommendations} = {}} = useBrowseRecommendations();
const [selectedTab, setSelectedTab] = useState('your-recommendations');
const buttons = (
<Button color='green' label='Add recommendation' link={true} onClick={() => {}} />
);
const tabs = [
{
id: 'your-recommendations',
title: 'Your recommendations',
contents: (<RecommendationList recommendations={recommendations ?? []} />)
},
{
id: 'recommending-you',
title: 'Recommending you',
contents: (<RecommendationList recommendations={[]} />)
}
];
return (
<SettingGroup
customButtons={buttons}
description="Recommend sites to your audience, and get recommended by others."
keywords={keywords}
navid='recommendations'
@ -18,8 +42,9 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
title="Recommendations"
onSave={handleSave}
>
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
</SettingGroup>
);
};
export default Recommendations;
export default Recommendations;

View file

@ -21,7 +21,7 @@ const SiteSettings: React.FC = () => {
{/* <Theme keywords={searchKeywords.theme} /> */}
<DesignSetting keywords={searchKeywords.design} />
<Navigation keywords={searchKeywords.navigation} />
{hasRecommendations && <Recommendations keywords={searchKeywords.navigation} />}
{hasRecommendations && <Recommendations keywords={searchKeywords.recommendations} />}
</SettingSection>
</>
);

View file

@ -0,0 +1,42 @@
import Button from '../../../../admin-x-ds/global/Button';
import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
import React from 'react';
import Table from '../../../../admin-x-ds/global/Table';
import TableCell from '../../../../admin-x-ds/global/TableCell';
import TableRow from '../../../../admin-x-ds/global/TableRow';
import {Recommendation} from '../../../../api/recommendations';
interface RecommendationListProps {
recommendations: Recommendation[]
}
const RecommendationItem: React.FC<{recommendation: Recommendation}> = ({recommendation}) => {
const action = <Button color='green' label='Delete' link onClick={() => {}} />;
const showDetails = () => {};
return (
<TableRow action={action} hideActions>
<TableCell onClick={showDetails}>
<div className={`flex grow flex-col`}>
<span className='font-medium'>{recommendation.title}</span>
<span className='whitespace-nowrap text-xs text-grey-700'>{recommendation.reason || 'No description'}</span>
</div>
</TableCell>
</TableRow>
);
};
const RecommendationList: React.FC<RecommendationListProps> = ({recommendations}) => {
if (recommendations.length) {
return <Table>
{recommendations.map(recommendation => <RecommendationItem key={recommendation.id} recommendation={recommendation} />)}
</Table>;
} else {
return <NoValueLabel icon='mail-block'>
No recommendations found.
</NoValueLabel>;
}
};
export default RecommendationList;

View file

@ -12,6 +12,7 @@ Object {
"one_click_subscribe": true,
"reason": "Because dogs are cute",
"title": "Dog Pictures",
"updated_at": null,
"url": "https://dogpictures.com",
},
],
@ -22,7 +23,7 @@ exports[`Recommendations Admin API Can add a full recommendation 2: [headers] 1`
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "335",
"content-length": "353",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -45,6 +46,7 @@ Object {
"one_click_subscribe": false,
"reason": null,
"title": "Dog Pictures",
"updated_at": null,
"url": "https://dogpictures.com",
},
],
@ -55,7 +57,7 @@ exports[`Recommendations Admin API Can add a minimal recommendation 2: [headers]
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "244",
"content-length": "262",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -110,6 +112,7 @@ Object {
"one_click_subscribe": true,
"reason": "Because dogs are cute",
"title": "Dog Pictures",
"updated_at": null,
"url": "https://dogpictures.com",
},
],
@ -120,7 +123,7 @@ exports[`Recommendations Admin API Can browse 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "335",
"content-length": "353",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@ -155,6 +158,7 @@ Object {
"one_click_subscribe": false,
"reason": "Because cats are cute",
"title": "Cat Pictures",
"updated_at": null,
"url": "https://catpictures.com",
},
],
@ -165,7 +169,7 @@ exports[`Recommendations Admin API Can edit recommendation 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "336",
"content-length": "354",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View file

@ -10,6 +10,11 @@ describe('Recommendations Admin API', function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('posts');
await agent.loginAsOwner();
// Clear placeholders
for (const recommendation of (await recommendationsService.repository.getAll())) {
await recommendationsService.repository.remove(recommendation.id);
}
});
afterEach(function () {

View file

@ -2,7 +2,17 @@ import {Recommendation} from "./Recommendation";
import {RecommendationRepository} from "./RecommendationRepository";
export class InMemoryRecommendationRepository implements RecommendationRepository {
recommendations: Recommendation[] = [];
recommendations: Recommendation[] = [
new Recommendation({
title: "The Pragmatic Programmer",
reason: "This is a great book for any developer, regardless of their experience level. It's a classic that's stood the test of time.",
excerpt: "The Pragmatic Programmer is one of those rare tech books youll read, re-read, and read again over the years. Whether youre new to the field or an experienced practitioner, youll come away with fresh insights each and every time.",
featuredImage: "https://www.thepragmaticprogrammer.com/image.png",
favicon: "https://www.thepragmaticprogrammer.com/favicon.ico",
url: "https://www.thepragmaticprogrammer.com/",
oneClickSubscribe: false
})
];
async add(recommendation: Recommendation): Promise<Recommendation> {
this.recommendations.push(recommendation);

View file

@ -10,8 +10,9 @@ export class Recommendation {
url: string
oneClickSubscribe: boolean
createdAt: Date
updatedAt: Date|null
constructor(data: {id?: string, title: string, reason: string|null, excerpt: string|null, featuredImage: string|null, favicon: string|null, url: string, oneClickSubscribe: boolean, createdAt?: Date}) {
constructor(data: {id?: string, title: string, reason: string|null, excerpt: string|null, featuredImage: string|null, favicon: string|null, url: string, oneClickSubscribe: boolean, createdAt?: Date, updatedAt?: Date|null}) {
this.id = data.id ?? ObjectId().toString();
this.title = data.title;
this.reason = data.reason;
@ -22,5 +23,7 @@ export class Recommendation {
this.oneClickSubscribe = data.oneClickSubscribe;
this.createdAt = data.createdAt ?? new Date();
this.createdAt.setMilliseconds(0);
this.updatedAt = data.updatedAt ?? null;
this.updatedAt?.setMilliseconds(0);
}
}

View file

@ -65,7 +65,7 @@ export class RecommendationController {
const recommendation = frame.data.recommendations[0];
const cleanedRecommendation: Omit<Recommendation, 'id'|'createdAt'> = {
const cleanedRecommendation: Omit<Recommendation, 'id'|'createdAt'|'updatedAt'> = {
title: validateString(recommendation, "title") ?? '',
url: validateString(recommendation, "url") ?? '',
@ -115,6 +115,7 @@ export class RecommendationController {
url: r.url,
one_click_subscribe: r.oneClickSubscribe,
created_at: r.createdAt,
updated_at: r.updatedAt
};
})
}