Skip to content

Commit

Permalink
feat(core): Studio announcements (#7515)
Browse files Browse the repository at this point in the history
* feat(core): update UpsellDescriptionSerializer to support h3, images & lists

* feat(core): studio announcements card and dialog gro-2493, gro-2498

* feat(core): add studio announcements telemetry events

* feat(core): add studioAnnouncement provider with unseen modals check

* feat(core): add studioAnnouncement menu item to Resources menu items

* chore(core): improvements to studio announcements

* chore(core): add tests for studio announcements

* chore(core): add tests to save seen announcements actions

* feat(core): update telemetry events for studioAnnouncements

* fix(core): update useSeenAnnouncements to handle state reset

* feat(core): add telemetry logs to announcement viewed and resources menu clicked

* chore(core): remove translations resources in tests

* fix(core): update query to check expiry date

* feat(core): add studioAnnouncements audienceRole check

* feat(core): replace client.fetch for internal api

* fix(core): move cardSeen telemetry log to card

* fix(core): add h2 to announcement dialog

* chore(core): add divider fade threshold details

* chore(core): refactor announcements provider fetch, use useObservable

* chore(core): update useSeenAnnouncements, handle seen and unseen through rxjs

* feat(core): update product announcement audience, (greater|less)-than-or-equal-version

* feat(core): add support for card preHeader

* fix(core): reduce studio announcements dialog height

* chore(core): update studio announcements telemetry, add internal_name
  • Loading branch information
pedrobonamin authored Sep 24, 2024
1 parent a218c88 commit 3593bf5
Show file tree
Hide file tree
Showing 25 changed files with 2,371 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {createContext} from 'sanity/_createContext'

import type {StudioAnnouncementsContextValue} from '../../core/studio/studioAnnouncements/types'

/**
* @internal
*/
export const StudioAnnouncementContext = createContext<StudioAnnouncementsContextValue | undefined>(
'sanity/_singletons/context/studioAnnouncements',
undefined,
)
1 change: 1 addition & 0 deletions packages/sanity/src/_singletons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export * from './context/SearchContext'
export * from './context/SortableItemIdContext'
export * from './context/SourceContext'
export * from './context/StructureToolContext'
export * from './context/StudioAnnouncementsContext'
export * from './context/TasksContext'
export * from './context/TasksEnabledContext'
export * from './context/TasksNavigationContext'
Expand Down
10 changes: 10 additions & 0 deletions packages/sanity/src/core/i18n/bundles/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ import {type LocaleResourceBundle} from '../types'
* @hidden
*/
export const studioLocaleStrings = defineLocalesResources('studio', {
/** The text used in the tooltip shown in the dialog close button */
'announcement.dialog.close': 'Close',
/** Aria label to be used in the dialog close button */
'announcement.dialog.close-label': 'Close dialog',
/**Text to be used in the tooltip in the button in the studio announcement card */
'announcement.floating-button.dismiss': 'Close',
/**Aria label to be used in the floating button in the studio announcement card, to dismiss the card */
'announcement.floating-button.dismiss-label': 'Dismiss announcements',
/**Aria label to be used in the floating button in the studio announcement card */
'announcement.floating-button.open-label': 'Open announcements',
/** Menu item for deleting the asset */
'asset-source.asset-list.menu.delete': 'Delete',
/** Menu item for showing where a particular asset is used */
Expand Down
5 changes: 4 additions & 1 deletion packages/sanity/src/core/studio/StudioProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
NotFoundScreen,
} from './screens'
import {type StudioProps} from './Studio'
import {StudioAnnouncementsProvider} from './studioAnnouncements/StudioAnnouncementsProvider'
import {StudioErrorBoundary} from './StudioErrorBoundary'
import {StudioTelemetryProvider} from './StudioTelemetryProvider'
import {StudioThemeProvider} from './StudioThemeProvider'
Expand Down Expand Up @@ -69,7 +70,9 @@ export function StudioProvider({
<LocaleProvider>
<PackageVersionStatusProvider>
<MaybeEnableErrorReporting errorReporter={errorReporter} />
<ResourceCacheProvider>{children}</ResourceCacheProvider>
<ResourceCacheProvider>
<StudioAnnouncementsProvider>{children}</StudioAnnouncementsProvider>
</ResourceCacheProvider>
</PackageVersionStatusProvider>
</LocaleProvider>
</StudioTelemetryProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {LoadingBlock} from '../../../../components/loadingBlock'
import {hasSanityPackageInImportMap} from '../../../../environment/hasSanityPackageInImportMap'
import {useTranslation} from '../../../../i18n'
import {SANITY_VERSION} from '../../../../version'
import {StudioAnnouncementsMenuItem} from '../../../studioAnnouncements/StudioAnnouncementsMenuItem'
import {type ResourcesResponse, type Section} from './helper-functions/types'

interface ResourcesMenuItemProps {
Expand Down Expand Up @@ -97,6 +98,8 @@ function SubSection({subSection}: {subSection: Section}) {
)
case 'internalAction': // TODO: Add support for internal actions (MVI-2)
if (!item.type) return null
if (item.type === 'studio-announcements-modal')
return <StudioAnnouncementsMenuItem text={item.title} />
return (
item.type === 'show-welcome-modal' && <MenuItem key={item._key} text={item.title} />
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface InternalAction extends Item {
type?: InternalActionType
}

type InternalActionType = 'show-welcome-modal'
type InternalActionType = 'show-welcome-modal' | 'studio-announcements-modal'

/**
* @hidden
Expand Down
1 change: 1 addition & 0 deletions packages/sanity/src/core/studio/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './copyPaste'
export * from './renderStudio'
export * from './source'
export * from './Studio'
export * from './studioAnnouncements'
export * from './StudioLayout'
export * from './StudioProvider'
export * from './upsell'
Expand Down
62 changes: 62 additions & 0 deletions packages/sanity/src/core/studio/studioAnnouncements/Divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {Box} from '@sanity/ui'
import {useEffect, useRef, useState} from 'react'
import {styled} from 'styled-components'

const Hr = styled.hr<{$show: boolean}>`
height: 1px;
background: var(--card-border-color);
width: 100%;
opacity: ${({$show}) => ($show ? 1 : 0)};
transition: opacity 0.3s ease;
margin: 0;
border: none;
`

interface DividerProps {
parentRef: React.RefObject<HTMLDivElement>
}

/**
* This is the threshold for the divider to start fading
* uses a negative value to start fading before reaching the top
* of the parent.
* We want to fade out the divider so it doesn't overlap with the close icon when reaching the top.
* It's the sum of the title height (48px) and the divider padding top (12px)
*/
const DIVIDER_FADE_THRESHOLD = '-60px 0px 0px 0px'

/**
* A divider that fades when reaching the top of the parent.
*/
export function Divider({parentRef}: DividerProps): JSX.Element {
const itemRef = useRef<HTMLHRElement | null>(null)
const [show, setShow] = useState(true)

useEffect(() => {
const item = itemRef.current
const parent = parentRef.current

if (!item || !parent) return
const observer = new IntersectionObserver(
([entry]) => {
setShow(entry.isIntersecting)
},
{root: parent, threshold: 0, rootMargin: DIVIDER_FADE_THRESHOLD},
)

observer.observe(item)

// eslint-disable-next-line consistent-return
return () => {
observer.disconnect()
}
}, [parentRef])

return (
<Box paddingBottom={4}>
<Box paddingY={3} paddingX={3}>
<Hr ref={itemRef} $show={show} />
</Box>
</Box>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/* eslint-disable camelcase */
import {RemoveIcon} from '@sanity/icons'
import {useTelemetry} from '@sanity/telemetry/react'
import {Box, Card, Stack, Text} from '@sanity/ui'
// eslint-disable-next-line camelcase
import {getTheme_v2} from '@sanity/ui/theme'
import {useEffect} from 'react'
import {useTranslation} from 'sanity'
import {css, keyframes, styled} from 'styled-components'

import {Button, Popover} from '../../../ui-components'
import {SANITY_VERSION} from '../../version'
import {ProductAnnouncementCardSeen} from './__telemetry__/studioAnnouncements.telemetry'

const keyframe = keyframes`
0% {
background-position: 100%;
}
100% {
background-position: -100%;
}
`

const Root = styled.div((props) => {
const theme = getTheme_v2(props.theme)
const cardHoverBg = theme.color.selectable.default.hovered.bg
const cardNormalBg = theme.color.selectable.default.enabled.bg

return css`
position: relative;
cursor: pointer;
// hide the close button
#close-floating-button {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
> [data-ui='whats-new-card'] {
--card-bg-color: ${cardHoverBg};
box-shadow: inset 0 0 2px 1px var(--card-skeleton-color-to);
background-image: linear-gradient(
to right,
var(--card-bg-color),
var(--card-bg-color),
${cardNormalBg},
var(--card-bg-color),
var(--card-bg-color),
var(--card-bg-color)
);
background-position: 100%;
background-size: 200% 100%;
background-attachment: fixed;
animation-name: ${keyframe};
animation-timing-function: ease-in;
animation-iteration-count: infinite;
animation-duration: 2000ms;
}
#close-floating-button {
opacity: 1;
background: transparent;
&:hover {
transition: all 0.2s;
box-shadow: 0 0 0 1px ${theme.color.selectable.default.hovered.border};
}
}
}
`
})

const ButtonRoot = styled.div`
z-index: 1;
position: absolute;
top: 4px;
right: 6px;
`

interface StudioAnnouncementCardProps {
title: string
id: string
name: string
isOpen: boolean
preHeader: string
onCardClick: () => void
onCardDismiss: () => void
}

/**
* @internal
* @hidden
*/
export function StudioAnnouncementsCard({
title,
id,
isOpen,
name,
preHeader,
onCardClick,
onCardDismiss,
}: StudioAnnouncementCardProps) {
const {t} = useTranslation()
const telemetry = useTelemetry()

useEffect(() => {
if (isOpen) {
telemetry.log(ProductAnnouncementCardSeen, {
announcement_id: id,
announcement_title: title,
announcement_internal_name: name,
source: 'studio',
studio_version: SANITY_VERSION,
})
}
}, [telemetry, id, title, isOpen, name])

return (
<Popover
open={isOpen}
shadow={3}
portal
style={{
bottom: 12,
left: 12,
top: 'none',
}}
width={0}
placement="bottom-start"
content={
<Root data-ui="whats-new-root">
<Card
data-ui="whats-new-card"
padding={3}
radius={3}
onClick={onCardClick}
role="button"
aria-label={t('announcement.floating-button.open-label')}
>
<Stack space={3}>
<Box marginRight={6}>
<Text as={'h3'} size={1} muted>
{preHeader}
</Text>
</Box>
<Text size={1} weight="medium">
{title}
</Text>
</Stack>
</Card>
<ButtonRoot>
<Button
id="close-floating-button"
mode="bleed"
onClick={onCardDismiss}
icon={RemoveIcon}
tone="default"
aria-label={t('announcement.floating-button.dismiss-label')}
tooltipProps={{
content: t('announcement.floating-button.dismiss'),
}}
/>
</ButtonRoot>
</Root>
}
/>
)
}
Loading

0 comments on commit 3593bf5

Please sign in to comment.