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:
parent
b54191dbe4
commit
78ae776c5e
9 changed files with 127 additions and 10 deletions
27
apps/admin-x-settings/src/api/recommendations.ts
Normal file
27
apps/admin-x-settings/src/api/recommendations.ts
Normal 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: {}
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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 \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 you’ll read, re-read, and read again over the years. Whether you’re new to the field or an experienced practitioner, you’ll 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue