0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-18 02:21:47 -05:00

Merged more-menu-des-623

This commit is contained in:
Sodbileg Gansukh 2024-08-07 11:24:20 +08:00
commit 6bd6ec4223
82 changed files with 1521 additions and 1283 deletions

View file

@ -38,6 +38,7 @@ jobs:
- [ ] Uses the correct utils
- [ ] Contains a minimal changeset
- [ ] Does not mix DDL/DML operations
- [ ] Tested in MySQL and SQLite
### Schema changes

View file

@ -4,7 +4,7 @@ import articleBodyStyles from './articleBodyStyles';
import getUsername from '../utils/get-username';
import {ActivityPubAPI} from '../api/activitypub';
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Avatar, Button, ButtonGroup, Heading, List, ListItem, Page, SelectOption, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system';
import {Avatar, Button, ButtonGroup, Heading, Icon, List, ListItem, Page, SelectOption, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system';
import {useBrowseSite} from '@tryghost/admin-x-framework/api/site';
import {useQuery} from '@tanstack/react-query';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@ -123,6 +123,7 @@ const ActivityPubComponent: React.FC = () => {
actor={activity.actor}
layout={selectedOption.value}
object={activity.object}
type={activity.type}
/>
</li>
))}
@ -167,6 +168,7 @@ const ActivityPubComponent: React.FC = () => {
actor={activity.actor}
layout={selectedOption.value}
object={activity.object}
type={activity.object.type}
/>
</li>
))}
@ -320,7 +322,7 @@ ${image &&
);
};
const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties, layout: string }> = ({actor, object, layout}) => {
const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties, layout: string, type: string }> = ({actor, object, layout, type}) => {
const parser = new DOMParser();
const doc = parser.parseFromString(object.content || '', 'text/html');
@ -392,27 +394,40 @@ const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProp
setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms
};
let author = actor;
if (type === 'Announce' && object.type === 'Note') {
author = typeof object.attributedTo === 'object' ? object.attributedTo as ActorProperties : actor;
}
if (layout === 'feed') {
return (
<>
{object && (
<div className='group/article relative flex cursor-pointer items-start gap-4 pt-4'>
<img className='z-10 w-9 rounded' src={actor.icon?.url}/>
<div className='border-1 z-10 -mt-1 flex flex-col items-start justify-between border-b border-b-grey-200 pb-4' data-test-activity>
<div className='relative z-10 flex w-full overflow-visible text-[1.5rem]'>
<p className='mr-1 truncate whitespace-nowrap font-bold' data-test-activity-heading>{actor.name}</p>
<span className='truncate text-grey-700'>{getUsername(actor)}</span>
<span className='whitespace-nowrap text-grey-700 before:mx-1 before:content-["·"]'>{timestamp}</span>
</div>
<div className='relative z-10 w-full gap-4'>
<div className='flex flex-col'>
{object.name && <Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>}
<p className='text-pretty text-[1.5rem] text-grey-900'>{plainTextContent}</p>
{/* <p className='text-pretty text-md text-grey-900'>{object.content}</p> */}
{renderAttachment()}
<div className='mt-3 flex gap-2'>
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id="like" size='md' unstyled={true} onClick={handleLikeClick}/>
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
<div className='group/article relative cursor-pointer pt-4'>
{(type === 'Announce' && object.type === 'Note') && <div className='z-10 mb-2 flex items-center gap-4 text-grey-700'>
<div className='z-10 flex w-10 justify-end'><Icon colorClass='text-grey-700' name='reload' size={'sm'}></Icon></div>
<span className='z-10'>{actor.name} reposted</span>
</div>}
<div className='flex items-start gap-4'>
<img className='z-10 w-10 rounded' src={author.icon?.url}/>
<div className='border-1 z-10 -mt-1 flex flex-col items-start justify-between border-b border-b-grey-200 pb-4' data-test-activity>
<div className='relative z-10 mb-2 flex w-full flex-col overflow-visible text-[1.5rem]'>
<span className='mr-1 truncate whitespace-nowrap font-bold' data-test-activity-heading>{author.name}</span>
<div className='flex'>
<span className='truncate text-grey-700'>{getUsername(author)}</span>
<span className='whitespace-nowrap text-grey-700 before:mx-1 before:content-["·"]'>{timestamp}</span>
</div>
</div>
<div className='relative z-10 w-full gap-4'>
<div className='flex flex-col'>
{object.name && <Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>}
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>
{/* <p className='text-pretty text-md text-grey-900'>{object.content}</p> */}
{renderAttachment()}
<div className='mt-3 flex gap-2'>
<Button className={`self-start text-grey-500 transition-all hover:text-grey-800 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`} hideLabel={true} icon='heart' id="like" size='md' unstyled={true} onClick={handleLikeClick}/>
<span className={`text-grey-800 ${isLiked ? 'opacity-100' : 'opacity-0'}`}>1</span>
</div>
</div>
</div>
</div>

View file

@ -22,4 +22,17 @@ animation: bump 0.3s ease-in-out;
.ap-red-heart path {
fill: #F50B23;
}
}
.ap-note-content a {
color: rgb(236 72 153) !important;
}
.ap-note-content a:hover {
color: rgb(219 39 119) !important;
}
.ap-note-content p + p {
margin-top: 1.5rem !important;
}

View file

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" id="Button-Refresh-Arrows--Streamline-Ultimate.svg" height="24" width="24"><desc>Button Refresh Arrows Streamline Icon: https://streamlinehq.com</desc><path d="m5.25 14.248 0 4.5 -4.5 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m18.75 9.748 0 -4.5 4.5 0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M19.032 5.245A9.752 9.752 0 0 1 8.246 21" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M4.967 18.751A9.753 9.753 0 0 1 15.754 3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 819 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Share-1--Streamline-Streamline--3.0.svg" height="24" width="24"><desc>Share 1 Streamline Icon: https://streamlinehq.com</desc><defs></defs><title>share-1</title><path d="M17.25 8.25h1.5a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1 -1.5 1.5H5.25a1.5 1.5 0 0 1 -1.5 -1.5v-12a1.5 1.5 0 0 1 1.5 -1.5h1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m12 0.75 0 10.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M8.25 4.5 12 0.75l3.75 3.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 750 B

View file

@ -96,4 +96,5 @@
/* Prose classes are for formatting arbitrary HTML that comes from the API */
.gh-prose-links a {
color: #30CF43;
}
}

View file

@ -18,6 +18,7 @@ module.exports = {
colors: {
transparent: 'transparent',
current: 'currentColor',
accent: 'var(--accent-color, #ff0095)',
white: '#FFF',
black: '#15171A',
grey: {

View file

@ -13,7 +13,7 @@ export type ObjectProperties = {
name: string;
content: string;
url?: string | undefined;
attributedTo?: string | object[] | undefined;
attributedTo?: object | string | object[] | undefined;
image?: string;
published?: string;
preview?: {type: string, content: string};

View file

@ -13,7 +13,7 @@ const OfferContainer: React.FC<{offerTitle: string, tier: Tier, cadence: string,
{offerTitle, tier, cadence, redemptions, type, amount, currency, offerId, offerCode, goToOfferEdit}) => {
const {discountOffer} = getOfferDiscount(type, amount, cadence, currency || 'USD', tier);
return <div className='group flex h-full cursor-pointer flex-col justify-between gap-4 break-words rounded-sm border border-transparent bg-grey-100 p-5 transition-all hover:border-grey-100 hover:bg-grey-75 hover:shadow-sm dark:bg-grey-950 dark:hover:border-grey-800 min-[900px]:min-h-[187px]' onClick={() => goToOfferEdit(offerId)}>
<span className='text-[1.65rem] font-bold leading-tight tracking-tight'>{offerTitle}</span>
<span className='text-[1.65rem] font-bold leading-tight tracking-tight text-black dark:text-white'>{offerTitle}</span>
<div className='flex flex-col'>
<span className={`text-sm font-semibold uppercase`}>{discountOffer}</span>
<div className='flex gap-1 text-xs'>

View file

@ -22,8 +22,8 @@ export const TrialDaysLabel: React.FC<{size?: 'sm' | 'md'; trialDays: number;}>
return (
<span className={containerClassName}>
<span className="absolute inset-0 block rounded-full bg-pink opacity-20"></span>
<span className='dark:text-pink'>{trialDays} days free</span>
<span className="absolute inset-0 block rounded-full bg-accent opacity-20 dark:bg-pink"></span>
<span className="dark:text-pink">{trialDays} days free</span>
</span>
);
};
@ -96,7 +96,7 @@ const TierDetailPreview: React.FC<TierDetailPreviewProps> = ({tier, isFreeTier})
<div className='rounded-sm border border-grey-200 bg-white dark:border-transparent'>
<div className="flex-column relative flex min-h-[200px] w-full max-w-[420px] scale-90 items-start justify-stretch rounded bg-white p-4">
<div className="min-h-[56px] w-full">
<h4 className={`-mt-1 mb-0 w-full break-words text-lg font-semibold leading-tight text-pink ${!name && 'opacity-30'}`}>{name || (isFreeTier ? 'Free' : 'Bronze')}</h4>
<h4 className={`-mt-1 mb-0 w-full break-words text-lg font-semibold leading-tight text-accent ${!name && 'opacity-30'}`}>{name || (isFreeTier ? 'Free' : 'Bronze')}</h4>
<div className="mt-4 flex w-full flex-row flex-wrap items-end justify-between gap-x-1 gap-y-[10px]">
<div className={`flex flex-wrap text-black ${((showingYearly && tier?.yearly_price === undefined) || (!showingYearly && tier?.monthly_price === undefined)) && !isFreeTier ? 'opacity-30' : ''}`}>
<span className="self-start text-[2.7rem] font-bold uppercase leading-[1.115]">{currencySymbol}</span>

View file

@ -30,7 +30,7 @@ const TierCard: React.FC<TierCardProps> = ({tier}) => {
<div className='w-full grow' onClick={() => {
updateRoute({route: `tiers/${tier.id}`});
}}>
<div className='text-[1.65rem] font-bold leading-tight tracking-tight text-pink'>{tier.name}</div>
<div className='text-[1.65rem] font-bold leading-tight tracking-tight text-black dark:text-white'>{tier.name}</div>
<div className='mt-2 flex items-baseline'>
<span className="ml-1 translate-y-[-3px] text-md font-bold uppercase">{currencySymbol}</span>
<span className='text-xl font-bold tracking-tighter'>{numberWithCommas(currencyToDecimal(tier.monthly_price || 0))}</span>

View file

@ -24,6 +24,6 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2"
"@tryghost/errors": "1.3.5"
}
}

View file

@ -95,7 +95,9 @@
@placeholder={{@bodyPlaceholder}}
@cardConfig={{@cardOptions}}
@onChange={{@onBodyChange}}
@updateSecondaryInstanceModel={{@updateSecondaryInstanceModel}}
@registerAPI={{this.registerEditorAPI}}
@registerSecondaryAPI={{this.registerSecondaryEditorAPI}}
@cursorDidExitAtTop={{if this.feature.editorExcerpt this.focusExcerpt this.focusTitle}}
@updateWordCount={{@updateWordCount}}
@updatePostTkCount={{@updatePostTkCount}}

View file

@ -15,6 +15,7 @@ export default class GhKoenigEditorLexical extends Component {
uploadUrl = `${ghostPaths().apiRoot}/images/upload/`;
editorAPI = null;
secondaryEditorAPI = null;
skipFocusEditor = false;
@tracked titleIsHovered = false;
@ -232,6 +233,12 @@ export default class GhKoenigEditorLexical extends Component {
this.args.registerAPI(API);
}
@action
registerSecondaryEditorAPI(API) {
this.secondaryEditorAPI = API;
this.args.registerSecondaryAPI(API);
}
// focus the editor when the editor canvas is clicked below the editor content,
// otherwise the browser will defocus the editor and the cursor will disappear
@action

View file

@ -853,6 +853,7 @@
post=this.post
editorAPI=this.editorAPI
toggleSettingsMenu=this.toggleSettingsMenu
secondaryEditorAPI=this.secondaryEditorAPI
}}
@close={{this.closePostHistory}}
@modifier="total-overlay post-history" />

View file

@ -669,34 +669,43 @@ export default class KoenigLexicalEditor extends Component {
const multiplayerDocId = cardConfig.post.id;
const multiplayerUsername = this.session.user.name;
const KGEditorComponent = ({isInitInstance}) => {
return (
<div style={isInitInstance ? {visibility: 'hidden', position: 'absolute'} : {}}>
<KoenigComposer
editorResource={this.editorResource}
cardConfig={cardConfig}
enableMultiplayer={enableMultiplayer}
fileUploader={{useFileUpload, fileTypes}}
initialEditorState={this.args.lexical}
multiplayerUsername={multiplayerUsername}
multiplayerDocId={multiplayerDocId}
multiplayerEndpoint={multiplayerEndpoint}
onError={this.onError}
darkMode={this.feature.nightShift}
isTKEnabled={true}
>
<KoenigEditor
editorResource={this.editorResource}
cursorDidExitAtTop={isInitInstance ? null : this.args.cursorDidExitAtTop}
placeholderText={isInitInstance ? null : this.args.placeholderText}
darkMode={isInitInstance ? null : this.feature.nightShift}
onChange={isInitInstance ? this.args.updateSecondaryInstanceModel : this.args.onChange}
registerAPI={isInitInstance ? this.args.registerSecondaryAPI : this.args.registerAPI}
/>
<WordCountPlugin editorResource={this.editorResource} onChange={isInitInstance ? () => {} : this.args.updateWordCount} />
<TKCountPlugin editorResource={this.editorResource} onChange={isInitInstance ? () => {} : this.args.updatePostTkCount} />
</KoenigComposer>
</div>
);
};
return (
<div className={['koenig-react-editor', 'koenig-lexical', this.args.className].filter(Boolean).join(' ')}>
<ErrorHandler config={this.config}>
<Suspense fallback={<p className="koenig-react-editor-loading">Loading editor...</p>}>
<KoenigComposer
editorResource={this.editorResource}
cardConfig={cardConfig}
enableMultiplayer={enableMultiplayer}
fileUploader={{useFileUpload, fileTypes}}
initialEditorState={this.args.lexical}
multiplayerUsername={multiplayerUsername}
multiplayerDocId={multiplayerDocId}
multiplayerEndpoint={multiplayerEndpoint}
onError={this.onError}
darkMode={this.feature.nightShift}
isTKEnabled={true}
>
<KoenigEditor
editorResource={this.editorResource}
cursorDidExitAtTop={this.args.cursorDidExitAtTop}
placeholderText={this.args.placeholder}
darkMode={this.feature.nightShift}
onChange={this.args.onChange}
registerAPI={this.args.registerAPI}
/>
<WordCountPlugin editorResource={this.editorResource} onChange={this.args.updateWordCount} />
<TKCountPlugin editorResource={this.editorResource} onChange={this.args.updatePostTkCount} />
</KoenigComposer>
<KGEditorComponent />
<KGEditorComponent isInitInstance={true} />
</Suspense>
</ErrorHandler>
</div>

View file

@ -33,6 +33,7 @@
@lexical={{this.selectedRevision.lexical}}
@cardConfig={{this.cardConfig}}
@registerAPI={{this.registerSelectedEditorApi}}
@registerSecondaryAPI={{this.registerSecondarySelectedEditorApi}}
/>
</div>
</div>

View file

@ -31,6 +31,7 @@ export default class ModalPostHistory extends Component {
super(...arguments);
this.post = this.args.model.post;
this.editorAPI = this.args.model.editorAPI;
this.secondaryEditorAPI = this.args.model.secondaryEditorAPI;
this.toggleSettingsMenu = this.args.model.toggleSettingsMenu;
}
@ -101,6 +102,11 @@ export default class ModalPostHistory extends Component {
this.selectedEditor = api;
}
@action
registerSecondarySelectedEditorApi(api) {
this.secondarySelectedEditor = api;
}
@action
registerComparisonEditorApi(api) {
this.comparisonEditor = api;
@ -130,6 +136,7 @@ export default class ModalPostHistory extends Component {
updateEditor: () => {
const state = this.editorAPI.editorInstance.parseEditorState(revision.lexical);
this.editorAPI.editorInstance.setEditorState(state);
this.secondaryEditorAPI.editorInstance.setEditorState(state);
},
closePostHistoryModal: () => {
this.closeModal();

View file

@ -35,11 +35,64 @@
{{/let}}
</div>
<div style="display: flex; gap: 4px;">
<button class="gh-post-list-cta edit">♻️<span>Refresh</span></button>
<button class="gh-post-list-cta edit" {{on "click" this.togglePublishFlowModal}}>🔁<span>Share</span></button>
<LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title="">
{{svg-jar "pen" title=""}}<span>Edit post</span>
</LinkTo>
<button class="gh-post-list-cta edit">
{{svg-jar "reload" title="Refresh post analytics"}}<span>Refresh</span>
</button>
<button class="gh-post-list-cta edit" {{on "click" this.togglePublishFlowModal}}>
{{svg-jar "share" title="Share post"}}<span>Share</span>
</button>
{{!-- <LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title="">
{{svg-jar "pen" title="Edit post"}}<span>Edit post</span>
</LinkTo> --}}
<span class="dropdown">
<GhDropdownButton
@dropdownName="analytics-actions-menu"
@classNames="gh-post-list-cta edit gh-btn-icon icon-only gh-btn-action-icon"
@title="Analytics Actions"
data-test-button="analytics-actions"
>
<span>
{{svg-jar "dotdotdot"}}
<span class="hidden">Actions</span>
</span>
</GhDropdownButton>
<GhDropdown
@name="analytics-actions-menu"
@tagName="ul"
@classNames="gh-analytics-actions-menu dropdown-menu dropdown-triangle-top-right"
>
<li>
<button
type="button"
class="mr2"
data-test-button="edit-post">
{{!-- <LinkTo @route="lexical-editor.edit" @models={{array this.post.displayName this.post.id}} class="gh-post-list-cta edit" title=""></LinkTo> --}}
<span>Edit post</span>
</button>
</li>
<li>
<button
type="button"
class="mr2"
{{!-- {{on "click" this.confirmLogoutMember}} --}}
data-test-button="view-post"
>
<span>View in browser</span>
</button>
</li>
<li>
<button
type="button"
class="mr2"
{{on "click" this.deletePosts}}
data-test-button="delete-post"
>
<span class="red">Delete post</span>
</button>
</li>
</GhDropdown>
</span>
</div>
</div>
</div>

View file

@ -297,6 +297,11 @@ export default class LexicalEditorController extends Controller {
this._timedSaveTask.perform();
}
@action
updateSecondaryInstanceModel(lexical) {
this.set('post.secondaryLexicalState', JSON.stringify(lexical));
}
@action
updateTitleScratch(title) {
this.set('post.titleScratch', title);
@ -423,6 +428,11 @@ export default class LexicalEditorController extends Controller {
this.editorAPI = API;
}
@action
registerSecondaryEditorAPI(API) {
this.secondaryEditorAPI = API;
}
@action
clearFeatureImage() {
this.post.set('featureImage', null);
@ -1221,7 +1231,6 @@ export default class LexicalEditorController extends Controller {
_timedSaveTask;
/* Private methods -------------------------------------------------------*/
_hasDirtyAttributes() {
let post = this.post;
@ -1229,8 +1238,7 @@ export default class LexicalEditorController extends Controller {
return false;
}
// if the Adapter failed to save the post isError will be true
// and we should consider the post still dirty.
// If the Adapter failed to save the post, isError will be true, and we should consider the post still dirty.
if (post.get('isError')) {
this._leaveModalReason = {reason: 'isError', context: post.errors.messages};
return true;
@ -1245,37 +1253,32 @@ export default class LexicalEditorController extends Controller {
return true;
}
// titleScratch isn't an attr so needs a manual dirty check
// Title scratch comparison
if (post.titleScratch !== post.title) {
this._leaveModalReason = {reason: 'title is different', context: {current: post.title, scratch: post.titleScratch}};
return true;
}
// scratch isn't an attr so needs a manual dirty check
// Lexical and scratch comparison
let lexical = post.get('lexical');
let scratch = post.get('lexicalScratch');
// additional guard in case we are trying to compare null with undefined
if (scratch || lexical) {
if (scratch !== lexical) {
// lexical can dynamically set direction on loading editor state (e.g. "rtl"/"ltr") per the DOM context
// and we need to ignore this as a change from the user; see https://github.com/facebook/lexical/issues/4998
const scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : [];
const lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : [];
let secondaryLexical = post.get('secondaryLexicalState');
// // nullling is typically faster than delete
scratchChildNodes.forEach(child => child.direction = null);
lexicalChildNodes.forEach(child => child.direction = null);
let lexicalChildNodes = lexical ? JSON.parse(lexical).root?.children : [];
let scratchChildNodes = scratch ? JSON.parse(scratch).root?.children : [];
let secondaryLexicalChildNodes = secondaryLexical ? JSON.parse(secondaryLexical).root?.children : [];
if (JSON.stringify(scratchChildNodes) === JSON.stringify(lexicalChildNodes)) {
return false;
}
lexicalChildNodes.forEach(child => child.direction = null);
scratchChildNodes.forEach(child => child.direction = null);
secondaryLexicalChildNodes.forEach(child => child.direction = null);
this._leaveModalReason = {reason: 'lexical is different', context: {current: lexical, scratch}};
return true;
}
}
// Compare initLexical with scratch
let isSecondaryDirty = secondaryLexical && scratch && JSON.stringify(secondaryLexicalChildNodes) !== JSON.stringify(scratchChildNodes);
// new+unsaved posts always return `hasDirtyAttributes: true`
// Compare lexical with scratch
let isLexicalDirty = lexical && scratch && JSON.stringify(lexicalChildNodes) !== JSON.stringify(scratchChildNodes);
// New+unsaved posts always return `hasDirtyAttributes: true`
// so we need a manual check to see if any
if (post.get('isNew')) {
let changedAttributes = Object.keys(post.changedAttributes());
@ -1286,15 +1289,26 @@ export default class LexicalEditorController extends Controller {
return changedAttributes.length ? true : false;
}
// we've covered all the non-tracked cases we care about so fall
// We've covered all the non-tracked cases we care about so fall
// back on Ember Data's default dirty attribute checks
let {hasDirtyAttributes} = post;
if (hasDirtyAttributes) {
this._leaveModalReason = {reason: 'post.hasDirtyAttributes === true', context: post.changedAttributes()};
return true;
}
return hasDirtyAttributes;
// If either comparison is not dirty, return false, because scratch is always up to date.
if (!isSecondaryDirty || !isLexicalDirty) {
return false;
}
// If both comparisons are dirty, consider the post dirty
if (isSecondaryDirty && isLexicalDirty) {
this._leaveModalReason = {reason: 'initLexical and lexical are different from scratch', context: {secondaryLexical, lexical, scratch}};
return true;
}
return false;
}
_showSaveNotification(prevStatus, status, delayed) {

View file

@ -136,6 +136,9 @@ export default Model.extend(Comparable, ValidationEngine, {
scratch: null,
lexicalScratch: null,
titleScratch: null,
//This is used to store the initial lexical state from the
// secondary editor to get the schema up to date in case its outdated
secondaryLexicalState: null,
// For use by date/time pickers - will be validated then converted to UTC
// on save. Updated by an observer whenever publishedAtUTC changes.

View file

@ -393,7 +393,7 @@ Post context menu
stroke-width: 1.8px;
}
.gh-posts-context-menu li:last-child::before {
.gh-posts-context-menu li:last-child::before, .gh-analytics-actions-menu li:last-child::before {
display: block;
position: relative;
content: "";

View file

@ -776,6 +776,17 @@
border-radius: var(--border-radius);
}
.gh-analytics-actions-menu {
top: calc(100% + 6px);
left: auto;
right: 0;
}
.gh-analytics-actions-menu.fade-out {
animation-duration: .001s;
pointer-events: none;
}
.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-newsletter-clicks,
.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-source-attribution,
.gh-post-analytics-box.gh-post-analytics-mentions {
@ -1523,6 +1534,10 @@
transition: all .1s linear;
}
span.dropdown .gh-post-list-cta > span {
padding: 0;
}
.gh-post-list-cta.edit.is-hovered > *,
.gh-post-list-cta.edit.is-hovered:hover > *,
.gh-post-list-cta.edit:not(.is-hovered):hover > * {

View file

@ -347,3 +347,5 @@
margin-left: 8px;
background: var(--whitegrey);
}

View file

@ -73,6 +73,7 @@
@body={{readonly this.post.lexicalScratch}}
@bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}}
@onBodyChange={{this.updateScratch}}
@updateSecondaryInstanceModel={{this.updateSecondaryInstanceModel}}
@headerOffset={{editor.headerHeight}}
@scrollContainerSelector=".gh-koenig-editor"
@scrollOffsetBottomSelector=".gh-mobile-nav-bar"
@ -97,6 +98,7 @@
}}
@postType={{this.post.displayName}}
@registerAPI={{this.registerEditorAPI}}
@registerSecondaryAPI={{this.registerSecondaryEditorAPI}}
@savePostTask={{this.savePostTask}}
/>
@ -136,6 +138,7 @@
@updateSlugTask={{this.updateSlugTask}}
@savePostTask={{this.savePostTask}}
@editorAPI={{this.editorAPI}}
@secondaryEditorAPI={{this.secondaryEditorAPI}}
@toggleSettingsMenu={{this.toggleSettingsMenu}}
/>
{{/if}}

View file

@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "5.88.3",
"version": "5.89.0",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" id="Share-1--Streamline-Streamline--3.0.svg" height="24" width="24"><desc>Share 1 Streamline Icon: https://streamlinehq.com</desc><defs></defs><title>share-1</title><path d="M17.25 8.25h1.5a1.5 1.5 0 0 1 1.5 1.5v12a1.5 1.5 0 0 1 -1.5 1.5H5.25a1.5 1.5 0 0 1 -1.5 -1.5v-12a1.5 1.5 0 0 1 1.5 -1.5h1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="m12 0.75 0 10.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path><path d="M8.25 4.5 12 0.75l3.75 3.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"></path></svg>

After

Width:  |  Height:  |  Size: 750 B

View file

@ -208,7 +208,8 @@ describe('Unit: Controller: lexical-editor', function () {
titleScratch: 'this is a title',
status: 'published',
lexical: initialLexicalString,
lexicalScratch: initialLexicalString
lexicalScratch: initialLexicalString,
secondaryLexicalState: initialLexicalString
}));
// synthetically update the lexicalScratch as if the editor itself made the modifications on loading the initial editorState
@ -225,5 +226,47 @@ describe('Unit: Controller: lexical-editor', function () {
isDirty = controller.get('hasDirtyAttributes');
expect(isDirty).to.be.true;
});
it('dirty is false if secondaryLexical and scratch matches, but lexical is outdated', async function () {
const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`;
const lexicalScratch = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`;
const secondLexicalInstance = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Here's some new text","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`;
let controller = this.owner.lookup('controller:lexical-editor');
controller.set('post', EmberObject.create({
title: 'this is a title',
titleScratch: 'this is a title',
status: 'published',
lexical: initialLexicalString,
lexicalScratch: lexicalScratch,
secondaryLexicalState: secondLexicalInstance
}));
let isDirty = controller.get('hasDirtyAttributes');
expect(isDirty).to.be.false;
});
it('dirty is true if secondaryLexical and lexical does not match scratch', async function () {
const initialLexicalString = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content","type": "extended-text","version": 1}],"direction": null,"format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`;
const lexicalScratch = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Sample content1234","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`;
const secondLexicalInstance = `{"root":{"children":[{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "Here's some new text","type": "extended-text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}`;
let controller = this.owner.lookup('controller:lexical-editor');
controller.set('post', EmberObject.create({
title: 'this is a title',
titleScratch: 'this is a title',
status: 'published',
lexical: initialLexicalString,
lexicalScratch: lexicalScratch,
secondaryLexicalState: secondLexicalInstance
}));
controller.send('updateScratch',JSON.parse(lexicalScratch));
let isDirty = controller.get('hasDirtyAttributes');
expect(isDirty).to.be.true;
});
});
});

View file

@ -24,11 +24,11 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/promise": "0.3.10",
"@tryghost/tpl": "0.1.30",
"@tryghost/validator": "0.2.11",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/promise": "0.3.12",
"@tryghost/tpl": "0.1.32",
"@tryghost/validator": "0.2.14",
"jsonpath": "1.1.1",
"lodash": "4.17.21"
}

View file

@ -24,8 +24,8 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.30",
"@tryghost/errors": "1.3.5",
"@tryghost/tpl": "0.1.32",
"bson-objectid": "2.0.4"
}
}

View file

@ -24,6 +24,6 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/logging": "2.4.15"
"@tryghost/logging": "2.4.18"
}
}

View file

@ -26,14 +26,14 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/in-memory-repository": "0.0.0",
"@tryghost/logging": "2.4.15",
"@tryghost/logging": "2.4.18",
"@tryghost/nql": "0.12.3",
"@tryghost/nql-filter-expansions": "0.0.0",
"@tryghost/post-events": "0.0.0",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"bson-objectid": "2.0.4",
"lodash": "4.17.21"
},

View file

@ -0,0 +1,64 @@
// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
const logging = require('@tryghost/logging');
const DatabaseInfo = require('@tryghost/database-info');
const {default: ObjectID} = require('bson-objectid');
// For DML - data changes
const {createTransactionalMigration} = require('../../utils');
module.exports = createTransactionalMigration(
async function up(knex) {
// Backfill missing offer redemptions
try {
// Select all subscriptions that have an `offer_id` but don't have a matching row in the `offer_redemptions` table
logging.info('Selecting subscriptions with missing offer redemptions');
const result = await knex.raw(`
SELECT
mscs.id AS subscription_id,
mscs.offer_id,
mscs.start_date AS created_at,
m.id AS member_id
FROM
members_stripe_customers_subscriptions mscs
LEFT JOIN
offer_redemptions r ON r.subscription_id = mscs.id
INNER JOIN
members_stripe_customers msc ON mscs.customer_id = msc.customer_id
INNER JOIN
members m ON msc.member_id = m.id
WHERE
mscs.offer_id IS NOT NULL and r.id IS NULL;
`);
// knex.raw() returns a different result depending on the database. We need to handle either case
let rows = [];
if (DatabaseInfo.isSQLite(knex)) {
rows = result;
} else {
rows = result[0];
}
// Do the backfil
if (rows && rows.length > 0) {
logging.info(`Backfilling ${rows.length} offer redemptions`);
// Generate IDs for each row
const offerRedemptions = rows.map((row) => {
return {
id: (new ObjectID()).toHexString(),
...row
};
});
// Batch insert rows into the offer_redemptions table
await knex.batchInsert('offer_redemptions', offerRedemptions, 1000);
} else {
logging.info('No offer redemptions to backfill');
}
} catch (error) {
logging.error(`Error backfilling offer redemptions: ${error.message}`);
}
},
async function down() {
// We don't want to un-backfill data, so do nothing here.
}
);

View file

@ -17,6 +17,7 @@ class StaffServiceWrapper {
const mailer = new GhostMailer();
const settingsCache = require('../../../shared/settings-cache');
const urlUtils = require('../../../shared/url-utils');
const {blogIcon} = require('../../../server/lib/image');
const settingsHelpers = require('../settings-helpers');
this.api = new StaffService({
@ -26,6 +27,7 @@ class StaffServiceWrapper {
settingsHelpers,
settingsCache,
urlUtils,
blogIcon,
DomainEvents,
memberAttributionService: memberAttribution.service,
labs

View file

@ -1,6 +1,6 @@
{
"name": "ghost",
"version": "5.88.3",
"version": "5.89.0",
"description": "The professional publishing platform",
"author": "Ghost Foundation",
"homepage": "https://ghost.org",
@ -76,7 +76,7 @@
"@tryghost/api-framework": "0.0.0",
"@tryghost/api-version-compatibility-service": "0.0.0",
"@tryghost/audience-feedback": "0.0.0",
"@tryghost/bookshelf-plugins": "0.6.19",
"@tryghost/bookshelf-plugins": "0.6.21",
"@tryghost/bootstrap-socket": "0.0.0",
"@tryghost/collections": "0.0.0",
"@tryghost/color-utils": "0.2.2",
@ -84,24 +84,24 @@
"@tryghost/constants": "0.0.0",
"@tryghost/custom-theme-settings-service": "0.0.0",
"@tryghost/data-generator": "0.0.0",
"@tryghost/database-info": "0.3.24",
"@tryghost/debug": "0.1.30",
"@tryghost/database-info": "0.3.27",
"@tryghost/debug": "0.1.32",
"@tryghost/domain-events": "0.0.0",
"@tryghost/donations": "0.0.0",
"@tryghost/dynamic-routing-events": "0.0.0",
"@tryghost/email-analytics-provider-mailgun": "0.0.0",
"@tryghost/email-analytics-service": "0.0.0",
"@tryghost/email-content-generator": "0.0.0",
"@tryghost/email-mock-receiver": "0.3.6",
"@tryghost/email-mock-receiver": "0.3.8",
"@tryghost/email-service": "0.0.0",
"@tryghost/email-suppression-list": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/express-dynamic-redirects": "0.0.0",
"@tryghost/external-media-inliner": "0.0.0",
"@tryghost/ghost": "0.0.0",
"@tryghost/helpers": "1.1.90",
"@tryghost/html-to-plaintext": "0.0.0",
"@tryghost/http-cache-utils": "0.1.15",
"@tryghost/http-cache-utils": "0.1.17",
"@tryghost/i18n": "0.0.0",
"@tryghost/image-transform": "1.3.0",
"@tryghost/importer-handler-content-files": "0.0.0",
@ -119,7 +119,7 @@
"@tryghost/link-redirects": "0.0.0",
"@tryghost/link-replacer": "0.0.0",
"@tryghost/link-tracking": "0.0.0",
"@tryghost/logging": "2.4.15",
"@tryghost/logging": "2.4.18",
"@tryghost/magic-link": "0.0.0",
"@tryghost/mail-events": "0.0.0",
"@tryghost/mailgun-client": "0.0.0",
@ -143,16 +143,16 @@
"@tryghost/mw-session-from-token": "0.0.0",
"@tryghost/mw-version-match": "0.0.0",
"@tryghost/mw-vhost": "0.0.0",
"@tryghost/nodemailer": "0.3.42",
"@tryghost/nodemailer": "0.3.45",
"@tryghost/nql": "0.12.3",
"@tryghost/oembed-service": "0.0.0",
"@tryghost/package-json": "0.0.0",
"@tryghost/post-revisions": "0.0.0",
"@tryghost/posts-service": "0.0.0",
"@tryghost/pretty-cli": "1.2.42",
"@tryghost/promise": "0.3.10",
"@tryghost/pretty-cli": "1.2.44",
"@tryghost/promise": "0.3.12",
"@tryghost/recommendations": "0.0.0",
"@tryghost/request": "1.0.5",
"@tryghost/request": "1.0.8",
"@tryghost/security": "0.0.0",
"@tryghost/session-service": "0.0.0",
"@tryghost/settings-path-manager": "0.0.0",
@ -162,14 +162,14 @@
"@tryghost/stats-service": "0.0.0",
"@tryghost/string": "0.2.12",
"@tryghost/tiers": "0.0.0",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"@tryghost/update-check-service": "0.0.0",
"@tryghost/url-utils": "4.4.8",
"@tryghost/validator": "0.2.11",
"@tryghost/validator": "0.2.14",
"@tryghost/verification-trigger": "0.0.0",
"@tryghost/version": "0.1.28",
"@tryghost/version": "0.1.30",
"@tryghost/webmentions": "0.0.0",
"@tryghost/zip": "1.1.43",
"@tryghost/zip": "1.1.46",
"amperize": "0.6.1",
"body-parser": "1.20.2",
"bookshelf": "1.2.0",
@ -210,7 +210,7 @@
"knex-migrator": "5.2.1",
"lib0": "0.2.94",
"lodash": "4.17.21",
"luxon": "3.4.4",
"luxon": "3.5.0",
"moment": "2.24.0",
"moment-timezone": "0.5.45",
"multer": "1.4.4",
@ -237,8 +237,8 @@
"devDependencies": {
"@actions/core": "1.10.1",
"@playwright/test": "1.38.1",
"@tryghost/express-test": "0.13.12",
"@tryghost/webhook-mock-receiver": "0.2.12",
"@tryghost/express-test": "0.13.15",
"@tryghost/webhook-mock-receiver": "0.2.14",
"@types/common-tags": "1.8.4",
"c8": "8.0.1",
"cli-progress": "3.12.0",
@ -265,8 +265,8 @@
"toml": "3.0.0"
},
"resolutions": {
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"jackspeak": "2.1.1",
"moment": "2.24.0",
"moment-timezone": "0.5.45"

View file

@ -78,7 +78,7 @@ const createPage = async (page, {title = 'Hello world', body = 'This is my post
await page.locator('[data-test-editor-title-input]').fill(title);
// wait for editor to be ready
await expect(page.locator('[data-lexical-editor="true"]')).toBeVisible();
await expect(page.locator('[data-lexical-editor="true"]').first()).toBeVisible();
// Continue to the body by pressing enter
await page.keyboard.press('Enter');
@ -304,7 +304,7 @@ test.describe('Publishing', () => {
await expect(publishedHeader).toContainText(date.toFormat('LLL d, yyyy'));
// add some extra text to the post
await adminPage.locator('[data-kg="editor"]').click();
await adminPage.locator('[data-kg="editor"]').first().click();
await adminPage.waitForTimeout(200); //
await adminPage.keyboard.type(' This is some updated text.');

View file

@ -423,7 +423,7 @@ const createPostDraft = async (page, {title = 'Hello world', body = 'This is my
await page.locator('[data-test-editor-title-input]').fill(title);
// wait for editor to be ready
await expect(page.locator('[data-lexical-editor="true"]')).toBeVisible();
await expect(page.locator('[data-lexical-editor="true"]').first()).toBeVisible();
// Continue to the body by pressing enter
await page.keyboard.press('Enter');

View file

@ -25,10 +25,10 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/nql": "0.12.3",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"lodash": "4.17.21"
}
}

View file

@ -18,7 +18,7 @@
"lib"
],
"devDependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/debug": "0.1.32",
"c8": "8.0.1",
"knex": "2.4.2",
"mocha": "10.2.0",
@ -27,7 +27,7 @@
},
"dependencies": {
"@faker-js/faker": "7.6.0",
"@tryghost/root-utils": "0.3.28",
"@tryghost/root-utils": "0.3.30",
"@tryghost/string": "0.2.12",
"csv-writer": "1.6.0",
"probability-distributions": "0.9.1"

View file

@ -19,7 +19,7 @@
"lib"
],
"devDependencies": {
"@tryghost/logging": "2.4.15",
"@tryghost/logging": "2.4.18",
"c8": "8.0.1",
"mocha": "10.2.0",
"should": "13.2.3"

View file

@ -23,7 +23,7 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/debug": "0.1.32",
"lodash": "4.17.21"
}
}

View file

@ -27,12 +27,12 @@
"dependencies": {
"@tryghost/color-utils": "0.2.2",
"@tryghost/email-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/html-to-plaintext": "0.0.0",
"@tryghost/kg-default-cards": "10.0.6",
"@tryghost/logging": "2.4.15",
"@tryghost/tpl": "0.1.30",
"@tryghost/validator": "0.2.11",
"@tryghost/logging": "2.4.18",
"@tryghost/tpl": "0.1.32",
"@tryghost/validator": "0.2.14",
"bson-objectid": "2.0.4",
"cheerio": "0.22.0",
"handlebars": "4.7.8",

View file

@ -36,7 +36,7 @@
"@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.10",
"@nestjs/platform-express": "10.3.10",
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"bson-objectid": "2.0.4",
"express": "4.19.2",
"reflect-metadata": "0.1.14",

View file

@ -21,7 +21,7 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/debug": "0.1.32",
"@tryghost/kg-default-cards": "10.0.6",
"@tryghost/string": "0.2.12",
"lodash": "4.17.21",

View file

@ -28,8 +28,8 @@
},
"dependencies": {
"@breejs/later": "4.2.0",
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"bree": "6.5.0",
"cron-validate": "1.4.5",
"fastq": "1.17.1",

View file

@ -24,10 +24,10 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/link-redirects": "0.0.0",
"@tryghost/nql": "0.12.3",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"bson-objectid": "2.0.4",
"lodash": "4.17.21",
"moment": "2.29.4"

View file

@ -26,9 +26,9 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.30",
"@tryghost/validator": "0.2.11",
"@tryghost/errors": "1.3.5",
"@tryghost/tpl": "0.1.32",
"@tryghost/validator": "0.2.14",
"jsonwebtoken": "8.5.1"
}
}

View file

@ -25,9 +25,9 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/in-memory-repository": "0.0.0",
"@tryghost/tpl": "0.1.30"
"@tryghost/tpl": "0.1.32"
},
"c8": {
"exclude": [

View file

@ -24,8 +24,8 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/logging": "2.4.15",
"@tryghost/debug": "0.1.32",
"@tryghost/logging": "2.4.18",
"@tryghost/metrics": "1.0.34",
"form-data": "4.0.0",
"lodash": "4.17.21",

View file

@ -31,14 +31,14 @@
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"@tryghost/magic-link": "0.0.0",
"@tryghost/member-events": "0.0.0",
"@tryghost/members-payments": "0.0.0",
"@tryghost/nql": "0.12.3",
"@tryghost/tpl": "0.1.30",
"@tryghost/validator": "0.2.11",
"@tryghost/tpl": "0.1.32",
"@tryghost/validator": "0.2.14",
"@types/jsonwebtoken": "9.0.6",
"body-parser": "1.20.2",
"bson-objectid": "2.0.4",

View file

@ -26,8 +26,8 @@
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"@tryghost/member-events": "0.0.0",
"moment-timezone": "0.5.34"
}

View file

@ -25,11 +25,11 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"@tryghost/members-csv": "0.0.0",
"@tryghost/metrics": "1.0.34",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"moment-timezone": "0.5.45"
}
}

View file

@ -26,8 +26,8 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"cookies": "0.9.1",
"jsonwebtoken": "8.5.1"
}

View file

@ -23,7 +23,7 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"bson-objectid": "2.0.4"
}
}

View file

@ -24,9 +24,9 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.30",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/tpl": "0.1.32",
"csso": "5.0.5",
"terser": "5.31.3",
"tiny-glob": "0.2.9"

View file

@ -18,7 +18,7 @@
"lib"
],
"devDependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"c8": "8.0.1",
"mocha": "10.2.0",
"sinon": "15.2.0"

View file

@ -23,10 +23,10 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/http-cache-utils": "0.1.15",
"@tryghost/tpl": "0.1.30",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/http-cache-utils": "0.1.17",
"@tryghost/tpl": "0.1.32",
"lodash": "4.17.21",
"semver": "7.6.3"
}

View file

@ -23,8 +23,8 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.30",
"@tryghost/errors": "1.3.5",
"@tryghost/tpl": "0.1.32",
"semver": "7.6.3"
}
}

