Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3a7a5ef
Add me-notfication-devices fetchers
gabrielcaires Sep 19, 2025
8c07f9f
Add missing types
gabrielcaires Sep 19, 2025
425c1fb
Add generic notification settings mutator
gabrielcaires Sep 19, 2025
9f4d79b
Add web settings component
gabrielcaires Sep 19, 2025
fe8d1b5
Add email settings component
gabrielcaires Sep 19, 2025
ccdc783
Add device settings
gabrielcaires Sep 19, 2025
60b35fc
Render all settings components
gabrielcaires Sep 19, 2025
0ed934f
Rename device settings folder
gabrielcaires Sep 19, 2025
a0dc176
Adjust card title
gabrielcaires Sep 19, 2025
9fc5735
Adjust card title
gabrielcaires Sep 19, 2025
fd09225
Only send mimimum necessary data to update the settings
gabrielcaires Sep 22, 2025
46356d8
Fix wrong device settings merge
gabrielcaires Sep 22, 2025
5b86f98
Fix tests
gabrielcaires Sep 22, 2025
18edccc
Remove manual snackbar trigger
gabrielcaires Sep 22, 2025
b716aca
Use section header
gabrielcaires Sep 22, 2025
38eb971
Add preload data
gabrielcaires Sep 22, 2025
9dc2386
Merge branch 'trunk' into update/add-settings-comments-screen
gabrielcaires Sep 22, 2025
a6bcb4d
Update empty state
gabrielcaires Sep 22, 2025
4b74cb2
WIP
gabrielcaires Sep 26, 2025
7ee2c7f
Add device settings component
gabrielcaires Sep 26, 2025
fc99d95
Add email settings component
gabrielcaires Sep 26, 2025
7678305
Add web settings component
gabrielcaires Sep 26, 2025
20ef6df
Implement the page
gabrielcaires Sep 26, 2025
d3e313f
Merge branch 'trunk' into update/cml-879-enable-configure-notificatio…
gabrielcaires Sep 26, 2025
45b2aef
Load data before render page
gabrielcaires Sep 29, 2025
c44f0bd
Rename to site-preview
gabrielcaires Sep 29, 2025
e9b257b
Remove external link
gabrielcaires Sep 29, 2025
819f3c3
mobile adjustments
gabrielcaires Sep 30, 2025
9663b0b
Merge branch 'trunk' into update/cml-879-enable-configure-notificatio…
gabrielcaires Sep 30, 2025
6807178
Remove unused placeholder
gabrielcaires Sep 30, 2025
7f73c16
Mobile adjustments
gabrielcaires Sep 30, 2025
35eb2b9
Fix app link
gabrielcaires Oct 1, 2025
e7aac58
Show confirmation modal
gabrielcaires Oct 1, 2025
70f69bc
Improve copy
gabrielcaires Oct 1, 2025
3490689
Add first version with loading
gabrielcaires Oct 1, 2025
f44ca79
Remove unused eslint config
gabrielcaires Oct 2, 2025
5f54ffe
Fix tests
gabrielcaires Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/dashboard/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
'@automattic/components/src/*',
'!@automattic/api-core',
'!@automattic/api-queries',
'!@automattic/search',
'!@automattic/components/src/circular-progress-bar',
'!@automattic/components/src/summary-button',
'!@automattic/components/src/breadcrumbs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
userNotificationsSettingsMutation,
userNotificationsDevicesQuery,
} from '@automattic/api-queries';
import { localizeUrl } from '@automattic/i18n-utils';
import { useSuspenseQuery, useMutation, useIsMutating } from '@tanstack/react-query';
import {
__experimentalVStack as VStack,
Expand Down Expand Up @@ -97,10 +98,12 @@ export const DevicesSettings = () => {
),
{
link: (
<ExternalLink href="https://wordpress.org" rel="noopener noreferrer">
{ /* Workaround for the fact that the ExternalLink component expects a children prop */ }
{ null }
</ExternalLink>
<ExternalLink
href={ localizeUrl( 'https://apps.wordpress.com/mobile' ) }
rel="noopener noreferrer"
//Workaround for the fact that the ExternalLink component expects a children prop
children={ null }
/>
),
}
) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.collapsible-card {
max-height: 100%;

.collapsible-card__toggle svg {
transition: transform 0.2s ease-in-out;
}

&.collapsed .collapsible-card__toggle svg {
transform: rotate(180deg);
}

.collapsed &__content {
display: none;
}

&.collapsed {
overflow: hidden;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Card, CardBody, __experimentalHStack as HStack, Button } from '@wordpress/components';
import { chevronUp } from '@wordpress/icons';
import clsx from 'clsx';
import { useState } from 'react';
import './index.scss';

interface CollapsibleCardProps {
header: React.ReactNode;
children: React.ReactNode;
}

export const CollapsibleCard = ( { header, children }: CollapsibleCardProps ) => {
const [ isCollapsed, setIsCollapsed ] = useState< boolean >( true );

const handleCollapsedChange = () => {
setIsCollapsed( ! isCollapsed );
};
return (
<Card className={ clsx( 'collapsible-card', { collapsed: isCollapsed } ) }>
<CardBody>
<HStack>
{ header }
<Button
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to be accessible.

Some issues:

  • Button needs an aria-label.
  • Need to communicate aria-expanded state.
  • Need to connect the button to the content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I will do it in the next PR because this one is already too big.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! Maybe consider moving this to the component folder as well. I know working on the performance tab, I was looking for something with this functionality.

icon={ chevronUp }
className={ clsx( 'collapsible-card__toggle', { collapsed: isCollapsed } ) }
variant="tertiary"
onClick={ handleCollapsedChange }
/>
</HStack>
{ ! isCollapsed && <div className="collapsible-card__content">{ children }</div> }
</CardBody>
</Card>
);
};
23 changes: 23 additions & 0 deletions client/dashboard/me/notifications-sites/helpers/translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { __ } from '@wordpress/i18n';

const translations = {
comment_like: __( 'Likes on my comments' ),
recommended_blog: __( 'Blog recommendations' ),
new_comment: __( 'Comments on my site' ),
post_like: __( 'Likes on my posts' ),
follow: __( 'Subscriptions' ),
achievement: __( 'Site achievements' ),
mentions: __( 'Username mentions' ),
scheduled_publicize: __( 'Jetpack Social' ),
blogging_prompt: __( 'Daily writing prompts' ),
draft_post_prompt: __( 'Draft post reminders' ),
store_order: __( 'New order' ),
comment_reply: __( 'Replies to my comments' ),
} as const;

type TranslationKey = keyof typeof translations;
type TranslationValue = ( typeof translations )[ TranslationKey ];

export const getFieldLabel = ( key: TranslationKey ): TranslationValue => {
return translations[ key ] ?? key;
};
26 changes: 26 additions & 0 deletions client/dashboard/me/notifications-sites/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
userNotificationsSettingsQuery,
userNotificationsSettingsMutation,
} from '@automattic/api-queries';
import { useSuspenseQuery, useMutation } from '@tanstack/react-query';
import { __ } from '@wordpress/i18n';

