diff --git a/apps/admin-x-design-system/src/global/form/toggle.tsx b/apps/admin-x-design-system/src/global/form/toggle.tsx index fae77340c9f..89560b412b9 100644 --- a/apps/admin-x-design-system/src/global/form/toggle.tsx +++ b/apps/admin-x-design-system/src/global/form/toggle.tsx @@ -23,6 +23,7 @@ export interface ToggleProps { hint?: React.ReactNode; onChange?: (event: React.ChangeEvent) => void; gap?: string; + align?: 'start' | 'center'; testId?: string; } @@ -42,6 +43,7 @@ const Toggle: React.FC = ({ name, onChange, gap = 'gap-2', + align = 'start', testId }) => { const id = useId(); @@ -53,19 +55,19 @@ const Toggle: React.FC = ({ case 'sm': sizeStyles = ' h-3 w-5'; thumbSizeStyles = ' h-2 w-2 data-[state=checked]:translate-x-[10px]'; - labelStyles = 'mt-[-5.5px]'; + labelStyles = align === 'start' ? 'mt-[-5.5px]' : ''; break; case 'lg': sizeStyles = ' h-5 w-8'; thumbSizeStyles = ' h-4 w-4 data-[state=checked]:translate-x-[14px]'; - labelStyles = 'mt-[-1px]'; + labelStyles = align === 'start' ? 'mt-[-1px]' : ''; break; default: sizeStyles = ' min-w-[28px] h-4 w-7'; thumbSizeStyles = ' h-3 w-3 data-[state=checked]:translate-x-[14px]'; - labelStyles = 'mt-[-3px]'; + labelStyles = align === 'start' ? 'mt-[-3px]' : ''; break; } @@ -104,7 +106,7 @@ const Toggle: React.FC = ({ return (
-
+
= ({ return ( span]:data-[state=active]:text-black [&>span]:data-[state=active]:dark:text-white', + 'relative z-[1] cursor-pointer appearance-none whitespace-nowrap pb-1.5 pt-1 text-md font-semibold text-grey-700 transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)] data-[state=active]:text-black dark:text-white [&>span]:data-[state=active]:text-black [&>span]:data-[state=active]:dark:text-white', border && 'border-b-2 border-transparent hover:border-grey-500 data-[state=active]:border-black data-[state=active]:dark:border-white data-[state=active]:dark:text-white' )} id={id} @@ -80,26 +80,24 @@ export const TabList: React.FC = ({ stickyHeader }) => { const containerClasses = clsx( - 'no-scrollbar mb-px flex w-full overflow-x-auto', + 'no-scrollbar relative flex w-full overflow-x-auto', width === 'narrow' && 'gap-3', width === 'normal' && 'gap-5', width === 'wide' && 'gap-7', - border && 'border-b border-grey-300 dark:border-grey-900' + border && 'after:absolute after:inset-x-0 after:bottom-0 after:h-px after:bg-grey-300 dark:after:bg-grey-900' ); return (
{tabs.map(tab => ( -
- -
+ ))} {topRightContent !== null ?
{topRightContent}
: diff --git a/apps/admin-x-framework/src/test/acceptance.ts b/apps/admin-x-framework/src/test/acceptance.ts index 34f3b3ba25b..5fbbc4f8807 100644 --- a/apps/admin-x-framework/src/test/acceptance.ts +++ b/apps/admin-x-framework/src/test/acceptance.ts @@ -81,7 +81,6 @@ export const responseFixtures = { }; const defaultLabFlags = { - audienceFeedback: false, collections: false, outboundLinkTagging: false, announcementBar: false, diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-preview.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-preview.tsx index beec2edcd7d..6b05c1c5578 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-preview.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-preview.tsx @@ -20,7 +20,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) => const headerSubtitle = (newsletter.show_header_title && newsletter.show_header_name) ? newsletter.name : undefined; const showCommentCta = newsletter.show_comment_cta && commentsEnabled !== 'off'; - const showFeedback = newsletter.feedback_enabled && config.labs.audienceFeedback; + const showFeedback = newsletter.feedback_enabled; const backgroundColor = () => { const value = newsletter.background_color; diff --git a/apps/admin-x-settings/src/components/settings/membership/analytics.tsx b/apps/admin-x-settings/src/components/settings/membership/analytics.tsx index 99121b3ac8c..1d8d6419de0 100644 --- a/apps/admin-x-settings/src/components/settings/membership/analytics.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/analytics.tsx @@ -46,7 +46,9 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => { const inputs = ( = ({keywords}) => { } label='Web analytics' - labelClasses='py-4 w-full' onChange={(e) => { handleToggleChange('web_analytics', e); }} @@ -88,49 +89,53 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => { ) )} { handleToggleChange('email_track_opens', e); }} /> { handleToggleChange('email_track_clicks', e); }} /> { handleToggleChange('members_track_sources', e); }} /> { handleToggleChange('outbound_link_tagging', e); }} diff --git a/apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx b/apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx index a9a3c082a07..7f580ddbd36 100644 --- a/apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx +++ b/apps/admin/src/layout/app-sidebar/app-sidebar-content.tsx @@ -8,6 +8,7 @@ import NavMain from "./nav-main"; import NavContent from "./nav-content"; import NavGhostPro from "./nav-ghost-pro"; import NavSettings from "./nav-settings"; +import ThemeErrorsBanner from "./theme-errors-banner"; import UpgradeBanner from "./upgrade-banner"; import { useUpgradeStatus } from "./hooks/use-upgrade-status"; @@ -23,6 +24,7 @@ function AppSidebarContent() {
{showUpgradeBanner ? : } +
diff --git a/apps/admin/src/layout/app-sidebar/hooks/use-theme-errors.ts b/apps/admin/src/layout/app-sidebar/hooks/use-theme-errors.ts new file mode 100644 index 00000000000..c7a81c9dcdc --- /dev/null +++ b/apps/admin/src/layout/app-sidebar/hooks/use-theme-errors.ts @@ -0,0 +1,29 @@ +import {useActiveTheme} from '@tryghost/admin-x-framework/api/themes'; +import {useCurrentUser} from '@tryghost/admin-x-framework/api/current-user'; +import {isContributorUser} from '@tryghost/admin-x-framework/api/users'; +import type {ThemeProblem} from '@tryghost/admin-x-framework/api/themes'; + +// This error is handled inline next to the related setting in the design +// customization panel rather than shown in the sidebar error banner +function isFilteredError(error: ThemeProblem<'error'>): boolean { + return error.code === 'GS110-NO-MISSING-PAGE-BUILDER-USAGE' + && !!error.failures?.[0]?.message?.includes('show_title_and_feature_image'); +} + +export function useActiveThemeErrors() { + const {data: currentUser} = useCurrentUser(); + const isContributor = currentUser && isContributorUser(currentUser); + + const {data: activeThemeData} = useActiveTheme({ + enabled: !isContributor + }); + + const activeTheme = activeThemeData?.themes?.[0]; + const allErrors = activeTheme?.errors ?? []; + const warnings = activeTheme?.warnings ?? []; + + const errors = allErrors.filter(error => !isFilteredError(error)); + const hasErrors = errors.length > 0; + + return {hasErrors, errors, warnings}; +} diff --git a/apps/admin/src/layout/app-sidebar/theme-errors-banner.tsx b/apps/admin/src/layout/app-sidebar/theme-errors-banner.tsx new file mode 100644 index 00000000000..4a8c3a48b87 --- /dev/null +++ b/apps/admin/src/layout/app-sidebar/theme-errors-banner.tsx @@ -0,0 +1,41 @@ +import {useState} from 'react'; +import {Banner, LucideIcon} from '@tryghost/shade'; +import {useActiveThemeErrors} from './hooks/use-theme-errors'; +import ThemeErrorsDialog from './theme-errors-dialog'; + +function ThemeErrorsBanner() { + const {hasErrors, errors, warnings} = useActiveThemeErrors(); + const [dialogOpen, setDialogOpen] = useState(false); + + if (!hasErrors) { + return null; + } + + return ( + <> + setDialogOpen(true)} + > +
+ +
+
Your theme has errors
+
Some functionality on your site may be limited →
+
+
+
+ + + ); +} + +export default ThemeErrorsBanner; diff --git a/apps/admin/src/layout/app-sidebar/theme-errors-dialog.tsx b/apps/admin/src/layout/app-sidebar/theme-errors-dialog.tsx new file mode 100644 index 00000000000..29ab5eaadf1 --- /dev/null +++ b/apps/admin/src/layout/app-sidebar/theme-errors-dialog.tsx @@ -0,0 +1,102 @@ +import {useState} from 'react'; +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + LucideIcon +} from '@tryghost/shade'; +import type {ThemeProblem} from '@tryghost/admin-x-framework/api/themes'; + +interface ThemeErrorsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + errors: ThemeProblem<'error'>[]; + warnings: ThemeProblem<'warning'>[]; +} + +function ThemeErrorItem({error}: {error: ThemeProblem}) { + const [expanded, setExpanded] = useState(false); + + return ( +
  • + + {expanded && ( +
    +

    + {error.failures?.length > 0 && ( +

    +
    Affected files:
    +
      + {error.failures.map((failure, i) => ( +
    • + {failure.ref} + {failure.message && <>: {failure.message}} +
    • + ))} +
    +
    + )} +
    + )} +
  • + ); +} + +function ThemeErrorsDialog({open, onOpenChange, errors, warnings}: ThemeErrorsDialogProps) { + return ( + + + + + Theme errors + + + +
    + {errors.length > 0 && ( +
    +

    Errors

    +

    + Highly recommended to fix, functionality could be restricted +

    +
      + {errors.map((error, i) => ( + + ))} +
    +
    + )} + + {warnings.length > 0 && ( +
    0 ? 'mt-4' : ''}> +

    Warnings

    +
      + {warnings.map((warning, i) => ( + + ))} +
    +
    + )} +
    + + + + +
    +
    + ); +} + +export default ThemeErrorsDialog; diff --git a/apps/admin/src/whats-new/components/whats-new-banner.tsx b/apps/admin/src/whats-new/components/whats-new-banner.tsx index f0c35a83e42..40cd51eb426 100644 --- a/apps/admin/src/whats-new/components/whats-new-banner.tsx +++ b/apps/admin/src/whats-new/components/whats-new-banner.tsx @@ -32,6 +32,7 @@ function WhatsNewBanner() { return ( { {variant: 'info' as const, expectedClass: 'bg-blue-50'}, {variant: 'success' as const, expectedClass: 'bg-green-50'}, {variant: 'warning' as const, expectedClass: 'bg-yellow-50'}, - {variant: 'destructive' as const, expectedClass: 'bg-red-50'} + {variant: 'destructive' as const, expectedClass: 'bg-white'} ])('applies $variant variant correctly', ({variant, expectedClass}) => { render(Content); const banner = screen.getByRole('status'); diff --git a/e2e/helpers/pages/admin/sidebar/sidebar-page.ts b/e2e/helpers/pages/admin/sidebar/sidebar-page.ts index 872e7313d82..2365bfd7a1e 100644 --- a/e2e/helpers/pages/admin/sidebar/sidebar-page.ts +++ b/e2e/helpers/pages/admin/sidebar/sidebar-page.ts @@ -43,6 +43,8 @@ export class SidebarPage extends AdminPage { public readonly networkNotificationBadge: Locator; public readonly ghostProLink: Locator; public readonly upgradeNowLink: Locator; + public readonly themeErrorBanner: Locator; + public readonly themeErrorDialog: Locator; constructor(page: Page) { super(page); @@ -59,6 +61,8 @@ export class SidebarPage extends AdminPage { .locator('[data-sidebar="menu-badge"]'); this.ghostProLink = this.sidebar.getByRole('link', {name: 'Ghost(Pro)'}); this.upgradeNowLink = this.sidebar.getByRole('link', {name: /upgrade/i}); + this.themeErrorBanner = page.getByRole('status').filter({hasText: /your theme has errors/i}); + this.themeErrorDialog = page.getByRole('dialog').filter({hasText: /theme errors/i}); } getNavLink(name: string): Locator { diff --git a/e2e/tests/admin/sidebar/theme-error-notification.test.ts b/e2e/tests/admin/sidebar/theme-error-notification.test.ts new file mode 100644 index 00000000000..fdc4e030ae4 --- /dev/null +++ b/e2e/tests/admin/sidebar/theme-error-notification.test.ts @@ -0,0 +1,94 @@ +import {Page} from '@playwright/test'; +import {SidebarPage} from '@/admin-pages'; +import {expect, test} from '@/helpers/playwright/fixture'; + +function mockActiveThemeWithErrors(page: Page, errors: object[] = [{ + code: 'GS001-DEPR-PURL', + rule: 'Replace deprecated helper', + details: 'The {{pageUrl}} helper has been deprecated.', + failures: [{ref: 'default.hbs', message: 'deprecated usage'}], + fatal: false, + level: 'error' +}]) { + return page.route('**/ghost/api/admin/themes/active/**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + themes: [{ + name: 'casper', + active: true, + package: {name: 'Casper', version: '1.0.0'}, + errors: errors, + warnings: [] + }] + }) + }); + }); +} + +function mockActiveThemeWithoutErrors(page: Page) { + return page.route('**/ghost/api/admin/themes/active/**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + themes: [{ + name: 'casper', + active: true, + package: {name: 'Casper', version: '1.0.0'}, + errors: [], + warnings: [] + }] + }) + }); + }); +} + +test.describe('Ghost Admin - Theme Error Notification', () => { + test('shows banner when theme has errors', async ({page}) => { + const sidebar = new SidebarPage(page); + await mockActiveThemeWithErrors(page); + + await sidebar.goto('/ghost'); + + await expect(sidebar.themeErrorBanner).toBeVisible(); + await expect(sidebar.themeErrorBanner).toContainText('Your theme has errors'); + }); + + test('opens dialog when clicking banner', async ({page}) => { + const sidebar = new SidebarPage(page); + await mockActiveThemeWithErrors(page); + + await sidebar.goto('/ghost'); + await sidebar.themeErrorBanner.click(); + + await expect(sidebar.themeErrorDialog).toBeVisible(); + await expect(sidebar.themeErrorDialog).toContainText('Replace deprecated helper'); + }); + + test('does not show banner when theme has no errors', async ({page}) => { + const sidebar = new SidebarPage(page); + await mockActiveThemeWithoutErrors(page); + + await sidebar.goto('/ghost'); + + await expect(sidebar.themeErrorBanner).toBeHidden(); + }); + + test('filters out GS110 show_title_and_feature_image errors', async ({page}) => { + const sidebar = new SidebarPage(page); + await mockActiveThemeWithErrors(page, [{ + code: 'GS110-NO-MISSING-PAGE-BUILDER-USAGE', + rule: 'Check page builder usage', + details: 'Missing page builder helper usage.', + failures: [{ref: 'post.hbs', message: 'show_title_and_feature_image'}], + fatal: false, + level: 'error' + }]); + + await sidebar.goto('/ghost'); + + await expect(sidebar.themeErrorBanner).toBeHidden(); + }); +}); diff --git a/ghost/admin/.lint-todo b/ghost/admin/.lint-todo index fb9c903f1d1..a8373aa3127 100644 --- a/ghost/admin/.lint-todo +++ b/ghost/admin/.lint-todo @@ -1,4 +1,3 @@ -add|ember-template-lint|no-action|2|54|2|54|8618d17e29821f45d8809ad2d6cf6053b825f7fe|1746489600000|||app/components/gh-billing-update-button.hbs add|ember-template-lint|no-action|5|11|5|11|8eaebb48eca1563c6e0b18581df84ab59188d971|1746489600000|||app/components/gh-cm-editor.hbs add|ember-template-lint|no-passed-in-event-handlers|5|4|5|4|3a763e253744b070633bb8bd424b6c8e55f6b20a|1746489600000|||app/components/gh-cm-editor.hbs add|ember-template-lint|no-invalid-interactive|1|103|1|103|534029ab0ba1b74eff4a2f31c8b4dd9f1460316a|1746489600000|||app/components/gh-context-menu.hbs @@ -152,21 +151,6 @@ add|ember-template-lint|no-passed-in-event-handlers|73|24|73|24|80681fcec2258c3d add|ember-template-lint|no-action|6|108|6|108|ccc38f66549f9baedaa3b9943ae6634ea8f99e69|1746489600000|||app/templates/tags.hbs add|ember-template-lint|no-action|7|110|7|110|c3819ce2b6989e8596be570ed0c9fb82b5012521|1746489600000|||app/templates/tags.hbs add|ember-template-lint|require-valid-alt-text|15|32|15|32|80c1ce6724481312363dc4e1db42bf28b41909f2|1746489600000|||app/templates/whatsnew.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|45|104|45|104|156670ca427c49c51f0a94f862b286ccc9466d92|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|65|131|65|131|156670ca427c49c51f0a94f862b286ccc9466d92|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|100|93|100|93|156670ca427c49c51f0a94f862b286ccc9466d92|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|require-context-role|50|89|50|89|0be75355d0dd43dafc60091285ba906c43350d19|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|require-context-role|73|57|73|57|0be75355d0dd43dafc60091285ba906c43350d19|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|require-context-role|78|57|78|57|0be75355d0dd43dafc60091285ba906c43350d19|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|require-context-role|89|131|89|131|0be75355d0dd43dafc60091285ba906c43350d19|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|no-action|19|16|19|16|3696846a8a04d429559abebaaf5dab2c6387c21f|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-action|47|26|47|26|3b76c38861ddcdfaa277e272a1d27293c2659524|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-action|175|24|175|24|c8306856104d54d12e3db50acab26094d0793fb3|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-invalid-interactive|47|26|47|26|da9f7c0f319619ff98a53fd679c47841cfaa3c1d|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-redundant-fn|5|70|5|70|d8c5269c9b4ca3aec0fc5b8e3d6a997e115dbc92|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|36|51|36|51|b8aae2daed1c14cf280800b3d282d11fb14851a4|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|49|28|49|28|571c3d774ed33480f528b54b323b23bfd7148eee|1746489600000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|79|24|79|24|b8aae2daed1c14cf280800b3d282d11fb14851a4|1746489600000|||app/components/gh-nav-menu/main.hbs add|ember-template-lint|no-invalid-interactive|8|51|8|51|66a27dbed218d15e49e91c72a93215ad0d90f778|1746489600000|||app/components/gh-power-select/trigger.hbs add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1746489600000|||app/components/gh-token-input/label-token.hbs add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1746489600000|||app/components/gh-token-input/tag-token.hbs @@ -177,26 +161,9 @@ add|ember-template-lint|require-iframe-title|42|16|42|16|a3292b469dc37f2f4791e7f add|ember-template-lint|no-autofocus-attribute|21|20|21|20|942419d05c04ded6716f09faecd6b1ab55418121|1746489600000|||app/components/modals/new-custom-integration.hbs add|ember-template-lint|no-invalid-interactive|2|37|2|37|e21ba31f54b631a428c28a1c9f88d0dc66f2f5fc|1746489600000|||app/components/modals/search.hbs add|ember-template-lint|no-redundant-role|11|20|11|20|bc4fbabe3d468440d8a2c70e92cec991caca41e2|1746489600000|||app/components/dashboard/onboarding/share-modal.hbs -add|ember-template-lint|no-action|74|30|74|30|3b76c38861ddcdfaa277e272a1d27293c2659524|1749427200000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-invalid-interactive|42|30|42|30|ffabb36d3b9e207aba8ff78ac29b648f2b314bb2|1749427200000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-invalid-interactive|74|30|74|30|ffabb36d3b9e207aba8ff78ac29b648f2b314bb2|1749427200000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-unknown-arguments-for-builtin-components|76|32|76|32|571c3d774ed33480f528b54b323b23bfd7148eee|1749427200000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-invalid-interactive|47|26|47|26|da9f7c0f319619ff98a53fd679c47841cfaa3c1d|1746489600000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-unknown-arguments-for-builtin-components|79|24|79|24|b8aae2daed1c14cf280800b3d282d11fb14851a4|1746489600000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-action|74|30|74|30|3b76c38861ddcdfaa277e272a1d27293c2659524|1749427200000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-invalid-interactive|74|30|74|30|ffabb36d3b9e207aba8ff78ac29b648f2b314bb2|1749427200000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-unknown-arguments-for-builtin-components|76|32|76|32|571c3d774ed33480f528b54b323b23bfd7148eee|1749427200000|||app/components/gh-nav-menu/main.hbs remove|ember-template-lint|no-action|6|108|6|108|ccc38f66549f9baedaa3b9943ae6634ea8f99e69|1746489600000|||app/templates/tags.hbs remove|ember-template-lint|no-action|7|110|7|110|c3819ce2b6989e8596be570ed0c9fb82b5012521|1746489600000|||app/templates/tags.hbs remove|ember-template-lint|require-valid-alt-text|4|12|4|12|8369d1b06deac93e8c8e05444670c15182aea434|1746489600000|||app/templates/application-error.hbs -remove|ember-template-lint|no-unknown-arguments-for-builtin-components|45|104|45|104|156670ca427c49c51f0a94f862b286ccc9466d92|1746489600000|||app/components/gh-nav-menu/footer.hbs -remove|ember-template-lint|no-unknown-arguments-for-builtin-components|65|131|65|131|156670ca427c49c51f0a94f862b286ccc9466d92|1746489600000|||app/components/gh-nav-menu/footer.hbs -remove|ember-template-lint|no-unknown-arguments-for-builtin-components|100|93|100|93|156670ca427c49c51f0a94f862b286ccc9466d92|1746489600000|||app/components/gh-nav-menu/footer.hbs -add|ember-template-lint|no-invalid-interactive|48|30|48|30|a7f41579b917cf51e3a82ca4726d7f03fcfb0bc3|1764028800000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-invalid-interactive|42|30|42|30|ffabb36d3b9e207aba8ff78ac29b648f2b314bb2|1749427200000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-unknown-arguments-for-builtin-components|49|28|49|28|571c3d774ed33480f528b54b323b23bfd7148eee|1746489600000|||app/components/gh-nav-menu/main.hbs -remove|ember-template-lint|no-invalid-interactive|48|30|48|30|a7f41579b917cf51e3a82ca4726d7f03fcfb0bc3|1764028800000|||app/components/gh-nav-menu/main.hbs -add|ember-template-lint|no-invalid-interactive|48|30|48|30|c67992c562a02caa8f4e94ad244fcab0dd998888|1764115200000|||app/components/gh-nav-menu/main.hbs remove|ember-template-lint|no-action|1|71|1|71|2e6351f546807d88cc8eb9dbe8baa149468b5cb9|1746489600000|||app/components/gh-view-title.hbs remove|ember-template-lint|no-invalid-role|1|0|1|0|3e651d38e0110e1be20e5082075db1b879b59a36|1746489600000|||app/components/gh-view-title.hbs add|ember-template-lint|no-yield-only|1|0|1|0|a5fa6e8c1e0f03fb31b5cd17770e4368d44932e4|1771200000000|||app/components/gh-view-title.hbs diff --git a/ghost/admin/app/components/gh-billing-update-button.hbs b/ghost/admin/app/components/gh-billing-update-button.hbs deleted file mode 100644 index 48443ed3134..00000000000 --- a/ghost/admin/app/components/gh-billing-update-button.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#if this.showUpgradeButton}} - -{{/if}} diff --git a/ghost/admin/app/components/gh-billing-update-button.js b/ghost/admin/app/components/gh-billing-update-button.js deleted file mode 100644 index 20b6dbdc097..00000000000 --- a/ghost/admin/app/components/gh-billing-update-button.js +++ /dev/null @@ -1,26 +0,0 @@ -import Component from '@ember/component'; -import classic from 'ember-classic-decorator'; -import {action} from '@ember/object'; -import {inject} from 'ghost-admin/decorators/inject'; -import {reads} from '@ember/object/computed'; -import {inject as service} from '@ember/service'; - -@classic -export default class GhBillingUpdateButton extends Component { - @service router; - @service ghostPaths; - @service ajax; - @service billing; - - @inject config; - - subscription = null; - - @reads('billing.subscription.isActiveTrial') - showUpgradeButton; - - @action - openBilling() { - this.billing.openBillingWindow(this.router.currentURL, '/pro/billing/plans'); - } -} diff --git a/ghost/admin/app/components/gh-link-to-custom-views-index.hbs b/ghost/admin/app/components/gh-link-to-custom-views-index.hbs deleted file mode 100644 index d273c3aad77..00000000000 --- a/ghost/admin/app/components/gh-link-to-custom-views-index.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - {{yield}} - \ No newline at end of file diff --git a/ghost/admin/app/components/gh-link-to-custom-views-index.js b/ghost/admin/app/components/gh-link-to-custom-views-index.js deleted file mode 100644 index 2d0d3920024..00000000000 --- a/ghost/admin/app/components/gh-link-to-custom-views-index.js +++ /dev/null @@ -1,68 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class GhCustomViewsIndexLinkComponent extends Component { - @service customViews; - @service router; - - _forceReset = false; - _lastIsActive = false; - - @action - watchRouterEvents() { - this.router.on('routeWillChange', this.handleRouteWillChange); - } - - @action - unwatchRouterEvents() { - this.router.off('routeWillChange', this.handleRouteWillChange); - } - - // the top-level custom nav link will reset the filter if you're currently - // viewing the associated screen. However, the filter will be remembered by - // Ember automatically if you leave the screen and come back. This causes - // odd behaviour in the nav if you were on a custom view, go to another - // screen, then click back on the top-level nav link as you'll jump from - // the top-level nav to the custom view. - // - // to get around this we keep track of the transitions so that we can force - // the link to be a "reset" link any time navigation occurs from a custom - // view to an unassociated screen - @action - handleRouteWillChange({from, to}) { - let normalizedToRoute = to && to.name.replace(/_loading$/, ''); - - if (from && from.name === this.args.route && normalizedToRoute !== this.args.route) { - if (this.customViews.activeView && this.customViews.activeView.route === this.args.route) { - this._forceReset = true; - } - } - - if (normalizedToRoute === this.args.route) { - this._forceReset = false; - } - } - - get isActive() { - if (this.router.currentRouteName.match(/_loading$/)) { - return this._lastIsActive; - } - - let currentRouteName = this.router.currentRouteName.replace(/_loading$/, ''); - - // eslint-disable-next-line ghost/ember/no-side-effects - this._lastIsActive = currentRouteName === this.args.route - && !this.customViews.activeView; - - return this._lastIsActive; - } - - get resetQuery() { - if (this._forceReset || this.router.currentRouteName === this.args.route) { - return this.args.query; - } - - return undefined; - } -} diff --git a/ghost/admin/app/components/gh-nav-menu.hbs b/ghost/admin/app/components/gh-nav-menu.hbs deleted file mode 100644 index 00fd801f994..00000000000 --- a/ghost/admin/app/components/gh-nav-menu.hbs +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/gh-nav-menu.js b/ghost/admin/app/components/gh-nav-menu.js deleted file mode 100644 index 7da89a8e5bc..00000000000 --- a/ghost/admin/app/components/gh-nav-menu.js +++ /dev/null @@ -1,21 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {schedule} from '@ember/runloop'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class GhNavMenuComponent extends Component { - @service navigation; - @service settings; - @service ui; - @service session; - - @tracked firstRender = true; - - @action - updateFirstRender() { - schedule('afterRender', this, () => { - this.firstRender = false; - }); - } -} diff --git a/ghost/admin/app/components/gh-nav-menu/footer.hbs b/ghost/admin/app/components/gh-nav-menu/footer.hbs deleted file mode 100644 index 699d703245f..00000000000 --- a/ghost/admin/app/components/gh-nav-menu/footer.hbs +++ /dev/null @@ -1,94 +0,0 @@ -
    -
    -
    - - -
    -
    - {{svg-jar "arrow-down" class="w3 mr1 fill-darkgrey" data-test-nav="arrow-down"}} -
    -
    - - - - -
    -
    -
    - {{#if (or (gh-user-can-admin this.session.user) this.session.user.isEitherEditor)}} - {{svg-jar "settings" title="Settings (CTRL/⌘ + ,)"}} - {{/if}} -
    -
    -
    {{svg-jar "sun"}}
    -
    {{svg-jar "moon"}}
    -
    -
    -
    -
    -
    -
    diff --git a/ghost/admin/app/components/gh-nav-menu/footer.js b/ghost/admin/app/components/gh-nav-menu/footer.js deleted file mode 100644 index e3d27ddf7b1..00000000000 --- a/ghost/admin/app/components/gh-nav-menu/footer.js +++ /dev/null @@ -1,36 +0,0 @@ -import Component from '@ember/component'; -import calculatePosition from 'ember-basic-dropdown/utils/calculate-position'; -import classic from 'ember-classic-decorator'; -import {action} from '@ember/object'; -import {and} from '@ember/object/computed'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; - -@classic -export default class Footer extends Component { - @service session; - @service feature; - - @inject config; - - @and('config.clientExtensions.dropdown', 'session.user.isOwnerOnly') - showDropdownExtension; - - // equivalent to "left: auto; right: -20px" - userDropdownPosition(trigger, dropdown) { - let {horizontalPosition, verticalPosition, style} = calculatePosition(...arguments); - let {width: dropdownWidth} = dropdown.firstElementChild.getBoundingClientRect(); - - style.right += (dropdownWidth - 20); - style['z-index'] = '1100'; - - return {horizontalPosition, verticalPosition, style}; - } - - @action - toggleNightShift() { - const newValue = !this.feature.nightShift; - this.feature.nightShift = newValue; - document.documentElement.classList.toggle('dark', newValue); - } -} diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs deleted file mode 100644 index b10b0abfdb4..00000000000 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ /dev/null @@ -1,177 +0,0 @@ -
    - -
    - -
    - - {{#unless this.session.user.isContributor}} -
    -
    -
    -
    {{this.config.blogTitle}}
    -
    - -
    - {{/unless}} - -
    - - {{#unless this.session.user.isContributor}} -
    - - {{#if this.session.user.isAdmin}} -
      - {{#if (and (gh-user-can-admin this.session.user))}} -
    • - {{svg-jar "graph-chart-up-arrow"}}Analytics -
    • - {{/if}} - {{#if (and this.settings.socialWebEnabled this.session.user.isAdmin)}} -
    • - {{svg-jar "ap-network"}}Network - {{#unless this.notificationsCount.isLoading}} - {{#if (gt this.notificationsCount.count 0)}} - {{format-number this.notificationsCount.count}} - {{/if}} - {{/unless}} - -
    • - {{/if}} -
    • - - - {{svg-jar "view-site"}} View site - - - - {{svg-jar "external"}} - -
    • -
    - {{/if}} -
      -
    • - {{svg-jar "posts" class="gh-nav-posts-icon"}}Posts - {{svg-jar "plus"}} - - {{#if this.session.user.isAuthorOrContributor}} - {{#if this.customViews.forPosts}} -
        - {{#each this.customViews.forPosts as |view|}} -
      • - - {{view.name}} - - -
      • - {{/each}} -
      - {{/if}} - {{else}} - {{#if this.customViews.forPosts}} - - {{#liquid-if this.navigation.settings.expanded.posts}} -
        - {{#each this.customViews.forPosts as |view|}} -
      • - - {{view.name}} - - -
      • - {{/each}} -
      - {{/liquid-if}} - {{/if}} - {{/if}} -
    • -
    • - {{!-- clicking the Content link whilst on the content screen should reset the filter --}} - {{svg-jar "page"}}Pages -
    • - {{#if this.showTagsNavigation}} -
    • - {{svg-jar "tag"}}Tags -
    • - {{/if}} - {{#if (gh-user-can-manage-members this.session.user)}} -
    • - {{svg-jar - "members"}}Members - {{#let (members-count-fetcher) as |count|}} - {{#unless count.isLoading}} - {{format-number count.count}} - {{/unless}} - {{/let}} - -
    • - {{/if}} -
    - - {{#if this.session.user.isOwnerOnly}} - - {{/if}} - - {{#if this.showMenuExtension}} -
      - {{#if this.config.clientExtensions.menu.title}} -
    • {{this.config.clientExtensions.menu.title}}
    • - {{/if}} - {{#each this.config.clientExtensions.menu.items as |menuItem| }} -
    • - {{svg-jar - menuItem.icon}}{{menuItem.text}} -
    • - {{/each}} -
    - {{/if}} -
    - {{/unless}} - - - -
    -
    diff --git a/ghost/admin/app/components/gh-nav-menu/main.js b/ghost/admin/app/components/gh-nav-menu/main.js deleted file mode 100644 index 8ede86f4952..00000000000 --- a/ghost/admin/app/components/gh-nav-menu/main.js +++ /dev/null @@ -1,141 +0,0 @@ -import Component from '@ember/component'; -import SearchModal from '../modals/search'; -import classic from 'ember-classic-decorator'; -import {action} from '@ember/object'; -import {and, equal, or, reads} from '@ember/object/computed'; -import {getOwner} from '@ember/application'; -import {htmlSafe} from '@ember/template'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {tagName} from '@ember-decorators/component'; - -@classic -@tagName('') -export default class Main extends Component { - @service billing; - @service customViews; - @service feature; - @service ghostPaths; - @service modals; - @service navigation; - @service router; - @service session; - @service ui; - @service membersStats; - @service settings; - @service explore; - @service notifications; - @service notificationsCount; - - @inject config; - - iconStyle = ''; - iconClass = ''; - previousRoute = null; - - // HACK: {{link-to}} should be doing this automatically but there appears to - // be a bug in Ember that's preventing it from working immediately after login - @equal('router.currentRouteName', 'site') - isOnSite; - - @or('session.user.isAdmin', 'session.user.isEitherEditor') - showTagsNavigation; - - @and('config.clientExtensions.menu', 'session.user.isOwnerOnly') - showMenuExtension; - - @reads('config.hostSettings.billing.enabled') - showBilling; - - init() { - super.init(...arguments); - - // Set initial previous route - this.previousRoute = this.router.currentRouteName; - - const currentRoute = this.router.currentRouteName || ''; - // Fetch notifications count if not on activitypub-x route - if (this.settings.socialWebEnabled && !currentRoute.startsWith('activitypub-x')) { - this.notificationsCount.fetchCount(); - } - - this._routeChangeHandler = () => { - const current = this.router.currentRouteName || ''; - const prev = this.previousRoute || ''; - - if (this.settings.socialWebEnabled && prev.startsWith('activitypub-x') && !current.startsWith('activitypub-x')) { - this.notificationsCount.fetchCount(); - } - - this.previousRoute = current; - }; - - this.router.on('routeDidChange', this._routeChangeHandler); - } - - // the menu has a rendering issue (#8307) when the the world is reloaded - // during an import which we have worked around by not binding the icon - // style directly. However we still need to keep track of changing icons - // so that we can refresh when a new icon is uploaded - didReceiveAttrs() { - super.didReceiveAttrs(...arguments); - this._setIconStyle(); - } - - willDestroyElement() { - super.willDestroyElement(...arguments); - if (this._routeChangeHandler) { - this.router.off('routeDidChange', this._routeChangeHandler); - } - } - - @action - transitionToOrRefreshSite() { - let {currentRouteName} = this.router; - if (currentRouteName === 'site') { - getOwner(this).lookup(`route:${currentRouteName}`).refresh(); - } else { - if (this.session.user.isContributor) { - this.router.transitionTo('posts'); - } else { - this.router.transitionTo('site'); - } - } - } - - @action - openSearchModal() { - return this.modals.open(SearchModal); - } - - @action - toggleBillingModal() { - this.billing.openBillingWindow(this.router.currentURL); - } - - @action - toggleExploreWindow() { - this.explore.openExploreWindow(); - } - - _setIconStyle() { - let icon = this.icon; - - if (icon === this._icon) { - return; - } - - this._icon = icon; - - if (icon && icon.match(/^https?:\/\//i)) { - this.set('iconClass', ''); - this.set('iconStyle', htmlSafe(`background-image: url(${icon})`)); - return; - } - - let iconUrl = 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png'; - - this.set('iconStyle', htmlSafe(`background-image: url(${iconUrl})`)); - this.set('iconClass', 'gh-nav-logo-default'); - } -} diff --git a/ghost/admin/app/components/member-attribution/modals/all-sources.hbs b/ghost/admin/app/components/member-attribution/modals/all-sources.hbs deleted file mode 100644 index 14cba3d40fb..00000000000 --- a/ghost/admin/app/components/member-attribution/modals/all-sources.hbs +++ /dev/null @@ -1,59 +0,0 @@ - \ No newline at end of file diff --git a/ghost/admin/app/components/member-attribution/modals/all-sources.js b/ghost/admin/app/components/member-attribution/modals/all-sources.js deleted file mode 100644 index 8a1a7632ef6..00000000000 --- a/ghost/admin/app/components/member-attribution/modals/all-sources.js +++ /dev/null @@ -1,10 +0,0 @@ -import Component from '@glimmer/component'; -import {inject as service} from '@ember/service'; - -export default class FullAttributionTable extends Component { - @service membersUtils; - - static modalOptions = { - className: 'epm-modal fullscreen-modal-action fullscreen-modal-wide' - }; -} diff --git a/ghost/admin/app/components/member-attribution/source-attribution-chart.hbs b/ghost/admin/app/components/member-attribution/source-attribution-chart.hbs deleted file mode 100644 index 0f6f2fe3250..00000000000 --- a/ghost/admin/app/components/member-attribution/source-attribution-chart.hbs +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/ghost/admin/app/components/member-attribution/source-attribution-chart.js b/ghost/admin/app/components/member-attribution/source-attribution-chart.js deleted file mode 100644 index 1f5f90cc7db..00000000000 --- a/ghost/admin/app/components/member-attribution/source-attribution-chart.js +++ /dev/null @@ -1,166 +0,0 @@ -import Component from '@glimmer/component'; -import {inject as service} from '@ember/service'; - -const CHART_COLORS = [ - '#8e42ff', - '#BB4AE5', - '#DD97C9', - '#E19A98', - '#F5C9C2', - '#E6E9EB' -]; - -export default class SourceAttributionChart extends Component { - @service feature; - - get sources() { - return this.args.sources; - } - - get chartOptions() { - let chartTitle = 'Free signups'; - if (this.args.sortColumn === 'signups') { - chartTitle = 'Free signups'; - } else { - chartTitle = 'Paid conversions'; - } - - return { - cutoutPercentage: 70, - title: { - display: false, - text: chartTitle, - position: 'bottom', - padding: 12, - fontFamily: 'Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Droid Sans,Helvetica Neue,sans-serif', - color: '#7c8b9a', - fontSize: 12, - fontStyle: '600' - }, - legend: { - display: false - }, - hover: { - onHover: function (e) { - e.target.style.cursor = 'pointer'; - } - }, - tooltips: { - enabled: false, - intersect: false, - mode: 'single', - custom: function (tooltip) { - // get tooltip element - const tooltipEl = document.getElementById('gh-dashboard-attribution-tooltip'); - - // only show tooltip when active - if (tooltip.opacity === 0) { - tooltipEl.style.opacity = 0; - return; - } - - let offsetX = 10; - let offsetY = 15; - - // update tooltip styles - tooltipEl.style.opacity = 1; - tooltipEl.style.position = 'absolute'; - tooltipEl.style.left = tooltip.x + offsetX + 'px'; - tooltipEl.style.top = tooltip.y + offsetY + 'px'; - }, - callbacks: { - label: (tooltipItems, data) => { - const tooltipTextEl = document.querySelector('#gh-dashboard-attribution-tooltip .gh-dashboard-tooltip-value'); - // const label = data.datasets[tooltipItems.datasetIndex].label || ''; - const label = data.labels[tooltipItems.index] || ''; - var value = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index] || 0; - if (value < 0) { - value = -value; - } - - tooltipTextEl.innerHTML = `${label}${value}%`; - }, - title: () => { - return null; - } - } - }, - aspectRatio: 1 - }; - } - - get chartData() { - let borderColor = this.feature.nightShift ? '#15171A' : '#fff'; - - return { - labels: this.allSources.map(source => source.source), - datasets: [{ - label: this.args.sortColumn === 'signups' ? 'Signups' : 'Paid conversions', - data: this.allSources.map((source) => { - return source.percentage; - }), - backgroundColor: CHART_COLORS.slice(0, 6), - borderWidth: 2, - borderColor: borderColor, - hoverBorderWidth: 2, - hoverBorderColor: borderColor - }] - }; - } - - get allSources() { - const mainSources = [...this.sortedSources.slice(0, 5)]; - if (this.others) { - mainSources.push(this.others); - } - // get percentage of each source - const total = mainSources.reduce((acc, source) => { - if (this.args.sortColumn === 'signups') { - return acc + source.signups; - } - return acc + source.paidConversions; - }, 0); - - return mainSources.map((source) => { - let value = 0; - if (this.args.sortColumn === 'signups') { - value = source.signups; - } else { - value = source.paidConversions; - } - return { - ...source, - percentage: Math.round((value / total) * 100) - }; - }); - } - - // Others data includes all sources except the first 5 - get others() { - if (this.sortedSources.length <= 5) { - return null; - } - - return this.sortedSources.slice(5).reduce((acc, source) => { - return { - ...acc, - signups: acc.signups + source.signups, - paidConversions: acc.paidConversions + source.paidConversions - }; - }, { - source: 'Others', - signups: 0, - paidConversions: 0 - }); - } - - get sortedSources() { - return this.args.sources?.filter(source => source.source).sort((a, b) => { - if (this.args.sortColumn === 'signups') { - return b.signups - a.signups; - } else { - return b.paidConversions - a.paidConversions; - } - }) || []; - } -} diff --git a/ghost/admin/app/components/member-attribution/source-attribution-table.hbs b/ghost/admin/app/components/member-attribution/source-attribution-table.hbs deleted file mode 100644 index 0162bacf7ca..00000000000 --- a/ghost/admin/app/components/member-attribution/source-attribution-table.hbs +++ /dev/null @@ -1,89 +0,0 @@ -
    -
    -
    Sources
    -
    - Free signups{{#if this.membersUtils.paidMembersEnabled}}{{svg-jar "arrow-down-fill"}}{{/if}} -
    - {{#if this.membersUtils.paidMembersEnabled}} -
    - Paid Conversions{{svg-jar "arrow-down-fill"}} -
    - {{/if}} -
    -
    - {{#each this.sources as |sourceData|}} -
    -
    - {{sourceData.source}} - {{!-- * --}} - -
    - - {{#if this.membersUtils.paidMembersEnabled}} -
    - {{#if sourceData.paidConversions}} - - {{format-number sourceData.paidConversions}} - - {{else}} - - — - - {{/if}} -
    - {{/if}} -
    - {{/each}} - {{#if this.others}} -
    -
    - Other sources -
    -
    - {{#if this.others.signups}} - - {{format-number this.others.signups}} - - {{else}} - - — - - {{/if}} -
    - {{#if this.membersUtils.paidMembersEnabled}} -
    - {{#if this.others.paidConversions}} - - {{format-number this.others.paidConversions}} - - {{else}} - - — - - {{/if}} -
    - {{/if}} -
    - {{/if}} -
    -
    -{{!--

    *New member signups originating from your newsletter are the result of it being shared and forwarded.

    --}} \ No newline at end of file diff --git a/ghost/admin/app/components/member-attribution/source-attribution-table.js b/ghost/admin/app/components/member-attribution/source-attribution-table.js deleted file mode 100644 index 1045d828828..00000000000 --- a/ghost/admin/app/components/member-attribution/source-attribution-table.js +++ /dev/null @@ -1,71 +0,0 @@ -import AllSourcesModal from './modals/all-sources'; -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; - -export default class SourceAttributionTable extends Component { - @service membersUtils; - @service modals; - - @action - openAllSources() { - // add unavailableSource to sortedSources array only if it has value - const allSources = this.unavailableSource ? [...this.sortedSources, this.unavailableSource] : this.sortedSources; - - this.modals.open(AllSourcesModal, { - sources: allSources, - unavailableSource: this.unavailableSource, - sortColumn: this.args.sortColumn - }); - } - - get unavailableSource() { - const emptySource = this.args.sources.find(sourceData => !sourceData.source); - if (!emptySource) { - return null; - } - return { - ...emptySource, - source: 'Unavailable' - }; - } - - // Others data includes all sources except the first 5 - get others() { - if (this.sortedSources.length < 5) { - return null; - } - - if (this.sortedSources.length === 5 && !this.unavailableSource?.length) { - return null; - } - - return this.sortedSources.slice(5).reduce((acc, source) => { - return { - signups: acc.signups + source.signups, - paidConversions: acc.paidConversions + source.paidConversions - }; - }, { - signups: 0, - paidConversions: 0 - }); - } - - get sortedSources() { - return this.args.sources?.filter(source => source.source).sort((a, b) => { - if (this.args.sortColumn === 'signups') { - return b.signups - a.signups; - } else { - return b.paidConversions - a.paidConversions; - } - }) || []; - } - - get sources() { - if (this.sortedSources.length >= 5) { - return this.sortedSources.slice(0, 5); - } - - return this.unavailableSource ? [...this.sortedSources, this.unavailableSource] : this.sortedSources; - } -} diff --git a/ghost/admin/app/components/members/filters/audience-feedback.js b/ghost/admin/app/components/members/filters/audience-feedback.js index f149eccb217..2adb962eb5c 100644 --- a/ghost/admin/app/components/members/filters/audience-feedback.js +++ b/ghost/admin/app/components/members/filters/audience-feedback.js @@ -9,7 +9,6 @@ export const AUDIENCE_FEEDBACK_FILTER = { valueType: 'string', resource: 'email', relationOptions: FEEDBACK_RELATION_OPTIONS, - feature: 'audienceFeedback', buildNqlFilter: (filter) => { // Added brackets to make sure we can parse as a single AND filter return `(feedback.post_id:'${filter.value}'+feedback.score:${filter.relation})`; diff --git a/ghost/admin/app/components/posts/analytics.hbs b/ghost/admin/app/components/posts/analytics.hbs deleted file mode 100644 index 4eb62431e08..00000000000 --- a/ghost/admin/app/components/posts/analytics.hbs +++ /dev/null @@ -1,269 +0,0 @@ -
    - -
    -
    - - Posts - - {{svg-jar "arrow-right-small"}}Analytics -
    -

    - {{this.post.title}} -

    -
    -
    - {{#if this.post.hasBeenEmailed }} - {{#if this.post.emailOnly}} - Sent - {{else}} - Published and sent - {{/if}} - {{else}} - Published - {{#if @post.didEmailFail}} - but failed to send - {{else}} - on your site - {{/if}} - {{/if}} - - {{#let (moment-site-tz this.post.publishedAtUTC) as |publishedAt|}} - on - {{moment-format publishedAt "D MMM YYYY"}} - at - {{moment-format publishedAt "HH:mm"}} - {{/let}} -
    -
    - - {{#unless this.post.emailOnly}} - - {{/unless}} - - - - - {{svg-jar "dotdotdot"}} - - - - -
  • - Edit post -
  • -
  • - View in browser -
  • -
  • - -
  • -
    -
    -
    -
    -
    -
    - -
    -
    - - {{#if this.post.hasBeenEmailed}} - -

    - {{svg-jar "analytics-tab-sent-large"}} - {{split-number this.post.email.emailCount this.previousSentCount}} -

    -

    {{svg-jar "analytics-tab-sent"}}Sent

    -
    - - - - - - {{#if this.post.showEmailOpenAnalytics }} - -

    - {{svg-jar "analytics-tab-opened-large"}} - {{split-number this.post.email.openedCount this.previousOpenedCount}} -

    -

    {{svg-jar "analytics-tab-opened"}}Opened — {{this.post.email.openRate}}%

    -
    - - - - - {{/if}} - - {{#if this.post.showEmailClickAnalytics }} - -

    - {{svg-jar "analytics-tab-clicked-large"}} - {{split-number this.post.count.clicks this.previousClickedCount}} -

    -

    {{svg-jar "analytics-tab-clicked"}}Clicked — {{this.post.clickRate}}%

    -
    - - - - - {{/if}} - - {{#if this.post.isFeedbackEnabledForEmail }} - -

    - {{svg-jar "analytics-tab-feedback-large"}} - -

    -

    {{svg-jar "analytics-tab-feedback"}}Feedback — {{this.post.sentiment}}%

    -
    - - - - - {{/if}} - {{/if}} - - {{#if this.post.showAttributionAnalytics }} - -

    - {{svg-jar "analytics-tab-conversions-large"}} - {{#if this.post.hasBeenEmailed}} - {{split-number this.post.count.conversions this.previousConversionsCount}} - {{else}} - Conversions - {{/if}} -

    -

    {{svg-jar "analytics-tab-conversions"}}{{gh-pluralize this.post.count.conversions "Conversions" without-count=true}}

    -
    - - - - - {{/if}} -
    - - {{#if this.isLoaded }} -
    - {{#if this.showLinks }} - {{#if (is-empty this.links) }} - {{!-- Empty state --}} - {{else}} - - {{/if}} - {{/if}} - - {{#if this.showMentions }} -
    -

    Mentions

    -
    - {{#if this.mentions}} - {{#each this.mentions as |mention|}} - - {{mention.sourceSiteTitle}} - {{if mention.sourceTitle mention.sourceTitle mention.source}} - {{moment-from-now mention.timestamp}} - - {{/each}} - {{else}} -
    -

    No mentions yet.

    -
    - {{/if}} -
    - {{#if this.mentions}} - - {{/if}} -
    - {{/if}} -
    - -
    - -
    -
    -
    -

    - Ghost help -

    -
    -

    Understanding analytics in Ghost

    -

    Find out how to review the performance of your content and get the most out of post analytics in Ghost.

    -
    -
    - -
    -
    - -
    -
    -
    -

    - Ghost resources -

    -
    -

    How to get your content seen online

    -

    Use these content distribution tactics to get more people to discover your work and increase engagement.

    -
    -
    - -
    -
    -
    - {{else}} -
    -
    -
    -
    -
    - {{/if}} -
    -
    -
    diff --git a/ghost/admin/app/components/posts/analytics.js b/ghost/admin/app/components/posts/analytics.js deleted file mode 100644 index e25127dcfd9..00000000000 --- a/ghost/admin/app/components/posts/analytics.js +++ /dev/null @@ -1,429 +0,0 @@ -import Component from '@glimmer/component'; -import DeletePostModal from '../modals/delete-post'; -import PostSuccessModal from '../modal-post-success'; -import anime from 'animejs/lib/anime.es.js'; -import {action} from '@ember/object'; -import {didCancel, task} from 'ember-concurrency'; -import {inject} from 'ghost-admin/decorators/inject'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -/** - * @typedef {import('../../services/dashboard-stats').SourceAttributionCount} SourceAttributionCount -*/ - -const DISPLAY_OPTIONS = [{ - name: 'Free signups', - value: 'signups' -}, { - name: 'Paid conversions', - value: 'paid' -}]; - -export default class Analytics extends Component { - @service ajax; - @service ghostPaths; - @service settings; - @service membersUtils; - @service utils; - @service feature; - @service store; - @service router; - @service modals; - @service notifications; - @service session; - - @inject config; - - @tracked sources = null; - @tracked links = null; - @tracked mentions = null; - @tracked sortColumn = 'signups'; - @tracked showSuccess; - @tracked updateLinkId; - @tracked _post = null; - @tracked postCount = null; - @tracked showPostCount = false; - @tracked shouldAnimate = false; - @tracked previousSentCount = this.post.email?.emailCount; - @tracked previousOpenedCount = this.post.email?.openedCount; - @tracked previousClickedCount = this.post.count.clicks; - @tracked previousFeedbackCount = this.totalFeedback; - @tracked previousConversionsCount = this.post.count.conversions; - displayOptions = DISPLAY_OPTIONS; - - constructor() { - super(...arguments); - this.checkPublishFlowModal(); - } - - openPublishFlowModal() { - this.modals.open(PostSuccessModal, { - post: this.post, - postCount: this.postCount, - showPostCount: this.showPostCount - }); - } - - async checkPublishFlowModal() { - if (localStorage.getItem('ghost-last-published-post')) { - await this.fetchPostCountTask.perform(); - this.showPostCount = true; - this.openPublishFlowModal(); - localStorage.removeItem('ghost-last-published-post'); - } - } - - get post() { - return this._post ?? this.args.post; - } - - set post(value) { - this._post = value; - } - - get allowedDisplayOptions() { - if (!this.hasPaidConversionData) { - return this.displayOptions.filter(d => d.value === 'signups'); - } - - if (!this.hasFreeSignups) { - return this.displayOptions.filter(d => d.value === 'paid'); - } - - return this.displayOptions; - } - - get isDropdownDisabled() { - if (!this.hasPaidConversionData || !this.hasFreeSignups) { - return true; - } - - return false; - } - - get selectedDisplayOption() { - if (!this.hasPaidConversionData) { - return this.displayOptions.find(d => d.value === 'signups'); - } - - if (!this.hasFreeSignups) { - return this.displayOptions.find(d => d.value === 'paid'); - } - - return this.displayOptions.find(d => d.value === this.sortColumn) ?? this.displayOptions[0]; - } - - get selectedSortColumn() { - if (!this.hasPaidConversionData) { - return 'signups'; - } - - if (!this.hasFreeSignups) { - return 'paid'; - } - return this.sortColumn; - } - - get hasPaidConversionData() { - return this.sources.some(sourceData => sourceData.paidConversions > 0); - } - - get hasFreeSignups() { - return this.sources.some(sourceData => sourceData.signups > 0); - } - - get totalFeedback() { - return this.post.count.positive_feedback + this.post.count.negative_feedback; - } - - get feedbackChartData() { - const values = [this.post.count.positive_feedback, this.post.count.negative_feedback]; - const labels = ['More like this', 'Less like this']; - const links = [ - {filterParam: '(feedback.post_id:\'' + this.post.id + '\'+feedback.score:1)'}, - {filterParam: '(feedback.post_id:\'' + this.post.id + '\'+feedback.score:0)'} - ]; - const colors = ['#F080B2', '#8452f633']; - return {values, labels, links, colors}; - } - - @action - onDisplayChange(selected) { - this.sortColumn = selected.value; - } - - @action - setSortColumn(column) { - this.sortColumn = column; - } - - @action - updateLink(linkId, linkTo) { - if (this._updateLinks.isRunning) { - return this._updateLinks.last; - } - return this._updateLinks.perform(linkId, linkTo); - } - - @action - loadData() { - if (this.showSources) { - this.fetchReferrersStats(); - } else { - this.sources = []; - } - - if (this.showLinks) { - this.fetchLinks(); - } else { - this.links = []; - } - - if (this.showMentions) { - this.fetchMentions(); - } else { - this.mentions = []; - } - } - - @action - togglePublishFlowModal() { - this.showPostCount = false; - this.openPublishFlowModal(); - } - - @action - confirmDeleteMember() { - this.modals.open(DeletePostModal, { - post: this.post - }); - } - - updateLinkData(linksData) { - let cleanedLinks = linksData.map((link) => { - return { - ...link, - link: { - ...link.link, - originalTo: link.link.to, - to: this.utils.cleanTrackedUrl(link.link.to, false), - title: this.utils.cleanTrackedUrl(link.link.to, true) - } - }; - }); - - const linksByTitle = cleanedLinks.reduce((acc, link) => { - if (!acc[link.link.title]) { - acc[link.link.title] = link; - } else { - if (!acc[link.link.title].count) { - acc[link.link.title].count = {clicks: 0}; - } - if (!acc[link.link.title].count.clicks) { - acc[link.link.title].count.clicks = 0; - } - - acc[link.link.title].count.clicks += (link.count?.clicks ?? 0); - } - return acc; - }, {}); - - this.links = Object.values(linksByTitle).sort((a, b) => { - const aClicks = a.count?.clicks || 0; - const bClicks = b.count?.clicks || 0; - return bClicks - aClicks; - }); - } - - async fetchReferrersStats() { - try { - if (this._fetchReferrersStats.isRunning) { - return this._fetchReferrersStats.last; - } - return this._fetchReferrersStats.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } - } - - async fetchLinks() { - try { - if (this._fetchLinks.isRunning) { - return this._fetchLinks.last; - } - - return this._fetchLinks.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } - } - - @task - *_updateLinks(linkId, newLink) { - this.updateLinkId = linkId; - let currentLink; - this.links = this.links?.map((link) => { - if (link.link.link_id === linkId) { - currentLink = new URL(link.link.originalTo); - return { - ...link, - link: { - ...link.link, - to: this.utils.cleanTrackedUrl(newLink, false), - title: this.utils.cleanTrackedUrl(newLink, true) - } - }; - } - return link; - }); - - const filter = `post_id:'${this.post.id}'+to:'${currentLink}'`; - let bulkUpdateUrl = this.ghostPaths.url.api(`links/bulk`) + `?filter=${encodeURIComponent(filter)}`; - yield this.ajax.put(bulkUpdateUrl, { - data: { - bulk: { - action: 'updateLink', - meta: {link: {to: newLink}} - } - } - }); - - // Refresh links data - const linksFilter = `post_id:'${this.post.id}'`; - let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(linksFilter)}`; - let result = yield this.ajax.request(statsUrl); - this.updateLinkData(result.links); - this.showSuccess = this.updateLinkId; - setTimeout(() => { - this.showSuccess = null; - }, 2000); - } - - @task - *_fetchReferrersStats() { - let statsUrl = this.ghostPaths.url.api(`stats/referrers/posts/${this.post.id}`); - let result = yield this.ajax.request(statsUrl); - this.sources = result.stats.map((stat) => { - return { - source: stat.source ?? 'Direct', - signups: stat.signups, - paidConversions: stat.paid_conversions - }; - }); - } - - @task - *_fetchLinks() { - const filter = `post_id:'${this.post.id}'`; - let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(filter)}`; - let result = yield this.ajax.request(statsUrl); - this.updateLinkData(result.links); - } - - async fetchMentions() { - if (this._fetchMentions.isRunning) { - return this._fetchMentions.last; - } - return this._fetchMentions.perform(); - } - - @task - *_fetchMentions() { - const filter = `resource_id:'${this.post.id}'+resource_type:post`; - this.mentions = yield this.store.query('mention', {limit: 5, order: 'created_at desc', filter}); - } - - @task - *fetchPostCountTask() { - if (!this.post.emailOnly) { - const result = yield this.store.query('post', {filter: 'status:published', limit: 1}); - let count = result.meta.pagination.total; - - this.postCount = count; - } - } - - @task - *fetchPostTask() { - const currentSentCount = this.post.email?.emailCount; - const currentOpenedCount = this.post.email?.openedCount; - const currentClickedCount = this.post.count.clicks; - const currentFeedbackCount = this.totalFeedback; - const currentConversionsCount = this.post.count.conversions; - - this.shouldAnimate = true; - - const result = yield this.store.query('post', {filter: `id:${this.post.id}`, include: 'email,count.clicks,count.conversions,count.positive_feedback,count.negative_feedback,sentiment', limit: 1}); - this.post = result.toArray()[0]; - - this.previousSentCount = currentSentCount; - this.previousOpenedCount = currentOpenedCount; - this.previousClickedCount = currentClickedCount; - this.previousFeedbackCount = currentFeedbackCount; - this.previousConversionsCount = currentConversionsCount; - - yield this.fetchLinks(); - - return true; - } - - @action - applyClasses(element) { - if (!this.shouldAnimate || - (element.classList.contains('sent') && this.post.email.emailCount === this.previousSentCount) || - (element.classList.contains('opened') && this.post.email.openedCount === this.previousOpenedCount) || - (element.classList.contains('clicked') && this.post.count.clicks === this.previousClickedCount) || - (element.classList.contains('feedback') && this.totalFeedback === this.previousFeedbackCount) || - (element.classList.contains('conversions') && this.post.count.conversions === this.previousConversionsCount) - ) { - return; - } - - anime({ - targets: `${Array.from(element.classList).map(className => `.${className}`).join('')} .new-number span`, - translateY: [10,0], - // translateZ: 0, - opacity: [0,1], - easing: 'easeOutElastic', - elasticity: 650, - duration: 1000, - delay: (el, i) => 100 + 30 * i - }); - - anime({ - targets: `${Array.from(element.classList).map(className => `.${className}`).join('')} .old-number span`, - translateY: [0,-10], - opacity: [1,0], - easing: 'easeOutExpo', - duration: 400, - delay: (el, i) => 100 + 10 * i - }); - } - - get showLinks() { - return this.post.showEmailClickAnalytics; - } - - get showSources() { - return this.post.showAttributionAnalytics; - } - - get showMentions() { - return this.feature.get('webmentions'); - } - - get isLoaded() { - return this.links !== null && this.souces !== null && this.mentions !== null; - } -} diff --git a/ghost/admin/app/components/posts/feedback-events-chart.hbs b/ghost/admin/app/components/posts/feedback-events-chart.hbs deleted file mode 100644 index c8a2da2de38..00000000000 --- a/ghost/admin/app/components/posts/feedback-events-chart.hbs +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    - -
    - -
    -
    -
    - - {{this.tooltipData.value}} - {{this.tooltipData.percent}}% -
    - - {{this.tooltipData.label}} -
    -
    -
    diff --git a/ghost/admin/app/components/posts/feedback-events-chart.js b/ghost/admin/app/components/posts/feedback-events-chart.js deleted file mode 100644 index 26dcbe29c22..00000000000 --- a/ghost/admin/app/components/posts/feedback-events-chart.js +++ /dev/null @@ -1,97 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class FeedbackEventsChart extends Component { - @service feature; - @tracked tooltipData = {}; - tooltipNode = null; - - @action - onTooltipInsert(node) { - this.tooltipNode = node; - } - - @action - onMouseleave() { - this.tooltipNode.style.display = 'none'; - } - - getSumOfData() { - return this.args.data.values.reduce((acc, value) => { - return acc + value; - }, 0); - } - - setTooltipData(data) { - this.tooltipData = data; - } - - get chartOptions() { - return { - cutoutPercentage: 70, - title: { - display: false - }, - legend: { - display: false - }, - tooltips: { - enabled: false, - mode: 'label', - custom: function (tooltip) { - // get tooltip element - const tooltipEl = document.getElementById('gh-feedback-events-tooltip'); - - let offsetX = -50; - let offsetY = -100; - - // update tooltip styles - tooltipEl.style.display = 'block'; - tooltipEl.style.opacity = '1'; - tooltipEl.style.position = 'absolute'; - tooltipEl.style.left = tooltip.x + offsetX + 'px'; - tooltipEl.style.top = tooltip.y + offsetY + 'px'; - tooltipEl.style.pointerEvents = 'all'; - }, - callbacks: { - label: (tooltipItems, data) => { - const label = data.labels[tooltipItems.index] || ''; - const value = data.datasets[tooltipItems.datasetIndex].data[tooltipItems.index] || 0; - const percent = Math.round(value / this.getSumOfData() * 100); - const tooltipData = { - color: data.datasets[tooltipItems.datasetIndex].backgroundColor[tooltipItems.index], - href: this.args.data.links[tooltipItems.index], - value: value.toLocaleString('en-US'), - label, - percent - }; - this.setTooltipData(tooltipData); - }, - title: () => { - return null; - } - } - }, - aspectRatio: 1 - }; - } - - get chartData() { - let borderColor = this.feature.nightShift ? '#15171A' : '#fff'; - - return { - labels: this.args.data.labels, - datasets: [{ - label: 'Feedback events', - data: this.args.data.values, - backgroundColor: this.args.data.colors, - borderWidth: 2, - borderColor: borderColor, - hoverBorderWidth: 2, - hoverBorderColor: borderColor - }] - }; - } -} diff --git a/ghost/admin/app/components/posts/links-table.hbs b/ghost/admin/app/components/posts/links-table.hbs deleted file mode 100644 index ea39a629882..00000000000 --- a/ghost/admin/app/components/posts/links-table.hbs +++ /dev/null @@ -1,144 +0,0 @@ -{{#if (not (feature "audienceFeedback"))}} -

    - Newsletter clicks -

    -{{/if}} -
    - {{#if (feature "audienceFeedback")}}

    Newsletter clicks

    {{/if}} - -
    diff --git a/ghost/admin/app/components/posts/links-table.js b/ghost/admin/app/components/posts/links-table.js deleted file mode 100644 index 75da6ae41f8..00000000000 --- a/ghost/admin/app/components/posts/links-table.js +++ /dev/null @@ -1,112 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {tracked} from '@glimmer/tracking'; - -const PAGE_SIZE = 5; - -export default class LinksTable extends Component { - @tracked page = 1; - - @tracked editingLink = null; - @tracked showError = null; - @tracked _linkValue = ''; - - @action - handleBlur(event) { - event?.preventDefault(); - if (this.editingLink && !event?.relatedTarget?.matches('.gh-links-list-item-update-button')) { - this.cancelEdit(); - } - } - - @action - editLink(linkId) { - this.editingLink = linkId; - const linkTo = this.links.find(link => link.link.link_id === linkId)?.link?.to; - this._linkValue = linkTo || ''; - } - - @action - cancelEdit(event) { - event?.preventDefault(); - this.editingLink = null; - this.showError = null; - } - - @action - updateLinkValue(event) { - this._linkValue = event.target.value; - } - - @action - setLink(event) { - event?.preventDefault(); - try { - const newUrl = new URL(this._linkValue); - const linkObj = this.links.find((_link) => { - return _link.link.link_id === this.editingLink; - }); - // Only call update if the new link is different from current link - if (linkObj.link.to !== newUrl.href) { - this.args.updateLink(this.editingLink, newUrl.href); - } - this.editingLink = null; - this.showError = null; - } catch (e) { - this.showError = this.editingLink; - } - } - - get links() { - return this.args.links; - } - - get visibleLinks() { - return this.links.slice(this.startOffset - 1, this.endOffset); - } - - get startOffset() { - return (this.page - 1) * PAGE_SIZE + 1; - } - - get endOffset() { - return Math.min(this.page * PAGE_SIZE, this.links.length); - } - - get totalPages() { - return Math.ceil(this.links.length / PAGE_SIZE); - } - - get totalLinks() { - return this.links.length; - } - - get showPagination() { - return this.totalPages > 1; - } - - get disablePreviousPage() { - return this.page === 1; - } - - get disableNextPage() { - return this.page === this.totalPages; - } - - @action - openPreviousPage() { - if (this.disablePreviousPage) { - return; - } - this.page -= 1; - } - - @action - openNextPage() { - if (this.disableNextPage) { - return; - } - - this.page += 1; - } -} diff --git a/ghost/admin/app/components/posts/old-analytics.hbs b/ghost/admin/app/components/posts/old-analytics.hbs deleted file mode 100644 index bc9129d1d16..00000000000 --- a/ghost/admin/app/components/posts/old-analytics.hbs +++ /dev/null @@ -1,161 +0,0 @@ -
    - - -
    -
    - - Posts - - {{svg-jar "arrow-right-small"}}Analytics -
    -

    - {{this.post.title}} -

    -
    -
    - {{#if this.post.hasBeenEmailed }} - {{#if this.post.emailOnly}} - Sent - {{else}} - Published and sent - {{/if}} - {{else}} - Published - {{#if @post.didEmailFail}} - but failed to send - {{else}} - on your site - {{/if}} - {{/if}} - - {{#let (moment-site-tz this.post.publishedAtUTC) as |publishedAt|}} - on - {{moment-format publishedAt "D MMM YYYY"}} - at - {{moment-format publishedAt "HH:mm"}} - {{/let}} -
    - - {{svg-jar "pen" title=""}}Edit post - -
    -
    -
    - -

    - Engagement -

    -
    - {{#if this.post.hasBeenEmailed}} -
    - -

    {{format-number this.post.email.emailCount}}

    -

    Sent

    -
    -
    - - {{#if this.post.showEmailOpenAnalytics }} -
    - -

    {{format-number this.post.email.openedCount}}

    -

    Opened — {{this.post.email.openRate}}%

    -
    -
    - {{/if}} - - {{#if this.post.showEmailClickAnalytics }} -
    - -

    {{format-number this.post.count.clicks}}

    -

    Clicked — {{this.post.clickRate}}%

    -
    -
    - {{/if}} - {{/if}} - - {{#if this.post.showAttributionAnalytics }} -
    - -

    {{format-number this.post.count.signups}}

    -

    {{gh-pluralize this.post.count.signups "signup" without-count=true}}

    -
    -
    - - {{#if this.post.showPaidAttributionAnalytics }} -
    - -

    {{format-number this.post.count.paid_conversions}}

    -

    Paid {{gh-pluralize this.post.count.paid_conversions "conversion" without-count=true}}

    -
    -
    - {{/if}} - {{/if}} -
    - - {{#if this.isLoaded }} - {{#if this.showLinks }} - {{#if (is-empty this.links) }} - {{!-- Empty state --}} - {{else}} - - {{/if}} - {{/if}} - - {{#if this.showSources }} - {{#if (is-empty this.sources) }} - {{!-- Empty state --}} - {{else}} -

    - Growth from this post -

    -
    -
    -
    - -
    -
    -
    - {{/if}} - {{/if}} - -

    - Get started with analytics -

    -
    - -
    -
    -
    -

    Understanding analytics in Ghost

    -

    Find out how to review the performance of your content and get the most out of post analytics in Ghost.

    -
    - -
    -
    - -
    -
    -
    -

    How to get your content seen online

    -

    Use these content distribution tactics to get more people to discover your work and increase engagement.

    -
    - -
    -
    -
    - {{else}} -
    -
    -
    -
    -
    - {{/if}} -
    diff --git a/ghost/admin/app/components/posts/old-analytics.js b/ghost/admin/app/components/posts/old-analytics.js deleted file mode 100644 index a57bfb37062..00000000000 --- a/ghost/admin/app/components/posts/old-analytics.js +++ /dev/null @@ -1,273 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {didCancel, task} from 'ember-concurrency'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -/** - * @typedef {import('../../services/dashboard-stats').SourceAttributionCount} SourceAttributionCount -*/ - -const DISPLAY_OPTIONS = [{ - name: 'Free signups', - value: 'signups' -}, { - name: 'Paid conversions', - value: 'paid' -}]; - -export default class Analytics extends Component { - @service ajax; - @service ghostPaths; - @service settings; - @service membersUtils; - @service utils; - @service feature; - - @tracked sources = null; - @tracked links = null; - @tracked sortColumn = 'signups'; - @tracked showSuccess; - @tracked updateLinkId; - displayOptions = DISPLAY_OPTIONS; - - get post() { - return this.args.post; - } - - get allowedDisplayOptions() { - if (!this.hasPaidConversionData) { - return this.displayOptions.filter(d => d.value === 'signups'); - } - - if (!this.hasFreeSignups) { - return this.displayOptions.filter(d => d.value === 'paid'); - } - - return this.displayOptions; - } - - get isDropdownDisabled() { - if (!this.hasPaidConversionData || !this.hasFreeSignups) { - return true; - } - - return false; - } - - get selectedDisplayOption() { - if (!this.hasPaidConversionData) { - return this.displayOptions.find(d => d.value === 'signups'); - } - - if (!this.hasFreeSignups) { - return this.displayOptions.find(d => d.value === 'paid'); - } - - return this.displayOptions.find(d => d.value === this.sortColumn) ?? this.displayOptions[0]; - } - - get selectedSortColumn() { - if (!this.hasPaidConversionData) { - return 'signups'; - } - - if (!this.hasFreeSignups) { - return 'paid'; - } - return this.sortColumn; - } - - get hasPaidConversionData() { - return this.sources.some(sourceData => sourceData.paidConversions > 0); - } - - get hasFreeSignups() { - return this.sources.some(sourceData => sourceData.signups > 0); - } - - @action - onDisplayChange(selected) { - this.sortColumn = selected.value; - } - - @action - setSortColumn(column) { - this.sortColumn = column; - } - - @action - updateLink(linkId, linkTo) { - if (this._updateLinks.isRunning) { - return this._updateLinks.last; - } - return this._updateLinks.perform(linkId, linkTo); - } - - @action - loadData() { - if (this.showSources) { - this.fetchReferrersStats(); - } else { - this.sources = []; - } - - if (this.showLinks) { - this.fetchLinks(); - } else { - this.links = []; - } - } - - updateLinkData(linksData) { - let updatedLinks; - if (this.links?.length) { - updatedLinks = this.links.map((link) => { - let linkData = linksData.find(l => l.link.link_id === link.link.link_id); - if (linkData) { - return { - ...linkData, - link: { - ...linkData.link, - originalTo: linkData.link.to, - to: this.utils.cleanTrackedUrl(linkData.link.to, false), - title: this.utils.cleanTrackedUrl(linkData.link.to, true) - } - }; - } - return link; - }); - } else { - updatedLinks = linksData.map((link) => { - return { - ...link, - link: { - ...link.link, - originalTo: link.link.to, - to: this.utils.cleanTrackedUrl(link.link.to, false), - title: this.utils.cleanTrackedUrl(link.link.to, true) - } - }; - }); - } - - // Remove duplicates by title ad merge - const linksByTitle = updatedLinks.reduce((acc, link) => { - if (!acc[link.link.title]) { - acc[link.link.title] = link; - } else { - acc[link.link.title].clicks += link.clicks; - } - return acc; - }, {}); - - this.links = Object.values(linksByTitle); - } - - async fetchReferrersStats() { - try { - if (this._fetchReferrersStats.isRunning) { - return this._fetchReferrersStats.last; - } - return this._fetchReferrersStats.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } - } - - async fetchLinks() { - try { - if (this._fetchLinks.isRunning) { - return this._fetchLinks.last; - } - - return this._fetchLinks.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } - } - - @task - *_updateLinks(linkId, newLink) { - this.updateLinkId = linkId; - let currentLink; - this.links = this.links?.map((link) => { - if (link.link.link_id === linkId) { - currentLink = new URL(link.link.originalTo); - return { - ...link, - link: { - ...link.link, - to: this.utils.cleanTrackedUrl(newLink, false), - title: this.utils.cleanTrackedUrl(newLink, true) - } - }; - } - return link; - }); - - const filter = `post_id:'${this.post.id}'+to:'${currentLink}'`; - let bulkUpdateUrl = this.ghostPaths.url.api(`links/bulk`) + `?filter=${encodeURIComponent(filter)}`; - yield this.ajax.put(bulkUpdateUrl, { - data: { - bulk: { - action: 'updateLink', - meta: {link: {to: newLink}} - } - } - }); - - // Refresh links data - const linksFilter = `post_id:'${this.post.id}'`; - let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(linksFilter)}`; - let result = yield this.ajax.request(statsUrl); - this.updateLinkData(result.links); - this.showSuccess = this.updateLinkId; - setTimeout(() => { - this.showSuccess = null; - }, 2000); - } - - @task({drop: true}) - *_fetchReferrersStats() { - let statsUrl = this.ghostPaths.url.api(`stats/referrers/posts/${this.post.id}`); - let result = yield this.ajax.request(statsUrl); - this.sources = result.stats.map((stat) => { - return { - source: stat.source ?? 'Direct', - signups: stat.signups, - paidConversions: stat.paid_conversions - }; - }); - } - - @task({drop: true}) - *_fetchLinks() { - const filter = `post_id:'${this.post.id}'`; - let statsUrl = this.ghostPaths.url.api(`links/`) + `?filter=${encodeURIComponent(filter)}`; - let result = yield this.ajax.request(statsUrl); - this.updateLinkData(result.links); - } - - get showLinks() { - return this.post.showEmailClickAnalytics; - } - - get showSources() { - return this.post.showAttributionAnalytics; - } - - get isLoaded() { - return this.links !== null && this.souces !== null; - } -} diff --git a/ghost/admin/app/components/posts/post-activity-feed.hbs b/ghost/admin/app/components/posts/post-activity-feed.hbs deleted file mode 100644 index a31ff95eef3..00000000000 --- a/ghost/admin/app/components/posts/post-activity-feed.hbs +++ /dev/null @@ -1,244 +0,0 @@ -
    - {{#let (activity-feed-fetcher filter=(members-event-filter post=@post.id includeEvents=this.getEventTypes) pageSize=this.pageSize) as |eventsFetcher|}} - {{compute (fn this.saveEvents eventsFetcher)}} - - {{#if eventsFetcher.isError}} -
    -
    -

    There was an error loading events

    - {{#if eventsFetcher.errorMessage}} - {{eventsFetcher.errorMessage}} - {{/if}} -
    -
    - {{/if}} - - {{#if eventsFetcher.isLoading}} - {{#if this.savedEventsFetcher.data}} -
    - {{#each this.savedEventsFetcher.data as |event|}} - {{#let (parse-member-event event) as |parsedEvent|}} -
    -
    - {{#if parsedEvent.member}} - - {{if parsedEvent.subject parsedEvent.subject "Deleted member"}} - {{else}} - {{#if parsedEvent.email}} - - {{parsedEvent.subject}} - {{else}} - - {{parsedEvent.subject}} - {{/if}} - {{/if}} -
    -
    - {{svg-jar parsedEvent.icon }} - - - {{capitalize-first-letter parsedEvent.action}} - {{#if parsedEvent.info}} -  ({{parsedEvent.info}}) - {{/if}} - - -
    - {{#if (eq this.eventType "conversion")}} -
    - {{#if parsedEvent.source}} - {{svg-jar "event-extras-source"}}{{parsedEvent.source.name}} - {{else}} - - {{/if}} -
    - {{/if}} -
    - {{moment-from-now parsedEvent.timestamp}} -
    -
    - {{/let}} - {{/each}} - - {{#if (compute (fn this.areStubsNeeded eventsFetcher))}} - {{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}} - {{#each stubs}} -
    - {{/each}} - {{/let}} - {{/if}} - -
    - - -
    - {{#if (compute (fn this.isPaginationNotNeeded this.savedEventsFetcher))}} - Showing {{this.savedEventsFetcher.totalEvents}} in total - {{else}} - Showing {{this.savedEventsFetcher.previousEvents}}-{{this.savedEventsFetcher.shownEvents}} of {{this.savedEventsFetcher.totalEvents}} - -
    - - - -
    - {{/if}} -
    -
    -
    - {{else}} -
    -
    -
    -
    -
    -
    -
    - {{/if}} - {{else}} - {{#if eventsFetcher.data}} -
    - {{#each eventsFetcher.data as |event|}} - {{#let (parse-member-event event) as |parsedEvent|}} -
    -
    - {{#if parsedEvent.member}} - - {{if parsedEvent.subject parsedEvent.subject "Deleted member"}} - {{else}} - {{#if parsedEvent.email}} - - {{parsedEvent.subject}} - {{else}} - - {{parsedEvent.subject}} - {{/if}} - {{/if}} -
    -
    - {{svg-jar parsedEvent.icon }} - - - {{capitalize-first-letter parsedEvent.action}} - {{#if parsedEvent.info}} -  ({{parsedEvent.info}}) - {{/if}} - - -
    - {{#if (eq this.eventType "conversion")}} -
    - {{#if parsedEvent.source}} - {{svg-jar "event-extras-source"}}{{parsedEvent.source.name}} - {{else}} - - {{/if}} -
    - {{/if}} -
    - {{moment-from-now parsedEvent.timestamp}} -
    -
    - {{/let}} - {{/each}} - - {{#if (compute (fn this.areStubsNeeded eventsFetcher))}} - {{#let (compute (fn this.getAmountOfStubs eventsFetcher)) as |stubs|}} - {{#each stubs}} -
    - {{/each}} - {{/let}} - {{/if}} - -
    - - -
    - {{#if (compute (fn this.isPaginationNotNeeded eventsFetcher))}} - Showing {{eventsFetcher.totalEvents}} in total - {{else}} - Showing {{eventsFetcher.previousEvents}}-{{eventsFetcher.shownEvents}} of {{eventsFetcher.totalEvents}} - -
    - - - -
    - {{/if}} -
    -
    -
    - - {{#if (eq @eventType 'feedback')}} - - {{/if}} - {{else}} -
    -
    -
    - {{#if (eq this.eventType "sent")}} - {{svg-jar "empty-sent"}} -

    No members have received your email yet

    -

    Once someone receives your email, you'll be able to see the member activity here.

    - {{else if (eq this.eventType "opened")}} - {{svg-jar "empty-opened"}} -

    No members have opened your newsletter

    -

    Once someone opens, you'll see them listed here.

    - {{else if (eq this.eventType "clicked")}} - {{svg-jar "empty-clicked"}} -

    No links have been clicked in your newsletter

    -

    Once a member clicks a link, you'll see them listed here.

    - {{else if (eq this.eventType "feedback")}} - {{svg-jar "empty-feedback"}} -

    No members have given feedback yet

    -

    When someone does, you'll see their response here.

    - {{else if (eq this.eventType "conversion")}} - {{svg-jar "empty-conversion"}} -

    No members have signed up on this post

    -

    When someone new signs up, you'll see them here.

    - {{/if}} -
    -
    -
    - {{/if}} - {{/if}} - {{/let}} -
    diff --git a/ghost/admin/app/components/posts/post-activity-feed.js b/ghost/admin/app/components/posts/post-activity-feed.js deleted file mode 100644 index 75814d88e6b..00000000000 --- a/ghost/admin/app/components/posts/post-activity-feed.js +++ /dev/null @@ -1,67 +0,0 @@ -import Component from '@glimmer/component'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class PostActivityFeed extends Component { - @service feature; - - _pageSize = 5; - _eventTypes = { - sent: ['email_sent_event', 'email_delivered_event', 'email_failed_event'], - opened: ['email_opened_event'], - clicked: ['aggregated_click_event'], - feedback: ['feedback_event'], - conversion: ['subscription_event', 'signup_event'] - }; - - @tracked savedEventsFetcher = null; - - @action - saveEvents(fetcher) { - if (fetcher && fetcher.data && fetcher.data.length > 0) { - this.savedEventsFetcher = fetcher; - } - return null; - } - - get getEventTypes() { - return this._eventTypes[this.args.eventType]; - } - - get pageSize() { - return this._pageSize; - } - - get eventType() { - return this.args.eventType; - } - - // calculate amount of empty rows which require to keep table height the same for each tab/page - @action - getAmountOfStubs({data}) { - const stubs = this._pageSize - data.length; - - return new Array(stubs).fill(1); - } - - @action - isPreviousButtonDisabled({hasReachedStart, isLoading}) { - return hasReachedStart || isLoading; - } - - @action - isNextButtonDisabled({hasReachedEnd, isLoading}) { - return hasReachedEnd || isLoading; - } - - @action - isPaginationNotNeeded({totalEvents}) { - return (totalEvents <= this._pageSize); - } - - @action - areStubsNeeded({totalEvents}) { - return totalEvents > this._pageSize || this.args.eventType === 'feedback'; - } -} diff --git a/ghost/admin/app/components/posts/post-activity-feed/footer-links.hbs b/ghost/admin/app/components/posts/post-activity-feed/footer-links.hbs deleted file mode 100644 index 396bf0e9c53..00000000000 --- a/ghost/admin/app/components/posts/post-activity-feed/footer-links.hbs +++ /dev/null @@ -1,44 +0,0 @@ -{{#if (eq @eventType "sent")}} - - - Sent - - -{{else if (eq @eventType "opened")}} - - - Opened - - -{{else if (eq @eventType "clicked")}} - - - Clicked - - -{{else if (eq @eventType "feedback")}} - - {{#each this.feedbackLinks as |link|}} - - {{link.label}} - - {{link.separator}} - {{/each}} - -{{/if}} diff --git a/ghost/admin/app/components/posts/post-activity-feed/footer-links.js b/ghost/admin/app/components/posts/post-activity-feed/footer-links.js deleted file mode 100644 index c0ed565b403..00000000000 --- a/ghost/admin/app/components/posts/post-activity-feed/footer-links.js +++ /dev/null @@ -1,48 +0,0 @@ -import Component from '@glimmer/component'; - -export default class FooterLinks extends Component { - get feedbackLinks() { - const post = this.args.post; - const positiveLink = {filterParam: '(feedback.post_id:\'' + post.id + '\'+feedback.score:1)', label: 'More like this'}; - const negativeLink = {filterParam: '(feedback.post_id:\'' + post.id + '\'+feedback.score:0)', label: 'Less like this'}; - - const data = [ - {link: positiveLink, hidden: !post.count.positive_feedback}, - {link: negativeLink, hidden: !post.count.negative_feedback} - ]; - - const links = this.collectLinks(data); - return this.addSeparator(links, 'and'); - } - - collectLinks(list) { - const data = []; - list.forEach((item) => { - if (item.hidden) { - return; - } - - data.push(item.link); - }); - - return data; - } - - addSeparator(links, separator) { - const data = []; - links.forEach((item, index) => { - const link = {...item}; - const isLastItem = links.length - 1 === index; - - if (isLastItem) { - data.push(link); - return; - } - - link.separator = separator; - data.push(link); - }); - - return data; - } -} diff --git a/ghost/admin/app/components/posts/post-activity-feed/link.hbs b/ghost/admin/app/components/posts/post-activity-feed/link.hbs deleted file mode 100644 index 8e087156d8c..00000000000 --- a/ghost/admin/app/components/posts/post-activity-feed/link.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
    - {{svg-jar "filter"}} - View members for - {{yield}} -
    \ No newline at end of file diff --git a/ghost/admin/app/controllers/application.js b/ghost/admin/app/controllers/application.js index 8bf13f8977b..820b40f2a47 100644 --- a/ghost/admin/app/controllers/application.js +++ b/ghost/admin/app/controllers/application.js @@ -65,28 +65,6 @@ export default class ApplicationController extends Controller { return this.config.clientExtensions?.script; } - get showNavMenu() { - if (this.feature.inAdminForward) { - return false; - } - - let {router, session, ui} = this; - - // if we're in fullscreen mode don't show the nav menu - if (ui.isFullScreen) { - return false; - } - - // we need to defer showing the navigation menu until the session.user - // is populated so that gh-user-can-admin has the correct data - if (!session.isAuthenticated || !session.user) { - return false; - } - - return (router.currentRouteName !== 'error404' || session.isAuthenticated) - && !router.currentRouteName.match(/(signin|signup|setup|reset)/); - } - @action async openUpdateTab() { if (!this.showUpdateBanner) { diff --git a/ghost/admin/app/controllers/posts/analytics.js b/ghost/admin/app/controllers/posts/analytics.js deleted file mode 100644 index d6f5e93de12..00000000000 --- a/ghost/admin/app/controllers/posts/analytics.js +++ /dev/null @@ -1,7 +0,0 @@ -import Controller from '@ember/controller'; - -export default class AnalyticsController extends Controller { - get post() { - return this.model; - } -} diff --git a/ghost/admin/app/helpers/gh-user-can-manage-members.js b/ghost/admin/app/helpers/gh-user-can-manage-members.js deleted file mode 100644 index e744e2d8d8f..00000000000 --- a/ghost/admin/app/helpers/gh-user-can-manage-members.js +++ /dev/null @@ -1,12 +0,0 @@ -import {helper} from '@ember/component/helper'; - -// e.g - {{#if (gh-user-can-manage-members session.user)}} 'block content' {{/if}} -// @param session.user - -export function ghUserCanManageMembers(params) { - return !!(params[0].get('canManageMembers')); -} - -export default helper(function (params) { - return ghUserCanManageMembers(params); -}); \ No newline at end of file diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index f98b0077a9a..6fa731f5018 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -187,7 +187,7 @@ export default Model.extend(Comparable, ValidationEngine, { }), showAudienceFeedback: computed('sentiment', function () { - return this.feature.get('audienceFeedback') && this.sentiment !== undefined; + return this.sentiment !== undefined; }), showEmailOpenAnalytics: computed('hasBeenEmailed', 'isSent', 'isPublished', function () { diff --git a/ghost/admin/app/routes/posts/analytics.js b/ghost/admin/app/routes/posts/analytics.js deleted file mode 100644 index 4c4fc695f91..00000000000 --- a/ghost/admin/app/routes/posts/analytics.js +++ /dev/null @@ -1,57 +0,0 @@ -import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; -import {inject} from 'ghost-admin/decorators/inject'; -import {pluralize} from 'ember-inflector'; -import {inject as service} from '@ember/service'; -export default class Analytics extends AuthenticatedRoute { - @inject config; - @service feature; - - model(params) { - let {post_id: id} = params; - - let query = { - id, - include: [ - 'tags', - 'authors', - 'authors.roles', - 'email', - 'tiers', - 'newsletter', - 'count.conversions', - 'count.clicks', - 'sentiment', - 'count.positive_feedback', - 'count.negative_feedback' - ].join(',') - }; - - return this.store.query('post', query) - .then(records => records.get('firstObject')); - } - - // the API will return a post even if the logged in user doesn't have - // permission to edit it (all posts are public) so we need to do our - // own permissions check and redirect if necessary - afterModel(post) { - super.afterModel(...arguments); - - const user = this.session.user; - const returnRoute = pluralize(post.constructor.modelName); - - if (user.isAuthorOrContributor && !post.isAuthoredByUser(user)) { - return this.replaceWith(returnRoute); - } - - // If the post is not a draft and user is contributor, redirect to index - if (user.isContributor && !post.isDraft) { - return this.replaceWith(returnRoute); - } - } - - serialize(model) { - return { - post_id: model.id - }; - } -} diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js index b77aa90c4c0..6dc9f766457 100644 --- a/ghost/admin/app/services/feature.js +++ b/ghost/admin/app/services/feature.js @@ -60,7 +60,6 @@ export default class FeatureService extends Service { @feature('referralInviteDismissed', {user: true}) referralInviteDismissed; // labs flags - @feature('audienceFeedback') audienceFeedback; @feature('welcomeEmails') welcomeEmails; @feature('webmentions') webmentions; @feature('stripeAutomaticTax') stripeAutomaticTax; diff --git a/ghost/admin/app/services/navigation.js b/ghost/admin/app/services/navigation.js deleted file mode 100644 index cb0019f9a0d..00000000000 --- a/ghost/admin/app/services/navigation.js +++ /dev/null @@ -1,68 +0,0 @@ -import Service, {inject as service} from '@ember/service'; -import {action} from '@ember/object'; -import {observes} from '@ember-decorators/object'; -import {tracked} from '@glimmer/tracking'; - -const DEFAULT_SETTINGS = { - expanded: { - posts: true - }, - menu: { - visible: true - } -}; - -export default class NavigationService extends Service { - @service session; - - @tracked settings; - - constructor() { - super(...arguments); - this.settings = Object.assign({}, DEFAULT_SETTINGS); - this.updateSettings(); - } - - // eslint-disable-next-line ghost/ember/no-observers - @observes('session.{isAuthenticated,user}', 'session.user.accessibility') - async updateSettings() { - // avoid fetching user before authenticated otherwise the 403 can fire - // during authentication and cause errors during setup/signin - if (!this.session.isAuthenticated || !this.session.user) { - return; - } - - let userSettings = JSON.parse(this.session.user.accessibility || '{}') || {}; - this.settings = {...DEFAULT_SETTINGS, ...userSettings.navigation}; - } - - @action - async toggleExpansion(key) { - if (!this.settings.expanded) { - this.settings.expanded = {}; - } - - this.settings.expanded[key] = !this.settings.expanded[key]; - - return await this._saveNavigationSettings(); - } - - @action - async toggleMenu() { - if (!this.settings.menu) { - this.settings.menu = {}; - } - - this.settings.menu.visible = !this.settings.menu.visible; - - return await this._saveNavigationSettings(); - } - - async _saveNavigationSettings() { - let user = this.session.user; - let userSettings = JSON.parse(user.get('accessibility')) || {}; - userSettings.navigation = this.settings; - user.set('accessibility', JSON.stringify(userSettings)); - return user.save(); - } -} diff --git a/ghost/admin/app/services/notifications-count.js b/ghost/admin/app/services/notifications-count.js deleted file mode 100644 index e351fa892bf..00000000000 --- a/ghost/admin/app/services/notifications-count.js +++ /dev/null @@ -1,51 +0,0 @@ -import Service from '@ember/service'; -import {inject as service} from '@ember/service'; -import {tracked} from '@glimmer/tracking'; - -export default class NotificationsCountService extends Service { - @service ajax; - @service session; - @tracked count = 0; - @tracked isLoading = false; - - async fetchCount() { - try { - this.count = 0; - this.isLoading = true; - - const identityResponse = await this.ajax.request('/ghost/api/admin/identities/'); - const token = identityResponse?.identities?.[0]?.token; - - if (!token) { - this.count = 0; - return 0; - } - - const siteInfoResponse = await this.ajax.request('/ghost/api/admin/site/'); - const siteUrl = siteInfoResponse?.site?.url; - - if (!siteUrl) { - this.count = 0; - return 0; - } - const notificationCountUrl = new URL('/.ghost/activitypub/stable/notifications/unread/count', siteUrl).toString(); - const response = await this.ajax.request(notificationCountUrl, { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/activity+json' - } - }); - this.count = response.count || 0; - return this.count; - } catch (error) { - this.count = 0; - return 0; - } finally { - this.isLoading = false; - } - } - - updateCount(newCount) { - this.count = newCount; - } -} diff --git a/ghost/admin/app/services/settings.js b/ghost/admin/app/services/settings.js index f7102da6853..6cd36a055f7 100644 --- a/ghost/admin/app/services/settings.js +++ b/ghost/admin/app/services/settings.js @@ -15,10 +15,6 @@ export default class SettingsService extends Service.extend(ValidationEngine) { validationType = 'setting'; _loadingPromise = null; - // this is an odd case where we only want to react to changes that we get - // back from the API rather than local updates - @tracked settledIcon = ''; - init() { super.init(...arguments); @@ -77,7 +73,6 @@ export default class SettingsService extends Service.extend(ValidationEngine) { const settingsModel = await this._loadSettings(); this.settingsModel = settingsModel; - this.settledIcon = settingsModel.icon; return this; } @@ -92,8 +87,6 @@ export default class SettingsService extends Service.extend(ValidationEngine) { await settingsModel.save(); await this.validate(); - this.settledIcon = settingsModel.icon; - return this; } diff --git a/ghost/admin/app/styles/app-dark.css b/ghost/admin/app/styles/app-dark.css index 607c6b1e033..d3f1763fa2d 100644 --- a/ghost/admin/app/styles/app-dark.css +++ b/ghost/admin/app/styles/app-dark.css @@ -80,7 +80,6 @@ --main-bg-color: #15171A; --dark-main-bg-color: #15171A; - --nav-bg-color: #101114; --hairline-color-1: #22252A; --hairline-color-2: #5E6874; @@ -513,7 +512,6 @@ input:focus, } .gh-about-logo svg, -.gh-nav-logo-default, .gh-unsplash-logo { filter: invert(100%) brightness(150%); } @@ -616,57 +614,6 @@ input:focus, box-shadow: none; } -.gh-nav { - background: var(--nav-bg-color); -} - -.gh-nav-hidden { - background: var(--main-bg-color); -} - -.gh-nav-hidden:hover { - box-shadow: - 100px 0px 80px 0px rgba(0, 0, 0, 0.06), - 41.78px 0px 33.422px 0px rgba(0, 0, 0, 0.05), - 22.34px 0px 17.869px 0px rgba(0, 0, 0, 0.05), - 12.52px 0px 10.017px 0px rgba(0, 0, 0, 0.05), - 6.65px 0px 5.32px 0px rgba(0, 0, 0, 0.05), - 2.77px 0px 2.214px 0px rgba(0, 0, 0, 0.05); -} - -.gh-nav-list a { - color: color-mod(var(--middarkgrey-d1)); -} - -.gh-nav-list a:hover { - background: var(--whitegrey-l2); -} - -.gh-nav-list .active { - background: var(--whitegrey-l2); -} - -.gh-nav-list a:not(.active):not(.gh-secondary-action):hover, -.gh-nav-list .gh-secondary-action:hover span, -.gh-nav-bottom .ember-basic-dropdown-trigger:hover, -.gh-nav-btn-search:hover, -.gh-nav-list button.main-menu-item:hover, -.gh-nav-bottom .ember-basic-dropdown-trigger:hover, -.gh-nav-bottom-tabicon:hover, -.gh-nav-bottom-tabicon.active, -.gh-nav-list .gh-secondary-action:not(.icon-only):hover span { - background: var(--whitegrey); -} - -.gh-nav-list a:not(.active):not(.gh-secondary-action):hover, -.gh-nav-menu-dropdown .dropdown-menu>li>a:hover, .gh-nav-menu-dropdown .dropdown-menu>li>button:hover { - background: var(--whitegrey-l2); -} - -.gh-nav-btn-search:hover { - background: var(--lightgrey); -} - .gh-contentfilter-menu-trigger, .gh-contentfilter-menu-trigger--active, .gh-contentfilter-menu-trigger:focus, @@ -707,21 +654,6 @@ input:focus, background-color: transparent; } -.nightshift-toggle { - background: var(--grey-800); -} - -.nightshift-toggle .thumb { - background: var(--black); -} - -.nightshift-toggle .moon svg { - color: var(--black); -} - -.gh-nav-list .gh-nav-nightshift span svg path { - fill: color-mod(var(--midgrey) l(-5%)); -} .gh-tag-setting-codeinjection .CodeMirror { background: var(--dark-main-bg-color); @@ -1200,7 +1132,7 @@ kbd { background: #1c1e21; } -.gh-dashboard-recents .gh-dashboard-list-item:hover, .gh-dashboard-attribution .gh-dashboard-list-item:hover, .gh-links-list-item:hover { +.gh-dashboard-recents .gh-dashboard-list-item:hover, .gh-dashboard-attribution .gh-dashboard-list-item:hover { background: linear-gradient(90deg, rgba(21,23,25,0) 0%, rgba(21,23,25,1) 100%); } @@ -1254,7 +1186,7 @@ kbd { } .explore-permissions { - background: var(--mainmenu-color-hover-bg); + background: var(--whitegrey-l1); } /* Settings Links */ @@ -1280,14 +1212,6 @@ kbd { /* Post Analytics */ -.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 - { - background: var(--white); - border-color: #26282b; -} - .gh-tabs-analytics { background: var(--white); border-color: #26282b; @@ -1309,35 +1233,10 @@ kbd { box-shadow: none; } -.gh-post-activity-feed-dummy { - opacity: 0.2; -} - -.gh-post-activity-feed .gh-dashboard-list-item + .gh-dashboard-list-item, -.feature-audienceFeedback .gh-links-list-item, -.gh-post-activity-feed-footer, .gh-tabs-analytics.no-tabs .tab-selected { border-color: #17191c; } -.gh-post-activity-feed .gh-dashboard-list-item:hover { - background: transparent; -} - -.gh-post-analytics-meta .gh-post-list-cta { - border: none!important; - box-shadow: none!important; -} - -.gh-post-analytics-meta .gh-post-list-cta:hover, .gh-post-analytics-meta .refresh:hover { - background: var(--lightgrey-d1); - color: var(--black); -} - -.gh-post-analytics-meta .gh-btn.refresh { - border: none; -} - /* Post rows */ diff --git a/ghost/admin/app/styles/layouts/content.css b/ghost/admin/app/styles/layouts/content.css index 4367bd6c80f..f8b9ee72e5e 100644 --- a/ghost/admin/app/styles/layouts/content.css +++ b/ghost/admin/app/styles/layouts/content.css @@ -928,234 +928,6 @@ justify-content: flex-start; } -.gh-post-analytics-box { - flex: 1; - position: relative; - display: flex; - margin: 0 0 2.4rem; - padding: 2.8rem 2.4rem; - background: var(--main-color-content-greybg); - 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 { - flex: 1; - border: 1px solid #ebeef0; - padding: 28px 24px 24px; - border-radius: 6px; - display: flex; - flex-direction: column; - position: relative; - align-items: stretch; - background: #ffffff; -} - -.feature-audienceFeedback .gh-post-analytics-box .gh-links-list-header .gh-links-pagination-progress { - visibility: hidden; -} - -.feature-audienceFeedback .gh-post-analytics-box .gh-links-list { - margin: -20px 0 0; - padding: 0; - background: transparent; - box-shadow: none; - border-radius: 0; - display: flex; - flex-direction: column; - align-items: stretch; -} - -.feature-audienceFeedback .gh-post-analytics-box .gh-links-pagination { - width: 100%; - margin-left: 0; - margin-right: 0; - padding: 1.6rem 0; - border-color: var(--whitegrey); - background: transparent; -} - -.gh-post-analytics-box.column { - display: flex; - flex-direction: column; -} - -.gh-post-analytics-box.resources { - display: flex; - flex-direction: row; - gap: 24px; -} - -.gh-newsletter-clicks-header, .gh-mentions-header { - align-items: center; - font-size: 1.55rem; - font-weight: 700; - line-height: 1em; - margin: 0 0 8px; - padding: 0; - color: var(--black); - white-space: nowrap; - letter-spacing: -.3px; -} - -.gh-post-analytics-resource { - padding: 2.4rem; - background: var(--white); - border-radius: var(--border-radius); - box-shadow: 0 1px 4px -1px rgba(0,0,0,.1); - flex: 1; - align-items: flex-start; - display: grid; - grid-template-columns: 2fr 3fr; - grid-gap: 24px; - min-width: 0; -} - -.gh-post-analytics-resource:hover { - box-shadow: 0 1px 5px -1px rgba(0,0,0,.2); -} - -.gh-post-analytics-resource .thumbnail { - border-radius: var(--border-radius); - width: 100%; - height: auto; - background: transparent; - background-repeat: no-repeat; - background-position: center; - background-size: cover; - aspect-ratio: 1; - filter: brightness(1.08); -} - -.gh-post-analytics-resource h3 { - font-size: 1.8rem; - font-weight: 700; - text-wrap: pretty; -} - -.gh-post-analytics-box h4.gh-main-section-header.small { - font-weight: 600; - text-transform: uppercase; - letter-spacing: .3px; - color: var(--midgrey); - padding: 4px 0 10px; -} - -.gh-post-analytics-resource p { - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.55; - color: var(--middarkgrey); - margin: 0 0 1rem; -} - -.gh-post-analytics-resource .gh-btn-link { - color: var(--green); -} - -.gh-post-analytics-resource:hover .gh-btn-link { - color: var(--green-d1); -} - -.feature-audienceFeedback .gh-post-analytics-resource .gh-btn-link { - font-size: 1.3rem; - font-weight: 400; -} - -.gh-post-analytics-title { - margin: 0; - padding: 0 0 2px; - color: var(--black); - font-size: 1.6rem; - font-weight: 600; -} - -.gh-post-analytics-item { - flex: 1; - border-left: 1px solid var(--whitegrey-d1); - padding-left: 2rem; - padding-right: 2rem; - white-space: nowrap; -} - -.gh-post-analytics-item:first-child { - border-left: none; - padding-left: 0; -} - -.gh-post-analytics-item h3 { - margin: 0 0 4px; - color: var(--black); - font-size: 2.4rem; - font-weight: 700; - letter-spacing: -.4px; - line-height: 1em; - white-space: nowrap; -} - -.gh-post-analytics-item h3 sup { - top: -0.2em; - font-size: 1.7rem; - margin: 0 0 0 2px; -} - -.gh-post-analytics-item p { - margin: 0; - color: var(--midgrey); - font-size: 1.3rem; - font-weight: 500; - letter-spacing: 0; -} - -.gh-post-analytics-item p.strong { - font-weight: 600; -} - -.gh-post-analytics-item p:first-letter { - text-transform: uppercase; -} - -.gh-post-analytics-item > a { - opacity: 1; - transition: opacity .1s linear; -} - -.gh-post-analytics-item > a:hover { - opacity: 0.7; -} - -.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-source-attribution .gh-dashboard-list-title-sources { - visibility: hidden; -} - -.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-source-attribution .gh-dashboard-list-body { - justify-content: flex-start; - padding-top: 0; - padding-bottom: 0; -} - -.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-source-attribution .gh-dashboard-list-item { - padding-top: 12px; - padding-bottom: 11px; - border-bottom: 1px solid var(--whitegrey); -} - -.feature-audienceFeedback .gh-post-analytics-box.gh-post-analytics-source-attribution .gh-dashboard-list-item:last-child { - border-bottom: none; -} - .gh-attribution-box { margin: 0; padding: 24px; @@ -1197,453 +969,122 @@ cursor: pointer; } -.gh-links-list { - flex: 1; - margin: 0; - padding: 20px; - background: var(--white); - box-shadow: 0 1px 4px -1px rgba(0,0,0,0.1); - border-radius: var(--border-radius); +.gh-posts-list-item:nth-of-type(2) .gh-list-data { + border-top: var(--whitegrey) 1px solid; } -.gh-links-list-items { - flex: 1; - display: flex; - flex-direction: column; - justify-content: flex-start; - padding: 0; +.gh-content-entry-title { + margin: 0 0 2px; + font-size: 1.55rem; + font-weight: 600; } -.gh-links-list-item { - flex: 0; - display: grid; - grid-template-columns: auto minmax(min-content,max-content); - align-items: center; - padding: 1.2rem 0; - min-height: 56px; - border-bottom: 1px solid var(--whitegrey); - font-size: 1.4rem; - font-weight: 500; - letter-spacing: 0; - text-decoration: none; +.gh-content-entry-meta, +.gh-content-entry-status { + max-width: max-content; + font-size: 1.35rem; + color: #99a3ad; } -.gh-links-list-item:hover { - background: linear-gradient(315deg,#fafafb 60%,#fff); +.gh-content-entry-meta .gh-badge { + margin-right: 3px; } -.gh-links-list-item-edit-mode { - padding: 1rem 0; +.gh-content-entry-status .draft { + display: flex; + align-items: center; + color: var(--pink); + font-weight: 500; } -.gh-links-list-item .gh-links-list-clicks { - text-align: right; - padding-right: 12px; +.gh-content-entry-status .scheduled { + display: flex; + align-items: center; + color: var(--green); + font-weight: 500; } -.gh-links-list-item:last-child { - border-bottom: none; +.gh-content-entry-status .status-dot { + display: inline-block; + width: 6px; + height: 6px; + margin: 1px 6px 0 0; + border-radius: 999px; } -.gh-links-list > .gh-links-list-item:last-child { - border-bottom: 0 none; +.gh-content-entry-status .scheduled .status-dot { + border-color: var(--green); + background: var(--green); } -.gh-links-list-url { - display: grid; - grid-template-columns: min-content minmax(auto,min-content) min-content min-content; - align-items: center; - padding-right: 32px; - /* overflow: hidden; */ +.gh-content-entry-status .draft .status-dot { + border-color: var(--pink); + background: var(--pink); } -.gh-links-list-item-edit-mode .gh-links-list-url { - grid-template-columns: auto; +.gh-content-entry-status .error { + color: var(--red); + font-weight: 500; } -.gh-links-list-item a { - margin: 0; - padding: 0; - color: var(--darkgrey); - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - cursor: pointer; +.schedule-details { + margin-left: 3px; + color: var(--midlightgrey-d1); } -.gh-links-list-item a:hover { - color: var(--midgrey-d2); +.schedule-details.absolute { + position: absolute; + transform: translateX(-100%); + min-width: max-content; } -.gh-links-list-item-edit { - margin-right: 8px; +.gh-content-entry-author, +.gh-content-entry-date { + display: inline-block; + transition: all 1s ease; } -.gh-links-list-item-edit span { - padding: 0 10px; - height: 24px; +.gh-content-email-stats, +.gh-post-list-cta { + margin: 0 0 3px; + color: var(--midlightgrey-d2); + font-size: 1.35rem; + font-weight: 400; } -.gh-links-list-item-edit span svg { - width: 14px; +.gh-content-email-stats { + margin-top: -1px; } -.gh-links-list-item .gh-input { - animation: fade-in .2s ease-in-out; +.gh-content-email-stats-value { + display: block; + color: var(--black); + font-size: 1.55rem; + font-weight: 600; + margin: 0 0 3px; } -.gh-links-list-input-container { - width: 100%; +.gh-content-email-stats-value sup { + top: -0.225em; + font-size: 75%; + padding: 0 0 0 1px; } -.gh-links-list-item-success { - opacity: 0; +.gh-post-list-cta { display: flex; -} - -.gh-links-list-item-success.gh-links-list-item-success-show { - animation: fade-in-out 3s; -} - -.gh-links-list-item-error { - display: none; - margin: 6px 0 0; - color: var(--red); - font-size: 1.25rem; - font-weight: 400; - line-height: 1; -} - -.error .gh-links-list-item-error { - display: block; -} - -.gh-links-list-item-edited { - display: none; - white-space: nowrap; - color: var(--midlightgrey); - font-weight: 400; -} - -.gh-links-list-item-edited-show { - display: inline; -} - -.gh-links-list-item-update-button { - border: 0; -} - -.gh-links-list-item-update-button span { - pointer-events: none; -} - -@keyframes fade-in-out { - 0% { opacity: 0; } - 20% { opacity: 1 } - 95% { opacity: 1; } - 100% {opacity: 0; } - } - -.gh-links-list-item-success svg { - width: 14px; - margin-left: 8px; -} - -.gh-links-list-item-success svg path { - stroke: var(--green); -} - -.gh-links-list-clicks { - margin: 0; - color: var(--darkgrey); -} - -.gh-links-list-header { - display: grid; - grid-template-columns: minmax(auto, 85%) minmax(min-content, 15%); - padding: 0 0 8px; - border-bottom: 1px solid var(--whitegrey); -} - -.gh-links-list-title { - align-items: center; - line-height: 1em; - white-space: nowrap; - font-size: 1.1rem; - font-weight: 500; - letter-spacing: .03em; - color: #7c8b9a; - padding: 0 20px 8px 0; - text-transform: uppercase; -} - -.gh-links-list-header .gh-links-list-title:last-child { - text-align: right; - padding-right: 12px; -} - -.gh-links-info { - display: flex; - align-items: center; - color: var(--green); - font-weight: 500; - padding-left: 11px; -} - -.gh-links-info svg { - width: 14px; - margin-right: 6px; - fill: var(--green); - transform: rotate(-90deg); -} - -.gh-links-info .gh-links-info-short { - display: none; -} - -.gh-links-pagination { - display: flex; - justify-content: space-between; - align-items: center; - width: calc(100% + 40px); - background: linear-gradient(180deg, #FBFBFB 0%, #FFFFFF 50%); - padding: 20px; - margin: 0px -20px -20px -20px; - border-top: 1px solid var(--lightgrey-l1); - border-radius: 0 0 3px 3px; -} - -.gh-links-pagination-progress { - font-weight: 500; - font-size: 13px; - line-height: 16px; - letter-spacing: 0.01em; - color: var(--midlightgrey); -} - -.gh-links-pagination-actions { - display: flex; - flex-direction: row; - gap: 12px; -} - -.gh-links-pagination-action { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; - gap: 6px; - color: var(--green); - font-weight: 600; - font-size: 13px; - line-height: 16px; - letter-spacing: 0.01em; - transition: color .2s linear; -} - -.gh-links-pagination-action.gh-links-pagination-prev { - position: relative; - padding-right: 12px; -} - -.gh-links-pagination-action.gh-links-pagination-prev:after { - content: ""; - position: absolute; - top: 20%; - bottom: 20%; - right: 0; - background: var(--lightgrey); - border-radius: 2px; - width: 1.5px; - height: 60%; -} - -.gh-links-pagination-disabled { - cursor: default; - opacity: 0.25; -} - -.gh-links-pagination-action svg { - width: 6px; - fill: var(--green); - stroke: var(--green); - transition: all .2s linear; -} - -.gh-links-pagination-action:hover { - color: var(--green-d2); -} - -.gh-links-pagination-action:hover svg { - fill: var(--green-d2); - stroke: var(--green-d2); -} - -.feature-audienceFeedback .gh-links-list-item { - border-color: rgba(235,238,240,.5); -} - -.feature-audienceFeedback .gh-links-list-item.gh-links-list-item-edit-mode { - padding-top: 0.8rem; - padding-bottom: 0.8rem; -} - -.feature-audienceFeedback .gh-links-info { - font-size: 1.3rem; -} - -.feature-audienceFeedback .gh-links-info svg { - width: 13px; - margin-bottom: 2px; -} - -.feature-audienceFeedback .gh-links-list-item:hover { - background: none; -} - -@media (max-width: 1000px), (min-width: 1360px) and (max-width: 1440px) { - .gh-links-info .gh-links-info-short { - display: inline; - } - - .gh-links-info .gh-links-info-normal { - display: none; - } -} - -.gh-post-activity-feed .gh-dashboard-list-item-sub .gh-members-activity-description { - display: flex; - flex-direction: row; - align-items: center; -} - -.gh-post-activity-feed .gh-dashboard-list-item-sub .gh-members-activity-description svg { - width: 18px; - min-width: 18px; - height: 16px; - margin: 0 0.4em 0 0; -} - -.gh-post-activity-feed .gh-dashboard-list-item-sub .gh-members-activity-description svg path { - stroke: var(--midgrey); -} - -.gh-posts-list-item:nth-of-type(2) .gh-list-data { - border-top: var(--whitegrey) 1px solid; -} - -.gh-content-entry-title { - margin: 0 0 2px; - font-size: 1.55rem; - font-weight: 600; -} - -.gh-content-entry-meta, -.gh-content-entry-status { - max-width: max-content; - font-size: 1.35rem; - color: #99a3ad; -} - -.gh-content-entry-meta .gh-badge { - margin-right: 3px; -} - -.gh-content-entry-status .draft { - display: flex; - align-items: center; - color: var(--pink); - font-weight: 500; -} - -.gh-content-entry-status .scheduled { - display: flex; - align-items: center; - color: var(--green); - font-weight: 500; -} - -.gh-content-entry-status .status-dot { - display: inline-block; - width: 6px; - height: 6px; - margin: 1px 6px 0 0; - border-radius: 999px; -} - -.gh-content-entry-status .scheduled .status-dot { - border-color: var(--green); - background: var(--green); -} - -.gh-content-entry-status .draft .status-dot { - border-color: var(--pink); - background: var(--pink); -} - -.gh-content-entry-status .error { - color: var(--red); - font-weight: 500; -} - -.schedule-details { - margin-left: 3px; - color: var(--midlightgrey-d1); -} - -.schedule-details.absolute { - position: absolute; - transform: translateX(-100%); - min-width: max-content; -} - -.gh-content-entry-author, -.gh-content-entry-date { - display: inline-block; - transition: all 1s ease; -} - -.gh-content-email-stats, -.gh-post-list-cta { - margin: 0 0 3px; - color: var(--midlightgrey-d2); - font-size: 1.35rem; - font-weight: 400; -} - -.gh-content-email-stats { - margin-top: -1px; -} - -.gh-content-email-stats-value { - display: block; - color: var(--black); - font-size: 1.55rem; - font-weight: 600; - margin: 0 0 3px; -} - -.gh-content-email-stats-value sup { - top: -0.225em; - font-size: 75%; - padding: 0 0 0 1px; -} - -.gh-post-list-cta { - display: flex; - align-items: center; - justify-content: flex-start; - margin: 0; - padding: 1px 12px; - border: 1px solid var(--whitegrey-d1); - background: var(--white); - color: var(--darkgrey); - border-radius: var(--border-radius); - transition: all .2s ease; - white-space: nowrap; - height: 34px; - overflow: hidden; - transition: all .1s linear; + align-items: center; + justify-content: flex-start; + margin: 0; + padding: 1px 12px; + border: 1px solid var(--whitegrey-d1); + background: var(--white); + color: var(--darkgrey); + border-radius: var(--border-radius); + transition: all .2s ease; + white-space: nowrap; + height: 34px; + overflow: hidden; + transition: all .1s linear; } .gh-post-list-cta:hover { @@ -1655,33 +1096,12 @@ width: 52px; } -.gh-post-analytics-header .gh-post-list-cta.edit { - margin-top: -14px; - margin-right: 0; +.gh-post-list-cta.is-hovered { + border-color: var(--whitegrey-d2); } -.gh-post-analytics-header .share svg { - margin-top: -2px; - width: 1.5rem; - height: 1.5rem; - color: var(--darkgrey); - stroke: var(--darkgrey); -} - -.gh-post-analytics-header .gh-btn-action-icon { - margin-right: 0; -} - -.gh-post-analytics-header .gh-btn.refresh svg path { - stroke-width: 2.25; -} - -.gh-post-list-cta.is-hovered { - border-color: var(--whitegrey-d2); -} - -.gh-post-list-cta.stats.is-hovered:hover { - border-color: var(--lightgrey-l2); +.gh-post-list-cta.stats.is-hovered:hover { + border-color: var(--lightgrey-l2); } .gh-post-list-cta.edit.is-hovered:hover, @@ -1693,820 +1113,268 @@ width: 1.5rem; height: 1.5rem; color: var(--darkgrey); - stroke: var(--darkgrey); - transition: all .1s linear; -} - -.gh-post-list-cta > svg path { - stroke-width: 1.75; -} - -.gh-post-list-cta > span { - line-height: 36px; - font-weight: 600; - padding: 0 0 0 0.75rem; - color: var(--darkgrey-l1); - transition: all .1s linear; -} - -span.dropdown .gh-post-list-cta > span { - padding: 0; -} - -@media screen and (max-width: 1200px) { - .gh-post-analytics-box.resources { - flex-direction: column; - } -} - -@media screen and (max-width: 1000px) { - .gh-post-analytics-box { - flex-direction: column; - } - - .gh-attribution-box { - flex-direction: column; - } - - .gh-attribution-chart-column-inner { - padding: 8px; - max-width: 300px; - } - - .gh-post-analytics-item { - border-left: 0 none; - border-bottom: 1px solid var(--whitegrey-d1); - padding-left: 0; - padding-bottom: 2rem; - padding-top: 2rem; - } - - .gh-post-analytics-item:first-child { - padding-top: 0; - } - - .gh-post-analytics-item:last-child { - padding-bottom: 0; - border-bottom: 0 none; - } - - .gh-post-analytics-item h3 { - font-size: 2.8rem; - margin-bottom: 2px; - } - - .gh-post-analytics-item h3 sup { - top: -0.3em; - font-size: 1.8rem; - } -} - -.gh-tabs-analytics { - margin-bottom: 22px; - border-radius: 5px; - border: 1px solid #ECEEF0; -} - -.gh-tabs-analytics.no-tabs { - padding: 28px 24px; -} - -.gh-tabs-analytics .tab { - display: flex; - flex-direction: column; - justify-content: flex-start; - border: 1px solid transparent; - border-bottom: none; - padding: 12px 14px 22px; - text-align: left; -} - -.gh-tabs-analytics .tab-selected { - box-sizing: border-box; - background: #ffffff; - border: 1px solid #E7E9EB; - border-bottom: none; - box-shadow: 0 4px 7px rgba(0, 0, 0, 0.05), 0 1px 0 0 #ffffff; - border-radius: 5px 5px 0 0; -} - -.gh-tabs-analytics.no-tabs .tab-selected { - padding: 0 0 20px; - border: 0; - border-bottom: 1px solid #eceef0; - box-shadow: none; -} - -.gh-tabs-analytics .tab-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); - padding: 8px 8px 0px; - background: linear-gradient(360deg, #F7F8F9 -6.38%, rgba(247, 248, 249, 0) 159.57%); - box-shadow: inset 0 -1px 0 #eceef0; -} - -.gh-tabs-analytics.no-tabs .tab-list { - padding: 0; - background: transparent; -} - -.gh-tabs-analytics .tab-panel { - display: none; -} - -.gh-tabs-analytics .tab-panel-selected { - display: block; - padding: 8px 26px 0; - /* help to hide shadow from selected tab */ - opacity: 0.99999; - background-color: #ffffff; - border-radius: 0 0 4px 4px; - min-height: 121px; -} - -.gh-tabs-analytics.no-tabs .tab-panel-selected { - padding: 0; -} - -.gh-tabs-analytics .tab-list h3 { - display: flex; - flex-direction: row; - align-items: center; - gap: 4px; - padding: 0 0 0 1px; - margin: 0 0 8px; - font-weight: 600; - font-size: 2.6rem; - line-height: 1em; - letter-spacing: -0.05em; - color: var(--black); - white-space: nowrap; -} - -.gh-tabs-analytics.no-tabs .tab-list h3 { - margin-bottom: 0; - font-size: 2rem; - font-weight: 700; - letter-spacing: -0.5px; -} - -.gh-tabs-analytics .tab-list h3 svg { - display: none; - width: 20px; - height: 20px; - color: var(--black); -} - -.gh-tabs-analytics .tab-list p { - display: flex; - flex-direction: row; - gap: 6px; - align-items: center; - line-height: 1em; - white-space: nowrap; - font-size: 1.1rem; - font-weight: 600; - letter-spacing: .03em; - color: var(--midgrey); - text-transform: uppercase; - padding: 0; - margin: 0; -} - -.gh-tabs-analytics.no-tabs .tab-list p { - display: none; -} - -.gh-tabs-analytics .tab-list p svg { - width: 16px; - height: 16px; -} - -.gh-tabs-analytics .animated-number { - position: relative; -} - -.gh-tabs-analytics .animated-number .new-number { - position: absolute; - top: 0; - left: 0; -} - -.gh-tabs-analytics .animated-number .new-char, -.gh-tabs-analytics .animated-number .old-char { - display: inline-block; -} - -.gh-tabs-analytics .animated-number .new-char { - opacity: 0; - transform: translateY(10px); -} - -.gh-tabs-analytics .gh-dashboard-list-item { - grid-template-columns: 40% 40% 20%; -} - -.gh-tabs-analytics .gh-dashboard-list-cols-conversion .gh-dashboard-list-item { - grid-template-columns: 28% 28% 24% 20%; -} - -.gh-post-activity-feed .gh-members-activity-description a { - font-weight: 500; -} - -.gh-post-activity-feed .gh-members-activity-description a:hover { - color: #697989; -} - -@media (max-width: 1200px) { - .gh-tabs-analytics .tab { - padding: 8px 7px 14px; - } - - .gh-tabs-analytics .tab .analytics-tab-percentage { - display: none; - } - - .gh-tabs-analytics .tab-panel-selected { - padding: 12px 18px 0; - } - - .gh-tabs-analytics .tab-list { - grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); - } - - .gh-tabs-analytics h3 { - font-size: 1.2rem; - } - - .gh-tabs-analytics p { - font-size: 1.6rem; - } -} - -@media (max-width: 1000px) { - .gh-tabs-analytics .tab-list h3 { - margin-bottom: 0; - font-size: 1.8rem; - } - - .gh-tabs-analytics .tab-list h3 svg { - display: block; - } - - .gh-tabs-analytics .tab-list p { - display: none; - } -} - -@media (max-width: 440px) { - .gh-tabs-analytics .tab-list { - padding: 4px 4px 0px; - } - - .gh-tabs-analytics .tab-panel-selected { - padding: 12px 14px 0; - } - - .gh-tabs-analytics p { - font-size: 1.2rem; - } - - .gh-tabs-analytics strong { - font-size: 1.2rem; - } -} - -.gh-post-activity-feed { - display: grid; - grid-template-columns: 1fr auto -} - -.gh-post-activity-feed .gh-member-list-avatar { - font-size: 1.1rem; - font-weight: 600; -} - -.gh-post-activity-feed .gh-dashboard-list-item { - align-items: center; -} - -.gh-post-activity-feed .gh-post-activity-feed-dummy { - width: 40%; - height: 8px; - border-radius: 3px; - background: linear-gradient(90deg, #F2F6F7 0%, rgba(242, 246, 247, 0.842589) 62.56%, rgba(247, 250, 252, 0.75) 99.36%); -} - -.gh-post-activity-feed .gh-post-activity-feed-dummy:nth-child(2) { - width: 30%; -} - -.gh-post-activity-feed .gh-post-activity-feed-dummy:nth-child(3) { - width: 10%; -} - -.gh-post-activity-feed-empty { - width: 100%; - height: 277px; - display: flex; - justify-content: center; - align-items: center; -} - -.gh-post-activity-feed-pagination svg { - width: 7px; - height: 12px; - fill: #2BBA3C; -} - -.gh-post-activity-feed-footer { - display: flex; - min-height: 65px; - align-items: center; - justify-content: space-between; - gap: 16px; - margin-top: 2px; - border-top: 1px solid #eceef0; - padding: 18px 0; -} - -.gh-post-activity-feed-pagination { - display: flex; - align-items: center; - gap: 8px; - white-space: nowrap; - font-size: 1.3rem; - font-weight: 600; - line-height: 1.3; - color: #ABB0B6; -} - -.gh-post-activity-feed-pagination-button { - padding: 8px 8px; -} - -.gh-post-activity-feed-pagination-button:disabled { - opacity: 0.25; -} - -.gh-post-activity-feed-pagination-button:hover:not(:disabled) { - filter: brightness(0.8); -} - -.gh-post-activity-feed-pagination-link-wrapper { - display: flex; - align-items: center; - gap: 4px; - font-size: 1.3rem; - font-weight: 500; - line-height: 1.3; - color: #959595; -} - -.gh-post-activity-feed-pagination-link-wrapper svg { - width: 15px; - height: 15px; -} - -.gh-post-activity-feed-pagination-link-wrapper path { - stroke: currentColor; -} - -.gh-post-activity-feed-pagination-link { - display: flex; - align-items: center; - gap: 8px; - font-size: 1.3rem; - font-weight: 500; - line-height: 1.3; - color: var(--green); -} - -.gh-post-activity-feed-pagination-link:hover { - color: var(--green-d1); -} - -.gh-post-activity-feed-pagination-link svg { - width: 15px; - height: 15px; -} - -.gh-post-activity-feed-pagination-link path { - stroke: currentColor; -} - -.gh-post-activity-feed .gh-dashboard-list-item + .gh-dashboard-list-item { - border-top: 1px solid rgba(235, 238, 240, 0.5); -} - -.gh-post-activity-feed .gh-dashboard-list-item { - min-height: 42px; -} - -.gh-post-activity-feed .gh-dashboard-list-subtext, -.gh-post-activity-feed .gh-members-activity-description { - font-size: 1.3rem; -} - -.gh-post-activity-feed-pagination-group { - font-size: 0px; -} - -.gh-feedback-events-tooltip { - opacity: 0; - position: fixed; - left: 200px; - padding: 16px 12px; - background: #FFFFFF; - border: 1px solid #E6E6E6; - box-shadow: 0px 6px 25px rgba(0, 0, 0, 0.07); - border-radius: 6px; - font-weight: 500; - font-size: 1.3rem; - line-height: 1.2; - color: #909cab; -} - -.gh-feedback-events-tooltip-badge { - display: inline-block; - width: 8px; - height: 8px; - margin-right: 8px; - border-radius: 50%; -} - -.gh-feedback-events-tooltip-metric { - margin-left: 16px; -} - -.gh-feedback-events-tooltip-info { - margin-right: 4px; - font-weight: 700; - font-size: 2rem; - line-height: 1.2; - color: #000000; -} - -.gh-feedback-events-tooltip-footer { - margin-top: 12px; - padding-top: 12px; - white-space: nowrap; - border-top: 1px solid #EBEEF0E5; -} - -.gh-feedback-events-tooltip-body { - display: flex; - align-items: center; - margin-bottom: 4px; -} - -.gh-feedback-events-feed { - position: relative; - padding: 24px 34px 16px 56px; -} - -.gh-feedback-events-feed-container { - width: 220px; - max-width: 220px; - margin: auto; -} - -.gh-prefix { - padding: 0 0.5em 0 0; - line-height: 1em; - white-space: nowrap; - font-size: 1.1rem; - font-weight: 600; - letter-spacing: .03em; - color: var(--midgrey); - text-transform: uppercase; -} - -@media (max-width: 1150px) { - .gh-post-activity-feed { - grid-template-columns: unset; - } - - .gh-dashboard-list-item-stub { - display: none !important; - } - - .gh-feedback-events-feed { - padding: 0 0 24px; - } -} - -.gh-post-analytics-split { - display: grid; - grid-column-gap: 24px; - grid-template-columns: 1fr; -} - -@media (min-width: 1360px) { - .gh-post-analytics-split.gh-post-analytics-with-mentions { - grid-template-columns: 1fr 1fr; - } -} - -.gh-post-analytics-mentions { - flex: 1; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.gh-post-analytics-mentions.is-full-width { - display: none; -} - -.gh-post-analytics-mentions-header { - font-size: 1.55rem; - font-weight: 700; - line-height: 1em; - margin: 0 0 8px; - padding: 0; - color: var(--darkgrey); - white-space: nowrap; - letter-spacing: -.3px; -} - -.gh-post-analytics-mentions-list { - flex: 1; - display: flex; - flex-direction: column; - justify-content: flex-start; - margin-top: 6px; -} - -.gh-post-analytics-mention { - display: grid; - grid-template-columns: 20px auto; - grid-template-rows: 20px auto; - grid-column-gap: 8px; - padding: 4px 0; - flex-direction: column; - padding: 6px 0; -} - -.gh-post-analytics-mention-title { - color: var(--darkgrey); - font-size: 1.5rem; - font-weight: 600; - line-height: 1.3; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; + stroke: var(--darkgrey); + transition: all .1s linear; } -.gh-post-analytics-mention-timestamp { - color: var(--midlightgrey); - font-size: 1.3rem; - font-weight: 500; - padding-left: 28px; - white-space: nowrap; +.gh-post-list-cta > svg path { + stroke-width: 1.75; } -.gh-post-analytics-mentions .gh-dashboard-list-footer { - margin-top: auto; - padding: 17px 0 0; +.gh-post-list-cta > span { + line-height: 36px; + font-weight: 600; + padding: 0 0 0 0.75rem; + color: var(--darkgrey-l1); + transition: all .1s linear; } -.gh-post-analytics-mentions .gh-dashboard-list-footer a { - font-size: 1.3rem; +span.dropdown .gh-post-list-cta > span { + padding: 0; } -.gh-post-list-button { - flex: 0 0 auto; -} +@media screen and (max-width: 1000px) { + .gh-attribution-box { + flex-direction: column; + } -.gh-post-list-metrics-container { - display: flex; - flex: 0 0 auto; + .gh-attribution-chart-column-inner { + padding: 8px; + max-width: 300px; + } } -.gh-post-list-metrics { - width: 100px; - flex-shrink: 0; - display: grid; - grid-template-rows: auto auto; +.gh-tabs-analytics { + margin-bottom: 22px; + border-radius: 5px; + border: 1px solid #ECEEF0; } -.gh-list-analytics-icon { - width: 16px; - height: 16px; +.gh-tabs-analytics.no-tabs { + padding: 28px 24px; } -.gh-post-list-button { - flex: 0 0 auto; +.gh-tabs-analytics .tab { + display: flex; + flex-direction: column; + justify-content: flex-start; + border: 1px solid transparent; + border-bottom: none; + padding: 12px 14px 22px; + text-align: left; } -/* Traffic Analytics beta related styles */ -.feature-trafficAnalytics .gh-canvas-header { +.gh-tabs-analytics .tab-selected { + box-sizing: border-box; + background: #ffffff; + border: 1px solid #E7E9EB; border-bottom: none; + box-shadow: 0 4px 7px rgba(0, 0, 0, 0.05), 0 1px 0 0 #ffffff; + border-radius: 5px 5px 0 0; +} + +.gh-tabs-analytics.no-tabs .tab-selected { + padding: 0 0 20px; + border: 0; + border-bottom: 1px solid #eceef0; + box-shadow: none; } -.feature-trafficAnalytics .gh-post-analytics-content { +.gh-tabs-analytics .tab-list { display: grid; - grid-template-columns: auto 260px; - gap: 32px; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + padding: 8px 8px 0px; + background: linear-gradient(360deg, #F7F8F9 -6.38%, rgba(247, 248, 249, 0) 159.57%); + box-shadow: inset 0 -1px 0 #eceef0; } -.feature-trafficAnalytics .gh-post-analytics-header { - padding-bottom: 0; +.gh-tabs-analytics.no-tabs .tab-list { + padding: 0; + background: transparent; } -.feature-trafficAnalytics .gh-tabs-analytics { - margin-top: 32px; - border-radius: 12px; +.gh-tabs-analytics .tab-panel { + display: none; } -.feature-trafficAnalytics .gh-post-analytics-box { - border-radius: 12px; +.gh-tabs-analytics .tab-panel-selected { + display: block; + padding: 8px 26px 0; + /* help to hide shadow from selected tab */ + opacity: 0.99999; + background-color: #ffffff; + border-radius: 0 0 4px 4px; + min-height: 121px; } -.feature-trafficAnalytics .gh-post-analytics-sidebar { - display: flex; - flex-direction: column; - gap: 1px; - padding: 32px 0; +.gh-tabs-analytics.no-tabs .tab-panel-selected { + padding: 0; } -.feature-trafficAnalytics .gh-post-analytics-sidebar-item { +.gh-tabs-analytics .tab-list h3 { display: flex; - height: 36px; + flex-direction: row; align-items: center; - gap: 8px; - border-radius: 7px; - color: var(--middarkgrey); - padding: 8px 12px; - font-weight: 500; - cursor: pointer; -} -.feature-trafficAnalytics .gh-post-analytics-sidebar-item:hover { - color: var(--black); - background: var(--whitegrey-l1); -} - -.feature-trafficAnalytics .gh-post-analytics-sidebar-item.active { - background: var(--whitegrey-l1); - color: var(--black); + gap: 4px; + padding: 0 0 0 1px; + margin: 0 0 8px; font-weight: 600; + font-size: 2.6rem; + line-height: 1em; + letter-spacing: -0.05em; + color: var(--black); + white-space: nowrap; } -.feature-trafficAnalytics .gh-post-analytics-sidebar-item svg { - width: 18px; - height: 18px; - stroke: 1.5px; +.gh-tabs-analytics.no-tabs .tab-list h3 { + margin-bottom: 0; + font-size: 2rem; + font-weight: 700; + letter-spacing: -0.5px; } -.feature-trafficAnalytics .gh-tabs-analytics .tab-list { - background: none; - box-shadow: none; - gap: 20px; - padding: 20px 0 0; - margin: 0 24px; - border-bottom: 1px solid var(--whitegrey); +.gh-tabs-analytics .tab-list h3 svg { + display: none; + width: 20px; + height: 20px; + color: var(--black); } -.feature-trafficAnalytics .gh-tabs-analytics.no-tabs .tab-panel-selected { - margin: 0 24px; +.gh-tabs-analytics .tab-list p { + display: flex; + flex-direction: row; + gap: 6px; + align-items: center; + line-height: 1em; + white-space: nowrap; + font-size: 1.1rem; + font-weight: 600; + letter-spacing: .03em; + color: var(--midgrey); + text-transform: uppercase; + padding: 0; + margin: 0; } -.feature-trafficAnalytics .gh-tabs-analytics.no-tabs { - padding: 20px 0; +.gh-tabs-analytics.no-tabs .tab-list p { + display: none; } -.feature-trafficAnalytics .gh-tabs-analytics .tab-list .tab { - border: none; - padding: 0 0 12px; +.gh-tabs-analytics .tab-list p svg { + width: 16px; + height: 16px; } -.feature-trafficAnalytics .gh-tabs-analytics .tab { - border: none; - box-shadow: none; +.gh-tabs-analytics .animated-number { position: relative; - color: var(--middarkgrey); } -.feature-trafficAnalytics .gh-tabs-analytics .tab:hover, -.feature-trafficAnalytics .gh-tabs-analytics .tab-selected { - color: var(--black); -} - -.feature-trafficAnalytics .gh-tabs-analytics .tab:before { - display: block; - content: ""; +.gh-tabs-analytics .animated-number .new-number { position: absolute; + top: 0; left: 0; - right: 0; - bottom: -1px; - height: 2px; - background: transparent; -} - -.feature-trafficAnalytics .gh-tabs-analytics .tab:not(.tab-selected):hover:before { - background: var(--lightgrey); -} - -.feature-trafficAnalytics .gh-tabs-analytics .tab-selected:before { - background: var(--black); } -.feature-trafficAnalytics .gh-tabs-analytics .tab-list .tab h3 { - order: 2; - font-size: 23px; - line-height: 35px; - margin-top: -2px; - margin-bottom: 0; - color: inherit; +.gh-tabs-analytics .animated-number .new-char, +.gh-tabs-analytics .animated-number .old-char { + display: inline-block; } -.feature-trafficAnalytics .gh-newsletter-clicks-header { - font-weight: 600; - font-size: 14px; - letter-spacing: 0; +.gh-tabs-analytics .animated-number .new-char { + opacity: 0; + transform: translateY(10px); } -.feature-trafficAnalytics .gh-tabs-analytics .tab-list .tab p { - order: 1; - font-size: 14px; - text-transform: none; - letter-spacing: -0.01em; - font-weight: 600; - line-height: 22.5px; - color: inherit; - letter-spacing: 0; +.gh-tabs-analytics .gh-dashboard-list-item { + grid-template-columns: 40% 40% 20%; } -.feature-trafficAnalytics .analytics-tab-percentage { - opacity: 0.75; - font-weight: 500; +.gh-tabs-analytics .gh-dashboard-list-cols-conversion .gh-dashboard-list-item { + grid-template-columns: 28% 28% 24% 20%; } -.feature-trafficAnalytics .gh-tabs-analytics .tab-list .tab p svg { - display: none; -} +@media (max-width: 1200px) { + .gh-tabs-analytics .tab { + padding: 8px 7px 14px; + } -.feature-trafficAnalytics .gh-tabs-analytics .tab-panel-selected { - padding-top: 16px; -} + .gh-tabs-analytics .tab .analytics-tab-percentage { + display: none; + } -.feature-trafficAnalytics .gh-feedback-events-feed { - padding-right: 0; - padding-left: 24px; -} + .gh-tabs-analytics .tab-panel-selected { + padding: 12px 18px 0; + } -.feature-trafficAnalytics .gh-feedback-events-feed-container { - width: 200px; - height: 200px; -} + .gh-tabs-analytics .tab-list { + grid-template-columns: repeat(auto-fit, minmax(60px, 1fr)); + } -.feature-trafficAnalytics .gh-post-activity-feed-pagination-link-wrapper { - display: block; - flex-grow: 1; -} + .gh-tabs-analytics h3 { + font-size: 1.2rem; + } -.feature-trafficAnalytics .gh-post-activity-feed-pagination-link { - display: inline-flex; + .gh-tabs-analytics p { + font-size: 1.6rem; + } } -.feature-trafficAnalytics .gh-post-activity-feed-pagination-link-wrapper svg { - margin-bottom: -2px; -} +@media (max-width: 1000px) { + .gh-tabs-analytics .tab-list h3 { + margin-bottom: 0; + font-size: 1.8rem; + } -.feature-trafficAnalytics .gh-tabs-analytics .tab-panel { - background: none; -} + .gh-tabs-analytics .tab-list h3 svg { + display: block; + } -.feature-trafficAnalytics .gh-tabs-analytics.no-tabs .tab-selected:before { - display: none; + .gh-tabs-analytics .tab-list p { + display: none; + } } -@media (max-width: 1320px) { - .feature-trafficAnalytics .gh-post-analytics-content { - grid-template-columns: auto 240px; +@media (max-width: 440px) { + .gh-tabs-analytics .tab-list { + padding: 4px 4px 0px; } - .feature-trafficAnalytics .gh-tabs-analytics .tab-list .tab p { - font-size: 13px; + .gh-tabs-analytics .tab-panel-selected { + padding: 12px 14px 0; } - .feature-trafficAnalytics .gh-tabs-analytics .tab-list .tab h3 { - font-size: 21px; - line-height: 25px; + .gh-tabs-analytics p { + font-size: 1.2rem; } - .feature-trafficAnalytics .analytics-tab-percentage { - font-size: 12px; + .gh-tabs-analytics strong { + font-size: 1.2rem; } } -@media (max-width: 1200px) { - .feature-trafficAnalytics .gh-tabs-analytics .tab-list { - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 8px; - } +.gh-post-list-button { + flex: 0 0 auto; +} + +.gh-post-list-metrics-container { + display: flex; + flex: 0 0 auto; +} + +.gh-post-list-metrics { + width: 100px; + flex-shrink: 0; + display: grid; + grid-template-rows: auto auto; +} + +.gh-list-analytics-icon { + width: 16px; + height: 16px; +} + +.gh-post-list-button { + flex: 0 0 auto; } /* Post list with analytics */ diff --git a/ghost/admin/app/styles/layouts/main.css b/ghost/admin/app/styles/layouts/main.css index 2a9ee85fe34..b2c42df28c7 100644 --- a/ghost/admin/app/styles/layouts/main.css +++ b/ghost/admin/app/styles/layouts/main.css @@ -1,11 +1,3 @@ -:root { - /* Main menu variables */ - --mainmenu-color-hover-bg: var(--whitegrey-l1); - --mainmenu-color-active: var(--black); - --mainmenu-color-active-bg: var(--whitegrey); - --mainmenu-width: 320px; -} - /* Utils */ .width-25 { width: 25%; } .width-34 { width: 34%; } @@ -79,344 +71,9 @@ body:not(.gh-body-fullscreen) .gh-viewport { position: relative; /* for the editor in safari */ } -.gh-user-avatar { - position: relative; - flex-shrink: 0; - display: block; - width: 34px; - height: 34px; - margin: 0px 8px 0 0; - background-position: 50%; - background-size: cover; - border-radius: 100%; - border: 1px solid var(--whitegrey); -} - -/* Global Nav -/* ---------------------------------------------------------- */ - -.gh-nav { - position: relative; - z-index: 800; - flex: 0 0 var(--mainmenu-width); - display: flex; - flex-direction: column; - min-width: 0; - /* background: linear-gradient(315deg,var(--whitegrey-l2),var(--white)); */ - /* background: var(--whitegrey-l2); */ - transform: translateX(0); - transition: flex-basis ease-in-out 250ms, opacity ease-in-out 250ms, transform ease-in-out 250ms, box-shadow ease-in-out 250ms; -} - -.gh-nav-hidden { - position: absolute; - top: 0; - left: 0; - transform: translateX(-320px); - width: 320px; - background-color: var(--white); - height: 100%; - z-index: 9999; -} - -.gh-nav-hidden:hover { - transform: translateX(0px); - box-shadow: 100px 0px 80px 0px rgba(0, 0, 0, 0.02), 41.78px 0px 33.422px 0px rgba(0, 0, 0, 0.01), 22.34px 0px 17.869px 0px rgba(0, 0, 0, 0.01), 12.52px 0px 10.017px 0px rgba(0, 0, 0, 0.01), 6.65px 0px 5.32px 0px rgba(0, 0, 0, 0.01), 2.77px 0px 2.214px 0px rgba(0, 0, 0, 0.01); -} - -.gh-toggle-nav-menu { - display: flex; - align-items: center; - justify-content: flex-start; - position: absolute; - top: 0; - bottom: 0; - width: 32px; - right: -32px; - transition: opacity ease-in-out 150ms; - opacity: 0; -} - -.gh-nav-hidden .gh-toggle-nav-menu { - width: 60px; - max-height: 400px; - top: 50%; - right: -60px; - transform: translateY(-50%); -} - -.gh-nav:hover .gh-toggle-nav-menu, -.gh-nav-hidden .gh-toggle-nav-menu { - opacity: 1; -} - -.gh-btn-toggle-menu { - width: 18px; - height: 32px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.gh-nav-hidden:hover .gh-btn-toggle-menu { - transform: translateX(-22px); -} - -.gh-btn-toggle-menu .segment-1, -.gh-btn-toggle-menu .segment-2 { - display: block; - width: 2px; - height: 10px; - background: var(--midlightgrey); - transition: transform ease-in-out 150ms; -} - -.gh-btn-toggle-menu .segment-1 { - border-radius: 10px 10px 0 0; - -} - -.gh-btn-toggle-menu .segment-2 { - border-radius: 0 0 10px 10px; -} - -.gh-btn-toggle-menu:hover .segment-1 { - transform: rotate(45deg) translateY(2px) scaleY(1.2); -} - -.gh-btn-toggle-menu:hover .segment-2 { - transform: rotate(-45deg) translateY(-2px) scaleY(1.2); -} - -.gh-nav-hidden .gh-btn-toggle-menu .segment-1, -.gh-nav-hidden .gh-btn-toggle-menu .segment-2 { - transform: translateX(2px); -} - -.gh-nav-hidden .gh-btn-toggle-menu:hover .segment-1 { - transform: rotate(-45deg) translateY(2px) scaleY(1.2) translateX(0); -} - -.gh-nav-hidden .gh-btn-toggle-menu:hover .segment-2 { - transform: rotate(45deg) translateY(-2px) scaleY(1.2) translateX(0); -} - -.gh-nav-contributor { - position: absolute; - bottom: 0; - border-right: none; - background: unset; - width: 100%; - overflow: unset; -} - -.gh-nav-contributor .gh-nav-body { - overflow: unset; -} - -.gh-nav-menu { - flex-shrink: 0; - display: flex; - align-items: center; - /* height: var(--main-layout-vpanel-height); */ - padding: 32px; -} - -.gh-nav-menu-dropdown .dropdown-menu { - top: -324px; - left: -13px; - margin: 10px 0 0; - box-shadow: var(--box-shadow-m); - min-width: 290px; - padding: 4px 0; - border-radius: 8px; -} - -.gh-nav-menu-dropdown-contributor .dropdown-menu { - top: -272px; -} - -.gh-nav-menu-dropdown .dropdown-menu>li>a, -.gh-nav-menu-dropdown .dropdown-menu>li>button { - font-size: 1.4rem; - margin: 1px 4px 0; - width: unset; - padding: 8px 14px 9px; - height: 36px; - font-weight: 400; -} - -.gh-nav-menu-dropdown .dropdown-menu>li>button { - width: calc(100% - 8px); -} - -.gh-nav-menu-dropdown .dropdown-menu .divider { - margin: 4px 0; -} - -.gh-nav-menu-dropdown .dropdown-menu svg { - width: 16px; - height: 16px; -} - -.gh-nav-menu-dropdown.ember-basic-dropdown--transitioning-in { - animation: fade-in-scale 0.2s; - animation-fill-mode: forwards; -} - -.gh-nav-menu-dropdown.ember-basic-dropdown--transitioning-out { - animation: fade-out 0.5s; - animation-fill-mode: forwards; -} - -.gh-nav-menu-icon { - flex-shrink: 0; - margin-right: 12px; - margin-left: -6px; - width: 32px; - height: 32px; - background-color: transparent; - background-size: cover; - border-radius: 6px; - transition: all ease-in-out 0.3s; - pointer-events: none; -} - -.gh-nav-menu-details { - position: relative; - display: flex; - align-items: center; - flex-grow: 1; - padding-right: 10px; - min-width: 0; /* TODO: This is a bullshit Firefox hack */ -} - -.gh-nav-menu-details-sitetitle { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.3em; - font-weight: 600; - color: var(--black); -} - -.gh-nav-menu-details-user { - overflow: hidden; - color: var(--midgrey); - text-overflow: ellipsis; - white-space: nowrap; - font-size: 1.2rem; - line-height: 1.2em; -} - -.gh-nav-body { - display: flex; - flex-direction: column; - justify-content: space-between; - flex-grow: 1; - overflow-y: auto; - padding: 0; -} - -.gh-nav-main-enter-active, -.gh-nav-main-leave-active { - position: absolute; - top: 0; - height: 100%; - transition: transform 400ms ease; -} - -.gh-nav-main-enter-active, -.gh-nav-main-leave-active { - width: calc(var(--mainmenu-width) - 1px); -} - -.gh-nav-main-enter, -.gh-nav-main-leave-to { - transform: translateX(-100%); -} - -.gh-account-menu-header { - position: relative; - display: flex; - align-items: center; - padding: 12px 16px; -} - -.gh-account-menu-header .gh-user-avatar { - width: 44px; - height: 44px; - flex-basis: 44px; - margin: 0; - padding: 0; -} - -.gh-account-menu-header .gh-user-info { - margin-left: 10px; -} - -.gh-account-menu-header .user-menu-signout { - position: absolute; - top: 16px; - right: 12px; -} - -.gh-user-name { - display: inline-block; - width: 188px; - margin: 1px 0 0; - padding: 0; - word-break: break-all; - font-size: 1.5rem; - letter-spacing: 0; - font-weight: 500; - line-height: 1; -} - -.gh-user-email { - display: inline-block; - width: 188px; - margin: 0; - padding: 0; - word-break: break-all; - font-size: 1.3rem; - font-weight: 400; - line-height: 1; - color: var(--middarkgrey); -} - -.gh-nav-list .gh-icon-ap-network { - width: 18px; - height: 18px; -} - /* Global search /* ---------------------------------------------------------- */ -.gh-nav-btn-search { - padding: 8px 8px 5px; - width: 36px; - height: 36px; - border-radius: 999px; - margin: 2px -8px 0 0; -} - -.gh-nav-btn-search svg { - width: 18px; - height: 18px; -} - -.gh-nav-btn-search svg path { - fill: var(--midgrey); -} - -.gh-nav-btn-search:hover { - background: var(--mainmenu-color-hover-bg); -} - .gh-nav-search-modal { position: relative; margin: -32px; @@ -511,465 +168,6 @@ body:not(.gh-body-fullscreen) .gh-viewport { } -/* Navigation -/* ---------------------------------------------------------- */ -.gh-nav-list { - margin: 32px 0 0; - padding: 0; - list-style: none; - font-size: 1.4rem; - line-height: 1.6em; - padding: 0 20px; -} - -.gh-nav-list:first-of-type { - margin-top: 0; -} - -.gh-nav-list li { - margin: 0; - padding: 0; -} - -.gh-nav-list .gh-nav-list-h { - overflow: hidden; - padding: 10px 27px; - color: color-mod(var(--darkgrey)); - text-overflow: ellipsis; - text-transform: uppercase; - white-space: nowrap; - letter-spacing: 0.4px; - font-size: 1.1rem; - line-height: 1.1em; - font-weight: 500; -} - -.gh-nav-list a, -.gh-nav-list button.main-menu-item { - display: flex; - align-items: center; - color: var(--middarkgrey); - transition: none; - font-weight: 500; - padding: 7px 14px; - font-size: 1.4rem; - height: 36px; - margin: 1px 0 0; - border-radius: var(--border-radius); - box-sizing: border-box; -} - -.gh-nav-list button.main-menu-item { - width: calc(100% - 12px); -} - -.gh-nav-list .active { - position: relative; - color: var(--black); - background-color: var(--mainmenu-color-active-bg); - font-weight: 600; -} - -.gh-nav-list a:not(.active):hover, -.gh-nav-list button.main-menu-item:hover { - color: var(--darkgrey); - background: var(--mainmenu-color-hover-bg); - opacity: 1; -} - -.gh-nav-list a:hover .gh-nav-member-count { - background: color-mod(var(--mainmenu-color-active-bg) l(-5%)); -} - -/* Icons */ -.gh-nav-list svg { - margin-right: 11px; - width: 16px; - height: 16px; - /* fill: var(--midgrey); */ - line-height: 1; - transition: none; - z-index: 999; -} - -.gh-nav-list-home svg { - margin-top: -2px; -} - -.gh-nav-list svg circle { - fill: none !important; -} - -.gh-nav-button-expand { - display: flex; - align-items: center; - padding-left: 8px; - height: 16px; - position: absolute; - left: 8px; - top: 10px; - z-index: 999; - opacity: 1; -} - -.gh-nav-button-expand { - padding-left: 9px; - opacity: 0; -} - -.gh-nav-list-posts:hover .gh-nav-button-expand { - opacity: 1; -} - -.gh-nav-posts-icon { - transition: all ease 0.2s !important; -} - -.gh-nav-list-posts:hover .gh-nav-posts-icon { - opacity: 0; -} - -.gh-nav-button-expand svg { - width: 9px; - height: 9px; - margin-bottom: 1px; -} - -.gh-nav-button-expand svg path { - stroke-width: 2.0px; - stroke: var(--midgrey); -} - -.gh-nav-button-expand:hover svg path { - stroke: color-mod(var(--darkgrey) l(-5%)); -} - -.gh-nav-list a[data-test-nav='posts'] svg { - margin-top: -3px; -} - -.gh-nav-list .gh-secondary-action { - position: absolute; - z-index: 999; - padding: 10px; - margin: 0; - right: -6px; - top: 0px; - opacity: 0; - transition: opacity ease 0.2s; -} - -.gh-nav-list li:hover .gh-secondary-action { - opacity: 1; -} - -.gh-nav-list .gh-secondary-action span { - width: 36px; - height: 36px; - border-radius: 100%; - display: flex; - align-items: center; - justify-content: center; -} -.gh-nav-list .gh-secondary-action span svg { - margin-right: 0; -} - -.gh-nav-list .gh-secondary-action:not(.icon-only):hover span { - background: var(--mainmenu-color-hover-bg); -} - -.gh-nav-list .active + .gh-secondary-action:hover span { - background: none; -} - -.gh-nav-list .gh-secondary-action:hover, -.gh-nav-list a.gh-secondary-action:hover { - background: none; -} - -.gh-nav-list .gh-secondary-action.icon-only, -.gh-nav-list .gh-secondary-action.icon-only span { - pointer-events: none; - transition: none; -} - -.gh-nav-list .gh-secondary-action.icon-only.arrow svg { - width: 16px; - height: 16px; -} - -.gh-nav-list .gh-nav-new-post { - opacity: 1; -} - -.gh-nav-list .gh-nav-new-post span svg path { - stroke: var(--darkgrey); - stroke-width: 1; -} - -.gh-nav-list .gh-nav-member-count { - position: absolute; - z-index: 999; - padding: 2px 7px; - margin: 0; - right: 10px; - top: 7px; - background: var(--mainmenu-color-active-bg); - color: var(--middarkgrey); - border-radius: 999px; - font-weight: 500; - font-size: 1.3rem; - min-width: 23px; - text-align: center; -} - -.gh-nav-list-ap .active .gh-nav-member-count { - display: none; -} - -.gh-nav-main { - margin: 24px 0; -} - -.gh-nav-labs { - margin-bottom: 32px; - padding: 0; -} - -.gh-nav-pro .gh-btn-green { - margin: 12px 0 9px 0px !important; - width: 100% !important; -} - -.gh-nav-view-list { - padding: 0; - margin: 0 0 22px; - list-style: none; - font-size: 1.4rem; - line-height: 1.6em; -} - -.gh-nav-view-list a { - position: relative; - padding-left: 42px; -} - -.gh-nav-viewname { - display: inline-block; - max-width: 160px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.gh-nav-view-list .circle { - position: absolute; - display: block; - border-radius: 999px; - border: 3px solid var(--midgrey); - width: 8px; - height: 8px; - margin: 0; - right: 18px; -} - -.gh-nav-view-list .svg-midgrey .circle { - border-color: var(--midgrey); - background: var(--midgrey); -} - -.gh-nav-view-list .svg-blue .circle { - border-color: var(--blue); - background: var(--blue); -} - -.gh-nav-view-list .svg-green .circle { - border-color: var(--green); - background: var(--green); -} - -.gh-nav-view-list .svg-red .circle { - border-color: var(--red); - background: var(--red); -} - -.gh-nav-view-list .svg-teal .circle { - border-color: #4dcddc; - background: #4dcddc; -} - -.gh-nav-view-list .svg-purple .circle { - border-color: var(--purple); - background: var(--purple); -} - -.gh-nav-view-list .svg-yellow .circle { - border-color: var(--yellow); - background: var(--yellow); -} - -.gh-nav-view-list .svg-orange .circle { - border-color: #fe8b05; - background: #fe8b05; -} - -.gh-nav-view-list .svg-pink .circle { - border-color: var(--pink); - background: var(--pink); -} - -/* Bottom nav -/* ---------------------------------------------------------- */ - -.gh-nav-bottom { - margin: 0; - padding: 32px; -} - -.gh-nav-bottom .ember-basic-dropdown-trigger { - padding: 4px 8px 4px 4px; - margin: -4px -8px -4px -4px; - border-radius: 999px; - /* background: var(--main-bg-color); */ -} - -.gh-nav-bottom .ember-basic-dropdown-trigger:hover { - background: var(--whitegrey); -} - -.gh-nav-contributor .gh-nav-bottom .ember-basic-dropdown-trigger { - padding: 8px 12px 8px 8px; - margin: -4px 0px -4px 0px; - /* background: var(--main-bg-color); */ - /* box-shadow: - 0px -1px 10px rgba(0, 0, 0, 0.08), - 0px 2.8px 2.2px rgba(0, 0, 0, 0.014), - 0px 6.7px 5.3px rgba(0, 0, 0, 0.02), - 0px 12.5px 10px rgba(0, 0, 0, 0.025), - 0px 22.3px 17.9px rgba(0, 0, 0, 0.03), - 0px 41.8px 33.4px rgba(0, 0, 0, 0.036), - 0px 100px 80px rgba(0, 0, 0, 0.05) - ; */ -} - -.gh-nav-bottom-tabicon { - display: flex; - align-items: center; - justify-content: center; - margin-left: 12px; - padding: 10px; - border-radius: 999px; - width: 40px; - height: 40px; - line-height: 1; - color: var(--darkgrey); -} - -.gh-nav-bottom-tabicon:hover { - background: var(--whitegrey); -} - -.gh-nav-bottom-tabicon.active { - background: var(--whitegrey-d1); - color: var(--black); -} - -.gh-nav-bottom-tabicon.active svg { - fill: var(--black); -} - -.gh-nav-bottom-tabicon svg { - width: 20px; - height: 20px; - fill: var(--darkgrey); - line-height: 1; - transition: none; -} - -.gh-nav-bottom-tabicon:last-child[data-tooltip]:before { - left: -12px; -} - -.nightshift-toggle-container { - padding: 8px 0 8px 12px; -} - -.nightshift-toggle-container[data-tooltip]:before { - left: -10px; -} - -.nightshift-toggle { - position: relative; - height: 22px; - width: 42px; - background: var(--black); - border-radius: 999px; - cursor: pointer; - transition: all ease-in-out 0.3s; -} - -.nightshift-toggle .thumb { - position: absolute; - top: 2px; - left: 2px; - width: 18px; - height: 18px; - background-color: var(--white); - border-radius: 999px; - transition: all ease-in-out 0.3s; -} - -.nightshift-toggle.on .thumb { - position: absolute; - left: 22px; - top: 2px; -} - -.nightshift-toggle .sun { - position: absolute; - top: 5px; - right: 6px; - color: var(--white); - line-height: 1; -} - -.nightshift-toggle .moon { - position: absolute; - top: 5px; - left: 6px; - color: var(--white); - line-height: 1; -} - -.nightshift-toggle .sun svg, -.nightshift-toggle .moon svg { - width: 12px; - height: 12px; - transition: all ease-in-out 0.3s; -} - -.nightshift-toggle .sun svg line, -.nightshift-toggle .sun svg path, -.nightshift-toggle .sun svg circle, -.nightshift-toggle .moon svg path { - stroke-width: 2.0px; -} - -/* Tablet/mid sizes -/* ---------------------------------------------------------- */ - -@media (max-width: 1240px) { - .gh-nav { - flex-basis: 280px; - } - - .gh-nav-main-enter-active, - .gh-nav-main-leave-active { - width: calc(280px - 1px); - } -} - /* Container for App View /* ---------------------------------------------------------- */ @@ -986,13 +184,6 @@ body:not(.gh-body-fullscreen) .gh-viewport { max-width: var(--main-layout-content-maxwidth); } -@media (min-width: 800px) { - .gh-nav-hidden + .gh-main .gh-canvas { - padding-left: 80px; - padding-right: 80px; - } -} - .gh-canvas.gh-canvas-sticky { padding-bottom: 0; /* No padding needed when tables are sticky */ } @@ -1640,42 +831,6 @@ body:not(.gh-body-fullscreen) .gh-viewport { } } -@media (max-width: 1280px) { - .gh-nav:not(.gh-nav-hidden) + .gh-main .gh-canvas-header.break.tablet .gh-canvas-header-content { - min-height: 72px; - align-items: flex-start; - } - - .gh-nav:not(.gh-nav-hidden) + .gh-main .gh-canvas-title.gh-post-title { - padding: 0; - } - - .gh-nav:not(.gh-nav-hidden) + .gh-main .gh-canvas-header.break.tablet .view-actions { - flex-direction: column; - align-items: flex-end; - max-height: 100px; - flex-wrap: nowrap; - } - - .gh-nav:not(.gh-nav-hidden) + .gh-main .gh-canvas-header.break.tablet .view-actions-bottom-row { - position: relative; - order: 2; - margin: 0; - padding: 0; - justify-content: space-between; - } - - .gh-nav:not(.gh-nav-hidden) + .gh-main .view-actions-bottom-row { - justify-content: flex-end; - order: 2; - } - - .gh-nav:not(.gh-nav-hidden) + .gh-main .view-actions-top-row > *:last-child, - .gh-nav:not(.gh-nav-hidden) + .gh-main .view-actions-bottom-row > *:last-child { - margin-right: 0 !important; - } -} - @media (max-width: 1000px) { .gh-canvas-header.break.tablet .gh-canvas-header-content { min-height: 72px; @@ -2051,10 +1206,6 @@ section.gh-ds h2 { padding: 32px; } -.gh-done .gh-nav { - flex: 0 0 420px !important; -} - @media (max-width: 1000px) { .gh-done-sidebar { flex: 0 0 360px; @@ -2098,23 +1249,6 @@ section.gh-ds h2 { } -/* Footer toast in navbar */ -.gh-footer-toast { - display: flex; - flex-direction: column; - font-size: 1.3rem; - text-align: left; - line-height: 1.35em; - padding: 20px; - margin-bottom: 32px; - color: #fff; - border-radius: 6px; -} - -.gh-footer-toast-title.gh-notification-title { - margin: 0 0 6px 0; -} - /* Theme error toast and modal */ .gh-theme-error-toast { background: var(--red); diff --git a/ghost/admin/app/templates/application.hbs b/ghost/admin/app/templates/application.hbs index a88d4856f34..40981313cb4 100644 --- a/ghost/admin/app/templates/application.hbs +++ b/ghost/admin/app/templates/application.hbs @@ -34,10 +34,6 @@
    - {{#if this.showNavMenu}} - - {{/if}} -
    {{outlet}} diff --git a/ghost/admin/app/templates/posts/analytics.hbs b/ghost/admin/app/templates/posts/analytics.hbs deleted file mode 100644 index f3d5511b347..00000000000 --- a/ghost/admin/app/templates/posts/analytics.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (feature 'audienceFeedback') }} - -{{else}} - -{{/if}} diff --git a/ghost/admin/app/utils/member-event-types.js b/ghost/admin/app/utils/member-event-types.js index a724e96bfd8..18adff7dec2 100644 --- a/ghost/admin/app/utils/member-event-types.js +++ b/ghost/admin/app/utils/member-event-types.js @@ -20,9 +20,7 @@ export function getAvailableEventTypes(settings, feature, hiddenEvents = []) { if (settings.commentsEnabled !== 'off') { extended.push({event: 'comment_event', icon: 'filter-dropdown-comments', name: 'Comments', group: 'others'}); } - if (feature.audienceFeedback) { - extended.push({event: 'feedback_event', icon: 'filter-dropdown-feedback', name: 'Feedback', group: 'others'}); - } + extended.push({event: 'feedback_event', icon: 'filter-dropdown-feedback', name: 'Feedback', group: 'others'}); if (settings.emailTrackClicks) { extended.push({event: 'click_event', icon: 'filter-dropdown-clicked-in-email', name: 'Clicked link in email', group: 'others'}); } diff --git a/ghost/admin/tests/acceptance/analytics-navigation-test.js b/ghost/admin/tests/acceptance/analytics-navigation-test.js index 34e2f2e9d84..65653b36caf 100644 --- a/ghost/admin/tests/acceptance/analytics-navigation-test.js +++ b/ghost/admin/tests/acceptance/analytics-navigation-test.js @@ -17,10 +17,6 @@ describe('Acceptance: Analytics Navigation', function () { return role; } - function findAnalyticsNavLink() { - return document.querySelector('.gh-nav-list a[href*="analytics"]'); - } - async function clickPostAnalytics(postId) { // The analytics link is in the post row, find the specific one for this post let postRow = document.querySelector(`[data-test-post-id="${postId}"]`); @@ -106,32 +102,6 @@ describe('Acceptance: Analytics Navigation', function () { }); }); - describe('Navigation Menu', function () { - it('shows Analytics link for admin users', async function () { - await visit('/site'); - - let analyticsLink = findAnalyticsNavLink(); - expect(analyticsLink).to.exist; - expect(analyticsLink.textContent).to.contain('Analytics'); - }); - - it('hides Analytics link for non-admin users', async function () { - updateUserRole(this.server, 'Editor'); - - await visit('/site'); - - let analyticsLink = findAnalyticsNavLink(); - expect(analyticsLink).to.not.exist; - }); - - it('navigates to Analytics when Analytics nav link is clicked', async function () { - await visit('/site'); - await click('.gh-nav-list a[href*="analytics"]'); - - expect(currentURL()).to.equal('/analytics'); - }); - }); - describe('Posts Index Navigation', function () { it('navigates to posts-x route when clicking on post analytics', async function () { let post = createPostWithEmail(this.server); diff --git a/ghost/admin/tests/acceptance/authentication-test.js b/ghost/admin/tests/acceptance/authentication-test.js index 653976c6008..c78ab77aefd 100644 --- a/ghost/admin/tests/acceptance/authentication-test.js +++ b/ghost/admin/tests/acceptance/authentication-test.js @@ -173,8 +173,7 @@ describe('Acceptance: Authentication', function () { })); await authenticateSession(); - await visit('/posts'); - await click(`[data-test-nav="pages"]`); + await visit('/pages'); expect(windowProxy.replaceLocation.calledWith('/ghost/'), 'replaceLocation called with /ghost/').to.be.true; }); @@ -193,15 +192,6 @@ describe('Acceptance: Authentication', function () { expect(findAll('nav.gh-nav').length, 'nav menu presence').to.equal(0); }); - it('shows nav menu on invalid url when authenticated', async function () { - await authenticateSession(); - await visit('/signin/invalidurl/'); - - expect(currentURL(), 'url after invalid url').to.equal('/signin/invalidurl/'); - expect(currentRouteName(), 'path after invalid url').to.equal('error404'); - expect(findAll('nav.gh-nav').length, 'nav menu presence').to.equal(1); - }); - it('has 2fa code happy path', async function () { setupVerificationRequired(this.server); setupVerificationSuccess(this.server); diff --git a/ghost/admin/tests/acceptance/content-test.js b/ghost/admin/tests/acceptance/content-test.js index 43ac5257d99..8718e6742db 100644 --- a/ghost/admin/tests/acceptance/content-test.js +++ b/ghost/admin/tests/acceptance/content-test.js @@ -813,109 +813,6 @@ describe('Acceptance: Posts / Pages', function () { }); }); }); - it('can add and edit custom views', async function () { - // actions are not visible when there's no filter - await visit('/posts'); - expect(find('[data-test-button="edit-view"]'), 'edit-view button (no filter)').to.not.exist; - expect(find('[data-test-button="add-view"]'), 'add-view button (no filter)').to.not.exist; - - // add action is visible after filtering to a non-default filter - await selectChoose('[data-test-author-select]', admin.name); - expect(find('[data-test-button="add-view"]'), 'add-view button (with filter)').to.exist; - - // adding view shows it in the sidebar - await click('[data-test-button="add-view"]'), 'add-view button'; - expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (on add)').to.exist; - expect(find('[data-test-modal="custom-view-form"] h1').textContent.trim()).to.equal('New view'); - await fillIn('[data-test-input="custom-view-name"]', 'Test view'); - await click('[data-test-button="save-custom-view"]'); - // modal closes on save - expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (after add save)').to.not.exist; - // UI updates - expect(find('[data-test-nav-custom="posts-Test view"]'), 'new view nav').to.exist; - expect(find('[data-test-nav-custom="posts-Test view"]').textContent.trim()).to.equal('Test view'); - expect(find('[data-test-button="add-view"]'), 'add-view button (on existing view)').to.not.exist; - expect(find('[data-test-button="edit-view"]'), 'edit-view button (on existing view)').to.exist; - - // editing view - await click('[data-test-button="edit-view"]'), 'edit-view button'; - expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (on edit)').to.exist; - expect(find('[data-test-modal="custom-view-form"] h1').textContent.trim()).to.equal('Edit view'); - await fillIn('[data-test-input="custom-view-name"]', 'Updated view'); - await click('[data-test-button="save-custom-view"]'); - // modal closes on save - expect(find('[data-test-modal="custom-view-form"]'), 'custom view modal (after edit save)').to.not.exist; - // UI updates - expect(find('[data-test-nav-custom="posts-Updated view"]')).to.exist; - expect(find('[data-test-nav-custom="posts-Updated view"]').textContent.trim()).to.equal('Updated view'); - expect(find('[data-test-button="add-view"]'), 'add-view button (after edit)').to.not.exist; - expect(find('[data-test-button="edit-view"]'), 'edit-view button (after edit)').to.exist; - }); - - it('can navigate to custom views', async function () { - this.server.schema.settings.findBy({key: 'shared_views'}).update({ - group: 'site', - key: 'shared_views', - value: JSON.stringify([{ - route: 'posts', - name: 'My posts', - filter: { - author: admin.slug - } - }]) - }); - - await visit('/posts'); - - // nav bar contains default + custom views - expect(find('[data-test-nav-custom="posts-Drafts"]'), 'drafts nav').to.exist; - expect(find('[data-test-nav-custom="posts-Scheduled"]'), 'scheduled nav').to.exist; - expect(find('[data-test-nav-custom="posts-Published"]'), 'published nav').to.exist; - expect(find('[data-test-nav-custom="posts-My posts"]'), 'my posts nav').to.exist; - - // screen has default title and sidebar is showing inactive custom view - expect(find('[data-test-screen-title]')).to.have.rendered.trimmed.text('Posts'); - expect(find('[data-test-nav="posts"]')).to.have.class('active'); - - // clicking sidebar custom view link works - await click('[data-test-nav-custom="posts-Scheduled"]'); - expect(currentURL()).to.equal('/posts?type=scheduled'); - expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/); - expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.have.class('active'); - - // clicking the main posts link resets - await click('[data-test-nav="posts"]'); - expect(currentURL()).to.equal('/posts'); - expect(find('[data-test-screen-title]')).to.have.rendered.trimmed.text('Posts'); - expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.not.have.class('active'); - - // changing a filter to match a custom view shows custom view - await selectChoose('[data-test-type-select]', 'Scheduled posts'); - expect(currentURL()).to.equal('/posts?type=scheduled'); - expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.have.class('active'); - expect(find('[data-test-screen-title]').innerText).to.match(/Scheduled/); - }); - - it('Shows edit view if order is null, which indicates a bad state', async function () { - this.server.schema.settings.findBy({key: 'shared_views'}).update({ - group: 'site', - key: 'shared_views', - value: JSON.stringify([{ - route: 'posts', - name: 'My posts', - filter: { - author: admin.slug, - order: null - } - }]) - }); - - await visit('/posts'); - expect(find('[data-test-nav-custom="posts-My posts"]'), 'my posts nav').to.exist; - // click on the custom view - await click('[data-test-nav-custom="posts-My posts"]'); - expect(find('[data-test-button="edit-view"]'), 'edit-view button (on existing view)').to.exist; - }); }); describe('analytics visibility', function () { diff --git a/ghost/admin/tests/acceptance/members-test.js b/ghost/admin/tests/acceptance/members-test.js index de4990562ab..966823b4acc 100644 --- a/ghost/admin/tests/acceptance/members-test.js +++ b/ghost/admin/tests/acceptance/members-test.js @@ -26,8 +26,6 @@ describe('Acceptance: Members Test', function () { await visit('/members'); expect(currentURL()).to.equal('/site'); - expect(find('[data-test-nav="members"]'), 'sidebar link') - .to.not.exist; }); describe('as owner', function () { @@ -53,12 +51,6 @@ describe('Acceptance: Members Test', function () { expect(findAll('[data-test-list="members-list-item"]').length, 'members list count') .to.equal(2); - // it highlights active state in nav menu - expect( - find('[data-test-nav="members"]'), - 'highlights nav menu item' - ).to.have.class('active'); - let member = find('[data-test-list="members-list-item"]'); expect(member.querySelector('.gh-members-list-name').textContent, 'member list item title') .to.equal(member1.name); @@ -76,12 +68,6 @@ describe('Acceptance: Members Test', function () { expect(find('[data-test-input="member-email"]').value, 'loads correct email into form') .to.equal(member1.email); - // it maintains active state in nav menu - expect( - find('[data-test-nav="members"]'), - 'highlights nav menu item' - ).to.have.class('active'); - // trigger save await fillIn('[data-test-input="member-name"]', 'New Name'); await blur('[data-test-input="member-name"]'); @@ -146,12 +132,6 @@ describe('Acceptance: Members Test', function () { expect(find('.gh-canvas-header h2').textContent, 'settings pane title') .to.contain('New'); - // it highlights active state in nav menu - expect( - find('[data-test-nav="members"]'), - 'highlights nav menu item' - ).to.have.class('active'); - // all fields start blank findAll('.gh-member-settings-primary .gh-input').forEach(function (elem) { expect(elem.value, `input field for ${elem.getAttribute('name')}`) @@ -401,12 +381,6 @@ describe('Acceptance: Members Test', function () { expect(findAll('[data-test-list="members-list-item"]').length, 'members list count') .to.equal(2); - // it highlights active state in nav menu - expect( - find('[data-test-nav="members"]'), - 'highlights nav menu item' - ).to.have.class('active'); - let member = find('[data-test-list="members-list-item"]'); expect(member.querySelector('.gh-members-list-name').textContent, 'member list item title') .to.equal(member1.name); @@ -424,12 +398,6 @@ describe('Acceptance: Members Test', function () { expect(find('[data-test-input="member-email"]').value, 'loads correct email into form') .to.equal(member1.email); - // it maintains active state in nav menu - expect( - find('[data-test-nav="members"]'), - 'highlights nav menu item' - ).to.have.class('active'); - // trigger save await fillIn('[data-test-input="member-name"]', 'New Name'); await blur('[data-test-input="member-name"]'); @@ -463,12 +431,6 @@ describe('Acceptance: Members Test', function () { expect(find('.gh-canvas-header h2').textContent, 'settings pane title') .to.contain('New'); - // it highlights active state in nav menu - expect( - find('[data-test-nav="members"]'), - 'highlights nav menu item' - ).to.have.class('active'); - // all fields start blank findAll('.gh-member-settings-primary .gh-input').forEach(function (elem) { expect(elem.value, `input field for ${elem.getAttribute('name')}`) diff --git a/ghost/admin/tests/acceptance/search-test.js b/ghost/admin/tests/acceptance/search-test.js index f43c13de3b8..4acef9f361f 100644 --- a/ghost/admin/tests/acceptance/search-test.js +++ b/ghost/admin/tests/acceptance/search-test.js @@ -9,7 +9,6 @@ import {setupApplicationTest} from 'ember-mocha'; import {setupMirage} from 'ember-cli-mirage/test-support'; import {typeInSearch} from 'ember-power-select/test-support/helpers'; -const SEARCH_BUTTON = '[data-test-button="search"]'; const SEARCH_MODAL = '[data-test-modal="search"]'; const SEARCH_TRIGGER = '[data-test-modal="search"] .ember-power-select-trigger'; const MODAL_BACKDROP = '.epm-backdrop'; @@ -31,11 +30,6 @@ const assertSearchModalClosed = () => { // Helper functions for common test operations const openSearch = async () => { - await click(SEARCH_BUTTON); - assertSearchModalOpen(); -}; - -const openSearchWithKeyboard = async () => { await triggerKeyEvent(document, 'keydown', 'K', { metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl' @@ -197,18 +191,12 @@ describe('Acceptance: Search', function () { expect(searchService.provider.constructor.name).to.equal('SearchProviderFlexService'); }); - it('opens search modal when clicking search icon', async function () { + it('opens search modal with Ctrl/Cmd+K', async function () { await visit('/analytics'); assertSearchModalClosed(); await openSearch(); }); - it('opens search modal with keyboard shortcut Ctrl/Cmd+K', async function () { - await visit('/analytics'); - assertSearchModalClosed(); - await openSearchWithKeyboard(); - }); - it('closes search modal with Escape key', async function () { await visit('/analytics'); await openSearch(); @@ -221,17 +209,6 @@ describe('Acceptance: Search', function () { await closeSearchWithBackdrop(); }); - it('does not open search modal if the sidebar is hidden', async function () { - const post = this.server.create('post', {title: 'Test post', slug: 'test-post', status: 'draft'}); - await visit(`/editor/post/${post.id}`); - assertSearchModalClosed(); - await triggerKeyEvent(document, 'keydown', 'K', { - metaKey: ctrlOrCmd === 'command', - ctrlKey: ctrlOrCmd === 'ctrl' - }); - assertSearchModalClosed(); - }); - it('finds all content types when searching for "first"', async function () { await visit('/analytics'); await openSearch(); @@ -414,18 +391,12 @@ describe('Acceptance: Search', function () { expect(searchService.provider.constructor.name).to.equal('SearchProviderBasicService'); }); - it('opens search modal when clicking search icon', async function () { + it('opens search modal with Ctrl/Cmd+K', async function () { await visit('/analytics'); assertSearchModalClosed(); await openSearch(); }); - it('opens search modal with keyboard shortcut Ctrl/Cmd+K', async function () { - await visit('/analytics'); - assertSearchModalClosed(); - await openSearchWithKeyboard(); - }); - it('closes search modal with Escape key', async function () { await visit('/analytics'); await openSearch(); diff --git a/ghost/admin/tests/acceptance/settings-button-test.js b/ghost/admin/tests/acceptance/settings-button-test.js deleted file mode 100644 index 248f2d9424f..00000000000 --- a/ghost/admin/tests/acceptance/settings-button-test.js +++ /dev/null @@ -1,35 +0,0 @@ -import loginAsRole from '../helpers/login-as-role'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {find} from '@ember/test-helpers'; -import {invalidateSession} from 'ember-simple-auth/test-support'; -import {setupApplicationTest} from 'ember-mocha'; -import {setupMirage} from 'ember-cli-mirage/test-support'; -import {visit} from '../helpers/visit'; - -describe('Acceptance: Settings button', function () { - const hooks = setupApplicationTest(); - setupMirage(hooks); - - describe('check by role', function () { - beforeEach(async function () { - await invalidateSession(); - }); - - it('is present for editors', async function () { - await loginAsRole('Editor', this.server); - await visit('site'); - expect(find('[data-test-nav="settings"]')).to.exist; - }); - it('is not present for authors', async function () { - await loginAsRole('Author', this.server); - await visit('site'); - expect(find('[data-test-nav="settings"]')).to.be.null; - }); - it('is present for super editors', async function () { - await loginAsRole('Super Editor', this.server); - await visit('site'); - expect(find('[data-test-nav="settings"]')).to.exist; - }); - }); -}); diff --git a/ghost/admin/tests/integration/components/posts/post-activity-feed/footer-links-test.js b/ghost/admin/tests/integration/components/posts/post-activity-feed/footer-links-test.js deleted file mode 100644 index 94516e99c2d..00000000000 --- a/ghost/admin/tests/integration/components/posts/post-activity-feed/footer-links-test.js +++ /dev/null @@ -1,54 +0,0 @@ -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {find, render} from '@ember/test-helpers'; -import {hbs} from 'ember-cli-htmlbars'; -import {setupRenderingTest} from 'ember-mocha'; - -describe('Integration: Component: posts/post-activity-feed/footer-links', function () { - setupRenderingTest(); - - it('renders just one link if negative feedback > 0', async function () { - this.set('post', {id: 'id', count: {positive_feedback: 0, negative_feedback: 1}}); - await render(hbs` - `); - - const link = find('.gh-post-activity-feed-pagination-link-wrapper'); - - expect(link).to.contain.text('Less like this'); - expect(link).not.to.contain.text('and'); - expect(link).not.to.contain.text('More like this'); - }); - - it('renders just one link if positive feedback > 0', async function () { - this.set('post', {id: 'id', count: {positive_feedback: 1, negative_feedback: 0}}); - await render(hbs` - `); - - const link = find('.gh-post-activity-feed-pagination-link-wrapper'); - - expect(link).not.to.contain.text('Less like this'); - expect(link).not.to.contain.text('and'); - expect(link).to.contain.text('More like this'); - }); - - it('renders positive and negative links with separator', async function () { - this.set('post', {id: 'id', count: {positive_feedback: 1, negative_feedback: 1}}); - await render(hbs` - `); - - const link = find('.gh-post-activity-feed-pagination-link-wrapper'); - - expect(link).to.contain.text('Less like this'); - expect(link).to.contain.text('and'); - expect(link).to.contain.text('More like this'); - }); -}); diff --git a/ghost/admin/tests/unit/components/posts/analytics-test.js b/ghost/admin/tests/unit/components/posts/analytics-test.js deleted file mode 100644 index 19adbaea382..00000000000 --- a/ghost/admin/tests/unit/components/posts/analytics-test.js +++ /dev/null @@ -1,131 +0,0 @@ -import Analytics from 'ghost-admin/components/posts/analytics'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; - -describe('Unit: Component: posts/analytics', function () { - describe('updateLinkData', function () { - it('Correctly orders `this.links`', function () { - const instance = Object.create(Analytics.prototype); - instance.utils = { - cleanTrackedUrl(url, title) { - if (title) { - return url.split('//')[1]; - } - return url; - } - }; - - const links = [{ - id: 1, - link: { - to: 'https://lone.com' - }, - count: { - clicks: 3 - } - }, { - id: 2, - link: { - to: 'https://duplicate.com' - }, - count: { - clicks: 2 - } - }, { - id: 3, - link: { - to: 'https://duplicate.com' - }, - count: { - clicks: 2 - } - }]; - - instance.updateLinkData(links); - - expect(instance.links.length).to.equal(2); - - expect(instance.links[0].count.clicks).to.equal(4); - expect(instance.links[0].id).to.equal(2); - - expect(instance.links[1].count.clicks).to.equal(3); - expect(instance.links[1].id).to.equal(1); - }); - - it('Correctly handles updates to `this.links`', function () { - const instance = Object.create(Analytics.prototype); - instance.utils = { - cleanTrackedUrl(url, title) { - if (title) { - return url.split('//')[1]; - } - return url; - } - }; - - const originalLinks = [{ - id: 1, - link: { - to: 'https://lone.com' - }, - count: { - clicks: 1 - } - }, { - id: 2, - link: { - to: 'https://duplicate.com' - }, - count: { - clicks: 1 - } - }, { - id: 3, - link: { - to: 'https://duplicate.com' - }, - count: { - clicks: 1 - } - }]; - - instance.updateLinkData(originalLinks); - - const updatedLinks = [{ - id: 1, - link: { - to: 'https://lone.com' - }, - count: { - clicks: 1 - } - }, { - id: 2, - link: { - to: 'https://duplicate.com' - }, - count: { - clicks: 1 - } - }, { - id: 3, - link: { - to: 'https://duplicate.com' - }, - count: { - clicks: 3 - } - }]; - - instance.updateLinkData(updatedLinks); - - expect(instance.links.length).to.equal(2); - - expect(instance.links[0].count.clicks).to.equal(4); - expect(instance.links[0].id).to.equal(2); - - expect(instance.links[1].count.clicks).to.equal(1); - expect(instance.links[1].id).to.equal(1); - }); - }); -}); diff --git a/ghost/admin/tests/unit/helpers/gh-user-can-manage-members-test.js b/ghost/admin/tests/unit/helpers/gh-user-can-manage-members-test.js deleted file mode 100644 index 010a499e675..00000000000 --- a/ghost/admin/tests/unit/helpers/gh-user-can-manage-members-test.js +++ /dev/null @@ -1,38 +0,0 @@ -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {ghUserCanManageMembers} from 'ghost-admin/helpers/gh-user-can-manage-members'; - -describe('Unit: Helper: gh-user-can-manage-members', function () { - // Mock up roles and test for truthy - describe('Owner, admin, super editor roles', function () { - let user = { - get(role) { - if (role === 'canManageMembers') { - return true; - } - throw new Error('unsupported'); - } - }; - - it(' - can manage members', function () { - let result = ghUserCanManageMembers([user]); - expect(result).to.equal(true); - }); - }); - - describe('Editor, Author & Contributor roles', function () { - let user = { - get(role) { - if (role === 'canManageMembers') { - return false; - } - throw new Error('unsupported'); - } - }; - - it(' - cannot manage members', function () { - let result = ghUserCanManageMembers([user]); - expect(result).to.equal(false); - }); - }); -}); \ No newline at end of file diff --git a/ghost/admin/tests/unit/services/notifications-count-test.js b/ghost/admin/tests/unit/services/notifications-count-test.js deleted file mode 100644 index 9c1c2e7187b..00000000000 --- a/ghost/admin/tests/unit/services/notifications-count-test.js +++ /dev/null @@ -1,150 +0,0 @@ -import Pretender from 'pretender'; -import {describe, it} from 'mocha'; -import {expect} from 'chai'; -import {setupTest} from 'ember-mocha'; - -describe('Unit: Service: notifications-count', function () { - setupTest(); - - let server; - - beforeEach(function () { - server = new Pretender(); - }); - - afterEach(function () { - server.shutdown(); - }); - - it('initializes with correct default values', function () { - const service = this.owner.lookup('service:notifications-count'); - expect(service.count).to.equal(0); - expect(service.isLoading).to.equal(false); - }); - - it('updates count correctly', function () { - const service = this.owner.lookup('service:notifications-count'); - service.updateCount(5); - expect(service.count).to.equal(5); - }); - - describe('fetchCount', function () { - it('returns 0 when no token is available', async function () { - server.get('/ghost/api/admin/identities/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - identities: [] - })]; - }); - - const service = this.owner.lookup('service:notifications-count'); - const count = await service.fetchCount(); - - expect(count).to.equal(0); - expect(service.count).to.equal(0); - expect(service.isLoading).to.equal(false); - }); - - it('returns 0 when no site URL is available', async function () { - server.get('/ghost/api/admin/identities/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - identities: [{token: 'test-token'}] - })]; - }); - - server.get('/ghost/api/admin/site/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - site: {} - })]; - }); - - const service = this.owner.lookup('service:notifications-count'); - const count = await service.fetchCount(); - - expect(count).to.equal(0); - expect(service.count).to.equal(0); - expect(service.isLoading).to.equal(false); - }); - - it('fetches and updates count successfully', async function () { - const siteUrl = 'https://example.com'; - const token = 'test-token'; - const expectedCount = 5; - - server.get('/ghost/api/admin/identities/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - identities: [{token}] - })]; - }); - - server.get('/ghost/api/admin/site/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - site: {url: siteUrl} - })]; - }); - - server.get(`${siteUrl}/.ghost/activitypub/stable/notifications/unread/count`, function (request) { - expect(request.requestHeaders.Authorization).to.equal(`Bearer ${token}`); - expect(request.requestHeaders.Accept).to.equal('application/activity+json'); - - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - count: expectedCount - })]; - }); - - const service = this.owner.lookup('service:notifications-count'); - const count = await service.fetchCount(); - - expect(count).to.equal(expectedCount); - expect(service.count).to.equal(expectedCount); - expect(service.isLoading).to.equal(false); - }); - - it('handles errors gracefully', async function () { - server.get('/ghost/api/admin/identities/', function () { - return [500, {'Content-Type': 'application/json'}, JSON.stringify({ - errors: [{message: 'Server error'}] - })]; - }); - - const service = this.owner.lookup('service:notifications-count'); - const count = await service.fetchCount(); - - expect(count).to.equal(0); - expect(service.count).to.equal(0); - expect(service.isLoading).to.equal(false); - }); - - it('sets loading state correctly during fetch', async function () { - const siteUrl = 'https://example.com'; - const token = 'test-token'; - - server.get('/ghost/api/admin/identities/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - identities: [{token}] - })]; - }); - - server.get('/ghost/api/admin/site/', function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - site: {url: siteUrl} - })]; - }); - - server.get(`${siteUrl}/.ghost/activitypub/stable/notifications/unread/count`, function () { - return [200, {'Content-Type': 'application/json'}, JSON.stringify({ - count: 5 - })]; - }); - - const service = this.owner.lookup('service:notifications-count'); - - const fetchPromise = service.fetchCount(); - - expect(service.isLoading).to.equal(true); - - await fetchPromise; - - expect(service.isLoading).to.equal(false); - }); - }); -}); diff --git a/ghost/admin/tests/unit/utils/member-event-types-test.js b/ghost/admin/tests/unit/utils/member-event-types-test.js index 71d5793a9b4..425a64c4c6a 100644 --- a/ghost/admin/tests/unit/utils/member-event-types-test.js +++ b/ghost/admin/tests/unit/utils/member-event-types-test.js @@ -8,9 +8,7 @@ describe('Unit | Utility | event-type-utils', function () { commentsEnabled: 'on', emailTrackClicks: true }; - const feature = { - audienceFeedback: true - }; + const feature = {}; const hiddenEvents = []; const eventTypes = getAvailableEventTypes(settings, feature, hiddenEvents); @@ -54,13 +52,16 @@ describe('Unit | Utility | event-type-utils', function () { commentsEnabled: 'off', emailTrackClicks: false }; - const feature = { - audienceFeedback: false - }; + const feature = {}; const hiddenEvents = []; const eventTypes = getAvailableEventTypes(settings, feature, hiddenEvents); - expect(eventTypes).to.deep.equal(ALL_EVENT_TYPES); + // Feedback is always included now (audienceFeedback is GA) + const expectedTypes = [ + ...ALL_EVENT_TYPES, + {event: 'feedback_event', icon: 'filter-dropdown-feedback', name: 'Feedback', group: 'others'} + ]; + expect(eventTypes).to.deep.equal(expectedTypes); }); }); diff --git a/ghost/core/core/server/services/members/members-api/repositories/event-repository.js b/ghost/core/core/server/services/members/members-api/repositories/event-repository.js index 3a138824a12..83207b31458 100644 --- a/ghost/core/core/server/services/members/members-api/repositories/event-repository.js +++ b/ghost/core/core/server/services/members/members-api/repositories/event-repository.js @@ -99,9 +99,7 @@ module.exports = class EventRepository { pageActions.push({type: 'email_complained_event', action: 'getEmailSpamComplaintEvents'}); - if (this._labsService.isSet('audienceFeedback')) { - pageActions.push({type: 'feedback_event', action: 'getFeedbackEvents'}); - } + pageActions.push({type: 'feedback_event', action: 'getFeedbackEvents'}); //Filter events to query let filteredPages = pageActions; diff --git a/ghost/core/core/server/services/newsletters/newsletters-service.js b/ghost/core/core/server/services/newsletters/newsletters-service.js index 770ce478606..a3adf51d0bc 100644 --- a/ghost/core/core/server/services/newsletters/newsletters-service.js +++ b/ghost/core/core/server/services/newsletters/newsletters-service.js @@ -303,13 +303,6 @@ class NewslettersService { } } - if (cleanedAttrs.feedback_enabled) { - if (!this.labs.isSet('audienceFeedback')) { - // Not allowed to set to true - cleanedAttrs.feedback_enabled = false; - } - } - // If one of the properties was changed, we need to reset sender_email in case it was not changed but is invalid in the database // which can happen after a config change (= auto correcting behaviour) const didChangeReplyTo = newsletter && attrs.sender_reply_to !== undefined && newsletter.get('sender_reply_to') !== attrs.sender_reply_to; diff --git a/ghost/core/core/server/web/members/app.js b/ghost/core/core/server/web/members/app.js index f9cdefed9ff..38a04cfdf69 100644 --- a/ghost/core/core/server/web/members/app.js +++ b/ghost/core/core/server/web/members/app.js @@ -123,7 +123,6 @@ module.exports = function setupMembersApp() { // Feedback membersApp.post( '/api/feedback', - labs.enabledMiddleware('audienceFeedback'), bodyParser.json({limit: '50mb'}), middleware.loadMemberSession, middleware.authMemberByUuid, diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 754bbf8c140..02f9eef6705 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -21,7 +21,6 @@ const messages = { // flags in this list always return `true`, allows quick global enable prior to full flag removal const GA_FEATURES = [ - 'audienceFeedback', 'announcementBar', 'customFonts', 'explore', diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap index 5d1755ee30f..97690aed12b 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap @@ -11,7 +11,6 @@ Object { "labs": Object { "additionalPaymentMethods": true, "announcementBar": true, - "audienceFeedback": true, "commentModeration": true, "customFonts": true, "editorExcerpt": true, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap index 96e7576b7e3..9c82e8c571c 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap @@ -1675,7 +1675,7 @@ exports[`Settings API Edit can edit Stripe settings when Stripe Connect limit is Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "5217", + "content-length": "5191", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-browser/admin/announcement-bar-settings.spec.js b/ghost/core/test/e2e-browser/admin/announcement-bar-settings.spec.js index db830cb5c00..242ba4c5aff 100644 --- a/ghost/core/test/e2e-browser/admin/announcement-bar-settings.spec.js +++ b/ghost/core/test/e2e-browser/admin/announcement-bar-settings.spec.js @@ -49,7 +49,7 @@ test.describe('Announcement Bar Settings', () => { async function goToAnnouncementBarSettings(sharedPage) { await test.step('Navigate to the announcement bar settings', async () => { - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await sharedPage.getByTestId('announcement-bar').getByRole('button', {name: 'Customize'}).click(); // Wait for the preview to load await getPreviewFrame(sharedPage).locator('body *:visible').first().waitFor(); diff --git a/ghost/core/test/e2e-browser/admin/membership-settings.spec.js b/ghost/core/test/e2e-browser/admin/membership-settings.spec.js index 1ca01db6a76..4ce8f6ab949 100644 --- a/ghost/core/test/e2e-browser/admin/membership-settings.spec.js +++ b/ghost/core/test/e2e-browser/admin/membership-settings.spec.js @@ -11,7 +11,7 @@ test.describe('Membership Settings', () => { // Open Portal settings await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await sharedPage.getByTestId('portal').getByRole('button', {name: 'Customize'}).click(); const modal = sharedPage.getByTestId('portal-modal'); diff --git a/ghost/core/test/e2e-browser/admin/portal-settings.spec.js b/ghost/core/test/e2e-browser/admin/portal-settings.spec.js index 64a0c9bda71..128163fea08 100644 --- a/ghost/core/test/e2e-browser/admin/portal-settings.spec.js +++ b/ghost/core/test/e2e-browser/admin/portal-settings.spec.js @@ -5,7 +5,7 @@ test.describe('Portal Settings', () => { test.describe('Links', () => { const openPortalLinks = async (sharedPage) => { await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await sharedPage.getByTestId('portal').getByRole('button', {name: 'Customize'}).click(); diff --git a/ghost/core/test/e2e-browser/admin/private-site.spec.js b/ghost/core/test/e2e-browser/admin/private-site.spec.js index 87b2b27064e..ab2171a88ce 100644 --- a/ghost/core/test/e2e-browser/admin/private-site.spec.js +++ b/ghost/core/test/e2e-browser/admin/private-site.spec.js @@ -7,7 +7,7 @@ test.describe('Site Settings', () => { // set private mode in admin "on" await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); const section = sharedPage.getByTestId('locksite'); diff --git a/ghost/core/test/e2e-browser/admin/publishing.spec.js b/ghost/core/test/e2e-browser/admin/publishing.spec.js index da781d0917e..83ba3bd062c 100644 --- a/ghost/core/test/e2e-browser/admin/publishing.spec.js +++ b/ghost/core/test/e2e-browser/admin/publishing.spec.js @@ -68,7 +68,7 @@ const checkPostPublished = async (page, {slug, title, body}) => { * @param {String} [options.body] */ const createPage = async (page, {title = 'Hello world', body = 'This is my post body.'} = {}) => { - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Pages'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Pages'}).click(); // Create a new post await page.locator('[data-test-new-page-button]').click(); @@ -620,7 +620,7 @@ test.describe('Updating post access', () => { await closePublishFlow(page); // go to settings and change the timezone - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await expect(page.getByTestId('timezone')).toContainText('UTC'); await page.getByTestId('timezone-select').click(); @@ -630,7 +630,7 @@ test.describe('Updating post access', () => { await expect(page.getByTestId('timezone')).toContainText('(GMT +9:00) Osaka, Sapporo, Tokyo'); await page.getByTestId('exit-settings').click(); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Posts'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Posts'}).click(); await page.locator('[data-test-post-id]', {hasText: /Published in timezones/}).click(); await openPostSettingsMenu(page); diff --git a/ghost/core/test/e2e-browser/admin/site-settings.spec.js b/ghost/core/test/e2e-browser/admin/site-settings.spec.js index a882f41767e..7c517d4246e 100644 --- a/ghost/core/test/e2e-browser/admin/site-settings.spec.js +++ b/ghost/core/test/e2e-browser/admin/site-settings.spec.js @@ -3,7 +3,7 @@ const test = require('../fixtures/ghost-test'); const {createPostDraft, createTier, disconnectStripe, generateStripeIntegrationToken, setupStripe, getStripeAccountId} = require('../utils'); const changeSubscriptionAccess = async (page, access) => { - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); const section = page.getByTestId('access'); const select = section.getByTestId('subscription-access-select'); diff --git a/ghost/core/test/e2e-browser/portal/invites.spec.js b/ghost/core/test/e2e-browser/portal/invites.spec.js index 5e76085acde..90151abd41f 100644 --- a/ghost/core/test/e2e-browser/portal/invites.spec.js +++ b/ghost/core/test/e2e-browser/portal/invites.spec.js @@ -10,7 +10,7 @@ test.describe('Portal', () => { test('New staff member can signup using an invite link', async ({sharedPage}) => { // Navigate to settings await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); const testEmail = `test-${Date.now()}@gmail.com`; @@ -72,7 +72,7 @@ test.describe('Portal', () => { test('New staff member can signup using an invite link with 2FA enabled', async ({sharedPage}) => { // Navigate to settings await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); const testEmail = `test-${Date.now()}@gmail.com`; diff --git a/ghost/core/test/e2e-browser/portal/member-actions.spec.js b/ghost/core/test/e2e-browser/portal/member-actions.spec.js index 3ea1faca1ef..9c7a41abcf2 100644 --- a/ghost/core/test/e2e-browser/portal/member-actions.spec.js +++ b/ghost/core/test/e2e-browser/portal/member-actions.spec.js @@ -7,7 +7,7 @@ const {createMember, impersonateMember} = require('../utils'); */ const addNewsletter = async (page) => { await page.goto('/ghost'); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); // create newsletter const section = page.getByTestId('newsletters'); diff --git a/ghost/core/test/e2e-browser/portal/offers.spec.js b/ghost/core/test/e2e-browser/portal/offers.spec.js index 5f4b60b205a..e24899493ad 100644 --- a/ghost/core/test/e2e-browser/portal/offers.spec.js +++ b/ghost/core/test/e2e-browser/portal/offers.spec.js @@ -30,7 +30,7 @@ test.describe('Portal', () => { // check that offer was added in the offer list screen await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await expect(await sharedPage.getByTestId('offers')).toContainText(offerName); await sharedPage.goto(offerLink); @@ -71,7 +71,7 @@ test.describe('Portal', () => { // go to member list on admin await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Members'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Members'}).click(); // // 1 member, should be Testy, on Portal Tier await expect(await sharedPage.getByRole('link', {name: 'Testy McTesterson testy+trial@example.com'}), 'Should have 1 paid member').toBeVisible(); @@ -107,7 +107,7 @@ test.describe('Portal', () => { // check that offer was added in the offer list screen await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await expect(sharedPage.getByTestId('offers')).toContainText(offerName); // open offer details page // await sharedPage.locator(`[data-test-offer="${offerName}"] a`).first().click(); @@ -153,7 +153,7 @@ test.describe('Portal', () => { // go to members list on admin await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Members'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Members'}).click(); // 1 member, should be Testy, on Portal Tier await expect(await sharedPage.getByRole('link', {name: 'Testy McTesterson testy+oneoff@example.com'}), 'Should have 1 paid member').toBeVisible(); @@ -184,7 +184,7 @@ test.describe('Portal', () => { }); await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await expect(await sharedPage.getByTestId('offers')).toContainText(offerName); await sharedPage.goto(offerLink); @@ -228,7 +228,7 @@ test.describe('Portal', () => { // Discounted price should not be visible for member for one-time offers await expect(portalFrameLocator.locator('text=$5.40/month'), 'Portal should show discounted price').toBeVisible(); await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Members'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Members'}).click(); // 1 member, should be Testy, on Portal Tier await expect(await sharedPage.getByRole('link', {name: 'Testy McTesterson testy+multi@example.com'}), 'Should have 1 paid member').toBeVisible(); @@ -259,7 +259,7 @@ test.describe('Portal', () => { // check that offer was added in the offer list screen await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await expect(sharedPage.getByTestId('offers')).toContainText(offerName); await sharedPage.goto(offerLink); @@ -301,7 +301,7 @@ test.describe('Portal', () => { // Discounted price should be visible for member for forever offers await expect(portalFrameLocator.locator('text=$5.40/month'), 'Portal should show discounted price').toBeVisible(); await sharedPage.goto('/ghost'); - await sharedPage.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Members'}).click(); + await sharedPage.getByRole('navigation').getByRole('link', {name: 'Members'}).click(); // 1 member, should be Testy, on Portal Tier await expect(await sharedPage.getByRole('link', {name: 'Testy McTesterson testy+forever@example.com'}), 'Should have 1 paid member').toBeVisible(); diff --git a/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js b/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js index ad929ab477a..1ab8a7c06af 100644 --- a/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js +++ b/ghost/core/test/e2e-browser/utils/e2e-browser-utils.js @@ -36,7 +36,7 @@ const setupGhost = async (page) => { const action = await Promise.race([ page.locator('.gh-signin').waitFor(options).then(() => actions.signin).catch(() => {}), page.locator('.gh-setup').waitFor(options).then(() => actions.setup).catch(() => {}), - page.locator('[data-sidebar="sidebar"]').waitFor(options).then(() => actions.noAction).catch(() => {}) + page.getByRole('navigation').waitFor(options).then(() => actions.noAction).catch(() => {}) ]); // Add owner user data from usual fixture @@ -56,7 +56,7 @@ const setupGhost = async (page) => { await page.getByPlaceholder('At least 10 characters').press('Enter'); - await page.locator('[data-sidebar="sidebar"]').waitFor(options); + await page.getByRole('navigation').waitFor(options); } }; @@ -70,7 +70,7 @@ const signInAsUserById = async (page, userId) => { await page.locator('#password').fill(user.password); await page.getByRole('button', {name: 'Sign in'}).click(); // Confirm we have reached Ghost Admin - await page.locator('[data-sidebar="sidebar"]').waitFor({state: 'visible', timeout: 10000}); + await page.getByRole('navigation').waitFor({state: 'visible', timeout: 10000}); }; const signOutCurrentUser = async (page) => { @@ -80,7 +80,7 @@ const signOutCurrentUser = async (page) => { const disconnectStripe = async (page) => { await deleteAllMembers(page); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await page.getByTestId('tiers').waitFor(); if (await page.isVisible('[data-testid="stripe-connected"]')) { await page.getByTestId('stripe-connected').first().click(); @@ -91,7 +91,7 @@ const disconnectStripe = async (page) => { const setupStripe = async (page, stripConnectIntegrationToken) => { await deleteAllMembers(page); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); await page.getByTestId('tiers').waitFor(); if (await page.isVisible('[data-testid="stripe-connected"]')) { // Disconnect if already connected @@ -114,7 +114,7 @@ const setupStripe = async (page, stripConnectIntegrationToken) => { // Setup Mailgun with fake data for Ghost Admin to allow bulk sending const setupMailgun = async (page) => { - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); const section = page.getByTestId('mailgun'); await section.getByRole('button', {name: 'Edit'}).click(); @@ -131,7 +131,7 @@ const setupMailgun = async (page) => { * @param {import('@playwright/test').Page} page */ const deleteAllMembers = async (page) => { - await page.locator('a[href="#/members/"]').first().click(); + await page.getByRole('navigation').getByRole('link', {name: 'Members'}).click(); const firstMember = page.locator('.gh-list tbody tr').first(); while (await Promise.race([ @@ -180,7 +180,7 @@ const impersonateMember = async (page) => { const createTier = async (page, {name, monthlyPrice, yearlyPrice, trialDays}, enableInPortal = true) => { await test.step('Create a tier', async () => { // Navigate to the member settings - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); // Tiers request can take time, so waiting until the Add tier button is visible before interacting await page.getByTestId('tiers').getByRole('button', {name: 'Add tier'}).waitFor(); @@ -252,7 +252,7 @@ const createOffer = async (page, {name, tierName, offerType, amount, discountTyp let offerLink; await test.step('Create an offer', async () => { await page.goto('/ghost'); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); // Keep offer names unique & <= 40 characters offerName = `${name} (${new ObjectID().toHexString().slice(0, 40 - name.length - 3)})`; @@ -368,7 +368,7 @@ const completeStripeSubscription = async (page, {awaitNetworkIdle = true} = {}) */ const createMember = async (page, {email, name, note, label = '', compedPlan}) => { await page.goto('/ghost'); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Members'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Members'}).click(); await page.waitForSelector('a[href="#/members/new/"] span'); await page.locator('a[href="#/members/new/"] span:has-text("New member")').click(); await page.waitForSelector('input[name="name"]'); @@ -408,7 +408,7 @@ const createMember = async (page, {email, name, note, label = '', compedPlan}) = * @param {String} [options.body] */ const createPostDraft = async (page, {title = 'Hello world', body = 'This is my post body.'} = {}) => { - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Posts'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Posts'}).click(); // Create a new post await page.locator('[data-test-new-post-button]').click(); @@ -435,7 +435,7 @@ const createPostDraft = async (page, {title = 'Hello world', body = 'This is my const goToMembershipPage = async (page) => { return await test.step('Open Membership settings', async () => { await page.goto('/ghost'); - await page.locator('[data-sidebar="sidebar"]').getByRole('link', {name: 'Settings'}).click(); + await page.getByRole('navigation').getByRole('link', {name: 'Settings'}).click(); // Tiers request can take time, so waiting until the tiers section is loaded before interacting with UI await page.getByTestId('tiers').waitFor(); }); diff --git a/ghost/core/test/unit/shared/labs.test.js b/ghost/core/test/unit/shared/labs.test.js index f77a8c1a6b1..a71d4393189 100644 --- a/ghost/core/test/unit/shared/labs.test.js +++ b/ghost/core/test/unit/shared/labs.test.js @@ -67,15 +67,15 @@ describe('Labs Service', function () { it('respects the value in config over GA keys', function () { configUtils.set('labs', { - audienceFeedback: false + announcementBar: false }); assert.deepEqual(labs.getAll(), expectedLabsObject({ - audienceFeedback: false, + announcementBar: false, members: true })); - assert.equal(labs.isSet('audienceFeedback'), false); + assert.equal(labs.isSet('announcementBar'), false); }); it('members flag is true when members_signup_access setting is "all"', function () {