🐛 Fixed button URL suggestions not loading for contributors, editors and authors (#20416)

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)
This commit is contained in:
Sag 2024-06-20 14:22:41 +02:00 committed by GitHub
parent 524fe6ee19
commit 1c972c7dd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 80 additions and 19 deletions

View File

@ -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];
};

View File

@ -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'}
]);
});
});
});