diff --git a/apps/admin-x-design-system/src/assets/icons/error-fill.svg b/apps/admin-x-design-system/src/assets/icons/error-fill.svg new file mode 100644 index 0000000000..dfa0fd2ea0 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/error-fill.svg @@ -0,0 +1 @@ +Alert Triangle Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/apps/admin-x-design-system/src/assets/icons/info-fill.svg b/apps/admin-x-design-system/src/assets/icons/info-fill.svg new file mode 100644 index 0000000000..9866e28014 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/info-fill.svg @@ -0,0 +1 @@ +Information Circle Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/apps/admin-x-design-system/src/assets/icons/success-fill.svg b/apps/admin-x-design-system/src/assets/icons/success-fill.svg new file mode 100644 index 0000000000..9028cd1028 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/success-fill.svg @@ -0,0 +1 @@ +Check Circle 1 Streamline Icon: https://streamlinehq.com \ No newline at end of file diff --git a/apps/admin-x-design-system/src/global/Icon.tsx b/apps/admin-x-design-system/src/global/Icon.tsx index fed52d8b8a..61e1621f8d 100644 --- a/apps/admin-x-design-system/src/global/Icon.tsx +++ b/apps/admin-x-design-system/src/global/Icon.tsx @@ -3,7 +3,7 @@ import React from 'react'; const icons: Record>}> = import.meta.glob('../assets/icons/*.svg', {eager: true}); -export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'custom' | number; +export type IconSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'custom' | number; export interface IconProps { name: string; @@ -36,6 +36,9 @@ const Icon: React.FC = ({name, size = 'md', colorClass = '', classNam switch (size) { case 'custom': break; + case '2xs': + styles = 'w-2 h-2'; + break; case 'xs': styles = 'w-3 h-3'; break; diff --git a/apps/admin-x-design-system/src/global/Toast.stories.tsx b/apps/admin-x-design-system/src/global/Toast.stories.tsx index 3206e6dbd6..90cef8de10 100644 --- a/apps/admin-x-design-system/src/global/Toast.stories.tsx +++ b/apps/admin-x-design-system/src/global/Toast.stories.tsx @@ -36,12 +36,50 @@ type Story = StoryObj; export const Default: Story = { args: { + title: 'Toast title', message: 'Hello notification in a toast' } }; +export const TitleOnly: Story = { + args: { + title: 'Hello notification in a toast' + } +}; + +export const MinWidth: Story = { + args: { + title: 'Min toast' + } +}; + +export const TitleWithIcon: Story = { + args: { + title: 'Hello notification in a toast', + type: 'info', + options: { + duration: Infinity + } + } +}; + +export const MessageOnly: Story = { + args: { + message: 'Hey, this is a message in a toast. Almost like a message in a bottle.' + } +}; + +export const Info: Story = { + args: { + title: 'Toast title', + message: 'Hello success message in a toast', + type: 'info' + } +}; + export const Success: Story = { args: { + title: 'Toast title', message: 'Hello success message in a toast', type: 'success' } @@ -49,13 +87,26 @@ export const Success: Story = { export const Error: Story = { args: { + title: 'Toast title', message: 'Hello error message in a toast', type: 'error' } }; +export const Infinite: Story = { + args: { + title: 'Toast title', + message: 'Hello error message in a toast', + type: 'error', + options: { + duration: Infinity + } + } +}; + export const PageError: Story = { args: { + title: 'Toast title', message: 'This is a page error which should not be automatically dismissed.', type: 'pageError' } diff --git a/apps/admin-x-design-system/src/global/Toast.tsx b/apps/admin-x-design-system/src/global/Toast.tsx index 0d7662c69a..00703433a2 100644 --- a/apps/admin-x-design-system/src/global/Toast.tsx +++ b/apps/admin-x-design-system/src/global/Toast.tsx @@ -3,9 +3,10 @@ import React from 'react'; import {Toast as HotToast, ToastOptions, toast} from 'react-hot-toast'; import Icon from './Icon'; -export type ToastType = 'neutral' | 'success' | 'error' | 'pageError'; +export type ToastType = 'neutral' | 'info' | 'success' | 'error' | 'pageError'; export interface ShowToastProps { + title?: React.ReactNode; message?: React.ReactNode; type?: ToastType; icon?: React.ReactNode | string; @@ -31,35 +32,41 @@ const Toast: React.FC = ({ children, props }) => { + let iconColorClass = 'text-grey-500'; + switch (props?.type) { + case 'info': + props.icon = props.icon || 'info-fill'; + iconColorClass = 'text-grey-500'; + break; case 'success': - props.icon = props.icon || 'check-circle'; + props.icon = props.icon || 'success-fill'; + iconColorClass = 'text-green'; break; case 'error': - props.icon = props.icon || 'warning'; + props.icon = props.icon || 'error-fill'; + iconColorClass = 'text-red'; break; } const classNames = clsx( - 'z-[90] flex items-start justify-between gap-6 rounded px-4 py-3 text-sm font-medium text-white', - (props?.type === 'success' || props?.type === 'neutral') && 'w-[300px] bg-black dark:bg-grey-950', - props?.type === 'error' && 'w-[300px] bg-red', - props?.options?.position === 'top-center' && 'w-full max-w-[520px] bg-red', + 'relative z-[90] mb-[14px] ml-[6px] flex min-w-[272px] items-start justify-between gap-3 rounded-lg bg-white p-4 text-sm text-black shadow-md-heavy dark:bg-grey-925 dark:text-white', + props?.options?.position === 'top-center' ? 'max-w-[520px]' : 'max-w-[320px]', t.visible ? (props?.options?.position === 'top-center' ? 'animate-toaster-top-in' : 'animate-toaster-in') : 'animate-toaster-out' ); return (
-
+
{props?.icon && (typeof props.icon === 'string' ? -
: props.icon)} +
: props.icon)} {children}
-
@@ -69,6 +76,7 @@ const Toast: React.FC = ({ export default Toast; export const showToast = ({ + title, message, type = 'neutral', icon = '', @@ -93,7 +101,12 @@ export const showToast = ({ icon: icon, options: options }} t={t}> - {message} +
+ {title && {title}} + {message && +
{message}
+ } +
), { diff --git a/apps/admin-x-design-system/tailwind.config.cjs b/apps/admin-x-design-system/tailwind.config.cjs index ca58d6f2a0..d554015aa4 100644 --- a/apps/admin-x-design-system/tailwind.config.cjs +++ b/apps/admin-x-design-system/tailwind.config.cjs @@ -97,6 +97,7 @@ module.exports = { xs: '0 0 1px rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.03), 0 8px 10px -12px rgba(0,0,0,.1)', sm: '0 0 1px rgba(0,0,0,.12), 0 1px 6px rgba(0,0,0,0.03), 0 8px 10px -8px rgba(0,0,0,.1)', md: '0 0 1px rgba(0,0,0,0.12), 0 1px 6px rgba(0,0,0,0.03), 0 8px 10px -8px rgba(0,0,0,0.05), 0px 24px 37px -21px rgba(0, 0, 0, 0.05)', + 'md-heavy': '0 0 1px rgba(0,0,0,0.22), 0 1px 6px rgba(0,0,0,0.15), 0 8px 10px -8px rgba(0,0,0,0.16), 0px 24px 37px -21px rgba(0, 0, 0, 0.46)', lg: '0 0 7px rgba(0, 0, 0, 0.08), 0 2.1px 2.2px -5px rgba(0, 0, 0, 0.011), 0 5.1px 5.3px -5px rgba(0, 0, 0, 0.016), 0 9.5px 10px -5px rgba(0, 0, 0, 0.02), 0 17px 17.9px -5px rgba(0, 0, 0, 0.024), 0 31.8px 33.4px -5px rgba(0, 0, 0, 0.029), 0 76px 80px -5px rgba(0, 0, 0, 0.04)', xl: '0 2.8px 2.2px rgba(0, 0, 0, 0.02), 0 6.7px 5.3px rgba(0, 0, 0, 0.028), 0 12.5px 10px rgba(0, 0, 0, 0.035), 0 22.3px 17.9px rgba(0, 0, 0, 0.042), 0 41.8px 33.4px rgba(0, 0, 0, 0.05), 0 100px 80px rgba(0, 0, 0, 0.07)', inner: 'inset 0 0 4px 0 rgb(0 0 0 / 0.08)', @@ -106,32 +107,26 @@ module.exports = { keyframes: { toasterIn: { '0.00%': { - opacity: '0', - transform: 'translateX(-232.05px)' + transform: 'translateY(100%)' }, '26.52%': { - opacity: '0.5', - transform: 'translateX(5.90px)' + transform: 'translateY(-3.90px)' }, '63.26%': { - opacity: '1', - transform: 'translateX(-1.77px)' + transform: 'translateY(1.2px)' }, '100.00%': { - transform: 'translateX(0px)' + transform: 'translateY(0px)' } }, toasterTopIn: { '0.00%': { - opacity: '0', transform: 'translateY(-82px)' }, '26.52%': { - opacity: '0.5', transform: 'translateY(5.90px)' }, '63.26%': { - opacity: '1', transform: 'translateY(-1.77px)' }, '100.00%': { @@ -264,7 +259,7 @@ module.exports = { sm: '0.3rem', DEFAULT: '0.4rem', md: '0.6rem', - lg: '0.7rem', + lg: '0.8rem', xl: '1.2rem', '2xl': '1.6rem', '3xl': '2.4rem', @@ -274,7 +269,7 @@ module.exports = { '2xs': '1.0rem', base: '1.4rem', xs: '1.2rem', - sm: '1.32rem', + sm: '1.3rem', md: '1.40rem', lg: '1.65rem', xl: '2rem', diff --git a/apps/admin-x-framework/src/hooks/useForm.ts b/apps/admin-x-framework/src/hooks/useForm.ts index d30657813a..b68646ff98 100644 --- a/apps/admin-x-framework/src/hooks/useForm.ts +++ b/apps/admin-x-framework/src/hooks/useForm.ts @@ -85,6 +85,7 @@ const useForm = ({initialState, savingDelay, savedDelay = 2000, onSave, o // function to save the changed settings via API const handleSave = useCallback(async (options = {}) => { if (!validate()) { + setSaveState('error'); return false; } @@ -122,10 +123,26 @@ const useForm = ({initialState, savingDelay, savedDelay = 2000, onSave, o setSaveState('unsaved'); }, []); + let okColor: ButtonColor = 'black'; + if (saveState === 'saved') { + okColor = 'green'; + } else if (saveState === 'error') { + okColor = 'red'; + } + + let okLabel = ''; + if (saveState === 'saved') { + okLabel = 'Saved'; + } else if (saveState === 'saving') { + okLabel = 'Saving...'; + } else if (saveState === 'error') { + okLabel = 'Retry'; + } + const okProps: OkProps = { disabled: saveState === 'saving', - color: saveState === 'saved' ? 'green' : 'black', - label: saveState === 'saved' ? 'Saved' : (saveState === 'saving' ? 'Saving...' : undefined) + color: okColor, + label: okLabel || undefined }; return { diff --git a/apps/admin-x-framework/src/hooks/useHandleError.ts b/apps/admin-x-framework/src/hooks/useHandleError.ts index 9e5e8d88a9..740ecaf1a3 100644 --- a/apps/admin-x-framework/src/hooks/useHandleError.ts +++ b/apps/admin-x-framework/src/hooks/useHandleError.ts @@ -46,17 +46,17 @@ const useHandleError = () => { } else if (error instanceof ValidationError && error.data?.errors[0]) { showToast({ message: error.data.errors[0].context || error.data.errors[0].message, - type: 'pageError' + type: 'error' }); } else if (error instanceof APIError) { showToast({ message: error.message, - type: 'pageError' + type: 'error' }); } else { showToast({ message: 'Something went wrong, please try again.', - type: 'pageError' + type: 'error' }); } }, [sentryDSN]); diff --git a/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx b/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx index 24b2cdeeab..3c1c50f344 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/DangerZone.tsx @@ -21,8 +21,8 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { try { await deleteAllContent(null); showToast({ - type: 'success', - message: 'All content deleted from database.' + title: 'All content deleted from database.', + type: 'success' }); modal?.remove(); await client.refetchQueries(); diff --git a/apps/admin-x-settings/src/components/settings/advanced/Integrations.tsx b/apps/admin-x-settings/src/components/settings/advanced/Integrations.tsx index b27da6c76d..b2b6061771 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/Integrations.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/Integrations.tsx @@ -184,8 +184,11 @@ const CustomIntegrations: React.FC<{integrations: Integration[]}> = ({integratio await deleteIntegration(integration.id); confirmModal?.remove(); showToast({ - message: 'Integration deleted', - type: 'success' + title: 'Integration deleted', + type: 'info', + options: { + position: 'bottom-left' + } }); } catch (e) { handleError(e); diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/CustomIntegrationModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/CustomIntegrationModal.tsx index 7037a694b8..18fddf31b1 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations/CustomIntegrationModal.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/CustomIntegrationModal.tsx @@ -4,12 +4,11 @@ import React, {useEffect, useState} from 'react'; import WebhooksTable from './WebhooksTable'; import {APIError} from '@tryghost/admin-x-framework/errors'; import {APIKey, useRefreshAPIKey} from '@tryghost/admin-x-framework/api/apiKeys'; -import {ConfirmationModal, Form, ImageUpload, Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {ConfirmationModal, Form, ImageUpload, Modal, TextField} from '@tryghost/admin-x-design-system'; import {Integration, useBrowseIntegrations, useEditIntegration} from '@tryghost/admin-x-framework/api/integrations'; import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images'; -import {toast} from 'react-hot-toast'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({integration}) => { @@ -37,7 +36,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in const newErrors: Record = {}; if (!formState.name) { - newErrors.name = 'Name is required.'; + newErrors.name = 'Enter integration title'; } return newErrors; @@ -88,16 +87,10 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in okLabel={okProps.label || 'Save & close'} size='md' testId='custom-integration-modal' - title={formState.name} + title={formState.name || 'Custom integration'} stickyFooter onOk={async () => { - toast.remove(); - if (!(await handleSave({fakeWhenUnchanged: true}))) { - showToast({ - type: 'pageError', - message: 'Can\'t save integration, please double check that you\'ve filled all mandatory fields.' - }); - } + await handleSave({fakeWhenUnchanged: true}); }} >
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/PinturaModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/PinturaModal.tsx index 91aeb61043..928f112a5d 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations/PinturaModal.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/PinturaModal.tsx @@ -62,7 +62,7 @@ const PinturaModal = NiceModal.create(() => { showToast({ type: 'success', - message: `Pintura ${form} uploaded successfully` + title: `Pintura ${form} uploaded` }); } catch (e) { setUploadingState({js: false, css: false}); diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/SlackModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/SlackModal.tsx index 6ff6d60860..f6ce113a57 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations/SlackModal.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/SlackModal.tsx @@ -32,8 +32,8 @@ const SlackModal = NiceModal.create(() => { if (await handleSave()) { await testSlack(null); showToast({ - message: 'Check your Slack channel for the test message', - type: 'neutral' + title: 'Check your Slack channel for the test message', + type: 'info' }); } }; diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhookModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhookModal.tsx index a657c78c45..5c9bdbc8bc 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhookModal.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhookModal.tsx @@ -1,9 +1,8 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React from 'react'; -import toast from 'react-hot-toast'; import validator from 'validator'; import webhookEventOptions from './webhookEventOptions'; -import {Form, Modal, Select, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Form, Modal, Select, TextField} from '@tryghost/admin-x-design-system'; import {Webhook, useCreateWebhook, useEditWebhook} from '@tryghost/admin-x-framework/api/webhooks'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -59,14 +58,8 @@ const WebhookModal: React.FC = ({webhook, integrationId}) => title='Add webhook' formSheet onOk={async () => { - toast.remove(); if (await handleSave()) { modal.remove(); - } else { - showToast({ - type: 'pageError', - message: 'Can\'t save webhook, please double check that you\'ve filled all mandatory fields.' - }); } }} > diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhooksTable.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhooksTable.tsx index 83f03a502d..10d188e719 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhooksTable.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/WebhooksTable.tsx @@ -22,7 +22,7 @@ const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => { confirmModal?.remove(); showToast({ message: 'Webhook deleted', - type: 'success' + type: 'info' }); } catch (e) { handleError(e); diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx index 84b47fd409..df819ae119 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/BetaFeatures.tsx @@ -32,8 +32,8 @@ const BetaFeatures: React.FC = () => { setRedirectsUploading(true); await uploadRedirects(file); showToast({ - type: 'success', - message: 'Redirects uploaded successfully' + title: 'Redirects uploaded', + type: 'success' }); } catch (e) { handleError(e); @@ -58,7 +58,7 @@ const BetaFeatures: React.FC = () => { await uploadRoutes(file); showToast({ type: 'success', - message: 'Routes uploaded successfully' + title: 'Routes uploaded' }); } catch (e) { handleError(e); diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/MigrationOptions.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/MigrationOptions.tsx index 80aa9b0d7a..57dfdfdd29 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/MigrationOptions.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/MigrationOptions.tsx @@ -65,7 +65,7 @@ const MigrationOptions: React.FC = () => { await deleteAllContent(null); showToast({ type: 'success', - message: 'All content deleted from database.' + title: 'All content deleted from database.' }); modal?.remove(); await client.refetchQueries(); diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/AddNewsletterModal.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/AddNewsletterModal.tsx index 7466604e0d..3b43deea91 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters/AddNewsletterModal.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters/AddNewsletterModal.tsx @@ -1,10 +1,9 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useEffect} from 'react'; -import {Form, LimitModal, Modal, TextArea, TextField, Toggle, showToast} from '@tryghost/admin-x-design-system'; +import {Form, LimitModal, Modal, TextArea, TextField, Toggle} from '@tryghost/admin-x-design-system'; import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter'; import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; import {numberWithCommas} from '../../../../utils/helpers'; -import {toast} from 'react-hot-toast'; import {useAddNewsletter} from '@tryghost/admin-x-framework/api/newsletters'; import {useBrowseMembers} from '@tryghost/admin-x-framework/api/members'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -78,14 +77,8 @@ const AddNewsletterModal: React.FC = () => { testId='add-newsletter-modal' title='Create newsletter' onOk={async () => { - toast.remove(); if (await handleSave()) { modal.remove(); - } else { - showToast({ - type: 'pageError', - message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.' - }); } }} > diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModal.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModal.tsx index eb150ebec1..af79431360 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterDetailModal.tsx @@ -140,7 +140,7 @@ const Sidebar: React.FC<{ modal?.remove(); showToast({ type: 'success', - message: 'Newsletter archived successfully' + message: 'Newsletter archived' }); } catch (e) { handleError(e); @@ -173,7 +173,7 @@ const Sidebar: React.FC<{ modal?.remove(); showToast({ type: 'success', - message: 'Newsletter reactivated successfully' + message: 'Newsletter reactivated' }); } }); @@ -528,7 +528,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b showToast({ icon: 'email', message: toastMessage, - type: 'neutral' + type: 'info' }); } }, @@ -581,12 +581,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b testId='newsletter-modal' title='Newsletter' onOk={async () => { - if (!(await handleSave({fakeWhenUnchanged: true}))) { - showToast({ - type: 'pageError', - message: 'Can\'t save newsletter, please double check that you\'ve filled all mandatory fields.' - }); - } + await handleSave({fakeWhenUnchanged: true}); }} />; }; diff --git a/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx b/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx index f7d4edd5f6..e6e066fb4a 100644 --- a/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx @@ -74,7 +74,7 @@ const InviteUserModal = NiceModal.create(() => { const roles = rolesQuery.data.roles; const assignableRoles = assignableRolesQuery.data.roles; - let okLabel = 'Send invitation now'; + let okLabel = 'Send invitation'; if (saveState === 'saving') { okLabel = 'Sending...'; } else if (saveState === 'saved') { @@ -123,7 +123,8 @@ const InviteUserModal = NiceModal.create(() => { setSaveState('saved'); showToast({ - message: `Invitation successfully sent to ${email}`, + title: `Invitation sent`, + message: `${email}`, type: 'success' }); @@ -131,18 +132,19 @@ const InviteUserModal = NiceModal.create(() => { updateRoute('staff?tab=invited'); } catch (e) { setSaveState('error'); - let message = (Your invitation failed to send.
If the problem persists, contact support..
); + let title = 'Failed to send invitation'; + let message = (If the problem persists, contact support..); if (e instanceof APIError) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let data = e.data as any; // we have unknown data types in the APIError/error classes if (data?.errors?.[0]?.type === 'EmailError') { - message = (Your invitation failed to send
Please check your Mailgun configuration. If the problem persists, contact support.
); + message = (Check your Mailgun configuration.); } } showToast({ + title, message, - type: 'neutral', - icon: 'warning' + type: 'error' }); handleError(e, {withToast: false}); return; @@ -178,12 +180,17 @@ const InviteUserModal = NiceModal.create(() => { }); }); + if (!!errors.email) { + okLabel = 'Retry'; + } + return ( { updateRoute('staff'); }} cancelLabel='' + okColor={saveState === 'error' || !!errors.email ? 'red' : 'black'} okLabel={okLabel} testId='invite-user-modal' title='Invite a new staff user' diff --git a/apps/admin-x-settings/src/components/settings/general/PublicationLanguage.tsx b/apps/admin-x-settings/src/components/settings/general/PublicationLanguage.tsx index fb169a627c..aca29daf85 100644 --- a/apps/admin-x-settings/src/components/settings/general/PublicationLanguage.tsx +++ b/apps/admin-x-settings/src/components/settings/general/PublicationLanguage.tsx @@ -13,8 +13,20 @@ const PublicationLanguage: React.FC<{ keywords: string[] }> = ({keywords}) => { handleCancel, updateSetting, focusRef, + errors, + clearError, handleEditingChange - } = useSettingGroup(); + } = useSettingGroup({ + onValidate: () => { + if (!publicationLanguage) { + return { + publicationLanguage: 'Enter a value' + }; + } + + return {}; + } + }); const [publicationLanguage] = getSettingValues(localSettings, ['locale']) as string[]; @@ -42,12 +54,14 @@ const PublicationLanguage: React.FC<{ keywords: string[] }> = ({keywords}) => { const inputFields = ( clearError('password')} /> ); diff --git a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx index fdeaf91d5c..f44833c327 100644 --- a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx @@ -16,7 +16,6 @@ import {HostLimitError, useLimiter} from '../../../hooks/useLimiter'; import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; import {User, canAccessSettings, hasAdminAccess, isAdminUser, isAuthorOrContributor, isEditorUser, isOwnerUser, useDeleteUser, useEditUser, useMakeOwner} from '@tryghost/admin-x-framework/api/users'; import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images'; -import {toast} from 'react-hot-toast'; import {useGlobalData} from '../../providers/GlobalDataProvider'; import {validateFacebookUrl, validateTwitterUrl} from '../../../utils/socialUrls'; @@ -36,7 +35,7 @@ const validators: Record) => string> = { }, email: ({email}) => { const valid = validator.isEmail(email || ''); - return valid ? '' : 'Please enter a valid email address'; + return valid ? '' : 'Enter a valid email address'; }, url: ({url}) => { const valid = !url || validator.isURL(url); @@ -52,7 +51,7 @@ const validators: Record) => string> = { }, website: ({website}) => { const valid = !website || (validator.isURL(website) && website.length <= 2000); - return valid ? '' : 'Website is not a valid url'; + return valid ? '' : 'Enter a valid URL'; }, facebook: ({facebook}) => { try { @@ -192,7 +191,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { setFormState(() => updatedUserData); modal?.remove(); showToast({ - message: _user.status === 'inactive' ? 'User un-suspended' : 'User suspended', + title: _user.status === 'inactive' ? 'User un-suspended' : 'User suspended', type: 'success' }); } catch (e) { @@ -220,7 +219,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { mainModal?.remove(); navigateOnClose(); showToast({ - message: 'User deleted', + title: 'User deleted', type: 'success' }); } catch (e) { @@ -241,7 +240,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { await makeOwner(user.id); modal?.remove(); showToast({ - message: 'Ownership transferred', + title: 'Ownership transferred', type: 'success' }); } catch (e) { @@ -361,14 +360,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { stickyFooter={true} testId='user-detail-modal' onOk={async () => { - toast.remove(); - - if (!(await handleSave({fakeWhenUnchanged: true}))) { - showToast({ - type: 'pageError', - message: 'Can\'t save user, please double check that you\'ve filled all mandatory fields.' - }); - } + await (handleSave({fakeWhenUnchanged: true})); }} >
diff --git a/apps/admin-x-settings/src/components/settings/general/Users.tsx b/apps/admin-x-settings/src/components/settings/general/Users.tsx index ed1b9486e5..aaede23f54 100644 --- a/apps/admin-x-settings/src/components/settings/general/Users.tsx +++ b/apps/admin-x-settings/src/components/settings/general/Users.tsx @@ -127,7 +127,8 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { setRevokeState('progress'); await deleteInvite(invite.id); showToast({ - message: `Invitation revoked (${invite.email})`, + title: `Invitation revoked`, + message: invite.email, type: 'success' }); } catch (e) { @@ -151,7 +152,8 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { roleId: invite.role_id }); showToast({ - message: `Invitation resent! (${invite.email})`, + title: `Invitation resent`, + message: invite.email, type: 'success' }); } catch (e) { diff --git a/apps/admin-x-settings/src/components/settings/general/users/ChangePasswordForm.tsx b/apps/admin-x-settings/src/components/settings/general/users/ChangePasswordForm.tsx index 9dbb0c6615..c7570c87f2 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/ChangePasswordForm.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/ChangePasswordForm.tsx @@ -218,8 +218,8 @@ const ChangePasswordForm: React.FC<{user: User}> = ({user}) => { } catch (e) { setSaveState(''); showToast({ - type: 'pageError', - message: e instanceof ValidationError ? e.message : `Couldn't update password. Please try again.` + type: 'error', + title: e instanceof ValidationError ? e.message : `Couldn't update password. Please try again.` }); handleError(e, {withToast: false}); } diff --git a/apps/admin-x-settings/src/components/settings/general/users/ProfileBasics.tsx b/apps/admin-x-settings/src/components/settings/general/users/ProfileBasics.tsx index 57afa44421..cac841a3fb 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/ProfileBasics.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/ProfileBasics.tsx @@ -5,7 +5,7 @@ import {UserDetailProps} from '../UserDetailModal'; import {hasAdminAccess} from '@tryghost/admin-x-framework/api/users'; import {useGlobalData} from '../../../providers/GlobalDataProvider'; -const BasicInputs: React.FC = ({errors, validateField, clearError, user, setUserData}) => { +const BasicInputs: React.FC = ({errors, clearError, user, setUserData}) => { const {currentUser} = useGlobalData(); return ( @@ -16,9 +16,6 @@ const BasicInputs: React.FC = ({errors, validateField, clearErr maxLength={191} title="Full name" value={user.name} - onBlur={(e) => { - validateField('name', e.target.value); - }} onChange={(e) => { setUserData({...user, name: e.target.value}); }} @@ -30,9 +27,6 @@ const BasicInputs: React.FC = ({errors, validateField, clearErr maxLength={191} title="Email" value={user.email} - onBlur={(e) => { - validateField('email', e.target.value); - }} onChange={(e) => { setUserData({...user, email: e.target.value}); }} diff --git a/apps/admin-x-settings/src/components/settings/general/users/ProfileDetails.tsx b/apps/admin-x-settings/src/components/settings/general/users/ProfileDetails.tsx index 0df0ac7f47..df7a0d875b 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/ProfileDetails.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/ProfileDetails.tsx @@ -2,6 +2,7 @@ import CustomHeader from './CustomHeader'; import {SettingGroup, SettingGroupContent, TextArea, TextField} from '@tryghost/admin-x-design-system'; import {UserDetailProps} from '../UserDetailModal'; import {facebookHandleToUrl, facebookUrlToHandle, twitterHandleToUrl, twitterUrlToHandle, validateFacebookUrl, validateTwitterUrl} from '../../../../utils/socialUrls'; + import {useState} from 'react'; export const DetailsInputs: React.FC = ({errors, clearError, validateField, user, setUserData}) => { @@ -16,22 +17,19 @@ export const DetailsInputs: React.FC = ({errors, clearError, va maxLength={65535} title="Location" value={user.location || ''} - onBlur={(e) => { - validateField('location', e.target.value); - }} onChange={(e) => { setUserData({...user, location: e.target.value}); }} onKeyDown={() => clearError('location')} /> { - validateField('url', e.target.value); - }} + // onBlur={(e) => { + // validateField('url', e.target.value); + // }} onChange={(e) => { setUserData({...user, website: e.target.value}); }} @@ -76,9 +74,6 @@ export const DetailsInputs: React.FC = ({errors, clearError, va maxLength={65535} title="Bio" value={user.bio || ''} - onBlur={(e) => { - validateField('bio', e.target.value); - }} onChange={(e) => { setUserData({...user, bio: e.target.value}); }} diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx index 159dba5d2c..2a6be523ce 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/AddOfferModal.tsx @@ -640,8 +640,9 @@ const AddOfferModal = () => { if (!isErrorsEmpty) { toast.remove(); showToast({ - type: 'pageError', - message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly' + title: 'Can\'t save offer', + type: 'info', + message: 'Make sure you filled all required fields' }); return; } @@ -649,13 +650,7 @@ const AddOfferModal = () => { try { if (await handleSave({force: true})) { return; - } else { - toast.remove(); - showToast({ - type: 'pageError', - message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly' - }); - }; + } } catch (e) { let message; @@ -664,10 +659,13 @@ const AddOfferModal = () => { } toast.remove(); - showToast({ - type: 'pageError', - message: message || 'Something went wrong while saving the offer, please try again' - }); + if (message) { + showToast({ + title: 'Can\'t save offer', + type: 'error', + message: message || 'Please try again later' + }); + } } }} />; diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx index 39e4e61b1a..1ea97c102b 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/EditOfferModal.tsx @@ -67,7 +67,7 @@ const Sidebar: React.FC<{ modal?.remove(); showToast({ type: 'success', - message: 'Offer archived successfully' + title: 'Offer archived' }); updateRoute('offers/edit'); } catch (e) { @@ -88,7 +88,7 @@ const Sidebar: React.FC<{ modal?.remove(); showToast({ type: 'success', - message: 'Offer reactivated successfully' + title: 'Offer reactivated' }); updateRoute('offers/edit'); } catch (e) { @@ -284,12 +284,6 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => { try { if (await handleSave({force: true})) { return; - } else { - toast.remove(); - showToast({ - type: 'pageError', - message: 'Can\'t save offer, please double check that you\'ve filled all mandatory fields correctly' - }); } } catch (e) { let message; @@ -299,10 +293,13 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => { } toast.remove(); - showToast({ - type: 'pageError', - message: message || 'Something went wrong while saving the offer, please try again' - }); + if (message) { + showToast({ + title: 'Can\'t save offer', + type: 'error', + message: 'Please try again later' + }); + } } }} /> : null; }; diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx index 5328706e42..338cdc689e 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers/OffersIndex.tsx @@ -212,8 +212,8 @@ export const OffersIndexModal = () => { onClick: () => { if (paidActiveTiers.length === 0) { showToast({ - type: 'neutral', - message: 'You must have an active tier to create an offer.' + type: 'info', + title: 'You must have an active tier to create an offer.' }); } else { updateRoute('offers/new'); diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations/AddRecommendationModal.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations/AddRecommendationModal.tsx index 926dd8d111..ca06d45d76 100644 --- a/apps/admin-x-settings/src/components/settings/growth/recommendations/AddRecommendationModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/recommendations/AddRecommendationModal.tsx @@ -131,8 +131,8 @@ const AddRecommendationModal: React.FC = ({r await addRecommendation(state); modal.remove(); showToast({ - message: 'Successfully added a recommendation', + title: 'Recommendation added', type: 'success' }); trackEvent('Recommendation Added', { @@ -38,14 +38,6 @@ const AddRecommendationModalConfirm: React.FC = ({r onSaveError: handleError, onValidate: (state) => { const newErrors = validateDescriptionForm(state); - - if (Object.keys(newErrors).length !== 0) { - showToast({ - type: 'pageError', - message: 'Can\'t add recommendation, please double check that you\'ve filled all mandatory fields correctly.' - }); - } - return newErrors; } }); @@ -119,8 +111,8 @@ const AddRecommendationModalConfirm: React.FC = ({r await handleSave({force: true}); } catch (e) { showToast({ - type: 'pageError', - message: 'Something went wrong when adding this recommendation, please try again.' + type: 'error', + title: 'Something went wrong when adding this recommendation, please try again.' }); } }} diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations/EditRecommendationModal.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations/EditRecommendationModal.tsx index 2bdb282159..b31d71d772 100644 --- a/apps/admin-x-settings/src/components/settings/growth/recommendations/EditRecommendationModal.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/recommendations/EditRecommendationModal.tsx @@ -34,14 +34,6 @@ const EditRecommendationModal: React.FC { const newErrors = validateDescriptionForm(state); - - if (Object.keys(newErrors).length !== 0) { - showToast({ - type: 'pageError', - message: 'Can\'t edit recommendation, please double check that you\'ve filled all mandatory fields correctly.' - }); - } - return newErrors; } }); @@ -63,13 +55,10 @@ const EditRecommendationModal: React.FC { testId='portal-modal' title='Portal' onOk={async () => { - if (Object.values(errors).filter(Boolean).length) { - showToast({ - type: 'pageError', - message: 'Can\'t save settings, please double check that you\'ve filled all mandatory fields.' - }); - } else { + if (!Object.values(errors).filter(Boolean).length) { await handleSave({force: true}); } }} diff --git a/apps/admin-x-settings/src/components/settings/membership/stripe/StripeConnectModal.tsx b/apps/admin-x-settings/src/components/settings/membership/stripe/StripeConnectModal.tsx index dde833afc9..a090fa441c 100644 --- a/apps/admin-x-settings/src/components/settings/membership/stripe/StripeConnectModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/stripe/StripeConnectModal.tsx @@ -225,8 +225,9 @@ const Direct: React.FC<{onClose: () => void}> = ({onClose}) => { } catch (e) { if (e instanceof JSONError) { showToast({ - type: 'pageError', - message: 'Failed to save settings. Please check you copied both keys correctly.' + title: 'Failed to save settings', + type: 'error', + message: 'Check you copied both keys correctly' }); return; } diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx index eb2862b3b5..2fcff193d8 100644 --- a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx @@ -9,7 +9,6 @@ import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing import {Tier, useAddTier, useBrowseTiers, useEditTier} from '@tryghost/admin-x-framework/api/tiers'; import {currencies, currencySelectGroups, validateCurrencyAmount} from '../../../../utils/currency'; import {getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; -import {toast} from 'react-hot-toast'; export type TierFormState = Partial> & { trial_days: string; @@ -155,7 +154,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { confirmModal?.remove(); showToast({ type: 'success', - message: `Tier ${tier.active ? 'archived' : 'reactivated'} successfully` + title: `Tier ${tier.active ? 'archived' : 'reactivated'}` }); } }); @@ -195,15 +194,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { title={(tier ? (tier.active ? 'Edit tier' : 'Edit archived tier') : 'New tier')} stickyFooter onOk={async () => { - toast.remove(); - - if (!await handleSave({fakeWhenUnchanged: true})) { - showToast({ - type: 'pageError', - message: 'Can\'t save tier, please double check that you\'ve filled all mandatory fields.' - }); - return; - } + await handleSave({fakeWhenUnchanged: true}); }} >
diff --git a/apps/admin-x-settings/src/components/settings/site/AnnouncementBarModal.tsx b/apps/admin-x-settings/src/components/settings/site/AnnouncementBarModal.tsx index 2966d5e132..d95236534d 100644 --- a/apps/admin-x-settings/src/components/settings/site/AnnouncementBarModal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/AnnouncementBarModal.tsx @@ -215,7 +215,7 @@ const AnnouncementBarModal: React.FC = () => { onOk={async () => { if (!(await handleSave({fakeWhenUnchanged: true}))) { showToast({ - type: 'pageError', + type: 'error', message: 'An error occurred while saving your changes. Please try again.' }); } diff --git a/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx b/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx index 688f00ec91..1af12ccf68 100644 --- a/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/ThemeModal.tsx @@ -169,7 +169,7 @@ const ThemeToolbar: React.FC = ({ let title = 'Upload successful'; let prompt = <> - {uploadedTheme.name} uploaded successfully. + {uploadedTheme.name} uploaded ; if (!uploadedTheme.active) { @@ -184,7 +184,7 @@ const ThemeToolbar: React.FC = ({ title = `Upload successful with ${hasErrors ? 'errors' : 'warnings'}`; prompt = <> - The theme "{uploadedTheme.name}" was installed successfully but we detected some {hasErrors ? 'errors' : 'warnings'}. + The theme "{uploadedTheme.name}" was installed but we detected some {hasErrors ? 'errors' : 'warnings'}. ; if (!uploadedTheme.active) { @@ -351,8 +351,9 @@ const ChangeThemeModal: React.FC = ({source, themeRef}) = if (data?.themes[0]) { await activateTheme(data.themes[0].name); showToast({ + title: 'Theme activated', type: 'success', - message:
{data.themes[0].name} is now your active theme.
+ message:
{data.themes[0].name} is now your active theme
}); } confirmModal?.remove(); diff --git a/apps/admin-x-settings/src/components/settings/site/theme/AdvancedThemeSettings.tsx b/apps/admin-x-settings/src/components/settings/site/theme/AdvancedThemeSettings.tsx index 642e5a9326..93c855ee36 100644 --- a/apps/admin-x-settings/src/components/settings/site/theme/AdvancedThemeSettings.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/AdvancedThemeSettings.tsx @@ -52,8 +52,9 @@ const ThemeActions: React.FC = ({ try { await activateTheme(theme.name); showToast({ + title: 'Theme activated', type: 'success', - message:
{theme.name} is now your active theme.
+ message:
{theme.name} is now your active theme
}); } catch (e) { handleError(e); diff --git a/apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx b/apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx index 53bad6185d..c1cea32d19 100644 --- a/apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/ThemeInstalledModal.tsx @@ -87,6 +87,7 @@ const ThemeInstalledModal: React.FC<{ const updatedTheme = resData.themes[0]; showToast({ + title: 'Theme activated', type: 'success', message:
{updatedTheme.name} is now your active theme.
}); diff --git a/apps/admin-x-settings/src/hooks/useSettingGroup.tsx b/apps/admin-x-settings/src/hooks/useSettingGroup.tsx index 7d6272a8ad..8af1ca7cdd 100644 --- a/apps/admin-x-settings/src/hooks/useSettingGroup.tsx +++ b/apps/admin-x-settings/src/hooks/useSettingGroup.tsx @@ -2,9 +2,8 @@ import React, {useEffect, useRef, useState} from 'react'; import {ErrorMessages, OkProps, SaveHandler, SaveState, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {Setting, SettingValue, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; import {SiteData} from '@tryghost/admin-x-framework/api/site'; -import {showToast, useGlobalDirtyState} from '@tryghost/admin-x-design-system'; -import {toast} from 'react-hot-toast'; import {useGlobalData} from '../components/providers/GlobalDataProvider'; +import {useGlobalDirtyState} from '@tryghost/admin-x-design-system'; interface LocalSetting extends Setting { dirty?: boolean; @@ -107,15 +106,10 @@ const useSettingGroup = ({savingDelay, onValidate}: {savingDelay?: number; onVal focusRef, siteData, handleSave: async () => { - toast.remove(); const result = await handleSave(); if (result) { setEditing(false); } else { - showToast({ - type: 'pageError', - message: 'Can\'t save settings! One or more fields have errors, please double check that you\'ve filled all mandatory fields.' - }); } return result; }, diff --git a/apps/admin-x-settings/test/acceptance/advanced/integrations/slack.test.ts b/apps/admin-x-settings/test/acceptance/advanced/integrations/slack.test.ts index 420a9f5ee2..1e98e3fae2 100644 --- a/apps/admin-x-settings/test/acceptance/advanced/integrations/slack.test.ts +++ b/apps/admin-x-settings/test/acceptance/advanced/integrations/slack.test.ts @@ -100,7 +100,7 @@ test.describe('Slack integration', async () => { await slackModal.getByLabel('Username').fill('My site'); await slackModal.getByRole('button', {name: 'Send test notification'}).click(); - await expect(page.getByTestId('toast-neutral')).toHaveText(/Check your Slack channel for the test message/); + await expect(page.getByTestId('toast-info')).toHaveText(/Check your Slack channel for the test message/); expect(lastApiRequests.editSettings?.body).toEqual({ settings: [ diff --git a/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts b/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts index 2b6f99d793..f7641d9560 100644 --- a/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts +++ b/apps/admin-x-settings/test/acceptance/advanced/labs.test.ts @@ -24,7 +24,7 @@ test.describe('Labs', async () => { const fileChooser = await fileChooserPromise; await fileChooser.setFiles(`${__dirname}/../../utils/files/redirects.yml`); - await expect(page.getByTestId('toast-success')).toContainText('Redirects uploaded successfully'); + await expect(page.getByTestId('toast-success')).toContainText('Redirects uploaded'); expect(lastApiRequests.uploadRedirects).toBeTruthy(); @@ -56,7 +56,7 @@ test.describe('Labs', async () => { const fileChooser = await fileChooserPromise; await fileChooser.setFiles(`${__dirname}/../../utils/files/routes.yml`); - await expect(page.getByTestId('toast-success')).toContainText('Routes uploaded successfully'); + await expect(page.getByTestId('toast-success')).toContainText('Routes uploaded'); expect(lastApiRequests.uploadRoutes).toBeTruthy(); diff --git a/apps/admin-x-settings/test/acceptance/email/newsletters.test.ts b/apps/admin-x-settings/test/acceptance/email/newsletters.test.ts index 9a083870fc..f2d0224603 100644 --- a/apps/admin-x-settings/test/acceptance/email/newsletters.test.ts +++ b/apps/admin-x-settings/test/acceptance/email/newsletters.test.ts @@ -27,7 +27,6 @@ test.describe('Newsletter settings', async () => { const modal = page.getByTestId('add-newsletter-modal'); await modal.getByRole('button', {name: 'Create'}).click(); - await expect(page.getByTestId('toast-error')).toHaveText(/Can't save newsletter/); await expect(modal).toHaveText(/Name is required/); // Shouldn't be necessary, but without these Playwright doesn't click Create the second time for some reason @@ -70,7 +69,6 @@ test.describe('Newsletter settings', async () => { await modal.getByPlaceholder('Weekly Roundup').fill(''); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-error')).toHaveText(/Can't save newsletter/); await expect(modal).toHaveText(/Name is required/); await modal.getByPlaceholder('Weekly Roundup').fill('Updated newsletter'); @@ -116,14 +114,13 @@ test.describe('Newsletter settings', async () => { await modal.getByLabel('Sender email').fill('not-an-email'); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-error')).toHaveText(/Can't save newsletter/); await expect(modal).toHaveText(/Invalid email/); await modal.getByLabel('Sender email').fill('test@test.com'); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-neutral')).toHaveCount(1); - await expect(page.getByTestId('toast-neutral')).toHaveText(/sent a confirmation email to the new address/); + await expect(page.getByTestId('toast-info')).toHaveCount(1); + await expect(page.getByTestId('toast-info')).toHaveText(/sent a confirmation email to the new address/); }); }); @@ -194,14 +191,13 @@ test.describe('Newsletter settings', async () => { await replyToEmail.fill('not-an-email'); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-error')).toHaveText(/Can't save newsletter/); await expect(modal).toHaveText(/Invalid email/); await replyToEmail.fill('test@test.com'); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-neutral')).toHaveCount(1); - await expect(page.getByTestId('toast-neutral')).toHaveText(/sent a confirmation email to the new address/); + await expect(page.getByTestId('toast-info')).toHaveCount(1); + await expect(page.getByTestId('toast-info')).toHaveText(/sent a confirmation email to the new address/); }); }); @@ -241,13 +237,11 @@ test.describe('Newsletter settings', async () => { // Error case #1: add invalid email address await senderEmail.fill('Harry Potter'); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-error').first()).toHaveText(/Can't save newsletter/); await expect(modal).toHaveText(/Invalid email/); // Error case #2: the sender email address doesn't match the custom sending domain await senderEmail.fill('harry@potter.com'); await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-error').first()).toHaveText(/Can't save newsletter/); await expect(modal).toHaveText(/Email must end with @customdomain.com/); // But can have any address on the same domain, without verification @@ -294,8 +288,8 @@ test.describe('Newsletter settings', async () => { // There is a verification popup for the new reply-to address await modal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-neutral')).toHaveCount(1); - await expect(page.getByTestId('toast-neutral')).toHaveText(/sent a confirmation email to the new address/); + await expect(page.getByTestId('toast-info')).toHaveCount(1); + await expect(page.getByTestId('toast-info')).toHaveText(/sent a confirmation email to the new address/); }); }); }); diff --git a/apps/admin-x-settings/test/acceptance/general/users/invite.test.ts b/apps/admin-x-settings/test/acceptance/general/users/invite.test.ts index 201e3fa709..7a05c9caa9 100644 --- a/apps/admin-x-settings/test/acceptance/general/users/invite.test.ts +++ b/apps/admin-x-settings/test/acceptance/general/users/invite.test.ts @@ -38,7 +38,7 @@ test.describe('User invitations', async () => { // Validation failures - await modal.getByRole('button', {name: 'Send invitation now'}).click(); + await modal.getByRole('button', {name: 'Send invitation'}).click(); await expect(modal).toContainText('Please enter a valid email address'); // Reset error with keydown event @@ -47,24 +47,24 @@ test.describe('User invitations', async () => { await modal.getByLabel('Email address').fill('test'); await expect(modal).not.toContainText('Please enter a valid email address'); - await modal.getByRole('button', {name: 'Send invitation now'}).click(); + await modal.getByRole('button', {name: 'Send invitation'}).click(); await expect(modal).toContainText('Please enter a valid email address'); await modal.getByLabel('Email address').fill('author@test.com'); - await modal.getByRole('button', {name: 'Send invitation now'}).click(); + await modal.getByRole('button', {name: 'Retry'}).click(); await expect(modal).toContainText('A user with that email address already exists.'); await modal.getByLabel('Email address').fill('invitee@test.com'); - await modal.getByRole('button', {name: 'Send invitation now'}).click(); + await modal.getByRole('button', {name: 'Retry'}).click(); await expect(modal).toContainText('A user with that email address was already invited.'); // Successful invitation await modal.getByLabel('Email address').fill('newuser@test.com'); await modal.locator('input[value=author]').check(); - await modal.getByRole('button', {name: 'Send invitation now'}).click(); + await modal.getByRole('button', {name: 'Retry'}).click(); - await expect(page.getByTestId('toast-success')).toHaveText(/Invitation successfully sent to newuser@test\.com/); + await expect(page.getByTestId('toast-success')).toHaveText(/Invitation sent/); await section.getByRole('tab', {name: 'Invited'}).click(); @@ -103,7 +103,7 @@ test.describe('User invitations', async () => { await listItem.getByRole('button', {name: 'Resend'}).click(); - await expect(page.getByTestId('toast-success')).toHaveText(/Invitation resent! \(invitee@test\.com\)/); + await expect(page.getByTestId('toast-success')).toHaveText(/Invitation resent/); // Resending works by deleting and re-adding the invite @@ -138,7 +138,7 @@ test.describe('User invitations', async () => { await listItem.getByRole('button', {name: 'Revoke'}).click(); - await expect(page.getByTestId('toast-success')).toHaveText(/Invitation revoked \(invitee@test\.com\)/); + await expect(page.getByTestId('toast-success')).toHaveText(/Invitation revoked/); expect(lastApiRequests.deleteInvite?.url).toMatch(new RegExp(`/invites/${responseFixtures.invites.invites[0].id}`)); }); diff --git a/apps/admin-x-settings/test/acceptance/general/users/profile.test.ts b/apps/admin-x-settings/test/acceptance/general/users/profile.test.ts index a9cb02819b..12e0b7f85e 100644 --- a/apps/admin-x-settings/test/acceptance/general/users/profile.test.ts +++ b/apps/admin-x-settings/test/acceptance/general/users/profile.test.ts @@ -40,7 +40,7 @@ test.describe('User profile', async () => { await modal.getByLabel('Email').fill('test'); await modal.getByRole('button', {name: 'Save & close'}).click(); - await expect(modal).toContainText('Please enter a valid email address'); + await expect(modal).toContainText('Enter a valid email address'); await modal.getByLabel('Location').fill(new Array(195).join('a')); await modal.getByRole('button', {name: 'Save & close'}).click(); @@ -51,7 +51,7 @@ test.describe('User profile', async () => { await expect(modal).toContainText('Bio is too long'); await modal.getByLabel('Website').fill('not-a-website'); - await modal.getByLabel('Website').blur(); + await modal.getByRole('button', {name: 'Save & close'}).click(); await expect(modal).toContainText('Enter a valid URL'); const facebookInput = modal.getByLabel('Facebook profile'); diff --git a/apps/admin-x-settings/test/acceptance/membership/offers.test.ts b/apps/admin-x-settings/test/acceptance/membership/offers.test.ts index a9ee04cf1e..c279be2d9b 100644 --- a/apps/admin-x-settings/test/acceptance/membership/offers.test.ts +++ b/apps/admin-x-settings/test/acceptance/membership/offers.test.ts @@ -104,8 +104,11 @@ test.describe('Offers Modal', () => { await modal.getByRole('button', {name: 'New offer'}).click(); const addModal = page.getByTestId('add-offer-modal'); await addModal.getByRole('button', {name: 'Publish'}).click(); - - await expect(page.getByTestId('toast-error')).toContainText(/Can't save offer, please double check that you've filled all mandatory fields./); + const sidebar = addModal.getByTestId('add-offer-sidebar'); + await expect(sidebar).toContainText(/Name is required/); + await expect(sidebar).toContainText(/Code is required/); + await expect(sidebar).toContainText(/Enter an amount greater than 0./); + await expect(sidebar).toContainText(/Display title is required/); }); test('Errors if the offer code is already taken', async ({page}) => { @@ -163,7 +166,6 @@ test.describe('Offers Modal', () => { await modal.getByRole('button', {name: 'New offer'}).click(); const addModal = page.getByTestId('add-offer-modal'); await addModal.getByRole('button', {name: 'Publish'}).click(); - await expect(page.getByTestId('toast-error')).toContainText(/Can't save offer, please double check that you've filled all mandatory fields./); const sidebar = addModal.getByTestId('add-offer-sidebar'); await expect(sidebar).toContainText(/Name is required/); await expect(sidebar).toContainText(/Code is required/); @@ -238,7 +240,6 @@ test.describe('Offers Modal', () => { await offerUpdateModal.getByPlaceholder('black-friday').fill(''); await offerUpdateModal.getByRole('button', {name: 'Save'}).click(); - await expect(page.getByTestId('toast-error')).toContainText(/Can't save offer, please double check that you've filled all mandatory fields./); await expect(offerUpdateModal).toContainText(/Please enter a code/); await offerUpdateModal.getByPlaceholder('black-friday').fill('black-friday-offer'); diff --git a/apps/admin-x-settings/test/acceptance/membership/recommendations.test.ts b/apps/admin-x-settings/test/acceptance/membership/recommendations.test.ts index 7ac6f50a82..9d2de9f7fa 100644 --- a/apps/admin-x-settings/test/acceptance/membership/recommendations.test.ts +++ b/apps/admin-x-settings/test/acceptance/membership/recommendations.test.ts @@ -171,7 +171,6 @@ test.describe('Recommendations', async () => { expect(confirmation).toContainText('Your recommendation Recommendation 1 title will no longer be visible to your audience.'); await confirmation.getByRole('button', {name: 'Delete'}).click(); - await expect(page.getByTestId('toast-success')).toContainText('Successfully deleted the recommendation'); expect(lastApiRequests.deleteRecommendation).toBeTruthy(); }); diff --git a/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts b/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts index 2b91953f5d..732c8737d6 100644 --- a/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts +++ b/apps/admin-x-settings/test/acceptance/membership/tiers.test.ts @@ -20,7 +20,6 @@ test.describe('Tier settings', async () => { await modal.getByRole('button', {name: 'Save & close'}).click(); - await expect(page.getByTestId('toast-error')).toHaveText(/Can't save tier/); await expect(modal).toHaveText(/Enter a name for the tier/); await expect(modal).toHaveText(/Amount must be at least \$1/); @@ -106,7 +105,6 @@ test.describe('Tier settings', async () => { await modal.getByLabel('Name').fill(''); await modal.getByRole('button', {name: 'Save & close'}).click(); - await expect(page.getByTestId('toast-error')).toHaveText(/Can't save tier/); await expect(modal).toHaveText(/Enter a name for the tier/); // Valid values diff --git a/apps/admin-x-settings/test/acceptance/site/theme.test.ts b/apps/admin-x-settings/test/acceptance/site/theme.test.ts index 2580a766d3..386ed97945 100644 --- a/apps/admin-x-settings/test/acceptance/site/theme.test.ts +++ b/apps/admin-x-settings/test/acceptance/site/theme.test.ts @@ -53,7 +53,7 @@ test.describe('Theme settings', async () => { await modal.getByRole('button', {name: 'Install Headline'}).click(); - await expect(page.getByTestId('confirmation-modal')).toHaveText(/successfully installed/); + await expect(page.getByTestId('confirmation-modal')).toHaveText(/installed/); await page.getByRole('button', {name: 'Activate'}).click(); diff --git a/ghost/admin/app/components/editor/publish-management.js b/ghost/admin/app/components/editor/publish-management.js index 8c93cf5197..7d97a31906 100644 --- a/ghost/admin/app/components/editor/publish-management.js +++ b/ghost/admin/app/components/editor/publish-management.js @@ -250,7 +250,7 @@ export default class PublishManagement extends Component { yield this.publishTask.perform({taskName: 'revertToDraftTask'}); const postType = capitalize(this.args.post.displayName); - this.notifications.showNotification(`${postType} successfully reverted to a draft.`, {type: 'success'}); + this.notifications.showNotification(`${postType} reverted to a draft.`, {type: 'success'}); return true; } catch (e) { diff --git a/ghost/admin/app/components/gh-members-no-members.js b/ghost/admin/app/components/gh-members-no-members.js index 3302ce3abc..42692f186c 100644 --- a/ghost/admin/app/components/gh-members-no-members.js +++ b/ghost/admin/app/components/gh-members-no-members.js @@ -35,13 +35,13 @@ export default class GhMembersNoMembersComponent extends Component { this.notifications.showNotification('Member added', { - description: 'You\'ve successfully added yourself as a member.' + description: 'You\'ve added yourself as a member.' } ); // force update the member count; this otherwise only updates every minute yield this.membersCountCache.count({}); - + return member; } catch (error) { if (error) { diff --git a/ghost/admin/app/components/gh-notification.hbs b/ghost/admin/app/components/gh-notification.hbs index a3f5173667..6b95770875 100644 --- a/ghost/admin/app/components/gh-notification.hbs +++ b/ghost/admin/app/components/gh-notification.hbs @@ -1,30 +1,36 @@
-
- {{#if @message.icon}} - {{svg-jar @message.icon}} - {{else}} - {{#if (eq @message.type "success")}} - {{svg-jar "check-circle"}} - {{else if (eq @message.type "error")}} - {{svg-jar "warning-stroke"}} - {{else if (eq @message.type "warn")}} - {{svg-jar "warning-stroke"}} - {{else}} - {{svg-jar "check-circle"}} - {{/if}} - {{/if}} -
- {{@message.message}} - +
+
+ {{#if @message.icon}} + {{svg-jar @message.icon}} + {{else}} + {{#if (eq @message.type "success")}} + {{svg-jar "check-circle-filled"}} + {{else if (eq @message.type "error")}} + {{svg-jar "warning-circle-filled"}} + {{else if (eq @message.type "warn")}} + {{svg-jar "warning-circle-filled"}} + {{else}} + {{svg-jar "check-circle-filled"}} + {{/if}} + {{/if}} +
+
+ {{@message.message}} +
+
+
+ {{#if (or @message.description @message.actions)}} +
{{#if @message.description}}

{{@message.description}}

{{/if}} - {{#if @message.actions}} {{@message.actions}} {{/if}}
+ {{/if}} diff --git a/ghost/admin/app/components/members/modals/logout-member.js b/ghost/admin/app/components/members/modals/logout-member.js index 1fe9cd28db..f07de06de5 100644 --- a/ghost/admin/app/components/members/modals/logout-member.js +++ b/ghost/admin/app/components/members/modals/logout-member.js @@ -19,7 +19,7 @@ export default class LogoutMemberModal extends Component { yield this.ajax.delete(url, options); this.args.data.afterLogout?.(); - this.notifications.showNotification(`${this.member.name || this.member.email} has been successfully signed out from all devices.`, {type: 'success'}); + this.notifications.showNotification(`${this.member.name || this.member.email} has been signed out from all devices.`, {type: 'success'}); this.args.close(true); return true; } catch (e) { diff --git a/ghost/admin/app/components/modals/restore-revision.js b/ghost/admin/app/components/modals/restore-revision.js index fcb576b2ea..4808d8a2e0 100644 --- a/ghost/admin/app/components/modals/restore-revision.js +++ b/ghost/admin/app/components/modals/restore-revision.js @@ -40,7 +40,7 @@ export default class RestoreRevisionModal extends Component { updateTitle(); updateEditor(); - this.notifications.showNotification('Revision successfully restored.', {type: 'success'}); + this.notifications.showNotification('Revision restored.', {type: 'success'}); closePostHistoryModal(); diff --git a/ghost/admin/app/components/posts-list/context-menu.js b/ghost/admin/app/components/posts-list/context-menu.js index 3faa8bc56d..f945cfda40 100644 --- a/ghost/admin/app/components/posts-list/context-menu.js +++ b/ghost/admin/app/components/posts-list/context-menu.js @@ -21,28 +21,28 @@ function tpl(str, data) { const messages = { deleted: { - single: '{Type} deleted successfully', - multiple: '{count} {type}s deleted successfully' + single: '{Type} deleted', + multiple: '{count} {type}s deleted' }, unpublished: { - single: '{Type} successfully reverted to a draft', - multiple: '{count} {type}s successfully reverted to drafts' + single: '{Type} reverted to a draft', + multiple: '{count} {type}s reverted to drafts' }, accessUpdated: { - single: '{Type} access successfully updated', - multiple: '{Type} access successfully updated for {count} {type}s' + single: '{Type} access updated', + multiple: '{Type} access updated for {count} {type}s' }, tagsAdded: { - single: 'Tags added successfully', - multiple: 'Tags added successfully to {count} {type}s' + single: 'Tags added', + multiple: 'Tags added to {count} {type}s' }, tagAdded: { - single: 'Tag added successfully', - multiple: 'Tag added successfully to {count} {type}s' + single: 'Tag added', + multiple: 'Tag added to {count} {type}s' }, duplicated: { - single: '{Type} duplicated successfully', - multiple: '{count} {type}s duplicated successfully' + single: '{Type} duplicated', + multiple: '{count} {type}s duplicated' } }; diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index e9644d7759..a01ec62cb6 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -15,7 +15,6 @@ import moment from 'moment-timezone'; import {GENERIC_ERROR_MESSAGE} from '../services/notifications'; import {action, computed} from '@ember/object'; import {alias, mapBy} from '@ember/object/computed'; -import {capitalize} from '@ember/string'; import {captureMessage} from '@sentry/ember'; import {dropTask, enqueueTask, restartableTask, task, taskGroup, timeout} from 'ember-concurrency'; import {htmlSafe} from '@ember/template'; @@ -1266,7 +1265,7 @@ export default class LexicalEditorController extends Controller { let actions, type, path; if (status === 'published' || status === 'scheduled') { - type = capitalize(this.get('post.displayName')); + type = this.get('post.displayName'); path = this.get('post.url'); actions = `View ${type}`; } diff --git a/ghost/admin/app/styles/app-dark.css b/ghost/admin/app/styles/app-dark.css index b2dfb0f5d0..49d75f2094 100644 --- a/ghost/admin/app/styles/app-dark.css +++ b/ghost/admin/app/styles/app-dark.css @@ -1031,6 +1031,10 @@ input:focus, background: var(--lightgrey); } +.gh-notification-close:hover svg { + stroke: #FFF; +} + /* Opacity needed to display correctly in Safari */ ::selection { background: rgba(88, 101, 116, 0.99); @@ -1415,7 +1419,7 @@ Onboarding checklist: Share publication modal */ opacity: .8; } -.gh-share-links li a { +.gh-share-links li a { border: 1px solid #394047; } diff --git a/ghost/admin/app/styles/components/notifications.css b/ghost/admin/app/styles/components/notifications.css index 3231ecf949..8c008a4764 100644 --- a/ghost/admin/app/styles/components/notifications.css +++ b/ghost/admin/app/styles/components/notifications.css @@ -5,7 +5,7 @@ .gh-notifications { position: absolute; bottom: 30px; - left: 30px; + left: 24px; z-index: 7000; display: flex; flex-direction: column; @@ -14,26 +14,33 @@ /* Base notification style */ .gh-notification { position: relative; - display: flex; margin-top: 8px; - padding: 4px 8px; - width: 286px; - background: var(--black); - border-radius: 6px; - box-shadow: - 0 1.1px 2.3px rgba(0, 0, 0, 0.028), - 0 3.8px 7.8px rgba(0, 0, 0, 0.042), - 0 17px 35px -7px rgba(0, 0, 0, 0.11) - ; - color: #fff; + min-width: 272px; + max-width: 320px; + background: var(--white); + border-radius: 8px; + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.28), 0px 100px 80px rgba(0, 0, 0, 0.0112458), 0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0161557), 0px 22.3363px 17.869px rgba(0, 0, 0, 0.02), 0px 12.5216px 10.0172px rgba(0, 0, 0, 0.0238443), 0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0287542), 0px 2.76726px 2.21381px rgba(0, 0, 0, 0.04); + color: var(--black); font-size: 1.3rem; line-height: 1.25em; opacity: 1.0; min-height: 44px; } +@media (max-width: 1240px) { + .gh-notification { + min-width: 232px; + } +} + .gh-notification-icon { - margin: 10px 0 0 6px; + margin-top: 1px; + line-height: 0; + color: #30CF43; +} + +:is(.gh-notification-error, .gh-notification-warn) .gh-notification-icon { + color: #F50B23; } .gh-notification-icon svg { @@ -41,24 +48,24 @@ height: 16px; } -.gh-notification-icon svg path { - stroke-width: 1.5px; - stroke: #fff; -} - .gh-notification-content { flex-grow: 1; display: flex; flex-direction: column; - padding: 9px 15px 10px 10px; + padding: 16px; border-radius: 3px; - max-width: 215px; + margin-right: 28px; } .gh-notification-content p span { white-space: nowrap; } +.gh-notification-header { + display: flex; + gap: 10px; +} + .gh-notification-title { display: block; margin-top: 1px; @@ -66,14 +73,18 @@ font-size: 1.4rem; } +.gh-notification-details { + margin: -18px 16px 16px 42px; +} + .gh-notification p { - margin: 6px 0 0; + margin: 8px 0 0; padding: 0; line-height: 1.35em; } .gh-notification a { - color: #fff; + color: inherit; text-decoration: underline; font-weight: 400; } @@ -84,7 +95,7 @@ } .gh-notification-actions { - margin-top: 6px; + margin-top: 10px; margin-bottom: 2px; display: flex; } @@ -92,12 +103,17 @@ .gh-notification-actions a { display: inline-block; margin-right: 10px; + color: var(--darkgrey); +} + +.gh-notification-actions a:hover { + color: var(--black); } .gh-notification-close { position: absolute; - top: 10px; - right: 10px; + top: 14px; + right: 14px; padding: 8px; background: none; border-radius: 999px; @@ -109,16 +125,20 @@ .gh-notification-close svg { height: 8px; width: 8px; - stroke: #fff; + stroke: #7C8B9A; +} + +.gh-notification-close:hover svg { + stroke: #394047; } .gh-notification-close svg path { stroke-width: 2px; } -.gh-notification-close:hover { +/* .gh-notification-close:hover { background: rgba(255, 255, 255, 0.3); -} +} */ .gh-notification-passive { animation: notification-fade-in-spring, fade-out; @@ -134,19 +154,16 @@ @keyframes notification-fade-in-spring { 0.00% { - opacity: 0; - transform: translateX(-232.05px); + transform: translateY(100%); } 26.52% { - opacity: 0.5; - transform: translateX(5.90px); + transform: translateY(-3.90px); } 63.26% { - transform: translateX(-1.77px); - opacity: 1; + transform: translateY(1.2px); } 100.00% { - transform: translateX(0px); + transform: translateY(0px); } } @@ -338,4 +355,4 @@ .gh-update-banner a { font-weight: 700; color: var(--green-l2); -} \ No newline at end of file +} diff --git a/ghost/admin/public/assets/icons/check-circle-filled.svg b/ghost/admin/public/assets/icons/check-circle-filled.svg new file mode 100644 index 0000000000..353fcc7533 --- /dev/null +++ b/ghost/admin/public/assets/icons/check-circle-filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ghost/admin/public/assets/icons/info-circle-filled.svg b/ghost/admin/public/assets/icons/info-circle-filled.svg new file mode 100644 index 0000000000..f9d2d0c210 --- /dev/null +++ b/ghost/admin/public/assets/icons/info-circle-filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ghost/admin/public/assets/icons/warning-circle-filled.svg b/ghost/admin/public/assets/icons/warning-circle-filled.svg new file mode 100644 index 0000000000..2be3dfbbc9 --- /dev/null +++ b/ghost/admin/public/assets/icons/warning-circle-filled.svg @@ -0,0 +1,10 @@ + + + + + + + + + +