2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Added Source as the new default theme

refs TryGhost/Product#3510

- Added `TryGhost/Source` as a submodule in `ghost/core/content/themes` so `Source` will ship with Ghost (along with Casper)
- With this change, new installs will use `Source` as the default theme. Existing sites will have `Source` installed, but not activated, as this is a large change and we don't want to drastically change existing sites without warning. Users can upgrade to use `Source` simply by clicking 'Activate' in design settings.
- Updated protections to prevent users from uploading their own conflicting version of `Source`
This commit is contained in:
Chris Raible 2023-09-06 13:22:12 -07:00 committed by Daniel Lockyer
parent add7f1283b
commit 80a6fe17d0
82 changed files with 1294 additions and 124 deletions

1
.gitignore vendored
View file

@ -97,6 +97,7 @@ typings/
/ghost/core/content/adapters/storage/**/*
/ghost/core/content/adapters/scheduling/**/*
/ghost/core/content/themes/casper
/ghost/core/content/themes/source
!/ghost/core/README.md
!/ghost/core/content/**/README.md

4
.gitmodules vendored
View file

@ -2,3 +2,7 @@
path = ghost/core/content/themes/casper
url = ../../TryGhost/Casper.git
ignore = all
[submodule "ghost/core/content/themes/source"]
path = ghost/core/content/themes/source
url = ../../TryGhost/Source.git
ignore = all

View file

@ -133,9 +133,13 @@ export function isActiveTheme(theme: Theme): boolean {
}
export function isDefaultTheme(theme: Theme): boolean {
return theme.name === 'source';
}
export function isLegacyTheme(theme: Theme): boolean {
return theme.name === 'casper';
}
export function isDeletableTheme(theme: Theme): boolean {
return !isDefaultTheme(theme) && !isActiveTheme(theme);
return !isDefaultTheme(theme) && !isLegacyTheme(theme) && !isActiveTheme(theme);
}

View file

@ -7,7 +7,7 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import useHandleError from '../../../../utils/api/handleError';
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, isLegacyTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
interface ThemeActionProps {
@ -23,6 +23,8 @@ function getThemeLabel(theme: Theme): React.ReactNode {
if (isDefaultTheme(theme)) {
label += ' (default)';
} else if (isLegacyTheme(theme)) {
label += ' (legacy)';
} else if (theme.package?.name !== theme.name) {
label =
<span className='text-sm md:text-base'>

View file

@ -4,6 +4,13 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import React from 'react';
import {OfficialTheme, useOfficialThemes} from '../../../providers/ServiceProvider';
import {getGhostPaths, resolveAsset} from '../../../../utils/helpers';
import {useEffect, useState} from 'react';
const sourceDemos = [
{image: 'Source.png', category: 'News'},
{image: 'Source-Magazine.png', category: 'Magazine'},
{image: 'Source-Newsletter.png', category: 'Newsletter'}
];
const OfficialThemes: React.FC<{
onSelectTheme?: (theme: OfficialTheme) => void;
@ -12,6 +19,20 @@ const OfficialThemes: React.FC<{
}) => {
const {adminRoot} = getGhostPaths();
const officialThemes = useOfficialThemes();
const [currentSourceDemoIndex, setCurrentSourceDemoIndex] = useState(0);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
if (isHovered) {
setCurrentSourceDemoIndex(prevIndex => (prevIndex + 1) % sourceDemos.length);
}
}, 3000);
return () => {
clearInterval(interval);
};
}, [isHovered]);
return (
<ModalPage heading='Themes'>
@ -22,16 +43,33 @@ const OfficialThemes: React.FC<{
onSelectTheme?.(theme);
}}>
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
<div className='w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'>
<img
alt={`${theme.name} Theme`}
className='h-full w-full object-contain'
src={resolveAsset(theme.image, adminRoot)}
/>
<div className='relative w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]' onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
{theme.name !== 'Source' ?
<img
alt={`${theme.name} Theme`}
className='h-full w-full object-contain'
src={resolveAsset(theme.image, adminRoot)}
/> :
<>
{sourceDemos.map((demo, index) => (
<img
key={`source-theme-${demo.category}`}
alt={`${theme.name} Theme - ${demo.category}`}
className={`${index === 0 ? 'relative' : 'absolute'} left-0 top-0 h-full w-full object-contain transition-opacity duration-500 ${index === currentSourceDemoIndex ? 'opacity-100' : 'opacity-0'}`}
src={resolveAsset(`assets/img/themes/${demo.image}`, adminRoot)}
/>
))}
</>
}
</div>
<div className='mt-3'>
<div className='relative mt-3'>
<Heading level={4}>{theme.name}</Heading>
<span className='text-sm text-grey-700'>{theme.category}</span>
{theme.name !== 'Source' ?
<span className='text-sm text-grey-700'>{theme.category}</span> :
sourceDemos.map((demo, index) => (
<span className={`${index === 0 ? 'relative' : 'absolute bottom-[1px]'} left-0 inline-block w-24 bg-white text-sm text-grey-700 ${index === currentSourceDemoIndex ? 'opacity-100' : 'opacity-0'}`}>{demo.category}</span>
))
}
</div>
</button>
);