View file

@ -373,6 +373,18 @@ class OEmbedService {
try {
const urlObject = new URL(url);
// YouTube has started not returning oembed <link>tags for some live URLs
// when fetched from an IP address that's in a non-EN region.
// We convert live URLs to watch URLs so we can go straight to the
// oembed request via a known provider rather than going through the page fetch routine.
const ytLiveRegex = /^\/live\/([a-zA-Z0-9_-]+)$/;
if (urlObject.hostname.match(/(?:www\.)?youtube\.com/) && ytLiveRegex.test(urlObject.pathname)) {
const videoId = ytLiveRegex.exec(urlObject.pathname)[1];
urlObject.pathname = '/watch';
urlObject.searchParams.set('v', videoId);
url = urlObject.toString();
}
// Trimming solves the difference of url validation between `new URL(url)`
// and metascraper.
url = url.trim();

View file

@ -23,9 +23,9 @@
},
"dependencies": {
"@extractus/oembed-extractor": "3.2.1",
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/tpl": "0.1.30",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"@tryghost/tpl": "0.1.32",
"charset": "1.0.1",
"cheerio": "0.22.0",
"iconv-lite": "0.6.3",

View file

@ -172,5 +172,57 @@ describe('oembed-service', function () {
assert.equal(response.url, 'https://www.example.com');
assert.equal(response.metadata.title, 'Example');
});
it('converts YT live URLs to watch URLs', async function () {
nock('https://www.youtube.com')
.get('/oembed')
.query((query) => {
// Ensure the URL is converted to a watch URL and retains existing query params.
const actual = query.url;
const expected = 'https://youtube.com/watch?param=existing&v=1234';
assert.equal(actual, expected, 'URL passed to oembed endpoint is incorrect');
return actual === expected;
})
.reply(200, {
type: 'rich',
version: '1.0',
title: 'Test Title',
author_name: 'Test Author',
author_url: 'https://example.com/user/testauthor',
html: '<iframe src="https://www.example.com/embed"></iframe>',
width: 640,
height: null
});
await oembedService.fetchOembedDataFromUrl('https://www.youtube.com/live/1234?param=existing');
});
it('converts YT live URLs to watch URLs (non-www)', async function () {
nock('https://www.youtube.com')
.get('/oembed')
.query((query) => {
// Ensure the URL is converted to a watch URL and retains existing query params.
const actual = query.url;
const expected = 'https://youtube.com/watch?param=existing&v=1234';
assert.equal(actual, expected, 'URL passed to oembed endpoint is incorrect');
return actual === expected;
})
.reply(200, {
type: 'rich',
version: '1.0',
title: 'Test Title',
author_name: 'Test Author',
author_url: 'https://example.com/user/testauthor',
html: '<iframe src="https://www.example.com/embed"></iframe>',
width: 640,
height: null
});
await oembedService.fetchOembedDataFromUrl('https://youtube.com/live/1234?param=existing');
});
});
});

