From d4e336dc2a94ac5fb3efc0b6d41493171e6a02e0 Mon Sep 17 00:00:00 2001 From: James Morris Date: Tue, 25 Oct 2022 16:59:29 +0100 Subject: [PATCH 01/16] Further tweaks and refactoring to engagement bar - Made a new function that checks if stubs are needed per tab - Renamed the check for pagination to be a little more clear - Made sure the pagination block is always to the right - Made sure the pagination buttons don't show when there is no pagination refs https://github.com/TryGhost/Team/issues/2149 --- .../components/posts/post-activity-feed.hbs | 48 ++++++++++--------- .../components/posts/post-activity-feed.js | 7 ++- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/ghost/admin/app/components/posts/post-activity-feed.hbs b/ghost/admin/app/components/posts/post-activity-feed.hbs index 719fb7ea39d..2ff443ee7f9 100644 --- a/ghost/admin/app/components/posts/post-activity-feed.hbs +++ b/ghost/admin/app/components/posts/post-activity-feed.hbs @@ -76,7 +76,7 @@ {{/let}} {{/each}} - {{#if (not (compute (fn this.isPaginationNeeded eventsFetcher)))}} + {{#if (compute (fn this.areStubsNeeded eventsFetcher))}} {{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}} {{#each stubs}}
@@ -94,36 +94,38 @@ {{svg-jar "filter"}} See members + {{else}} +
{{/if}}
- {{#if (compute (fn this.isPaginationNeeded eventsFetcher))}} + {{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}} Showing {{eventsFetcher.totalEvents}} in total {{else}} Showing {{eventsFetcher.previousEvents}}-{{eventsFetcher.shownEvents}} of {{eventsFetcher.totalEvents}} - {{/if}} -
- +
+ - -
+ +
+ {{/if}}
diff --git a/ghost/admin/app/components/posts/post-activity-feed.js b/ghost/admin/app/components/posts/post-activity-feed.js index 2cc21530752..9f2fdfc6f90 100644 --- a/ghost/admin/app/components/posts/post-activity-feed.js +++ b/ghost/admin/app/components/posts/post-activity-feed.js @@ -69,7 +69,12 @@ export default class PostActivityFeed extends Component { } @action - isPaginationNeeded({totalEvents}) { + isPaginationNotNeeded({totalEvents}) { return (totalEvents <= this._pageSize); } + + @action + areStubsNeeded({totalEvents}) { + return totalEvents > this._pageSize || this.args.eventType === 'feedback'; + } } From 176af30788d05ee33fb4ed4444c6c82bb7ab8520 Mon Sep 17 00:00:00 2001 From: James Morris Date: Tue, 25 Oct 2022 17:50:23 +0100 Subject: [PATCH 02/16] Added in clicks URLs for the members table under clicked tab refs https://github.com/TryGhost/Team/issues/2149 --- .../app/components/posts/post-activity-feed.hbs | 6 +++++- ghost/admin/app/styles/layouts/content.css | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ghost/admin/app/components/posts/post-activity-feed.hbs b/ghost/admin/app/components/posts/post-activity-feed.hbs index 2ff443ee7f9..5b37ba4e743 100644 --- a/ghost/admin/app/components/posts/post-activity-feed.hbs +++ b/ghost/admin/app/components/posts/post-activity-feed.hbs @@ -40,7 +40,7 @@ {{else}} -
+
{{#each eventsFetcher.data as |event|}} {{#let (parse-member-event event) as |parsedEvent|}}
@@ -53,6 +53,10 @@ {{capitalize-first-letter parsedEvent.action}} + {{#if parsedEvent.url}} + + {{parsedEvent.description}} + {{/if}}
diff --git a/ghost/admin/app/styles/layouts/content.css b/ghost/admin/app/styles/layouts/content.css index f45de1b84cf..0f99099f808 100644 --- a/ghost/admin/app/styles/layouts/content.css +++ b/ghost/admin/app/styles/layouts/content.css @@ -1669,10 +1669,22 @@ a.gh-post-list-cta.stats.is-hovered:hover > * { grid-template-columns: 40% 40% 20%; } +.gh-tabs-analytics .gh-dashboard-list-larger-cols .gh-dashboard-list-item { + grid-template-columns: 26% 54% 20%; +} + .gh-tabs-analytics .gh-dashboard-list-four-cols .gh-dashboard-list-item { grid-template-columns: 35% 25% 20% 20%; } +.gh-post-activity-feed .gh-members-activity-description a { + font-weight: 500; +} + +.gh-post-activity-feed .gh-members-activity-description a:hover { + color: #697989; +} + @media (max-width: 1200px) { .gh-tabs-analytics .tab{ padding: 8px 10px; From f8cf22076231f1781b84f9114e07e9ce30bc8bba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Oct 2022 20:20:41 +0000 Subject: [PATCH 03/16] Update dependency mailgun.js to v8.0.2 --- ghost/mailgun-client/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ghost/mailgun-client/package.json b/ghost/mailgun-client/package.json index 74805b63114..354313acb5a 100644 --- a/ghost/mailgun-client/package.json +++ b/ghost/mailgun-client/package.json @@ -29,6 +29,6 @@ "@tryghost/metrics": "1.0.16", "form-data": "4.0.0", "lodash": "4.17.21", - "mailgun.js": "8.0.1" + "mailgun.js": "8.0.2" } } diff --git a/yarn.lock b/yarn.lock index 8113eab393f..811c4740ba3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18520,10 +18520,10 @@ magic-string@^0.26.0: dependencies: sourcemap-codec "^1.4.8" -mailgun.js@8.0.1, mailgun.js@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/mailgun.js/-/mailgun.js-8.0.1.tgz#918fb7bf72474ef16f47f10659a2588534252af3" - integrity sha512-YeyugcYx01j96RmbrMFPbv0EQmDXtSlEeQTCYdmyewSBI2/Z+F3NulLLlkTGDIvf9+sCyD0MVxBBszPNSesbug== +mailgun.js@8.0.2, mailgun.js@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/mailgun.js/-/mailgun.js-8.0.2.tgz#a917b38c162da590f90c9f06945b3bcfa79cab49" + integrity sha512-s0esDPrHlo72rLylEj61akLO49HvUltmjKIj/lrbY89kWcL/lcPy23yh4ifFkWTuiBO6oHVqXwzfQI3lhSvK9A== dependencies: axios "^0.27.2" base-64 "^1.0.0" From 857dacbf16e73d72730b859fab4ea478d63c2a4f Mon Sep 17 00:00:00 2001 From: Daniel Lockyer Date: Tue, 25 Oct 2022 21:35:54 +0700 Subject: [PATCH 04/16] Fixed missing column values for default paid tiers fixes https://github.com/TryGhost/Toolbox/issues/455 refs https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/data/migrations/versions/5.19/2022-09-02-20-52-backfill-new-product-columns.js - the referenced migration does not handle backfilling the currency/monthly_price/yearly_price for the default paid tiers where they do not originate from Stripe - this is causing issues in Ghost because of the missing data - this migration backfills the columns for products where they are paid but do not currently contain values due to the bug above with the values for the default tier we usually use --- ...-12-05-backfill-missed-products-columns.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 ghost/core/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js diff --git a/ghost/core/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js b/ghost/core/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js new file mode 100644 index 00000000000..fd9b1be0bc0 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.21/2022-10-25-12-05-backfill-missed-products-columns.js @@ -0,0 +1,35 @@ +const logging = require('@tryghost/logging'); +const {createTransactionalMigration} = require('../../utils'); + +module.exports = createTransactionalMigration( + async function up(knex) { + logging.info(`Fixing currency/monthly_price/yearly_price values for default paid tiers`); + + const currencyUpdated = await knex('products') + .update('currency', 'usd') + .where({ + currency: null, + type: 'paid' + }); + logging.info(`Updated ${currencyUpdated} tier(s) where currency=null, type=paid to currency=USD`); + + const monthlyPriceUpdated = await knex('products') + .update('monthly_price', 500) + .where({ + monthly_price: null, + type: 'paid' + }); + logging.info(`Updated ${monthlyPriceUpdated} tier(s) where monthly_price=null, type=paid to monthly_price=500`); + + const yearlyPriceUpdated = await knex('products') + .update('yearly_price', 5000) + .where({ + yearly_price: null, + type: 'paid' + }); + logging.info(`Updated ${yearlyPriceUpdated} tier(s) where yearly_price=null, type=paid to yearly_price=5000`); + }, + async function down(/* knex */) { + // no-op: we don't want to revert to bad data + } +); From d034526fe6e95e4a044622df36adf77fa5bb1889 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 02:54:43 +0000 Subject: [PATCH 05/16] Update dependency supertest to v6.3.1 --- ghost/core/package.json | 2 +- ghost/mw-vhost/package.json | 2 +- yarn.lock | 22 +++++++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ghost/core/package.json b/ghost/core/package.json index 061564d8f97..a85067402d7 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -211,7 +211,7 @@ "rewire": "6.0.0", "should": "13.2.3", "sinon": "14.0.1", - "supertest": "6.3.0", + "supertest": "6.3.1", "tmp": "0.2.1" }, "resolutions": { diff --git a/ghost/mw-vhost/package.json b/ghost/mw-vhost/package.json index 67b080c3136..a5de1cac73a 100644 --- a/ghost/mw-vhost/package.json +++ b/ghost/mw-vhost/package.json @@ -18,6 +18,6 @@ "devDependencies": { "c8": "7.12.0", "mocha": "10.1.0", - "supertest": "6.3.0" + "supertest": "6.3.1" } } diff --git a/yarn.lock b/yarn.lock index 811c4740ba3..5ba2e12ac9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23494,7 +23494,7 @@ semver@7.3.7: dependencies: lru-cache "^6.0.0" -semver@7.3.8, semver@^7.0.0, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: +semver@7.3.8, semver@^7.0.0, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== @@ -24657,10 +24657,10 @@ sum-up@^1.0.1: dependencies: chalk "^1.0.0" -superagent@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.2.tgz#23e753da16f2a0a5afa2fe2b41cafed5b22a5bde" - integrity sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q== +superagent@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.3.tgz#15c8ec5611a1f01386994cfeeda5aa138bcb7b17" + integrity sha512-oBC+aNsCjzzjmO5AOPBPFS+Z7HPzlx+DQr/aHwM08kI+R24gsDmAS1LMfza1fK+P+SKlTAoNZpOvooE/pRO1HA== dependencies: component-emitter "^1.3.0" cookiejar "^2.1.3" @@ -24671,15 +24671,15 @@ superagent@^8.0.0: methods "^1.1.2" mime "2.6.0" qs "^6.11.0" - semver "^7.3.7" + semver "^7.3.8" -supertest@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.0.tgz#06c21234ff0be9e398ba19f7a75f237930d57442" - integrity sha512-QgWju1cNoacP81Rv88NKkQ4oXTzGg0eNZtOoxp1ROpbS4OHY/eK5b8meShuFtdni161o5X0VQvgo7ErVyKK+Ow== +supertest@6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.1.tgz#a8ad362fc6f323c88730ac191ce30427dc869088" + integrity sha512-hRohNeIfk/cA48Cxpa/w48hktP6ZaRqXb0QV5rLvW0C7paRsBU3Q5zydzYrslOJtj/gd48qx540jKtcs6vG1fQ== dependencies: methods "^1.1.2" - superagent "^8.0.0" + superagent "^8.0.3" supports-color@1.2.0: version "1.2.0" From cdd65f25acefc76c1f5f5d0da2c38239bba7d982 Mon Sep 17 00:00:00 2001 From: Naz Date: Wed, 26 Oct 2022 14:20:19 +0800 Subject: [PATCH 06/16] Migrated members importer to use tiers refs https://github.com/TryGhost/Team/issues/2077 - The "productRepository" methods have been deprecated in favor of "tiers" and "Tiers API". - The changes migrated usages of "productRepository.getDefaultProduct" to Tiers API's "readDefaultTier" --- .../core/core/server/services/members/service.js | 6 +++--- ghost/members-importer/lib/importer.js | 2 +- ghost/tiers/lib/InMemoryTierRepository.js | 2 ++ ghost/tiers/lib/TiersAPI.js | 15 +++++++++++++++ ghost/tiers/test/TiersAPI.test.js | 6 ++++++ 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/ghost/core/core/server/services/members/service.js b/ghost/core/core/server/services/members/service.js index 9b66c07473c..7a58252234f 100644 --- a/ghost/core/core/server/services/members/service.js +++ b/ghost/core/core/server/services/members/service.js @@ -16,6 +16,7 @@ const config = require('../../../shared/config'); const models = require('../../models'); const {GhostMailer} = require('../mail'); const jobsService = require('../jobs'); +const tiersService = require('../tiers'); const VerificationTrigger = require('@tryghost/verification-trigger'); const DatabaseInfo = require('@tryghost/database-info'); const settingsHelpers = require('../settings-helpers'); @@ -51,9 +52,8 @@ const membersImporter = new MembersCSVImporter({ const api = await module.exports.api; return api.members; }, - getDefaultTier: async () => { - const api = await module.exports.api; - return api.productRepository.getDefaultProduct; + getDefaultTier: () => { + return tiersService.api.readDefaultTier(); }, sendEmail: ghostMailer.send.bind(ghostMailer), isSet: labsService.isSet.bind(labsService), diff --git a/ghost/members-importer/lib/importer.js b/ghost/members-importer/lib/importer.js index 66c9e1c8566..f2ad24f50a0 100644 --- a/ghost/members-importer/lib/importer.js +++ b/ghost/members-importer/lib/importer.js @@ -30,7 +30,7 @@ module.exports = class MembersCSVImporter { * @param {string} options.storagePath - The path to store CSV's in before importing * @param {Function} options.getTimezone - function returning currently configured timezone * @param {() => Object} options.getMembersRepository - member model access instance for data access and manipulation - * @param {() => Object} options.getDefaultTier - async function returning default Member Tier + * @param {() => Promise} options.getDefaultTier - async function returning default Member Tier * @param {Function} options.sendEmail - function sending an email * @param {(string) => boolean} options.isSet - Method checking if specific feature is enabled * @param {({job, offloaded}) => void} options.addJob - Method registering an async job diff --git a/ghost/tiers/lib/InMemoryTierRepository.js b/ghost/tiers/lib/InMemoryTierRepository.js index 5b6d461704e..682efcaacf2 100644 --- a/ghost/tiers/lib/InMemoryTierRepository.js +++ b/ghost/tiers/lib/InMemoryTierRepository.js @@ -17,6 +17,8 @@ class InMemoryTierRepository { toPrimitive(tier) { return { ...tier, + active: (tier.status === 'active'), + type: tier.type, id: tier.id.toHexString() }; } diff --git a/ghost/tiers/lib/TiersAPI.js b/ghost/tiers/lib/TiersAPI.js index 2693f03d059..32db320a70e 100644 --- a/ghost/tiers/lib/TiersAPI.js +++ b/ghost/tiers/lib/TiersAPI.js @@ -76,6 +76,21 @@ module.exports = class TiersAPI { return tier; } + /** + * Fetches the default tier + * @param {object} [options] + * @returns {Promise} + */ + async readDefaultTier(options = {}) { + const [defaultTier] = await this.#repository.getAll({ + filter: 'type:paid+active:true', + limit: 1, + ...options + }); + + return defaultTier; + } + /** * @param {string} id * @param {object} data diff --git a/ghost/tiers/test/TiersAPI.test.js b/ghost/tiers/test/TiersAPI.test.js index e430810adf0..8ec29240695 100644 --- a/ghost/tiers/test/TiersAPI.test.js +++ b/ghost/tiers/test/TiersAPI.test.js @@ -72,4 +72,10 @@ describe('TiersAPI', function () { assert(page.data.length === 2); assert(page.meta.pagination.total === 2); }); + + it('Can read a default tier', async function () { + const defaultTier = await api.readDefaultTier(); + + assert.equal(defaultTier?.name, 'My testing Tier'); + }); }); From b79006d7f3ae25be5a9bb42c58d2d45592be5309 Mon Sep 17 00:00:00 2001 From: Aileen Nowak Date: Tue, 25 Oct 2022 16:02:35 +0100 Subject: [PATCH 07/16] =?UTF-8?q?=E2=9C=A8=20Added=20Ghost=20Explore=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no issue - bumps Ghost Explore integrated app to GA from alpha --- ghost/admin/app/components/gh-nav-menu/main.hbs | 8 +++----- ghost/admin/app/routes/explore/index.js | 4 ++-- ghost/admin/app/services/feature.js | 1 - ghost/admin/app/templates/application.hbs | 5 ++--- ghost/admin/app/templates/settings/labs.hbs | 13 ------------- ghost/core/core/shared/labs.js | 1 - 6 files changed, 7 insertions(+), 25 deletions(-) diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs index d5cbff5a300..5b14387dda9 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ b/ghost/admin/app/components/gh-nav-menu/main.hbs @@ -33,11 +33,9 @@ {{#if (gh-user-can-admin this.session.user)}} - {{#if (feature "exploreApp")}} -
  • - {{svg-jar "globe-simple"}} Explore -
  • - {{/if}} +
  • + {{svg-jar "globe-simple"}} Explore +
  • {{/if}}
      diff --git a/ghost/admin/app/routes/explore/index.js b/ghost/admin/app/routes/explore/index.js index bfc607d0574..aa621b66f23 100644 --- a/ghost/admin/app/routes/explore/index.js +++ b/ghost/admin/app/routes/explore/index.js @@ -15,13 +15,13 @@ export default class ExploreIndexRoute extends AuthenticatedRoute { // older versions of Ghost where the `connect` part lives in the // explore route directly. By using the query param, we avoid causing // a 404 and handle the redirect here. - if (transition.to?.queryParams?.new === 'true' || !this.feature.exploreApp) { + if (transition.to?.queryParams?.new === 'true') { this.explore.isIframeTransition = false; return this.router.transitionTo('explore.connect'); } // Ensure the explore window is set to open - if (this.feature.get('exploreApp') && transition.to?.localName === 'index' && !this.explore.exploreWindowOpen) { + if (transition.to?.localName === 'index' && !this.explore.exploreWindowOpen) { this.explore.openExploreWindow(this.router.currentURL); } } diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js index af84c9cf338..34e8238f45b 100644 --- a/ghost/admin/app/services/feature.js +++ b/ghost/admin/app/services/feature.js @@ -65,7 +65,6 @@ export default class FeatureService extends Service { @feature('emailAlerts') emailAlerts; @feature('sourceAttribution') sourceAttribution; @feature('lexicalEditor') lexicalEditor; - @feature('exploreApp') exploreApp; @feature('audienceFeedback') audienceFeedback; @feature('fixNewsletterLinks') fixNewsletterLinks; diff --git a/ghost/admin/app/templates/application.hbs b/ghost/admin/app/templates/application.hbs index fecceb6f3fa..a2e6a071846 100644 --- a/ghost/admin/app/templates/application.hbs +++ b/ghost/admin/app/templates/application.hbs @@ -14,9 +14,8 @@ {{#if this.showBilling}} {{/if}} - {{#if (feature "exploreApp")}} - - {{/if}} + + diff --git a/ghost/admin/app/templates/settings/labs.hbs b/ghost/admin/app/templates/settings/labs.hbs index 248803e1abf..a42cadffeb3 100644 --- a/ghost/admin/app/templates/settings/labs.hbs +++ b/ghost/admin/app/templates/settings/labs.hbs @@ -258,19 +258,6 @@
    -
    -
    -
    -

    Explore app

    -

    - Enable the Explore iframe app. -

    -
    -
    - -
    -
    -
    diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 02ce9685a26..ca551663786 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -35,7 +35,6 @@ const ALPHA_FEATURES = [ 'urlCache', 'beforeAfterCard', 'lexicalEditor', - 'exploreApp', 'audienceFeedback' ]; From 4c6a57c8f3817030f7af1142a6efe9b3b47ede44 Mon Sep 17 00:00:00 2001 From: James Morris Date: Wed, 26 Oct 2022 11:59:57 +0100 Subject: [PATCH 08/16] Updated the feedback copy for Portal modal based on recent feedback refs https://github.com/TryGhost/Team/issues/2170 --- ghost/portal/src/components/pages/FeedbackPage.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ghost/portal/src/components/pages/FeedbackPage.js b/ghost/portal/src/components/pages/FeedbackPage.js index bbe95ccbd2b..0a8ae5694da 100644 --- a/ghost/portal/src/components/pages/FeedbackPage.js +++ b/ghost/portal/src/components/pages/FeedbackPage.js @@ -92,7 +92,6 @@ export default function FeedbackPage() { const positive = score === 1; const icon = positive ? : ; - const text = positive ? 'It has been noted that you want to see more posts like this.' : 'It has been noted that you want to see less posts like this.'; return (
    @@ -102,7 +101,7 @@ export default function FeedbackPage() { {icon}

    Thanks for the feedback!

    -

    {text}

    +

    Your input helps shape what gets published.

    Date: Wed, 26 Oct 2022 12:17:07 +0100 Subject: [PATCH 09/16] Updated the copy for the audience feedback in preview and email based on feedback refs https://github.com/TryGhost/Team/issues/2171 --- ghost/admin/app/components/modals/newsletters/edit/preview.hbs | 2 +- ghost/core/core/server/services/mega/feedback-buttons.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/admin/app/components/modals/newsletters/edit/preview.hbs b/ghost/admin/app/components/modals/newsletters/edit/preview.hbs index a400dcaeae4..76061107371 100644 --- a/ghost/admin/app/components/modals/newsletters/edit/preview.hbs +++ b/ghost/admin/app/components/modals/newsletters/edit/preview.hbs @@ -43,7 +43,7 @@ {{#if (and @newsletter.feedbackEnabled (feature "audienceFeedback"))}}
    -

    What did you think of this post?

    +

    Give feedback on this post

    { return (` -

    What did you think of this post?

    +

    Give feedback on this post

    ${likeButtonHtml} From 3cb56bad94a70064df57a170efa4025e31d56f41 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Wed, 26 Oct 2022 14:12:34 +0200 Subject: [PATCH 10/16] Added confirmation step when sending feedback fixes https://github.com/TryGhost/Team/issues/2161 Confirmation step is skipped when already signed in. --- .../src/components/pages/FeedbackPage.js | 187 +++++++++++++++--- 1 file changed, 156 insertions(+), 31 deletions(-) diff --git a/ghost/portal/src/components/pages/FeedbackPage.js b/ghost/portal/src/components/pages/FeedbackPage.js index 0a8ae5694da..98015cda38d 100644 --- a/ghost/portal/src/components/pages/FeedbackPage.js +++ b/ghost/portal/src/components/pages/FeedbackPage.js @@ -1,13 +1,13 @@ -import ActionButton from '../common/ActionButton'; -import AppContext from '../../AppContext'; import {useContext, useEffect, useState} from 'react'; +import AppContext from '../../AppContext'; +import {ReactComponent as ThumbDownIcon} from '../../images/icons/thumbs-down.svg'; +import {ReactComponent as ThumbUpIcon} from '../../images/icons/thumbs-up.svg'; +import {ReactComponent as WarningIcon} from '../../images/icons/warning-fill.svg'; import setupGhostApi from '../../utils/api'; +import {HumanReadableError} from '../../utils/errors'; +import ActionButton from '../common/ActionButton'; import CloseButton from '../common/CloseButton'; import LoadingPage from './LoadingPage'; -import {HumanReadableError} from '../../utils/errors'; -import {ReactComponent as ThumbUpIcon} from '../../images/icons/thumbs-up.svg'; -import {ReactComponent as ThumbDownIcon} from '../../images/icons/thumbs-down.svg'; -import {ReactComponent as WarningIcon} from '../../images/icons/warning-fill.svg'; const React = require('react'); @@ -31,6 +31,60 @@ export const FeedbackPageStyles = ` .gh-portal-feedback .gh-portal-text-center { padding: 15px 0; } + + .gh-portal-confirm-title { + line-height: inherit; + text-align: left; + box-sizing: border-box; + margin: 0; + margin-bottom: .4rem; + font-size: 24px; + font-weight: 700; + letter-spacing: -.018em; + } + + .gh-portal-confirm-description { + font-size: 1.5rem; + text-align: left; + box-sizing: border-box; + margin: 0; + line-height: 2.25rem; + padding-right: 1.6rem; + padding-left: 0; + color: rgb(115, 115, 115); + } + + .gh-portal-confirm-buttons { + line-height: inherit; + font-size: 1.5rem; + text-align: left; + box-sizing: border-box; + margin-top: 4rem; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 1.6rem; + flex-direction: row; + } + + .gh-portal-confirm-button-secundary { + -webkit-text-size-adjust: 100%; + tab-size: 4; + box-sizing: border-box; + border: 0 solid #e5e7eb; + line-height: inherit; + margin: 0; + padding: 0; + text-transform: none; + cursor: pointer; + -webkit-appearance: button; + background-color: initial; + background-image: none; + font-size: 1.4rem; + font-weight: 500; + color: rgb(115, 115, 115); + border: 0; + } `; function ErrorPage({error}) { @@ -61,36 +115,64 @@ function ErrorPage({error}) { ); } -export default function FeedbackPage() { - const {site, pageData, brandColor, onAction} = useContext(AppContext); - const {uuid, postId, score} = pageData; - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); +const ConfirmDialog = ({onConfirm, loading, positive}) => { + const {onAction, brandColor} = useContext(AppContext); - useEffect(() => { - const ghostApi = setupGhostApi({siteUrl: site.url}); - (async () => { - await ghostApi.feedback.add({uuid, postId, score}); + const stopPropagation = (event) => { + event.stopPropagation(); + }; - // Clear query params once finished - setLoading(false); - })().catch((e) => { - const text = HumanReadableError.getMessageFromError(e, 'There was a problem submitting your feedback. Please try again or contact the site owner.'); - setError(text); - }); - }, [uuid, postId, score, site.url]); + const close = (event) => { + onAction('closePopup'); + }; + + const submit = async (event) => { + event.stopPropagation(); + + await onConfirm(); + }; + + const title = positive ? 'You want more posts like this?' : 'You want less posts like this?'; + + return ( +
    +

    {title}

    +

    Your feedback will be sent to the owner of this site.

    +
    + + + +
    + close(false)} /> +
    + ); +}; + +async function sendFeedback({siteUrl, uuid, postId, score}) { + const ghostApi = setupGhostApi({siteUrl}); + await ghostApi.feedback.add({uuid, postId, score}); +} + +const LoadingFeedbackView = ({action}) => { + useEffect(() => { + action(); + }); - // Case: failed - if (error) { - return ; - } + return ; +}; - // Case: still loading - if (loading) { - return ; - } +const ConfirmFeedback = ({positive}) => { + const {onAction, brandColor} = useContext(AppContext); - const positive = score === 1; const icon = positive ? : ; return ( @@ -115,4 +197,47 @@ export default function FeedbackPage() { /> ); +}; + +export default function FeedbackPage() { + const {site, pageData, member} = useContext(AppContext); + const {uuid, postId, score} = pageData; + const positive = score === 1; + + const isLoggedIn = !!member; + + const [confirmed, setConfirmed] = useState(isLoggedIn); + const [loading, setLoading] = useState(isLoggedIn); + const [error, setError] = useState(null); + + const doSendFeedback = async () => { + setLoading(true); + try { + await sendFeedback({siteUrl: site.url, uuid, postId, score}); + } catch (e) { + const text = HumanReadableError.getMessageFromError(e, 'There was a problem submitting your feedback. Please try again or contact the site owner.'); + setError(text); + } + setLoading(false); + }; + + const onConfirm = async (event) => { + await doSendFeedback(); + setConfirmed(true); + }; + + // Case: failed + if (error) { + return ; + } + + if (!confirmed) { + return (); + } else { + if (loading) { + return ; + } + } + + return (); } From 310459c3b938520c817d633be8cebc0f70f0b354 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Wed, 26 Oct 2022 14:26:57 +0200 Subject: [PATCH 11/16] Cleaned up FeedbackPage --- ghost/portal/src/components/pages/FeedbackPage.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ghost/portal/src/components/pages/FeedbackPage.js b/ghost/portal/src/components/pages/FeedbackPage.js index 98015cda38d..486e639bd00 100644 --- a/ghost/portal/src/components/pages/FeedbackPage.js +++ b/ghost/portal/src/components/pages/FeedbackPage.js @@ -55,7 +55,6 @@ export const FeedbackPageStyles = ` } .gh-portal-confirm-buttons { - line-height: inherit; font-size: 1.5rem; text-align: left; box-sizing: border-box; @@ -68,10 +67,8 @@ export const FeedbackPageStyles = ` } .gh-portal-confirm-button-secundary { - -webkit-text-size-adjust: 100%; tab-size: 4; box-sizing: border-box; - border: 0 solid #e5e7eb; line-height: inherit; margin: 0; padding: 0; @@ -122,7 +119,7 @@ const ConfirmDialog = ({onConfirm, loading, positive}) => { event.stopPropagation(); }; - const close = (event) => { + const close = () => { onAction('closePopup'); }; @@ -147,7 +144,6 @@ const ConfirmDialog = ({onConfirm, loading, positive}) => { label={'Yes!'} isRunning={loading} tabindex='3' - classes={'sticky bottom'} /> From 4501c828209f0dff746d8b6da0507d3f6cb16c75 Mon Sep 17 00:00:00 2001 From: Simon Backx Date: Wed, 26 Oct 2022 14:32:31 +0200 Subject: [PATCH 12/16] Fixed breadcrumbs when going from analytics to member refs https://github.com/TryGhost/Team/issues/2140 --- ghost/admin/app/routes/member.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ghost/admin/app/routes/member.js b/ghost/admin/app/routes/member.js index 368812e3489..11713005885 100644 --- a/ghost/admin/app/routes/member.js +++ b/ghost/admin/app/routes/member.js @@ -34,7 +34,19 @@ export default class MembersRoute extends AdminRoute { controller.fetchMemberTask.perform(member.id); } - if (transition.from?.name === 'members.index' && transition.from?.parent?.name === 'members') { + if (transition.from?.name === 'posts.analytics') { + // Sadly transition.from.params is not reliable to use (not populated on transitions) + const oldParams = transition.router?.oldState?.params['posts.analytics'] ?? {}; + + // We need to store analytics in 'this' to have it accessible for the member route + this.fromAnalytics = Object.values(oldParams); + controller.fromAnalytics = this.fromAnalytics; + } else if (transition.from?.metadata?.fromAnalytics) { + // Handle returning from member route + const fromAnalytics = transition.from?.metadata.fromAnalytics ?? null; + controller.fromAnalytics = fromAnalytics; + this.fromAnalytics = fromAnalytics; + } else if (transition.from?.name === 'members.index' && transition.from?.parent?.name === 'members') { const fromAnalytics = transition.from?.parent?.metadata.fromAnalytics ?? null; controller.fromAnalytics = fromAnalytics; this.fromAnalytics = fromAnalytics; From b7eadb748d2f97639c967b45408c513ed8832466 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Wed, 26 Oct 2022 15:05:21 +0200 Subject: [PATCH 13/16] Added analytics settings icon refs https://github.com/TryGhost/Team/issues/2168 --- ghost/admin/public/assets/icons/chart.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 ghost/admin/public/assets/icons/chart.svg diff --git a/ghost/admin/public/assets/icons/chart.svg b/ghost/admin/public/assets/icons/chart.svg new file mode 100644 index 00000000000..02ebac8f453 --- /dev/null +++ b/ghost/admin/public/assets/icons/chart.svg @@ -0,0 +1 @@ + \ No newline at end of file From 957d789857f48f0c04d1391b7179a2ba71e81056 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Wed, 26 Oct 2022 15:13:05 +0200 Subject: [PATCH 14/16] Added mock component for analytics settings page refs https://github.com/TryGhost/Team/issues/2168 --- .../app/controllers/settings/analytics.js | 170 ++++++++++++++++++ ghost/admin/app/router.js | 1 + ghost/admin/app/routes/settings/analytics.js | 73 ++++++++ .../app/templates/settings/analytics.hbs | 96 ++++++++++ 4 files changed, 340 insertions(+) create mode 100644 ghost/admin/app/controllers/settings/analytics.js create mode 100644 ghost/admin/app/routes/settings/analytics.js create mode 100644 ghost/admin/app/templates/settings/analytics.hbs diff --git a/ghost/admin/app/controllers/settings/analytics.js b/ghost/admin/app/controllers/settings/analytics.js new file mode 100644 index 00000000000..0bd6769027a --- /dev/null +++ b/ghost/admin/app/controllers/settings/analytics.js @@ -0,0 +1,170 @@ +import classic from 'ember-classic-decorator'; +import {action, computed} from '@ember/object'; +import {inject as service} from '@ember/service'; +/* eslint-disable ghost/ember/alias-model-in-controller */ +import Controller from '@ember/controller'; +import generatePassword from 'ghost-admin/utils/password-generator'; +import { + IMAGE_EXTENSIONS, + IMAGE_MIME_TYPES +} from 'ghost-admin/components/gh-image-uploader'; +import {TrackedObject} from 'tracked-built-ins'; +import {run} from '@ember/runloop'; +import {task} from 'ember-concurrency'; +import {tracked} from '@glimmer/tracking'; + +function randomPassword() { + let word = generatePassword(6); + let randomN = Math.floor(Math.random() * 1000); + + return word + randomN; +} + +@classic +export default class AnalyticsController extends Controller { + @service config; + @service ghostPaths; + @service notifications; + @service session; + @service settings; + @service frontend; + @service ui; + + @tracked scratchValues = new TrackedObject(); + + availableTimezones = this.config.availableTimezones; + imageExtensions = IMAGE_EXTENSIONS; + imageMimeTypes = IMAGE_MIME_TYPES; + + @computed('config.blogUrl', 'settings.publicHash') + get privateRSSUrl() { + let blogUrl = this.config.blogUrl; + let publicHash = this.settings.publicHash; + + return `${blogUrl}/${publicHash}/rss`; + } + + @action + save() { + this.saveTask.perform(); + } + + @action + setTimezone(timezone) { + this.settings.timezone = timezone.name; + } + + @action + removeImage(image) { + // setting `null` here will error as the server treats it as "null" + this.settings[image] = ''; + } + + /** + * Opens a file selection dialog - Triggered by "Upload Image" buttons, + * searches for the hidden file input within the .gh-setting element + * containing the clicked button then simulates a click + * @param {MouseEvent} event - MouseEvent fired by the button click + */ + @action + triggerFileDialog(event) { + event?.target.closest('.gh-setting-action')?.querySelector('input[type="file"]')?.click(); + } + + /** + * Fired after an image upload completes + * @param {string} property - Property name to be set on `this.settings` + * @param {UploadResult[]} results - Array of UploadResult objects + * @return {string} The URL that was set on `this.settings.property` + */ + @action + imageUploaded(property, results) { + if (results[0]) { + return this.settings[property] = results[0].url; + } + } + + @action + toggleIsPrivate(isPrivate) { + let settings = this.settings; + + settings.isPrivate = isPrivate; + settings.errors.remove('password'); + + let changedAttrs = settings.changedAttributes(); + + // set a new random password when isPrivate is enabled + if (isPrivate && changedAttrs.isPrivate) { + settings.password = randomPassword(); + + // reset the password when isPrivate is disabled + } else if (changedAttrs.password) { + settings.password = changedAttrs.password[0]; + } + } + + @action + setScratchValue(property, value) { + this.scratchValues[property] = value; + } + + clearScratchValues() { + this.scratchValues = new TrackedObject(); + } + + _deleteTheme() { + let theme = this.store.peekRecord('theme', this.themeToDelete.name); + + if (!theme) { + return; + } + + return theme.destroyRecord().catch((error) => { + this.notifications.showAPIError(error); + }); + } + + @task + *saveTask() { + let notifications = this.notifications; + let config = this.config; + + try { + let changedAttrs = this.settings.changedAttributes(); + let settings = yield this.settings.save(); + + this.clearScratchValues(); + + config.set('blogTitle', settings.title); + + if (changedAttrs.password) { + this.frontend.loginIfNeeded(); + } + + // this forces the document title to recompute after a blog title change + this.ui.updateDocumentTitle(); + + return settings; + } catch (error) { + if (error) { + notifications.showAPIError(error, {key: 'settings.save'}); + } + throw error; + } + } + + @action + saveViaKeyboard(event) { + event.preventDefault(); + + // trigger any set-on-blur actions + const focusedElement = document.activeElement; + focusedElement?.blur(); + + // schedule save for when set-on-blur actions have finished + run.schedule('actions', this, function () { + focusedElement?.focus(); + this.saveTask.perform(); + }); + } +} diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 717ef4c2ac0..6814e567455 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -53,6 +53,7 @@ Router.map(function () { this.route('settings.membership', {path: '/settings/members'}); this.route('settings.code-injection', {path: '/settings/code-injection'}); this.route('settings.history', {path: '/settings/history'}); + this.route('settings.analytics', {path: '/settings/analytics'}); // redirect from old /settings/members-email to /settings/newsletters this.route('settings.members-email', {path: '/settings/members-email'}); diff --git a/ghost/admin/app/routes/settings/analytics.js b/ghost/admin/app/routes/settings/analytics.js new file mode 100644 index 00000000000..665c8edfd60 --- /dev/null +++ b/ghost/admin/app/routes/settings/analytics.js @@ -0,0 +1,73 @@ +import AdminRoute from 'ghost-admin/routes/admin'; +import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes'; +import RSVP from 'rsvp'; +import {action} from '@ember/object'; +import {inject as service} from '@ember/service'; + +export default class AnalyticsSettingsRoute extends AdminRoute { + @service modals; + @service config; + @service settings; + + model() { + return RSVP.hash({ + settings: this.settings.reload() + }); + } + + deactivate() { + this.confirmModal = null; + this.hasConfirmed = false; + } + + @action + async willTransition(transition) { + if (this.hasConfirmed) { + return true; + } + + transition.abort(); + + // wait for any existing confirm modal to be closed before allowing transition + if (this.confirmModal) { + return; + } + + if (this.controller.saveTask?.isRunning) { + await this.controller.saveTask.last; + } + + const shouldLeave = await this.confirmUnsavedChanges(); + + if (shouldLeave) { + this.settings.rollbackAttributes(); + this.hasConfirmed = true; + return transition.retry(); + } + } + + async confirmUnsavedChanges() { + if (this.settings.hasDirtyAttributes) { + this.confirmModal = this.modals + .open(ConfirmUnsavedChangesModal) + .finally(() => { + this.confirmModal = null; + }); + + return this.confirmModal; + } + + return true; + } + + @action + reloadSettings() { + return this.settings.reload(); + } + + buildRouteInfoMetadata() { + return { + titleToken: 'Settings - Analytics' + }; + } +} diff --git a/ghost/admin/app/templates/settings/analytics.hbs b/ghost/admin/app/templates/settings/analytics.hbs new file mode 100644 index 00000000000..ee5d9013968 --- /dev/null +++ b/ghost/admin/app/templates/settings/analytics.hbs @@ -0,0 +1,96 @@ +
    + +
    +
    + + Settings + + {{svg-jar "arrow-right-small"}} Analytics +
    +

    + Analytics +

    +
    +
    + +
    +
    + +
    +
    +

    Newsletters

    +
    +
    +
    +
    +

    Track newsletter opens

    +

    + Record when member opens a newsletter email +

    +
    +
    + +
    +
    +
    + +
    +
    +
    +

    Track newsletter clicks

    +

    + Record when a member clicks on any link in a newsletter email +

    +
    +
    + +
    +
    +
    +
    +
    + +
    +

    Sources

    +
    +
    +
    +
    +

    Track member sources

    +

    + Record the post/page and referring domain for signups and subscriptions +

    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +{{outlet}} \ No newline at end of file From 9719f2545275ca239f6f9616203f88d54ca1da08 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Wed, 26 Oct 2022 15:13:35 +0200 Subject: [PATCH 15/16] Added link to analytics settings to the settings page refs https://github.com/TryGhost/Team/issues/2168 --- ghost/admin/app/templates/settings.hbs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ghost/admin/app/templates/settings.hbs b/ghost/admin/app/templates/settings.hbs index 0431ed393b8..bb029087c9b 100644 --- a/ghost/admin/app/templates/settings.hbs +++ b/ghost/admin/app/templates/settings.hbs @@ -67,6 +67,15 @@

    Customize emails and set email addresses

    + {{#if (feature 'sourceAttribution')}} + + {{svg-jar "chart"}} +
    +

    Analytics

    +

    Set up what you track for emails and members

    +
    +
    + {{/if}}
    Advanced
    From 2bacff028bc477eb19080d3d895adc58fe332a3b Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Wed, 26 Oct 2022 15:15:03 +0200 Subject: [PATCH 16/16] Updated analytics settings icon refs https://github.com/TryGhost/Team/issues/2168 --- ghost/admin/public/assets/icons/chart.svg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ghost/admin/public/assets/icons/chart.svg b/ghost/admin/public/assets/icons/chart.svg index 02ebac8f453..4190689f9ba 100644 --- a/ghost/admin/public/assets/icons/chart.svg +++ b/ghost/admin/public/assets/icons/chart.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file