export const useSiteSettings = ( blogId: number ) => {
return useSuspenseQuery( {
...userNotificationsSettingsQuery(),
select: ( data ) => data?.blogs.find( ( blog ) => blog.blog_id === blogId ),
staleTime: 1000 * 60 * 5,
} );
};

export const useSettingsMutation = () => {
return useMutation( {
...userNotificationsSettingsMutation(),
meta: {
snackbar: {
success: __( 'Settings saved successfully.' ),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now have guidelines re: snackbar copy, please take a look 😄

error: __( 'There was a problem saving your changes. Please, try again.' ),
},
},
} );
};
8 changes: 7 additions & 1 deletion client/dashboard/me/notifications-sites/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { notificationPushPermissionStateQuery } from '@automattic/api-queries';
import { useQuery } from '@tanstack/react-query';
import { __experimentalVStack as VStack } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { Suspense } from 'react';
import { PageHeader } from '../../components/page-header';
import PageLayout from '../../components/page-layout';
import { BrowserNotificationCard } from './browser-notification-card';
import { BrowserNotificationNotice } from './browser-notification-notice';
import { Loading } from './loading';
import { SiteListSettings } from './site-list-settings';

export default function NotificationsSites() {
const { data: status } = useQuery( notificationPushPermissionStateQuery() );
Expand All @@ -25,8 +28,11 @@ export default function NotificationsSites() {
>
{ status === 'denied' && <BrowserNotificationNotice /> }

<VStack spacing={ 4 }>
<VStack spacing={ 8 }>
<BrowserNotificationCard status={ status } />
<Suspense fallback={ <Loading /> }>
<SiteListSettings />
</Suspense>
</VStack>
</PageLayout>
);
Expand Down
9 changes: 9 additions & 0 deletions client/dashboard/me/notifications-sites/loading/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Spinner, __experimentalHStack as HStack } from '@wordpress/components';

