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
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const features: Feature[] = [{
title: 'Retention Offers',
description: 'Enable retention offers for canceling members',
flag: 'retentionOffers'
}, {
title: 'Welcome Email Editor',
description: 'Enable the new welcome email editor experience',
flag: 'welcomeEmailEditor'
}];

const AlphaFeatures: React.FC = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ const Sidebar: React.FC<{
}}
onKeyDown={() => clearError('name')}
/>
<TextField
containerClassName='group'
error={Boolean(errors.code)}
hint={errors.code || (offer?.code !== '' ? <span className='truncate text-grey-700'>{homepageUrl}<span className='font-bold text-black dark:text-white'>{offer?.code}</span></span> : null)}
placeholder='black-friday'
rightPlaceholder={offer?.code !== '' ? <Button className='mr-0.5 mt-1' color='green' label={isCopied ? 'Copied!' : 'Copy link'} size='sm' onClick={handleCopyClick} /> : null}
title='Offer code'
value={offer?.code}
onChange={e => updateOffer({code: e.target.value})}
onKeyDown={() => clearError('code')}
/>
<TextField
error={Boolean(errors.displayTitle)}
hint={errors.displayTitle}
Expand All @@ -157,15 +168,6 @@ const Sidebar: React.FC<{
value={offer?.display_description}
onChange={e => updateOffer({display_description: e.target.value})}
/>
<TextField
error={Boolean(errors.code)}
hint={errors.code || (offer?.code !== '' ? <div className='flex items-center justify-between'><div>{homepageUrl}<span className='font-bold'>{offer?.code}</span></div><span></span><Button className='text-xs' color='green' label={`${isCopied ? 'Copied' : 'Copy'}`} size='sm' link onClick={handleCopyClick} /></div> : null)}
placeholder='black-friday'
title='Offer code'
value={offer?.code}
onChange={e => updateOffer({code: e.target.value})}
onKeyDown={() => clearError('code')}
/>
</div>
</section>
</Form>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 27 additions & 4 deletions apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
SidebarContent,
} from "@tryghost/shade"
import type { ReactNode } from "react";

import WhatsNewBanner from "@/whats-new/components/whats-new-banner";

Expand All @@ -11,20 +12,42 @@ import NavSettings from "./nav-settings";
import ThemeErrorsBanner from "./theme-errors-banner";
import UpgradeBanner from "./upgrade-banner";
import { useUpgradeStatus } from "./hooks/use-upgrade-status";
import { useWhatsNewStatus } from "./hooks/use-whats-new-status";
import { useActiveThemeErrors } from "./hooks/use-theme-errors";