View file

@ -26,7 +26,7 @@
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/mongo-utils": "0.6.2",
"@tryghost/string": "0.2.12",
"lodash": "4.17.21"

View file

@ -25,8 +25,8 @@
"tmp": "0.2.1"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.30",
"@tryghost/errors": "1.3.5",
"@tryghost/tpl": "0.1.32",
"fs-extra": "11.2.0",
"lodash": "4.17.21"
}

View file

@ -25,7 +25,7 @@
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/members-offers": "0.0.0",
"@tryghost/tiers": "0.0.0"
}

View file

@ -23,10 +23,10 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/nql": "0.12.3",
"@tryghost/post-events": "0.0.0",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"bson-objectid": "2.0.4"
}
}

View file

@ -30,10 +30,10 @@
"typescript": "5.4.5"
},
"dependencies": {
"@tryghost/tpl": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/in-memory-repository": "0.0.0",
"@tryghost/bookshelf-repository": "0.0.0",
"@tryghost/logging": "2.4.15"
"@tryghost/logging": "2.4.18"
}
}

View file

@ -25,6 +25,6 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2"
"@tryghost/errors": "1.3.5"
}
}

View file

@ -24,8 +24,8 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/tpl": "0.1.30",
"@tryghost/errors": "1.3.5",
"@tryghost/tpl": "0.1.32",
"date-fns": "2.30.0"
}
}

