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
81 changes: 14 additions & 67 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ jobs:
if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_core == 'true'
services:
mysql:
image: ${{ matrix.env.DB == 'mysql8' && 'mysql:8.0' || '' }}
image: mysql:8.0
env:
MYSQL_DATABASE: ghost_testing
MYSQL_ROOT_PASSWORD: root
Expand All @@ -794,59 +794,30 @@ jobs:
--health-interval=2s
--health-timeout=5s
--health-retries=60
strategy:
matrix:
node: ${{ fromJSON(needs.job_setup.outputs.node_test_matrix) }}
env:
- DB: mysql8
NODE_ENV: testing-mysql
include:
- node: ${{ needs.job_setup.outputs.node_version }}
env:
DB: better-sqlite3
NODE_ENV: testing
env:
DB: ${{ matrix.env.DB }}
NODE_ENV: ${{ matrix.env.NODE_ENV }}
# The `test:ci:*` targets wrap the suites in c8. Only the sqlite leg is
# instrumented (mysql covers the same code), and only on the canonical
# repo — see job_setup's coverage_enabled output.
COVERAGE_ENABLED: ${{ needs.job_setup.outputs.coverage_enabled == 'true' && matrix.env.DB == 'better-sqlite3' }}
name: Acceptance tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }})
DB: mysql8
NODE_ENV: testing-mysql
COVERAGE_ENABLED: ${{ needs.job_setup.outputs.coverage_enabled }}
name: Acceptance tests (Node ${{ needs.job_setup.outputs.node_version }}, mysql8)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
env:
FORCE_COLOR: 0
with:
node-version: ${{ matrix.node }}
node-version: ${{ needs.job_setup.outputs.node_version }}
cache: pnpm

- name: Install dependencies
# better-sqlite3 is an optionalDependency. Without --force, pnpm may skip
# installing/linking it when restoring from a cached store. --force
# ensures all optional deps are installed regardless. The mysql leg
# doesn't need better-sqlite3, so it skips --force and gets a fast cache
# restore (matches the legacy-tests job below).
run: |
if [ "${{ matrix.env.DB }}" = "better-sqlite3" ]; then
pnpm install --frozen-lockfile --force
else
pnpm install --frozen-lockfile
fi
run: pnpm install --frozen-lockfile

- name: Set timezone (non-UTC)
uses: szenius/set-timezone@1f9716b0f7120e344f0c62bb7b1ee98819aefd42 # v2.0
with:
timezoneLinux: 'America/New_York'

- name: Set env vars (SQLite)
if: contains(matrix.env.DB, 'sqlite')
run: echo "database__connection__filename=/dev/shm/ghost-test.db" >> "$GITHUB_ENV"

- name: Set env vars (MySQL)
if: contains(matrix.env.DB, 'mysql')
run: |
{
echo "database__connection__host=127.0.0.1"
Expand Down Expand Up @@ -899,7 +870,7 @@ jobs:
fi

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: env.COVERAGE_ENABLED == 'true' && matrix.node == env.NODE_VERSION
if: env.COVERAGE_ENABLED == 'true'
with:
name: e2e-coverage
path: |
Expand All @@ -919,7 +890,7 @@ jobs:
if: needs.job_setup.outputs.is_tag == 'true' || needs.job_setup.outputs.changed_core == 'true'
services:
mysql:
image: ${{ matrix.env.DB == 'mysql8' && 'mysql:8.0' || '' }}
image: mysql:8.0
env:
MYSQL_DATABASE: ghost_testing
MYSQL_ROOT_PASSWORD: root
Expand All @@ -931,21 +902,10 @@ jobs:
--health-interval=10s
--health-timeout=5s
--health-retries=12
strategy:
matrix:
include:
- node: ${{ needs.job_setup.outputs.node_version }}
env:
DB: mysql8
NODE_ENV: testing-mysql
- node: ${{ needs.job_setup.outputs.node_version }}
env:
DB: better-sqlite3
NODE_ENV: testing
env:
DB: ${{ matrix.env.DB }}
NODE_ENV: ${{ matrix.env.NODE_ENV }}
name: Legacy tests (Node ${{ matrix.node }}, ${{ matrix.env.DB }})
DB: mysql8
NODE_ENV: testing-mysql
name: Legacy tests (Node ${{ needs.job_setup.outputs.node_version }}, mysql8)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand All @@ -955,26 +915,13 @@ jobs:
env:
FORCE_COLOR: 0
with:
node-version: ${{ matrix.node }}
node-version: ${{ needs.job_setup.outputs.node_version }}
cache: pnpm

- name: Install dependencies
# better-sqlite3 is an optionalDependency. Without --force, pnpm may skip
# installing/linking it when restoring from a cached store. --force
# ensures all optional deps are installed regardless.
run: |
if [ "${{ matrix.env.DB }}" = "better-sqlite3" ]; then
pnpm install --frozen-lockfile --force
else
pnpm install --frozen-lockfile
fi

- name: Set env vars (SQLite)
if: contains(matrix.env.DB, 'better-sqlite')
run: echo "database__connection__filename=/dev/shm/ghost-test.db" >> $GITHUB_ENV
run: pnpm install --frozen-lockfile

- name: Set env vars (MySQL)
if: contains(matrix.env.DB, 'mysql')
run: |
echo "database__connection__host=127.0.0.1" >> $GITHUB_ENV
echo "database__connection__port=${{ job.services.mysql.ports['3306'] }}" >> $GITHUB_ENV
Expand Down
2 changes: 1 addition & 1 deletion apps/admin-x-framework/src/api/automations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type AutomationStats = {
};

export type AutomationBrowseItem = Automation & {
stats: AutomationStats;
stats?: AutomationStats;
};

export type AutomationWaitAction = {
Expand Down
45 changes: 43 additions & 2 deletions apps/admin-x-framework/src/api/pages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { InfiniteData } from '@tanstack/react-query';
import { Meta, createInfiniteQuery, createQuery } from '../utils/api/hooks';
import { Meta, createInfiniteQuery, createMutation, createQuery } from '../utils/api/hooks';
import type { Email, PostBulkAction, PostListFields } from './posts';

// A page is a post with `displayName: 'page'` server-side, so the list screens
// read the same fields off both.
export type Page = {
id: string;
title: string;
Expand All @@ -10,7 +13,17 @@ export type Page = {
published_at?: string;
visibility?: string;
uuid?: string;
};
feature_image?: string;
email?: Email;
count?: {
clicks?: number;
};
// Pages are never emailed, but the list reads these off both resources
// through one type, so they have to be addressable here too.
email_only?: boolean;
email_segment?: string;
newsletter?: object;
} & PostListFields;

export interface PagesResponseType {
meta?: Meta;
Expand Down Expand Up @@ -49,3 +62,31 @@ export const useBrowsePagesInfinite = createInfiniteQuery<PagesResponseType & {
};
},
});