function AppSidebarContent() {
const {hasErrors} = useActiveThemeErrors();
const { showUpgradeBanner, trialDaysRemaining } = useUpgradeStatus();
const { showWhatsNewBanner } = useWhatsNewStatus();
let banner: ReactNode = null;
let bannerContainerClassName = '';

if (hasErrors) {
banner = <ThemeErrorsBanner />;
bannerContainerClassName = 'pb-[110px]';
} else {
if (showUpgradeBanner) {
banner = <UpgradeBanner trialDaysRemaining={trialDaysRemaining} />;
bannerContainerClassName = 'pb-[254px]';
} else if (showWhatsNewBanner) {
banner = <WhatsNewBanner />;
bannerContainerClassName = 'pb-[180px]';
}
}

return (
<SidebarContent className="px-3 pt-4 justify-between">
<SidebarContent className={`px-3 pt-4 ${!banner && 'justify-between'}`}>
<div className="flex flex-col gap-2 sidebar:gap-4">
<NavMain />
<NavContent />
<NavGhostPro />
</div>
<div className="flex flex-col gap-2 sidebar:gap-4">
{showUpgradeBanner ? <UpgradeBanner trialDaysRemaining={trialDaysRemaining} /> : <WhatsNewBanner />}
<ThemeErrorsBanner />
<div className={`flex flex-col gap-2 sidebar:gap-4 ${bannerContainerClassName}`}>
{banner &&
<div className="fixed left-3 bottom-[92px] max-w-[276px] z-50">
{banner}
</div>
}
<NavSettings className="pb-0" />
</div>
</SidebarContent>
Expand Down
10 changes: 9 additions & 1 deletion apps/admin/src/layout/app-sidebar/app-sidebar-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import {
} from "@tryghost/shade"
import WhatsNewDialog from "@/whats-new/components/whats-new-dialog";
import { UserMenu } from "./user-menu";
import { useUpgradeStatus } from "./hooks/use-upgrade-status";
import { useWhatsNewStatus } from "./hooks/use-whats-new-status";
import { useActiveThemeErrors } from "./hooks/use-theme-errors";

function AppSidebarFooter({ ...props }: React.ComponentProps<typeof SidebarFooter>) {
const [isWhatsNewDialogOpen, setIsWhatsNewDialogOpen] = useState(false);
const { showUpgradeBanner } = useUpgradeStatus();
const { showWhatsNewBanner } = useWhatsNewStatus();
const {hasErrors} = useActiveThemeErrors();
const banner = showUpgradeBanner || showWhatsNewBanner || hasErrors;

return (
<>
<SidebarFooter {...props}>
<SidebarGroup>
<SidebarGroup className={banner ? 'pt-3' : ''}>
<SidebarMenu>
<SidebarMenuItem>
<UserMenu onOpenWhatsNew={() => setIsWhatsNewDialogOpen(true)} />
Expand Down
16 changes: 16 additions & 0 deletions apps/admin/src/layout/app-sidebar/hooks/use-whats-new-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useChangelog } from "@/whats-new/hooks/use-changelog";
import { useWhatsNew } from "@/whats-new/hooks/use-whats-new";

export interface WhatsNewStatus {
showWhatsNewBanner: boolean;
}

export function useWhatsNewStatus(): WhatsNewStatus {
const { data: whatsNewData } = useWhatsNew();
const { data: changelog } = useChangelog();
const latestEntry = changelog?.entries[0];

return {
showWhatsNewBanner: !!whatsNewData?.hasNew && !!latestEntry,
};
}
2 changes: 1 addition & 1 deletion apps/admin/src/layout/app-sidebar/theme-errors-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function ThemeErrorsBanner() {
<LucideIcon.AlertTriangle className="mt-0.5 size-4 shrink-0 text-red" />
<div>
<div className="font-semibold text-red">Your theme has errors</div>
<div className="text-sm text-muted-foreground">Some functionality on your site may be limited &rarr;</div>
<div className="text-sm text-foreground">Some functionality on your site may be limited &rarr;</div>
</div>
</div>
</Banner>
Expand Down
8 changes: 5 additions & 3 deletions apps/admin/src/layout/app-sidebar/upgrade-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import {
} from "@tryghost/shade"

import ghostProLogo from "@/assets/images/ghost-pro-logo.png";
import ghostProLogoDark from "@/assets/images/ghost-pro-logo-dark.png";

function UpgradeBanner({ trialDaysRemaining }: { trialDaysRemaining: number }) {
return (
<Banner variant='gradient' size='lg' className="mx-2 my-5 flex flex-col items-stretch">
<Banner variant='gradient' size='lg' className="mx-2 flex flex-col items-stretch">
<div>
<img src={ghostProLogo} alt="Ghost Pro" className="max-h-[33px]" />
<img src={ghostProLogo} alt="Ghost Pro" className="max-h-[33px] dark:hidden" />
<img src={ghostProLogoDark} alt="Ghost Pro" className="max-h-[33px] hidden dark:block" />
</div>
<div className="text-base mt-3 font-semibold">Unlock every feature</div>
<div className="mt-2 text-gray-700 text-sm mb-4">
Choose a plan to access the full power of Ghost right away, you have <span className="font-semibold text-black">{trialDaysRemaining} days</span> free trial remaining.
Choose a plan to access the full power of Ghost right away, you have <span className="font-semibold text-foreground">{trialDaysRemaining} days</span> free trial remaining.
</div>
<Button asChild><a href="#/pro/billing/plans">Upgrade now</a></Button>
</Banner>
Expand Down
6 changes: 3 additions & 3 deletions apps/admin/src/whats-new/components/whats-new-banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ function WhatsNewBanner() {
data-test-toast-link
>
<div className="flex items-center gap-2 mb-2">
<LucideIcon.Sparkles className="size-4 text-purple-600" />
<span className="text-xs font-semibold text-gray-700 uppercase tracking-wide">What’s new?</span>
<LucideIcon.Sparkles className="size-4 text-purple-600 dark:text-purple" />
<span className="text-xs font-semibold text-gray-700 dark:text-gray-400 uppercase tracking-wide">What’s new?</span>
</div>
<div className="text-base font-semibold text-gray-900 mb-1" data-test-toast-title>
<div className="text-base font-semibold text-gray-900 dark:text-foreground mb-1" data-test-toast-title>
{latestEntry.title}
</div>
<div className="text-sm text-gray-700" data-test-toast-excerpt>
Expand Down
10 changes: 6 additions & 4 deletions apps/shade/src/components/ui/banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ const bannerVariants = cva(
{
variants: {
variant: {
default: 'border border-border bg-background shadow-sm hover:shadow-md',
default: 'border border-border bg-background shadow-sm hover:shadow-md dark:border-gray-900 dark:bg-gray-925',
gradient: [
'cursor-pointer border border-gray-100 bg-white',
'cursor-pointer border border-gray-100 bg-white dark:border-gray-950 dark:bg-black',
'shadow-[rgb(75_225_226_/_28%)_-7px_-6px_42px_8px,rgb(202_103_255_/_32%)_7px_6px_42px_8px]',
'dark:shadow-[rgb(75_225_226_/_36%)_-7px_-6px_42px_8px,rgb(202_103_255_/_38%)_7px_6px_42px_8px]',
'hover:shadow-[rgb(75_225_226_/_38%)_-7px_-4px_42px_10px,rgb(202_103_255_/_42%)_7px_8px_42px_10px]',
'dark:hover:shadow-[rgb(75_225_226_/_50%)_-7px_-4px_42px_10px,rgb(202_103_255_/_52%)_7px_8px_42px_10px]',
'hover:translate-y-[-2px] hover:scale-[1.01]'
],
info: 'bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800 border',
success: 'bg-green-50 border-green-200 dark:bg-green-950/30 dark:border-green-800 border',
warning: 'bg-yellow-50 border-yellow-200 dark:bg-yellow-950/30 dark:border-yellow-800 border',
destructive: 'bg-white shadow-sm dark:bg-black'
destructive: 'bg-white shadow-sm dark:bg-gray-950'
},
size: {
sm: 'p-2 text-sm',
Expand Down Expand Up @@ -85,7 +87,7 @@ const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
{dismissible && (
<Button
aria-label="Dismiss notification"
className="absolute right-1 top-1 size-8 text-gray-600"
className="absolute right-1 top-1 size-8 text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"
size="icon"
variant="ghost"
onClick={handleDismiss}
Expand Down
3 changes: 2 additions & 1 deletion ghost/core/core/shared/labs.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ const PRIVATE_FEATURES = [
'themeTranslation',
'indexnow',
'transistor',
'retentionOffers'
'retentionOffers',
'welcomeEmailEditor'
];

module.exports.GA_KEYS = [...GA_FEATURES];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Object {
"transistor": true,
"urlCache": true,
"webmentions": true,
"welcomeEmailEditor": true,
"welcomeEmails": true,
},
"mail": "",
Expand Down
8 changes: 5 additions & 3 deletions ghost/core/test/e2e-browser/portal/upgrade.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ test.describe('Portal', () => {
delay: 500
});

await sharedPage.waitForLoadState('networkidle');
await expect(sharedPage.locator('[data-test-tier]').first()).toBeVisible();
await impersonateMember(sharedPage);

const portalTriggerButton = sharedPage.frameLocator('[data-testid="portal-trigger-frame"]').locator('[data-testid="portal-trigger-button"]');
Expand All @@ -50,8 +50,9 @@ test.describe('Portal', () => {
await completeStripeSubscription(sharedPage);

// open portal and check that member has been upgraded to paid tier
await expect(portalTriggerButton).toBeVisible();
await portalTriggerButton.click();
await expect(portalFrame.getByText('$50.00/year')).toBeVisible();
await expect(portalFrame.getByText('$50.00/year')).toBeVisible({timeout: 30000});
await expect(portalFrame.getByRole('heading', {name: 'Billing info'})).toBeVisible();
await expect(portalFrame.getByText('**** **** **** 4242')).toBeVisible();

Expand Down Expand Up @@ -102,10 +103,11 @@ test.describe('Portal', () => {
await completeStripeSubscription(sharedPage);

// open portal and check that member has been upgraded to paid tier
await expect(portalTriggerButton).toBeVisible();
await portalTriggerButton.click();
// verify member's tier, price and card details
await expect(portalFrame.getByText(tierName)).toBeVisible();
await expect(portalFrame.getByText('$50.00/year')).toBeVisible();
await expect(portalFrame.getByText('$50.00/year')).toBeVisible({timeout: 30000});
await expect(portalFrame.getByText('**** **** **** 4242')).toBeVisible();

// verify member's tier on member detail page in admin
Expand Down
11 changes: 5 additions & 6 deletions ghost/core/test/e2e-browser/utils/e2e-browser-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,14 @@ const createOffer = async (page, {name, tierName, offerType, amount, discountTyp

const confirmModal = await page.getByTestId('confirmation-modal');
await confirmModal.getByRole('button', {name: 'Archive'}).click();
}
await confirmModal.waitFor({state: 'hidden'});

if (isCTA) {
// Still in the offers modal after archiving — click "New offer" directly
await page.getByText('New offer').click();
} else if (await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).isVisible()) {
await page.getByTestId('offers').getByRole('button', {name: 'Add offer'}).click();
} else {
// ensure the modal is open
if (!page.getByTestId('offers-modal').isVisible()) {
await page.getByTestId('offers').getByRole('button', {name: 'Manage offers'}).click();
}
await page.getByTestId('offers').getByRole('button', {name: 'Manage offers'}).click();
await page.getByText('New offer').click();
}

Expand Down
6 changes: 5 additions & 1 deletion ghost/core/test/integration/exporter/exporter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ describe('Exporter', function () {
// NOTE: using `Object.keys` here instead of `should.have.only.keys` assertion
// because when `have.only.keys` fails there's no useful diff
assert.deepEqual(Object.keys(exportData.data).sort(), tables.sort());
Object.keys(exportData.data).sort().should.containDeep(Object.keys(exportedBodyLatest().db[0].data));
assert(
Object.keys(exportedBodyLatest().db[0].data).every(key => (
Object.hasOwnProperty.call(exportData.data, key)
))
);
assert.equal(exportData.meta.version, ghostVersion.full);

// excludes table should contain no data
Expand Down
2 changes: 1 addition & 1 deletion ghost/core/test/integration/importer/v5.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('Importing 5.x export', function () {

// Imported user
assert.equal(user2.email, 'import-test-user@ghost.org');
user2.id.should.not.equal(LEGACY_HARDCODED_USER_ID);
assert.notEqual(user2.id, LEGACY_HARDCODED_USER_ID);

assert.equal(posts.data.length, 2);

Expand Down
10 changes: 5 additions & 5 deletions ghost/core/test/legacy/api/admin/db.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ describe('DB API', function () {
assert.equal(res2.body.posts.length, 1);

// Ensure the author is not imported with the legacy hardcoded user id
res2.body.posts[0].authors[0].id.should.not.equal(LEGACY_HARDCODED_USER_ID);
res2.body.posts[0].primary_author.id.should.not.equal(LEGACY_HARDCODED_USER_ID);
assert.notEqual(res2.body.posts[0].authors[0].id, LEGACY_HARDCODED_USER_ID);
assert.notEqual(res2.body.posts[0].primary_author.id, LEGACY_HARDCODED_USER_ID);

const usersResponse = await request.get(localUtils.API.getApiQuery('users/'))
.set('Origin', config.get('url'))
Expand All @@ -320,9 +320,9 @@ describe('DB API', function () {
assert.equal(usersResponse.body.users.length, 3);

// Ensure user is not imported with the legacy hardcoded user id
usersResponse.body.users[0].id.should.not.equal(LEGACY_HARDCODED_USER_ID);
usersResponse.body.users[1].id.should.not.equal(LEGACY_HARDCODED_USER_ID);
usersResponse.body.users[2].id.should.not.equal(LEGACY_HARDCODED_USER_ID);
assert.notEqual(usersResponse.body.users[0].id, LEGACY_HARDCODED_USER_ID);
assert.notEqual(usersResponse.body.users[1].id, LEGACY_HARDCODED_USER_ID);
assert.notEqual(usersResponse.body.users[2].id, LEGACY_HARDCODED_USER_ID);
});

it('Can import a JSON database with products', async function () {
Expand Down
Loading
Loading