View file

@ -23,9 +23,9 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/validator": "0.2.11",
"@tryghost/version": "0.1.28",
"@tryghost/errors": "1.3.5",
"@tryghost/validator": "0.2.14",
"@tryghost/version": "0.1.30",
"got": "9.6.0"
}
}

View file

@ -4,7 +4,7 @@ const {MilestoneCreatedEvent} = require('@tryghost/milestones');
// @NOTE: 'StaffService' is a vague name that does not describe what it's actually doing.
// Possibly, "StaffNotificationService" or "StaffEventNotificationService" would be a more accurate name
class StaffService {
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, DomainEvents, labs, memberAttributionService}) {
constructor({logging, models, mailer, settingsCache, settingsHelpers, urlUtils, blogIcon, DomainEvents, labs, memberAttributionService}) {
this.logging = logging;
this.labs = labs;
/** @private */
@ -22,6 +22,7 @@ class StaffService {
settingsHelpers,
settingsCache,
urlUtils,
blogIcon,
labs
});
}

View file

@ -5,12 +5,13 @@ const glob = require('glob');
const {EmailAddressParser} = require('@tryghost/email-addresses');
class StaffServiceEmails {
constructor({logging, models, mailer, settingsHelpers, settingsCache, urlUtils, labs}) {
constructor({logging, models, mailer, settingsHelpers, settingsCache, blogIcon, urlUtils, labs}) {
this.logging = logging;
this.models = models;
this.mailer = mailer;
this.settingsHelpers = settingsHelpers;
this.settingsCache = settingsCache;
this.blogIcon = blogIcon;
this.urlUtils = urlUtils;
this.labs = labs;
@ -44,6 +45,7 @@ class StaffServiceEmails {
attributionUrl: attribution?.url || '',
referrerSource: attribution?.referrerSource,
siteTitle: this.settingsCache.get('title'),
siteIconUrl: this.blogIcon.getIconUrl(true),
siteUrl: this.urlUtils.getSiteUrl(),
siteDomain: this.siteDomain,
accentColor: this.settingsCache.get('accent_color'),
@ -103,6 +105,7 @@ class StaffServiceEmails {
offerData,
subscriptionData,
siteTitle: this.settingsCache.get('title'),
siteIconUrl: this.blogIcon.getIconUrl(true),
siteUrl: this.urlUtils.getSiteUrl(),
siteDomain: this.siteDomain,
accentColor: this.settingsCache.get('accent_color'),
@ -153,6 +156,7 @@ class StaffServiceEmails {
tierData,
subscriptionData,
siteTitle: this.settingsCache.get('title'),
siteIconUrl: this.blogIcon.getIconUrl(true),
siteUrl: this.urlUtils.getSiteUrl(),
siteDomain: this.siteDomain,
accentColor: this.settingsCache.get('accent_color'),
@ -182,6 +186,7 @@ class StaffServiceEmails {
return {
siteTitle: this.settingsCache.get('title'),
siteIconUrl: this.blogIcon.getIconUrl(true),
siteUrl: this.urlUtils.getSiteUrl(),
siteDomain: this.siteDomain,
accentColor: this.settingsCache.get('accent_color'),
@ -282,6 +287,7 @@ class StaffServiceEmails {
const templateData = {
siteTitle: this.settingsCache.get('title'),
siteUrl: this.urlUtils.getSiteUrl(),
siteIconUrl: this.blogIcon.getIconUrl(true),
siteDomain: this.siteDomain,
fromEmail: this.fromEmailAddress,
toEmail: to,

View file

@ -21,21 +21,33 @@
<tr>
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
{{#if siteIconUrl}}
<tr>
<td align="center" style="padding-bottom: 56px; text-align: center;"><a href="{{siteUrl}}"><img src="{{siteIconUrl}}" alt="{{siteTitle}}" border="0" width="48" height="48"></a></td>
</tr>
{{/if}}
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
<h1 style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 26px; color: #15212A; font-weight: bold; line-height: 28px; margin: 0; margin-bottom: 15px;">Cha-ching! 💰 You received {{donation.amount}} from {{donation.name}}.</h1>
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
<h1 style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 26px; color: #15212A; font-weight: bold; line-height: 28px; margin: 0; padding-bottom: 24px;">Cha-ching! You received a {{donation.amount}} tip from {{donation.name}}.</h1>
<table width="100" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F4F5F6; border-radius: 8px;">
<tbody>
<tr>
<td align="center" style="padding: 32px 24px; text-align: center;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Type:</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; line-height: 26px; padding: 0; text-align: left; margin: 0; margin-bottom: 20px; color: #15171A; font-weight: 700;">One-time payment</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">From:</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; line-height: 26px; padding: 0; text-align: left; margin: 0; margin-bottom: 20px; color: #15171A; font-weight: 700;">{{#if memberData}}<a style="color:{{accentColor}}" href="{{memberData.adminUrl}}">{{donation.name}} ({{donation.email}})</a>{{else}}{{donation.name}} ({{donation.email}}){{/if}}</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Amount received:</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
</td>
</tr>
<tr>
<td align="center" style="padding: 24px; text-align: center;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">From:</p>
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; line-height: 26px; padding: 0; text-align: left; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 700;">{{donation.name}} (<span style="color:{{accentColor}}; text-decoration: none;">{{donation.email}}</span>)</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 400;">Amount received:</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; line-height: 26px; padding: 0; text-align: left; margin: 0; color: #15171A; font-weight: 700;">{{donation.amount}}</p>
</td>
</tr>
<tr>
<td style="padding:0 24px 24px;">
{{#if memberData}}
<a href="{{memberData.adminUrl}}" style="border:solid 1px {{accentColor}};border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:{{accentColor}};border-color:{{accentColor}};color:#ffffff">View member</a>
{{else}}
<a href="{{adminUrl}}" style="border:solid 1px {{accentColor}};border-radius:8px;box-sizing:border-box;display:inline-block;font-size:15px;font-weight:normal;margin:0;padding:10px 20px;text-decoration:none;background-color:{{accentColor}};border-color:{{accentColor}};color:#ffffff">View dashboard</a>
{{/if}}
</td>
</tr>
</tbody>
</table>
</td>
@ -43,13 +55,13 @@
<!-- START FOOTER -->
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{toEmail}}</a></p>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; vertical-align: top; padding-top: 56px;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #7C8B9A; font-weight: normal; margin: 0;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">{{toEmail}}</a></p>
</td>
</tr>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">Dont want to receive these emails? Manage your preferences <a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">here</a>.</p>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; vertical-align: top;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #7C8B9A; font-weight: normal; margin: 0;">Dont want to receive these emails? Manage your preferences <a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">here</a>.</p>
</td>
</tr>

View file

@ -26,64 +26,46 @@
<tr>
<td class="wrapper" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; box-sizing: border-box;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
{{#if siteIconUrl}}
<tr>
<td align="center" style="padding-bottom: 56px; text-align: center;"><a href="{{siteUrl}}"><img src="{{siteIconUrl}}" alt="{{siteTitle}}" border="0" width="48" height="48"></a></td>
</tr>
{{/if}}
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Congratulations!</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">You have a <span style="font-weight: bold; color: #15212A;">new free member</span>.</p>
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
<tbody>
<tr>
<td align="left" style="padding: 16px;">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td style="padding-right: 14px; background-color: #F9F9FA; text-align: left; vertical-align: middle;" valign="middle">
<div style="width: 48px; height: 48px; background-color: #15171A; border-radius: 999px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 19px; color: #FFFFFF; text-align: center; vertical-align: center; font-weight: 500; line-height: 47px;">
{{memberData.initials}}
</div>
</td>
<td style="padding-right: 8px; background-color: #F9F9FA; text-align: left; vertical-align: middle;" valign="middle">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{memberData.name}}</p>
{{#if memberData.showEmail}}
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #394047; font-weight: 400;">{{memberData.email}}</p>
{{/if}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Created on {{memberData.createdAt}}{{#if memberData.location}} &#8226; {{memberData.location}} {{/if}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tr>
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
{{#if referrerSource}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Signup info</p>
<hr style="border-bottom: 1px solid #F4F4F5; margin-top: 4px; margin-bottom: 8px;" />
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600; padding-bottom: 4px;">Source
<span style="font-weight: normal; color:#3A464C;"> - {{referrerSource}}</span>
</p>
{{#if attributionTitle}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">Page
<span style="font-weight: normal; color:#3A464C;"> - <a href="{{attributionUrl}}" style="font-weight: normal; color:#3A464C;text-decoration:none">{{attributionTitle}}</a></span>
</p>
{{/if}}
{{/if}}
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<h1 style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 26px; color: #15212A; font-weight: bold; line-height: 28px; margin: 0; padding-bottom: 24px;">New free subscriber to {{siteTitle}}</h1>
<table width="100" border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F4F5F6; border-radius: 8px;">
<tbody>
<tr>
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; padding-top: 32px; padding-bottom: 12px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<td align="left" style="padding: 24px;">
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td style="padding-right: 8px; background-color: #F4F5F6; text-align: left; vertical-align: middle;" valign="middle">
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Name:</p>
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{memberData.name}}{{#if memberData.showEmail}} ({{memberData.email}}){{/if}}</p>
{{#if referrerSource}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Source:</p>
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;">{{referrerSource}}</p>
{{#if attributionTitle}}
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; margin: 0; padding-bottom: 4px; color: #15171A; font-weight: 400;">Page:</p>
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 17px; margin: 0; padding-bottom: 24px; color: #15171A; font-weight: 600;"><a href="{{attributionUrl}}">{{attributionTitle}}</a></p>
{{/if}}
{{/if}}
</td>
</tr>
</table>
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top; background-color: {{accentColor}}; border-radius: 5px; text-align: center;"> <a href="{{memberData.adminUrl}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 16px; font-weight: normal; margin: 0; padding: 9px 22px 10px; border-color: {{accentColor}};">View member</a></td>
<td align="left" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; vertical-align: top;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; vertical-align: top; background-color: {{accentColor}}; border-radius: 8px; text-align: center;"> <a href="{{memberData.adminUrl}}" target="_blank" style="display: inline-block; color: #ffffff; background-color: {{accentColor}}; border: solid 1px {{accentColor}}; border-radius: 8px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: 15px; font-weight: normal; margin: 0; padding: 10px 20px; border-color: {{accentColor}};">View member</a></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
@ -91,21 +73,29 @@
</tr>
</tbody>
</table>
<hr/>
<p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 5px;">You can also copy & paste this URL into your browser:</p>
<p class="text-link" style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin-top:0; color: #3A464C;">{{memberData.adminUrl}}</p>
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td align="left" style="padding-top: 24px;">
<p style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; color: #7c8b9a; font-weight: normal; margin: 0; line-height: 25px;">Or copy and paste this URL into your browser:</p>
<p class="text-link" style="word-break: break-all; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 15px; line-height: 25px; margin:0; color: #7c8b9a; font-weight: normal;">{{memberData.adminUrl}}</p>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<!-- START FOOTER -->
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 80px;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">{{toEmail}}</a></p>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; vertical-align: top; padding-top: 56px;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #7C8B9A; font-weight: normal; margin: 0;">This message was sent from <a class="small" href="{{siteUrl}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">{{siteDomain}}</a> to <a class="small" href="mailto:{{toEmail}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">{{toEmail}}</a></p>
</td>
</tr>
<tr>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top; padding-top: 2px">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 11px; color: #738A94; font-weight: normal; margin: 0; margin-bottom: 2px;">Dont want to receive these emails? Manage your preferences <a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #738A94; font-size: 11px;">here</a>.</p>
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 12px; vertical-align: top;">
<p class="small" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 18px; font-size: 12px; color: #7C8B9A; font-weight: normal; margin: 0;">Dont want to receive these emails? Manage your preferences <a class="small" href="{{staffUrl}}" style="text-decoration: underline; color: #7C8B9A; font-size: 12px;">here</a>.</p>
</td>
</tr>

View file

@ -149,6 +149,12 @@ describe('StaffService', function () {
}
};
const blogIcon = {
getIconUrl: () => {
return 'https://ghost.example/siteicon.png';
}
};
const settingsHelpers = {
getDefaultEmailDomain: () => {
return 'ghost.example';
@ -184,6 +190,7 @@ describe('StaffService', function () {
},
settingsCache,
urlUtils,
blogIcon,
settingsHelpers,
labs
});
@ -220,6 +227,7 @@ describe('StaffService', function () {
DomainEvents,
settingsCache,
urlUtils,
blogIcon,
settingsHelpers
});
service.subscribeEvents();
@ -339,6 +347,7 @@ describe('StaffService', function () {
},
settingsCache,
urlUtils,
blogIcon,
settingsHelpers,
labs: {
isSet: () => {
@ -430,9 +439,6 @@ describe('StaffService', function () {
mailStub.calledWith(
sinon.match.has('html', sinon.match('🥳 Free member signup: Ghost'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 &#8226; France'))
).should.be.true();
});
it('sends free member signup alert without member name', async function () {
@ -455,18 +461,13 @@ describe('StaffService', function () {
mailStub.calledWith(
sinon.match.has('html', sinon.match('🥳 Free member signup: member@example.com'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 &#8226; France'))
).should.be.true();
});
it('sends free member signup alert with attribution', async function () {
const member = {
name: 'Ghost',
email: 'member@example.com',
id: 'abc',
geolocation: '{"country": "France"}',
created_at: '2022-08-01T07:30:39.882Z'
id: 'abc'
};
const attribution = {
@ -487,9 +488,6 @@ describe('StaffService', function () {
mailStub.calledWith(
sinon.match.has('html', sinon.match('🥳 Free member signup: Ghost'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Created on 1 Aug 2022 &#8226; France'))
).should.be.true();
mailStub.calledWith(
sinon.match.has('html', sinon.match('Source'))

View file

@ -25,7 +25,7 @@
"@types/sinon": "10.0.16",
"c8": "8.0.1",
"knex": "2.4.2",
"luxon": "3.4.4",
"luxon": "3.5.0",
"mocha": "10.2.0",
"should": "13.2.3",
"sinon": "15.2.0",

View file

@ -25,10 +25,10 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/debug": "0.1.32",
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"@tryghost/member-events": "0.0.0",
"leaky-bucket": "2.2.0",
"lodash": "4.17.21",

View file

@ -22,9 +22,9 @@
"mocha": "10.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/string": "0.2.12",
"@tryghost/tpl": "0.1.30",
"@tryghost/tpl": "0.1.32",
"bson-objectid": "2.0.4"
}
}

View file

@ -25,10 +25,10 @@
"uuid": "9.0.1"
},
"dependencies": {
"@tryghost/debug": "0.1.30",
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/tpl": "0.1.30",
"@tryghost/debug": "0.1.32",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"@tryghost/tpl": "0.1.32",
"lodash": "4.17.21",
"moment": "2.24.0"
}

View file

@ -25,7 +25,7 @@
},
"dependencies": {
"@tryghost/domain-events": "0.0.0",
"@tryghost/errors": "1.3.2",
"@tryghost/errors": "1.3.5",
"@tryghost/member-events": "0.0.0"
}
}

View file

@ -25,8 +25,8 @@
"sinon": "15.2.0"
},
"dependencies": {
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"cheerio": "0.22.0"
}
}

View file

@ -43,8 +43,8 @@
"prepare": "husky install .github/hooks"
},
"resolutions": {
"@tryghost/errors": "1.3.2",
"@tryghost/logging": "2.4.15",
"@tryghost/errors": "1.3.5",
"@tryghost/logging": "2.4.18",
"jackspeak": "2.1.1",
"moment": "2.24.0",
"moment-timezone": "0.5.45"
@ -115,9 +115,9 @@
"eslint-plugin-ghost": "3.4.0",
"eslint-plugin-react": "7.33.0",
"husky": "8.0.3",
"lint-staged": "15.2.7",
"lint-staged": "15.2.8",
"nx": "16.8.1",
"rimraf": "5.0.9",
"rimraf": "5.0.10",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"inquirer": "8.2.6"

1889
yarn.lock

File diff suppressed because it is too large Load diff