export const Loading = ( { style }: { style?: React.CSSProperties } ) => {
return (
<HStack style={ style } justify="center" alignment="center">
<Spinner />
</HStack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.site-list-settings {
.components-tab-panel__tabs {
border-bottom: solid 1px #ddd;
}
&__site-settings {
margin-left: -24px;
margin-right: -24px;
}
.components-tab-panel__tabs, .components-tab-panel__tab-content {
padding-left: 24px;
padding-right: 24px;
}
.components-tab-panel__tab-content {
padding-top: 16px;
}
&__card-placeholder {
height: 112px;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Site } from '@automattic/api-core';
import { sitesQuery } from '@automattic/api-queries';
import { useFuzzySearch } from '@automattic/search';
import { useSuspenseQuery } from '@tanstack/react-query';
import {
Card,
CardBody,
__experimentalVStack as VStack,
SearchControl,
} from '@wordpress/components';
import { createInterpolateElement } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { useDeferredValue, useState } from 'react';
import { CollapsibleCard } from '../collapsible-card';
import { SitePreview } from '../site-preview';
import { SiteSettings } from '../site-settings';

import './index.scss';

export const SiteListSettings = () => {
const [ search, setSearch ] = useState< string | undefined >();
const { data: sites } = useSuspenseQuery( {
...sitesQuery( { include_a8c_owned: true, site_visibility: 'visible' } ),
} );

const deferredSearch = useDeferredValue( search );

const filteredSites = useFuzzySearch< Site >( {
data: sites,
keys: [ 'name', 'URL' ],
query: deferredSearch,
} );

const handleSearchChange = ( value: string | undefined ) => {
setSearch( value );
};

return (
<VStack spacing={ 8 } className="site-list-settings">
<SearchControl
value={ search }
placeholder={ __( 'Search for a site' ) }
onChange={ handleSearchChange }
__nextHasNoMarginBottom
/>
<VStack spacing={ 4 }>
{ filteredSites.map( ( site: Site ) => (
<CollapsibleCard key={ site.ID } header={ <SitePreview site={ site } /> }>
<SiteSettings siteId={ site.ID } className="site-list-settings__site-settings" />
</CollapsibleCard>
) ) }
{ filteredSites.length === 0 && (
<Card>
<CardBody>
{ createInterpolateElement(
sprintf(
// translators: %s is the search query
__( 'No sites found with the search query <strong>%(search)s</strong>' ),
{
search: search,
}
),
{
strong: <strong />,
}
) }
</CardBody>
</Card>
) }
</VStack>
</VStack>
);
};
37 changes: 37 additions & 0 deletions client/dashboard/me/notifications-sites/site-preview/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.site-preview {
width: 100%;
overflow: hidden;

&__badge {
grid-column-start: 2;
}

&__url {
color: $gray-700;
display: flex;
flex: 1 1 100%;
min-width: 0;
width: 100%;

.components-external-link__contents {
max-width: 170px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;

@include break-mobile {
overflow: unset;
max-width: unset;
max-width: 250px;
}
@include break-small {
max-width: unset;
}
}

&:hover {
color: var(--wp-admin-theme-color);
}
}

}
64 changes: 64 additions & 0 deletions client/dashboard/me/notifications-sites/site-preview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Site } from '@automattic/api-core';
import { Badge } from '@automattic/ui';
import {
__experimentalGrid as Grid,
__experimentalVStack as VStack,
ExternalLink,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import RouterLinkButton from '../../../components/router-link-button';
import { isSitePlanTrial } from '../../../sites/plans';
import { getSiteManagementUrl } from '../../../sites/site-fields';
import SiteIcon from '../../../sites/site-icon';
import { isP2, isStagingSite } from '../../../utils/site-types';

import './index.scss';

const getSiteBadge = ( site: Site ) => {
if ( isStagingSite( site ) ) {
return __( 'Staging' );
}
if ( isSitePlanTrial( site ) ) {
return __( 'Trial' );
}
if ( isP2( site ) ) {
return __( 'P2' );
}
return null;
};

interface Props {
site: Site;
}

export const SitePreview = ( { site }: Props ) => {
const badge = getSiteBadge( site );

return (
<Grid
className="site-preview"
columns={ 2 }
columnGap={ 12 }
rowGap={ badge ? 12 : 0 }
alignment="topLeft"
templateColumns="44px 1fr"
>
<SiteIcon site={ site } size={ 44 } />
<VStack alignment="topLeft" spacing={ 1 }>
{ site.name !== '' && (
<RouterLinkButton
variant="link"
to={ getSiteManagementUrl( site ) ?? '' }
disabled={ site.is_deleted }
>
{ site.name }
</RouterLinkButton>
) }
<ExternalLink className="site-preview__url" href={ site.URL }>
{ site.URL }
</ExternalLink>
</VStack>
<div className="site-preview__badge">{ badge && <Badge>{ badge }</Badge> }</div>
</Grid>
);
};
Loading