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:
commit
6bd6ec4223
82 changed files with 1521 additions and 1283 deletions
1
.github/workflows/migration-review.yml
vendored
1
.github/workflows/migration-review.yml
vendored
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
1
apps/admin-x-design-system/src/assets/icons/reload.svg
Normal file
1
apps/admin-x-design-system/src/assets/icons/reload.svg
Normal 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 |
1
apps/admin-x-design-system/src/assets/icons/share.svg
Normal file
1
apps/admin-x-design-system/src/assets/icons/share.svg
Normal 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 |
|
@ -96,4 +96,5 @@
|
|||
/* Prose classes are for formatting arbitrary HTML that comes from the API */
|
||||
.gh-prose-links a {
|
||||
color: #30CF43;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ module.exports = {
|
|||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
accent: 'var(--accent-color, #ff0095)',
|
||||
white: '#FFF',
|
||||
black: '#15171A',
|
||||
grey: {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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'>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -24,6 +24,6 @@
|
|||
"sinon": "15.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/errors": "1.3.2"
|
||||
"@tryghost/errors": "1.3.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -853,6 +853,7 @@
|
|||
post=this.post
|
||||
editorAPI=this.editorAPI
|
||||
toggleSettingsMenu=this.toggleSettingsMenu
|
||||
secondaryEditorAPI=this.secondaryEditorAPI
|
||||
}}
|
||||
@close={{this.closePostHistory}}
|
||||
@modifier="total-overlay post-history" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
@lexical={{this.selectedRevision.lexical}}
|
||||
@cardConfig={{this.cardConfig}}
|
||||
@registerAPI={{this.registerSelectedEditorApi}}
|
||||
@registerSecondaryAPI={{this.registerSecondarySelectedEditorApi}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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: "";
|
||||
|
|
|
@ -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 > * {
|
||||
|
|
|
@ -347,3 +347,5 @@
|
|||
margin-left: 8px;
|
||||
background: var(--whitegrey);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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",
|
||||
|
|
1
ghost/admin/public/assets/icons/share.svg
Normal file
1
ghost/admin/public/assets/icons/share.svg
Normal 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 |
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,6 @@
|
|||
"sinon": "15.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/logging": "2.4.15"
|
||||
"@tryghost/logging": "2.4.18"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
);
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.');
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"sinon": "15.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/debug": "0.1.30",
|
||||
"@tryghost/debug": "0.1.32",
|
||||
"lodash": "4.17.21"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"sinon": "15.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/errors": "1.3.2",
|
||||
"@tryghost/errors": "1.3.5",
|
||||
"bson-objectid": "2.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,6 @@
|
|||
"sinon": "15.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tryghost/errors": "1.3.2"
|
||||
"@tryghost/errors": "1.3.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;">Don’t 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;">Don’t 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>
|
||||
|
||||
|
|
|
@ -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}} • {{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;">Don’t 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;">Don’t 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>
|
||||
|
||||
|
|
|
@ -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 • 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 • 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 • France'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Source'))
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue