From 1c972c7dd1eb166764a387c2ea351b2a59b68ee2 Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 20 Jun 2024 14:22:41 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20button=20URL=20suggestio?= =?UTF-8?q?ns=20not=20loading=20for=20contributors,=20editors=20and=20auth?= =?UTF-8?q?ors=20(#20416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref https://linear.app/tryghost/issue/SLO-127 - problem: when using a card with a button (Button, Email CTA, Header, Product), the Button URL suggestions fail to load for Contributors, Authors, and Editors - cause: Contributors, Authors and Editors don’t have permission to fetch offers, and this causes the entire list of button url suggestions to break - solution: if offers fail to fetch for any reason, the rest of the url suggestions for cards with a button is now still populated (i.e. offers URLs are ignored) --- .../app/components/koenig-lexical-editor.js | 41 +++++++------ .../components/koenig-lexical-editor-test.js | 58 ++++++++++++++++++- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index af4a4e7214..0e4979b232 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -83,6 +83,28 @@ export function decoratePostSearchResult(item, settings) { } } +/** + * Fetches the URLs of all active offers + * @returns {Promise<{label: string, value: string}[]>} + */ +export async function offerUrls() { + let offers = []; + + try { + offers = await this.fetchOffersTask.perform(); + } catch (e) { + // No-op: if offers are not available (e.g. missing permissions), return an empty array + return []; + } + + return offers.map((offer) => { + return { + label: `Offer — ${offer.name}`, + value: this.config.getSiteUrl(offer.code) + }; + }); +} + class ErrorHandler extends React.Component { state = { hasError: false @@ -273,18 +295,6 @@ export default class KoenigLexicalEditor extends Component { }; const fetchAutocompleteLinks = async () => { - let offers = []; - try { - offers = await this.fetchOffersTask.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } - const defaults = [ {label: 'Homepage', value: window.location.origin + '/'}, {label: 'Free signup', value: '#/portal/signup/free'} @@ -329,12 +339,7 @@ export default class KoenigLexicalEditor extends Component { return []; }; - const offersLinks = offers.toArray().map((offer) => { - return { - label: `Offer - ${offer.name}`, - value: this.config.getSiteUrl(offer.code) - }; - }); + const offersLinks = await offerUrls.call(this); return [...defaults, ...memberLinks(), ...donationLink(), ...recommendationLink(), ...offersLinks]; }; diff --git a/ghost/admin/tests/unit/components/koenig-lexical-editor-test.js b/ghost/admin/tests/unit/components/koenig-lexical-editor-test.js index 034ea84425..74e9525fff 100644 --- a/ghost/admin/tests/unit/components/koenig-lexical-editor-test.js +++ b/ghost/admin/tests/unit/components/koenig-lexical-editor-test.js @@ -1,4 +1,5 @@ -import {decoratePostSearchResult} from 'ghost-admin/components/koenig-lexical-editor'; +import sinon from 'sinon'; +import {decoratePostSearchResult, offerUrls} from 'ghost-admin/components/koenig-lexical-editor'; import {describe, it} from 'mocha'; import {expect} from 'chai'; @@ -67,4 +68,59 @@ describe('Unit: Component: koenig-lexical-editor', function () { expect(result.metaIconTitle).to.be.undefined; }); }); + + describe('offersUrls', function () { + let context; + let performStub; + + beforeEach(function () { + context = { + fetchOffersTask: { + perform: () => {} + }, + config: { + getSiteUrl: code => `https://example.com?offer=${code}` + } + }; + + performStub = sinon.stub(context.fetchOffersTask, 'perform'); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('returns an empty array if fetching offers gives no result', async function () { + performStub.resolves([]); + + const results = await offerUrls.call(context); + + expect(performStub.callCount).to.equal(1); + expect(results).to.deep.equal([]); + }); + + it('returns an empty array if fetching offers fails', async function () { + performStub.rejects(new Error('Failed to fetch offers')); + + const results = await offerUrls.call(context); + + expect(performStub.callCount).to.equal(1); + expect(results).to.deep.equal([]); + }); + + it(('returns an array of offers urls if fetching offers is successful'), async function () { + performStub.resolves([ + {name: 'Yellow Thursday', code: 'yellow-thursday'}, + {name: 'Green Friday', code: 'green-friday'} + ]); + + const results = await offerUrls.call(context); + + expect(performStub.callCount).to.equal(1); + expect(results).to.deep.equal([ + {label: 'Offer — Yellow Thursday', value: 'https://example.com?offer=yellow-thursday'}, + {label: 'Offer — Green Friday', value: 'https://example.com?offer=green-friday'} + ]); + }); + }); });