/** Duplicate a page. As with posts, the copy is always a draft. */
export const useCopyPage = createMutation<PagesResponseType, string>({
method: 'POST',
path: (id) => `/pages/${id}/copy/`,
});

/** Bulk-edit pages matching an NQL filter. See `useBulkEditPosts`. */
export const useBulkEditPages = createMutation<unknown, { filter: string; action: PostBulkAction }>(
{
method: 'PUT',
path: () => '/pages/bulk/',
searchParams: ({ filter }) => ({ filter }),
body: ({ action }) => ({
bulk: {
action: action.type,
meta: 'meta' in action ? action.meta : {},
},
}),
},
);

/** Bulk-delete pages matching an NQL filter. */
export const useBulkDeletePages = createMutation<unknown, { filter: string }>({
method: 'DELETE',
path: () => '/pages/',
searchParams: ({ filter }) => ({ filter }),
});
84 changes: 83 additions & 1 deletion apps/admin-x-framework/src/api/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,43 @@ export type Email = {
opened_count: number;
email_count: number;
status?: string;
track_opens?: boolean;
track_clicks?: boolean;
};

// Every field optional: these are supertypes of the narrower author/tag shapes
// already declared around the analytics screens, so widening `Post` doesn't
// invalidate them. The list only reads names and slugs.
export type PostAuthor = {
id?: string;
name?: string;
email?: string;
slug?: string;
};

export type PostTag = {
id?: string;
name?: string;
slug?: string;
visibility?: string;
};

/**
* Fields the list screens need on top of the analytics-shaped core. All
* optional: the analytics endpoints don't return them, and the list gets them
* from the server's default relations rather than an explicit `include`.
*/
export type PostListFields = {
featured?: boolean;
updated_at?: string;
created_at?: string;
excerpt?: string;
custom_excerpt?: string;
authors?: PostAuthor[];
primary_author?: PostAuthor | null;
tags?: PostTag[];
primary_tag?: PostTag | null;
tiers?: object[];
};

export type Post = {
Expand All @@ -36,7 +73,7 @@ export type Post = {
email_recipient_filter?: string;
send_email_when_published?: boolean;
email_stats?: object;
};
} & PostListFields;

export interface PostsResponseType {
meta?: Meta;
Expand Down Expand Up @@ -86,6 +123,51 @@ export const useDeletePost = createMutation<unknown, string>({
path: (id) => `/posts/${id}/`,
});

export type PostBulkAction =
| { type: 'feature' }
| { type: 'unfeature' }
| { type: 'unpublish' }
| { type: 'unschedule' }
| { type: 'addTag'; meta: { tags: { id?: string; name: string; slug?: string }[] } }
| { type: 'access'; meta: { visibility: string; tiers?: { id: string }[] } };

/**
* Bulk-edit posts matching an NQL filter.
*
* The filter is the point: after Cmd+A the selection is inverted and covers
* posts that were never loaded, so the action has to be expressed as a query
* rather than as a list of ids.
*/
export const useBulkEditPosts = createMutation<unknown, { filter: string; action: PostBulkAction }>(
{
method: 'PUT',
path: () => '/posts/bulk/',
searchParams: ({ filter }) => ({ filter }),
body: ({ action }) => ({
bulk: {
action: action.type,
meta: 'meta' in action ? action.meta : {},
},
}),
},
);

/** Bulk-delete posts matching an NQL filter. */
export const useBulkDeletePosts = createMutation<unknown, { filter: string }>({
method: 'DELETE',
path: () => '/posts/',
searchParams: ({ filter }) => ({ filter }),
});

/**
* Duplicate a post. The copy is always a draft, whatever the source was, so
* callers place it at the top of the list rather than beside its original.
*/
export const useCopyPost = createMutation<PostsResponseType, string>({
method: 'POST',
path: (id) => `/posts/${id}/copy/`,
});

export interface ImportContentCSVPayload {
file: File;
mapping: Record<string, string>;
Expand Down
Loading
Loading