View file

@ -7,9 +7,16 @@ import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome';
import NiceModal from '@ebay/nice-modal-react';
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
import React, {useState} from 'react';
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
import {OfficialTheme} from '../../../providers/ServiceProvider';
import {Theme} from '../../../../api/themes';
const sourceDemos = [
{label: 'News', value: 'news', url: 'https://source.ghost.io'},
{label: 'Magazine', value: 'magazine', url: 'https://source-magazine.ghost.io'},
{label: 'Newsletter', value: 'newsletter', url: 'https://source-newsletter.ghost.io'}
];
const ThemePreview: React.FC<{
selectedTheme?: OfficialTheme;
isInstalling?: boolean;
@ -26,6 +33,7 @@ const ThemePreview: React.FC<{
onInstall
}) => {
const [previewMode, setPreviewMode] = useState('desktop');
const [currentSourceDemo, setCurrentSourceDemo] = useState<SelectOption>(sourceDemos[0]);
if (!selectedTheme) {
return null;
@ -68,6 +76,7 @@ const ThemePreview: React.FC<{
<div className='flex items-center gap-2'>
<Breadcrumbs
activeItemClassName='hidden md:!block md:!visible'
containerClassName='whitespace-nowrap'
itemClassName='hidden md:!block md:!visible'
items={[
{label: 'Design', onClick: onClose},
@ -78,6 +87,24 @@ const ThemePreview: React.FC<{
backIcon
onBack={onBack}
/>
{selectedTheme.name === 'Source' ?
<>
<span className='hidden md:!visible md:!block'></span>
<Select
border={false}
containerClassName='text-sm font-bold'
controlClasses={{menu: 'w-24'}}
fullWidth={false}
options={sourceDemos}
selectedOption={currentSourceDemo}
onSelect={(option) => {
if (option) {
setCurrentSourceDemo(option);
}
}}
/>
</> : null
}
</div>;
const right =
@ -118,13 +145,19 @@ const ThemePreview: React.FC<{
<div className='flex h-[calc(100%-74px)] grow flex-col items-center justify-center bg-grey-50 dark:bg-black'>
{previewMode === 'desktop' ?
<DesktopChrome>
<iframe className='h-full w-full'
src={selectedTheme?.previewUrl} title='Theme preview' />
<iframe
className='h-full w-full'
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
title='Theme preview'
/>
</DesktopChrome>
:
<MobileChrome>
<iframe className='h-full w-full'
src={selectedTheme?.previewUrl} title='Theme preview' />
<iframe
className='h-full w-full'
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
title='Theme preview'
/>
</MobileChrome>
}
</div>

View file

@ -13,6 +13,12 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
}}
ghostVersion='5.x'
officialThemes={[{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png'
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',

View file

@ -12,18 +12,17 @@ import {tracked} from '@glimmer/tracking';
// TODO: Long term move asset management directly in AdminX
const officialThemes = [{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png'
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
ref: 'TryGhost/Casper',
image: 'assets/img/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Edition',
category: 'Newsletter',
@ -108,6 +107,13 @@ const officialThemes = [{
previewUrl: 'https://ease.ghost.io',
ref: 'TryGhost/Ease',
image: 'assets/img/themes/Ease.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Ruby',
category: 'Magazine',

View file

@ -18,6 +18,14 @@ export default class GhThemeTableComponent extends Component {
this.activateTaskInstance?.cancel();
}
isDefaultTheme(theme) {
return theme.name.toLowerCase() === 'source';
}
isLegacyTheme(theme) {
return theme.name.toLowerCase() === 'casper';
}
get sortedThemes() {
let themes = this.args.themes.map((t) => {
let theme = {};
@ -30,7 +38,6 @@ export default class GhThemeTableComponent extends Component {
theme.package = themePackage;
theme.active = get(t, 'active');
theme.isDeletable = !theme.active;
return theme;
});
let duplicateThemes = [];
@ -44,19 +51,24 @@ export default class GhThemeTableComponent extends Component {
});
duplicateThemes.forEach((theme) => {
if (theme.name !== 'casper') {
if (!this.isDefaultTheme(theme) && !this.isLegacyTheme(theme)) {
theme.label = `${theme.label} (${theme.name})`;
}
});
// "(default)" needs to be added to casper manually as it's always
// displayed and would mess up the duplicate checking if added earlier
let casper = themes.findBy('name', 'casper');
if (casper) {
casper.label = `${casper.label} (default)`;
casper.isDefault = true;
casper.isDeletable = false;
}
// add (default) or (legacy) as appropriate and prevent deletion of default/legacy themes
// this needs to be after deduplicating by label
themes.filter(this.isDefaultTheme).forEach((theme) => {
theme.label = `${theme.label} (default)`;
theme.isDefault = true;
theme.isDeletable = false;
});
themes.filter(this.isLegacyTheme).forEach((theme) => {
theme.label = `${theme.label} (legacy)`;
theme.isLegacy = true;
theme.isDeletable = false;
});
// sorting manually because .sortBy('label') has a different sorting
// algorithm to [...strings].sort()

View file

@ -31,8 +31,8 @@ export default class InstallThemeModal extends Component {
return this.args.data.theme?.ref || this.args.data.ref;
}
get isDefaultTheme() {
return this.themeName.toLowerCase() === 'casper';
get isDefaultOrLegacyTheme() {
return this.themeName.toLowerCase() === 'casper' || this.themeName.toLowerCase() === 'source';
}
get isConfirming() {
@ -48,7 +48,7 @@ export default class InstallThemeModal extends Component {
}
get willOverwriteExisting() {
return !this.isDefaultTheme && this.themes.findBy('name', this.themeName.toLowerCase());
return !this.isDefaultOrLegacyTheme && this.themes.findBy('name', this.themeName.toLowerCase());
}
get hasWarningsOrErrors() {
@ -67,9 +67,10 @@ export default class InstallThemeModal extends Component {
@task
*installThemeTask() {
try {
if (this.isDefaultTheme) {
if (this.isDefaultOrLegacyTheme) {
// default theme can't be installed, only activated
const defaultTheme = this.store.peekRecord('theme', 'casper');
const themeName = this.themeName.toLowerCase();
const defaultTheme = this.store.peekRecord('theme', themeName);
yield this.themeManagement.activateTask.perform(defaultTheme, {skipErrors: true});
this.installedTheme = defaultTheme;

View file

@ -91,8 +91,8 @@ export default class UploadThemeModal extends Component {
return new UnsupportedMediaTypeError();
}
if (file.name.match(/^casper\.zip$/i)) {
return {payload: {errors: [{message: 'Sorry, the default Casper theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
if (file.name.match(/^casper\.zip$/i) || file.name.match(/^source\.zip$/i)) {
return {payload: {errors: [{message: 'Sorry, the default theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
}
if (!this._allowOverwrite && this.currentThemeNames.includes(themeName)) {

View file

@ -13,18 +13,17 @@ export default class ChangeThemeController extends Controller {
themes = this.store.peekAll('theme');
officialThemes = [{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png'
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
ref: 'TryGhost/Casper',
image: 'assets/img/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Edition',
category: 'Newsletter',
@ -109,6 +108,13 @@ export default class ChangeThemeController extends Controller {
previewUrl: 'https://ease.ghost.io',
ref: 'TryGhost/Ease',
image: 'assets/img/themes/Ease.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Ruby',
category: 'Magazine',

View file

@ -6,6 +6,7 @@ import {tracked} from '@glimmer/tracking';
const THEME_PROPERTIES = {
casper: ['description', 'color', 'coverImage'],
source: ['description', 'color', 'coverImage'],
edition: ['description', 'color', 'coverImage'],
dawn: ['description', 'color', 'icon'],
dope: ['description', 'color', 'logo'],

View file

@ -19,7 +19,7 @@ function setting(group, key, value) {
}
// These settings represent a default new site setup
// Real default settings can be found in https://github.com/TryGhost/Ghost/blob/main/core/server/data/schema/default-settings/default-settings.json
// Real default settings can be found in https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/data/schema/default-settings/default-settings.json
export default [
// SITE
setting('site', 'title', 'Test Blog'),
@ -49,7 +49,7 @@ export default [
setting('site', 'twitter_description', null),
// THEME
setting('theme', 'active_theme', 'Casper'),
setting('theme', 'active_theme', 'Source'),
// PRIVATE
setting('private', 'is_private', false),

View file

@ -1,11 +1,18 @@
export default [
{
name: 'source',
package: {
name: 'source',
version: '1.0'
},
active: true
},
{
name: 'casper',
package: {
name: 'casper',
version: '1.0'
},
active: true
}
},
{
name: 'foo',

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -46,7 +46,7 @@ describe('Acceptance: Settings - Design', function () {
expect(findAll('[data-test-nav-group]'), 'no of groups open').to.have.lengthOf(1);
// current theme is shown in nav menu
expect(find('[data-test-text="current-theme"]')).to.contain.text('casper - v1.0');
expect(find('[data-test-text="current-theme"]')).to.contain.text('source - v1.0');
// defaults to "home" desktop preview
expect(find('[data-test-button="desktop-preview"]')).to.have.class('gh-btn-group-selected');
@ -143,7 +143,7 @@ describe('Acceptance: Settings - Design', function () {
config.hostSettings = {
limits: {
customThemes: {
allowlist: ['casper', 'dawn', 'lyra'],
allowlist: ['source', 'casper', 'dawn', 'lyra'],
error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.'
}
}

View file

@ -11,6 +11,7 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('themes', [
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
{name: 'source', package: {name: 'Source', version: '1.0.0'}},
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
{name: 'foo'}
]);
@ -18,14 +19,15 @@ describe('Integration: Component: gh-theme-table', function () {
await render(hbs`<GhThemeTable @themes={{themes}} />`);
expect(findAll('[data-test-themes-list]').length, 'themes list is present').to.equal(1);
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(4);
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(5);
let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim());
expect(packageNames[0]).to.match(/Casper \(default\)/);
expect(packageNames[0]).to.match(/Casper \(legacy\)/);
expect(packageNames[1]).to.match(/Daring\s+Active/);
expect(packageNames[2]).to.match(/foo/);
expect(packageNames[3]).to.match(/Lanyon/);
expect(packageNames[4]).to.match(/Source \(default\)/);
expect(
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-title]'),
@ -35,7 +37,7 @@ describe('Integration: Component: gh-theme-table', function () {
expect(
findAll('[data-test-button="activate"]').length,
'non-active themes have an activate link'
).to.equal(3);
).to.equal(4);
expect(
find('[data-test-theme-active="true"]').querySelector('[data-test-button="activate"]'),
@ -80,22 +82,31 @@ describe('Integration: Component: gh-theme-table', function () {
}
});
it('does not show delete action for casper', async function () {
it('does not show delete action for default themes', async function () {
const themes = [
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
{name: 'foo'}
{name: 'foo'},
{name: 'source', package: {name: 'Source', version: '1.0.0'}}
];
this.set('themes', themes);
await render(hbs`<GhThemeTable @themes={{themes}} />`);
// Casper should not be deletable
await click(`[data-test-theme-id="casper"] [data-test-button="actions"]`);
expect(find('[data-test-actions-for="casper"]')).to.exist;
expect(
find(`[data-test-actions-for="casper"] [data-test-button="delete"]`)
).to.not.exist;
// Source should not be deletable
await click(`[data-test-theme-id="source"] [data-test-button="actions"]`);
expect(find('[data-test-actions-for="source"]')).to.exist;
expect(
find(`[data-test-actions-for="source"] [data-test-button="delete"]`)
).to.not.exist;
});
it('does not show delete action for active theme', async function () {
@ -120,6 +131,7 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('themes', [
{name: 'daring', package: {name: 'Daring', version: '0.1.4'}},
{name: 'daring-0.1.5', package: {name: 'Daring', version: '0.1.4'}},
{name: 'source', package: {name: 'Source', version: '1.0.0'}},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
{name: 'another', package: {name: 'Casper', version: '1.3.1'}},
{name: 'mine', package: {name: 'Casper', version: '1.3.1'}},
@ -135,11 +147,12 @@ describe('Integration: Component: gh-theme-table', function () {
'themes are ordered by label, folder names shown for duplicates'
).to.deep.equal([
'Casper (another)',
'Casper (default)',
'Casper (legacy)',
'Casper (mine)',
'Daring (daring)',
'Daring (daring-0.1.5)',
'foo'
'foo',
'Source (default)'
]);
});
});

View file

@ -32,6 +32,8 @@ content/settings/**
content/themes/**
!content/themes/casper/**
content/themes/casper/yarn.lock
!content/themes/source/**
content/themes/source/yarn.lock
node_modules/**
core/server/lib/members/static/auth/node_modules/**
**/*.db
@ -50,6 +52,7 @@ core/built/**/tests-*
test/**
CONTRIBUTING.md
content/themes/casper/SECURITY.md
content/themes/source/SECURITY.md
SECURITY.md
renovate.json
*.html
@ -61,7 +64,9 @@ bower_components/**
.editorconfig
gulpfile.js
!content/themes/casper/gulpfile.js
!content/themes/source/gulpfile.js
package-lock.json
content/themes/casper/config.*.json
content/themes/source/config.*.json
config.*.json
!core/shared/config/env/**

@ -0,0 +1 @@
Subproject commit e0483fd7f4d6a9e2d5c26c1e89d4322f1f2702d6

View file

@ -0,0 +1,40 @@
// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
const logging = require('@tryghost/logging');
const {createTransactionalMigration} = require('../../utils');
// For DDL - schema changes
// const {createNonTransactionalMigration} = require('../../utils');
// For DML - data changes
// const {createTransactionalMigration} = require('../../utils');
// Or use a specific helper
// const {addTable, createAddColumnMigration} = require('../../utils');
module.exports = createTransactionalMigration(
async function up() {
// don't do anything
// we don't want to change the active theme automatically
},
async function down(knex) {
// If the active theme is `source`, we want to revert the active theme to `casper`
// `source` is introduced in 5.67, so it does not exist in < 5.67
// Without this change, rolling back from 5.67 to < 5.67 results in an error:
// The currently active theme "source" is missing.
const rows = await knex
.select('value')
.from('settings')
.where({key: 'active_theme', value: 'source'});
if (rows.length === 0) {
logging.info(`Currently installed theme is not source - skipping migration`);
return;
}
logging.info(`Resetting the active theme to casper`);
await knex('settings')
.where('key', 'active_theme')
.update({value: 'casper'});
}
);

View file

@ -216,7 +216,7 @@
},
"theme": {
"active_theme": {
"defaultValue": "casper",
"defaultValue": "source",
"flags": "RO",
"type": "string"
}

View file

@ -200,8 +200,8 @@ async function installTheme(data, api) {
return data;
}
if (themeName.toLowerCase() === 'tryghost/casper') {
logging.warn('Skipping theme install as Casper is the default theme.');
if (themeName.toLowerCase() === 'tryghost/source') {
logging.warn('Skipping theme install as Source is the default theme.');
return data;
}
@ -219,7 +219,7 @@ async function installTheme(data, api) {
context: {internal: true}
});
} catch (error) {
//Fallback to Casper by doing nothing as the theme setting update is the last step
//Fallback to Casper/Source by doing nothing as the theme setting update is the last step
logging.warn(tpl(messages.failedThemeInstall, {themeName, error: error.message}));
}

View file

@ -18,8 +18,8 @@ const settingsCache = require('../../../shared/settings-cache');
const messages = {
themeDoesNotExist: 'Theme does not exist.',
invalidThemeName: 'Please select a valid theme.',
overrideCasper: 'Please rename your zip, it\'s not allowed to override the default casper theme.',
destroyCasper: 'Deleting the default casper theme is not allowed.',
overrideDefaultTheme: 'Please rename your zip, it\'s not allowed to override the default theme.',
destroyDefaultTheme: 'Deleting the default theme is not allowed.',
destroyActive: 'Deleting the active theme is not allowed.'
};
@ -49,10 +49,10 @@ module.exports = {
const themeName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
const backupName = `${themeName}_${ObjectID()}`;
// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
// check if zip name matches one of the default themes
if (zip.name === 'casper.zip' || zip.name === 'source.zip') {
throw new errors.ValidationError({
message: tpl(messages.overrideCasper)
message: tpl(messages.overrideDefaultTheme)
});
}
@ -127,9 +127,9 @@ module.exports = {
}
},
destroy: async function (themeName) {
if (themeName === 'casper') {
if (themeName === 'casper' || themeName === 'source') {
throw new errors.ValidationError({
message: tpl(messages.destroyCasper)
message: tpl(messages.destroyDefaultTheme)
});
}

View file

@ -1,2 +1,3 @@
test/coverage/**
test/utils/fixtures/themes/casper/assets/**
test/utils/fixtures/themes/source/assets/**

View file

@ -98,7 +98,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -367,7 +367,7 @@ Object {
"settings": Array [
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
],
}
@ -508,7 +508,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -866,7 +866,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -1223,7 +1223,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -1585,7 +1585,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -2035,7 +2035,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -2457,7 +2457,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",

View file

@ -45,17 +45,17 @@ describe('Themes API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.themes);
localUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(6);
jsonResponse.themes.length.should.eql(7);
localUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
jsonResponse.themes[0].name.should.eql('broken-theme');
jsonResponse.themes[0].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[0].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme', 'templates');
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme');
jsonResponse.themes[1].name.should.eql('casper');
jsonResponse.themes[1].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[1].active.should.be.true();
jsonResponse.themes[1].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[2], 'theme');
jsonResponse.themes[2].name.should.eql('locale-theme');
@ -67,15 +67,20 @@ describe('Themes API', function () {
jsonResponse.themes[3].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[3].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme');
jsonResponse.themes[4].name.should.eql('test-theme');
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme', 'templates');
jsonResponse.themes[4].name.should.eql('source');
jsonResponse.themes[4].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[4].active.should.be.false();
jsonResponse.themes[4].active.should.be.true();
localUtils.API.checkResponse(jsonResponse.themes[5], 'theme');
jsonResponse.themes[5].name.should.eql('test-theme-channels');
jsonResponse.themes[5].package.should.be.false();
jsonResponse.themes[5].name.should.eql('test-theme');
jsonResponse.themes[5].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[5].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[6], 'theme');
jsonResponse.themes[6].name.should.eql('test-theme-channels');
jsonResponse.themes[6].package.should.be.false();
jsonResponse.themes[6].active.should.be.false();
});
it('Can download a theme', async function () {
@ -129,13 +134,13 @@ describe('Themes API', function () {
should.exist(jsonResponse3.themes);
localUtils.API.checkResponse(jsonResponse3, 'themes');
jsonResponse3.themes.length.should.eql(7);
jsonResponse3.themes.length.should.eql(8);
// Casper should be present and still active
const casperTheme = _.find(jsonResponse3.themes, {name: 'casper'});
should.exist(casperTheme);
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
casperTheme.active.should.be.true();
// Source should be present and still active
const sourceTheme = _.find(jsonResponse3.themes, {name: 'source'});
should.exist(sourceTheme);
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
sourceTheme.active.should.be.true();
// The added theme should be here
const addedTheme = _.find(jsonResponse3.themes, {name: 'valid'});
@ -149,6 +154,7 @@ describe('Themes API', function () {
'casper',
'locale-theme',
'members-test-theme',
'source',
'test-theme',
'test-theme-channels',
'valid'
@ -171,7 +177,7 @@ describe('Themes API', function () {
tmpFolderContents.splice(i, 1);
}
}
tmpFolderContents.should.be.an.Array().with.lengthOf(10);
tmpFolderContents.should.be.an.Array().with.lengthOf(11);
tmpFolderContents.should.eql([
'broken-theme',
@ -180,6 +186,7 @@ describe('Themes API', function () {
'invalid.zip',
'locale-theme',
'members-test-theme',
'source',
'test-theme',
'test-theme-channels',
'valid.zip',
@ -196,13 +203,13 @@ describe('Themes API', function () {
should.exist(jsonResponse2.themes);
localUtils.API.checkResponse(jsonResponse2, 'themes');
jsonResponse2.themes.length.should.eql(6);
jsonResponse2.themes.length.should.eql(7);
// Casper should be present and still active
const casperTheme = _.find(jsonResponse2.themes, {name: 'casper'});
should.exist(casperTheme);
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
casperTheme.active.should.be.true();
// Source should be present and still active
const sourceTheme = _.find(jsonResponse2.themes, {name: 'source'});
should.exist(sourceTheme);
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
sourceTheme.active.should.be.true();
// The deleted theme should not be here
const deletedTheme = _.find(jsonResponse2.themes, {name: 'valid'});
@ -238,12 +245,12 @@ describe('Themes API', function () {
should.exist(jsonResponse.themes);
localUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(6);
jsonResponse.themes.length.should.eql(7);
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
should.exist(casperTheme);
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
casperTheme.active.should.be.true();
const sourceTheme = _.find(jsonResponse.themes, {name: 'source'});
should.exist(sourceTheme);
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
sourceTheme.active.should.be.true();
const testTheme = _.find(jsonResponse.themes, {name: 'test-theme'});
should.exist(testTheme);
@ -263,8 +270,8 @@ describe('Themes API', function () {
localUtils.API.checkResponse(jsonResponse2, 'themes');
jsonResponse2.themes.length.should.eql(1);
const casperTheme2 = _.find(jsonResponse2.themes, {name: 'casper'});
should.not.exist(casperTheme2);
const sourceTheme2 = _.find(jsonResponse2.themes, {name: 'source'});
should.not.exist(sourceTheme2);
const testTheme2 = _.find(jsonResponse2.themes, {name: 'test-theme'});
should.exist(testTheme2);
@ -318,7 +325,7 @@ describe('Themes API', function () {
});
it('Can re-upload the active theme to override', async function () {
// The tricky thing about this test is the default active theme is Casper and you're not allowed to override it.
// The tricky thing about this test is the default active theme is Source and you're not allowed to override it.
// So we upload a valid theme, activate it, and then upload again.
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
if (key === 'active_theme') {

View file

@ -68,7 +68,6 @@ describe('Default Frontend routing', function () {
$('body.home-template').length.should.equal(1);
$('article.post').length.should.equal(7);
$('article.tag-getting-started').length.should.equal(7);
res.text.should.not.containEql('__GHOST_URL__');
});

View file

@ -114,7 +114,7 @@ describe('Importer', function () {
return models.Settings.findOne(_.merge({key: 'active_theme'}, testUtils.context.internal));
})
.then(function (result) {
result.attributes.value.should.eql('casper');
result.attributes.value.should.eql('source');
});
});

View file

@ -195,7 +195,7 @@ describe('Authentication API', function () {
const password = 'thisissupersafe';
const requestMock = nock('https://api.github.com')
.get('/repos/tryghost/casper/zipball')
.get('/repos/tryghost/source/zipball')
.query(true)
.replyWithFile(200, fixtureManager.getPathForFixture('themes/valid.zip'));
@ -207,7 +207,7 @@ describe('Authentication API', function () {
email,
password,
blogTitle: 'a test blog',
theme: 'TryGhost/Casper',
theme: 'TryGhost/Source',
accentColor: '#85FF00',
description: 'Custom Site Description on Setup &mdash; great for everyone'
}]
@ -236,7 +236,7 @@ describe('Authentication API', function () {
const activeTheme = await settingsCache.get('active_theme');
const accentColor = await settingsCache.get('accent_color');
const description = await settingsCache.get('description');
assert.equal(activeTheme, 'casper', 'The theme casper should have been installed');
assert.equal(activeTheme, 'source', 'The theme Source should have been installed');
assert.equal(accentColor, '#85FF00', 'The accent color should have been set');
assert.equal(description, 'Custom Site Description on Setup &mdash; great for everyone', 'The site description should have been set');

View file

@ -65,8 +65,7 @@ describe('Dynamic Routing', function () {
$('title').text().should.equal('Ghost');
$('body.home-template').length.should.equal(1);
$('article.post').length.should.equal(5);
$('article.tag-getting-started').length.should.equal(5);
$('article.post').length.should.equal(7);
done();
});
@ -142,9 +141,8 @@ describe('Dynamic Routing', function () {
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
$('body').attr('class').should.eql('tag-template tag-getting-started');
$('body').attr('class').should.eql('tag-template tag-getting-started has-sans-title has-sans-body');
$('article.post').length.should.equal(5);
$('article.tag-getting-started').length.should.equal(5);
done();
});

View file

@ -37,7 +37,7 @@ describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = '1b75aae9befefea53b17c9c1991c8a1d';
const currentFixturesHash = '4db87173699ad9c9d8a67ccab96dfd2d';
const currentSettingsHash = '3a7ca0aa6a06cba47e3e898aef7029c2';
const currentSettingsHash = '3128d4ec667a50049486b0c21f04be07';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
// If this test is failing, then it is likely a change has been made that requires a DB version bump,

View file

@ -124,7 +124,7 @@ const prepareContentFolder = async ({contentFolder, redirectsFile = true, routes
await fs.ensureDir(path.join(contentFolderForTests, 'adapters'));
await fs.ensureDir(path.join(contentFolderForTests, 'settings'));
// Copy all themes into the new test content folder. Default active theme is always casper.
// Copy all themes into the new test content folder. Default active theme is always source.
// If you want to use a different theme, you have to set the active theme (e.g. stub)
await fs.copy(
path.join(__dirname, 'fixtures', 'themes'),

View file

@ -75,12 +75,12 @@ const prepareContentFolder = (options) => {
fs.ensureDirSync(path.join(contentFolderForTests, 'settings'));
if (options.copyThemes) {
// Copy all themes into the new test content folder. Default active theme is always casper. If you want to use a different theme, you have to set the active theme (e.g. stub)
// Copy all themes into the new test content folder. Default active theme is always source. If you want to use a different theme, you have to set the active theme (e.g. stub)
fs.copySync(path.join(__dirname, 'fixtures', 'themes'), path.join(contentFolderForTests, 'themes'));
}
// Copy theme even if frontend is disabled, as admin can use casper when viewing themes section
fs.copySync(path.join(__dirname, 'fixtures', 'themes', 'casper'), path.join(contentFolderForTests, 'themes', 'casper'));
// Copy theme even if frontend is disabled, as admin can use source when viewing themes section
fs.copySync(path.join(__dirname, 'fixtures', 'themes', 'source'), path.join(contentFolderForTests, 'themes', 'source'));
if (options.redirectsFile) {
redirects.setupFile(contentFolderForTests, options.redirectsFileExt);

View file

@ -212,7 +212,7 @@
},
"theme": {
"active_theme": {
"defaultValue": "casper",
"defaultValue": "source",
"flags": "RO",
"type": "string"
}

View file

@ -224,7 +224,7 @@
},
"theme": {
"active_theme": {
"defaultValue": "casper",