Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ jobs:
- name: Run migrations
working-directory: ghost/core
run: yarn knex-migrator init
env:
NODE_OPTIONS: "--import=tsx"

- name: Setup Playwright
uses: ./.github/actions/setup-playwright
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,6 @@ const features: Feature[] = [{
title: 'Sniper Links',
description: 'Enable mail app links on signup/signin',
flag: 'sniperlinks'
}, {
title: 'Paid Breakdown Charts',
description: 'Show paid member change and subscription cadence breakdown charts on the Growth page',
flag: 'paidBreakdownCharts'
}];

const AlphaFeatures: React.FC = () => {
Expand Down
364 changes: 17 additions & 347 deletions apps/stats/src/views/Stats/Growth/components/growth-kpis.tsx

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions apps/stats/src/views/Stats/Growth/growth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {getPeriodText} from '@src/utils/chart-helpers';
import {useAppContext} from '@src/app';
import {useGlobalData} from '@src/providers/global-data-provider';
import {useGrowthStats} from '@hooks/use-growth-stats';
import {useLabsFlag} from '@hooks/use-labs-flag';
import {useNavigate, useSearchParams} from '@tryghost/admin-x-framework';
import {useTopPostsStatsWithRange} from '@hooks/use-top-posts-stats-with-range';
import type {TopPostStatItem} from '@tryghost/admin-x-framework/api/stats';
Expand Down Expand Up @@ -52,7 +51,6 @@ const Growth: React.FC = () => {
const [selectedContentType, setSelectedContentType] = useState<ContentType>(CONTENT_TYPES.POSTS_AND_PAGES);
const [searchParams] = useSearchParams();
const {appSettings} = useAppContext();
const paidBreakdownChartsEnabled = useLabsFlag('paidBreakdownCharts');

// Get the initial tab from URL search parameters
const initialTab = searchParams.get('tab') || 'total-members';
Expand Down Expand Up @@ -156,13 +154,12 @@ const Growth: React.FC = () => {
currencySymbol={currencySymbol}
initialTab={initialTab}
isLoading={isPageLoading}
subscriptionData={subscriptionData}
totals={totals}
onTabChange={setCurrentKpiTab}
/>
</CardContent>
</Card>
{appSettings?.paidMembersEnabled && currentKpiTab === 'paid-members' && paidBreakdownChartsEnabled && (
{appSettings?.paidMembersEnabled && currentKpiTab === 'paid-members' && (
<div className='grid grid-cols-1 gap-6 lg:grid-cols-2 xl:grid-cols-[2fr_minmax(460px,1fr)]'>
<PaidMembersChangeChart
isLoading={isPageLoading}
Expand Down
2 changes: 1 addition & 1 deletion compose.dev.analytics.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

services:
analytics:
image: ghost/traffic-analytics:1.0.42
image: ghost/traffic-analytics:1.0.43
container_name: ghost-dev-analytics
platform: linux/amd64
command: ["node", "--enable-source-maps", "dist/server.js"]
Expand Down
2 changes: 1 addition & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ services:

analytics:
profiles: [ analytics, all ]
image: ghost/traffic-analytics:1.0.42
image: ghost/traffic-analytics:1.0.43
platform: linux/amd64
command: ["node", "--enable-source-maps", "dist/server.js"]
entrypoint: [ "/app/entrypoint.sh" ]
Expand Down
3 changes: 2 additions & 1 deletion e2e/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
working_dir: /home/ghost
command: ["yarn", "knex-migrator", "init"]
environment:
NODE_OPTIONS: "--import=tsx"
database__client: mysql2
database__connection__host: mysql
database__connection__user: root
Expand Down Expand Up @@ -52,7 +53,7 @@ services:
condition: service_healthy

analytics:
image: ghost/traffic-analytics:1.0.42
image: ghost/traffic-analytics:1.0.43
platform: linux/amd64
command: ["node", "--enable-source-maps", "dist/server.js"]
entrypoint: [ "/app/entrypoint.sh" ]
Expand Down
2 changes: 1 addition & 1 deletion ghost/admin/app/components/gh-canvas-header.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div
{{on-scroll this.onScroll scrollContainer=".gh-main"}}
{{on-scroll this.onScroll scrollContainer=".shade-admin main:not(main main)"}}
...attributes
>
<header class="gh-canvas-header-content">
Expand Down
2 changes: 1 addition & 1 deletion ghost/admin/app/templates/mentions.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
{{/if}}
<GhInfinityLoader
@infinityModel={{this.mentionsInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
</div>
{{#if this.mentionsInfinityModel}}
Expand Down
6 changes: 3 additions & 3 deletions ghost/admin/app/templates/pages.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,19 @@
{{#if @model.scheduledInfinityModel}}
<GhInfinityLoader
@infinityModel={{@model.scheduledInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
{{/if}}
{{#if (and @model.draftInfinityModel (or (not @model.scheduledInfinityModel) (and @model.scheduledInfinityModel @model.scheduledInfinityModel.reachedInfinity)))}}
<GhInfinityLoader
@infinityModel={{@model.draftInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
{{/if}}
{{#if (and @model.publishedAndSentInfinityModel (and (or (not @model.scheduledInfinityModel) @model.scheduledInfinityModel.reachedInfinity) (or (not @model.draftInfinityModel) @model.draftInfinityModel.reachedInfinity)))}}
<GhInfinityLoader
@infinityModel={{@model.publishedAndSentInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
{{/if}}

Expand Down
6 changes: 3 additions & 3 deletions ghost/admin/app/templates/posts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,19 @@
{{#if @model.scheduledInfinityModel}}
<GhInfinityLoader
@infinityModel={{@model.scheduledInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
{{/if}}
{{#if (and @model.draftInfinityModel (or (not @model.scheduledInfinityModel) (and @model.scheduledInfinityModel @model.scheduledInfinityModel.reachedInfinity)))}}
<GhInfinityLoader
@infinityModel={{@model.draftInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
{{/if}}
{{#if (and @model.publishedAndSentInfinityModel (and (or (not @model.scheduledInfinityModel) @model.scheduledInfinityModel.reachedInfinity) (or (not @model.draftInfinityModel) @model.draftInfinityModel.reachedInfinity)))}}
<GhInfinityLoader
@infinityModel={{@model.publishedAndSentInfinityModel}}
@scrollable=".gh-main"
@scrollable=".shade-admin main"
@triggerOffset={{1000}} />
{{/if}}

Expand Down
2 changes: 1 addition & 1 deletion ghost/admin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "6.15.0",
"version": "6.16.0",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ const _ = require('lodash');
const utils = require('../../..');
const url = require('../utils/url');
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
const {MemberCommentingCodec} = require('../../../../../../services/members/commenting');
const {serializeCommenting} = require('../utils/member-commenting');

const commentFields = [
'id',
Expand All @@ -30,7 +28,9 @@ const memberFieldsAdmin = [
'name',
'email',
'expertise',
'avatar_image'
'avatar_image',
'can_comment',
'commenting'
];

const postFields = [
Expand Down Expand Up @@ -73,14 +73,6 @@ const commentMapper = (model, frame) => {

if (jsonModel.member) {
response.member = _.pick(jsonModel.member, isPublicRequest ? memberFields : memberFieldsAdmin);

// For admin requests, always add computed commenting fields for consistent response shape
// MemberCommentingCodec.parse() handles null/undefined by returning enabled state
if (!isPublicRequest) {
const commenting = MemberCommentingCodec.parse(jsonModel.member.commenting);
response.member.can_comment = commenting.canComment;
response.member.commenting = serializeCommenting(commenting);
}
} else {
response.member = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ const {unparse} = require('@tryghost/members-csv');
const mappers = require('./mappers');
const {Transform} = require('stream');
const papaparse = require('papaparse');
const {serializeCommenting} = require('./utils/member-commenting');

module.exports = {
browse: createSerializer('browse', paginatedMembers),
read: createSerializer('read', singleMember),
Expand Down Expand Up @@ -182,7 +180,7 @@ function serializeMember(member, options) {
attribution: serializeAttribution(json.attribution),
unsubscribe_url: json.unsubscribe_url,
can_comment: json.can_comment,
commenting: serializeCommenting(json.commenting)
commenting: json.commenting
};

if (json.products) {
Expand Down

This file was deleted.

40 changes: 40 additions & 0 deletions ghost/core/core/server/models/member.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const ghostBookshelf = require('./base');
const crypto = require('crypto');
const _ = require('lodash');
const config = require('../../shared/config');
const {MemberCommentingCodec} = require('../services/members/commenting');

const Member = ghostBookshelf.Model.extend({
tableName: 'members',
Expand All @@ -21,6 +22,40 @@ const Member = ghostBookshelf.Model.extend({
};
},

/**
* Transform data coming from the database.
* Parses the `commenting` JSON string into a MemberCommenting domain object
* and computes the `can_comment` boolean.
*/
parse(attrs) {
attrs = ghostBookshelf.Model.prototype.parse.call(this, attrs);

if (attrs.commenting !== undefined) {
const commenting = MemberCommentingCodec.parse(attrs.commenting);
attrs.commenting = commenting;
attrs.can_comment = commenting.canComment;
}

return attrs;
},

/**
* Transform data going to the database.
* Converts the MemberCommenting domain object back to a JSON string
* and removes the computed `can_comment` field.
*/
format(attrs) {
// Remove computed field - it should not be persisted
delete attrs.can_comment;

// Convert MemberCommenting domain object to JSON string for storage
if (attrs.commenting) {
attrs.commenting = MemberCommentingCodec.format(attrs.commenting);
}

return ghostBookshelf.Model.prototype.format.call(this, attrs);
},

filterExpansions() {
return [{
key: 'label',
Expand Down Expand Up @@ -403,6 +438,11 @@ const Member = ghostBookshelf.Model.extend({
attrs.avatar_image = gravatar.url(attrs.email, {size: 250, default: 'blank'});
}

// Serialize commenting domain object to API format
if (attrs.commenting) {
attrs.commenting = MemberCommentingCodec.toJSON(attrs.commenting);
}

return attrs;
}
}, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ interface StoredCommenting {
* Codec for bidirectional serialization of MemberCommenting.
* Handles conversion between domain objects and persistence format.
*/
interface SerializedCommenting {
disabled: boolean;
disabled_reason: string | null;
disabled_until: string | null;
}

export const MemberCommentingCodec = {
/**
* Parse a raw JSON string from storage into a MemberCommenting domain object.
Expand Down Expand Up @@ -47,5 +53,21 @@ export const MemberCommentingCodec = {
disabledReason: commenting.disabledReason,
disabledUntil: commenting.disabledUntil?.toISOString() ?? null
});
},

/**
* Serialize a MemberCommenting domain object for API responses.
* Converts camelCase domain properties to snake_case API format.
*/
toJSON(commenting: MemberCommenting | null): SerializedCommenting | null {
if (!commenting) {
return commenting;
}

return {
disabled: commenting.disabled,
disabled_reason: commenting.disabledReason,
disabled_until: commenting.disabledUntil?.toISOString() ?? null
};
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ module.exports = class RouterController {

// Honeypot field is filled, this is a bot.
// Pretend that the email was sent successfully.
res.writeHead(201);
res.writeHead(201, {'Content-Type': 'application/json'});
return res.end('{}');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const validator = require('@tryghost/validator');
const crypto = require('crypto');
const StartOutboxProcessingEvent = require('../../../outbox/events/start-outbox-processing-event');
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../../../member-welcome-emails/constants');
const {MemberCommentingCodec} = require('../../commenting');
const messages = {
noStripeConnection: 'Cannot {action} without a Stripe Connection',
moreThanOneProduct: 'A member cannot have more than one Product',
Expand Down Expand Up @@ -2031,7 +2030,7 @@ module.exports = class MemberRepository {
*/
async saveCommenting(memberId, commenting, actionName, context) {
return this._Member.edit(
{commenting: MemberCommentingCodec.format(commenting)},
{commenting},
{
id: memberId,
context,
Expand Down
Loading
Loading