Skip to content

Commit

Permalink
feat(core): add studioAnnouncements audienceRole check
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Sep 18, 2024
1 parent 2328969 commit d4dbf7c
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {createClient} from '@sanity/client'
import {useTelemetry} from '@sanity/telemetry/react'
import {useCallback, useEffect, useMemo, useState} from 'react'
import {useWorkspace} from 'sanity'
import {StudioAnnouncementContext} from 'sanity/_singletons'

import {SANITY_VERSION} from '../../version'
Expand All @@ -20,7 +21,7 @@ import {
type StudioAnnouncementsContextValue,
} from './types'
import {useSeenAnnouncements} from './useSeenAnnouncements'
import {isValidAudience} from './utils'
import {isValidAnnouncementAudience, isValidAnnouncementRole} from './utils'

interface StudioAnnouncementsProviderProps {
children: React.ReactNode
Expand All @@ -35,6 +36,7 @@ export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProvi
const [isCardDismissed, setIsCardDismissed] = useState(false)
const [studioAnnouncements, setStudioAnnouncements] = useState<StudioAnnouncementDocument[]>([])
const [seenAnnouncements, setSeenAnnouncements] = useSeenAnnouncements()
const {currentUser} = useWorkspace()

const unseenAnnouncements: StudioAnnouncementDocument[] = useMemo(() => {
// If it's loading return an empty array to avoid showing the card
Expand All @@ -57,21 +59,33 @@ export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProvi

useEffect(() => {
// TODO: Replace for internal api
const client = createClient({projectId: '3do82whm', dataset: 'next'})
const client = createClient({
projectId: 'm5jza465',
dataset: 'dev',
useCdn: false,
apiVersion: 'vX',
})

const subscription = client.observable
.fetch<StudioAnnouncementDocument[]>(studioAnnouncementQuery)
.subscribe({
next: (docs) => {
const validDocs = docs.filter((doc) => isValidAudience(doc, SANITY_VERSION))
const validDocs = docs.filter(
(doc) =>
isValidAnnouncementAudience(
{audience: doc.audience, studioVersion: doc.studioVersion},
SANITY_VERSION,
) && isValidAnnouncementRole(doc.audienceRole, currentUser?.roles),
)
setStudioAnnouncements(validDocs)
},
error: (error) => {
console.error('Error fetching studio announcements:', error)
},
})
// eslint-disable-next-line consistent-return
return () => subscription.unsubscribe()
}, [])
}, [currentUser?.roles])

const saveSeenAnnouncements = useCallback(() => {
// Mark all the announcements as seen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ describe('StudioAnnouncementsProvider', () => {
expect(result.current.unseenAnnouncements).toEqual(announcements)
expect(result.current.studioAnnouncements).toEqual(announcements)
})
test('if the audience is specific-version and studio matches ', () => {
test('if the audience is specific-version and studio matches', () => {
const {createClient} = require('@sanity/client')
const announcements: StudioAnnouncementDocument[] = [
{
Expand Down Expand Up @@ -546,7 +546,7 @@ describe('StudioAnnouncementsProvider', () => {
expect(result.current.unseenAnnouncements).toEqual(announcements)
expect(result.current.studioAnnouncements).toEqual(announcements)
})
test('if the audience is specific-version and studio doesnt match ', () => {
test("if the audience is specific-version and studio doesn't match ", () => {
const {createClient} = require('@sanity/client')
const announcements: StudioAnnouncementDocument[] = [
{
Expand Down Expand Up @@ -639,6 +639,72 @@ describe('StudioAnnouncementsProvider', () => {
wrapper,
})

expect(result.current.unseenAnnouncements).toEqual(announcements)
expect(result.current.studioAnnouncements).toEqual(announcements)
})
test("if the audienceRole is fixed and user doesn't have the role", () => {
// mocked workspace roles is [ { name: 'administrator', title: 'Administrator' } ]
const {createClient} = require('@sanity/client')
const announcements: StudioAnnouncementDocument[] = [
{
_id: 'studioAnnouncement-1',
_type: 'productAnnouncement',
_rev: '1',
_createdAt: '2024-09-10T14:44:00.000Z',
_updatedAt: "2024-09-10T14:44:00.000Z'",
title: 'Announcement 1',
body: [],
announcementType: 'whats-new',
publishedDate: '2024-09-10T14:44:00.000Z',
audienceRole: ['developer'],
audience: 'everyone',
},
]
const mockFetch = createClient().observable.fetch as jest.Mock
mockFetch.mockReturnValue({
subscribe: ({next}: any) => {
next(announcements)
return {unsubscribe: jest.fn()}
},
})

const {result} = renderHook(() => useStudioAnnouncements(), {
wrapper,
})

expect(result.current.unseenAnnouncements).toEqual([])
expect(result.current.studioAnnouncements).toEqual([])
})
test('if the audienceRole is fixed and user has the role', () => {
// mocked workspace roles is [ { name: 'administrator', title: 'Administrator' } ]
const {createClient} = require('@sanity/client')
const announcements: StudioAnnouncementDocument[] = [
{
_id: 'studioAnnouncement-1',
_type: 'productAnnouncement',
_rev: '1',
_createdAt: '2024-09-10T14:44:00.000Z',
_updatedAt: "2024-09-10T14:44:00.000Z'",
title: 'Announcement 1',
body: [],
announcementType: 'whats-new',
publishedDate: '2024-09-10T14:44:00.000Z',
audienceRole: ['administrator'],
audience: 'everyone',
},
]
const mockFetch = createClient().observable.fetch as jest.Mock
mockFetch.mockReturnValue({
subscribe: ({next}: any) => {
next(announcements)
return {unsubscribe: jest.fn()}
},
})

const {result} = renderHook(() => useStudioAnnouncements(), {
wrapper,
})

expect(result.current.unseenAnnouncements).toEqual(announcements)
expect(result.current.studioAnnouncements).toEqual(announcements)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {describe, expect, test} from '@jest/globals'

import {isValidAnnouncementAudience, isValidAnnouncementRole} from '../utils'

describe('isValidAnnouncementRole', () => {
const userRoles = [
{name: 'developer', title: 'Developer'},
{name: 'administrator', title: 'Administrator'},
]

test('returns true when audienceRole is undefined', () => {
expect(isValidAnnouncementRole(undefined, userRoles)).toBe(true)
})
test('returns true when user is undefined', () => {
expect(isValidAnnouncementRole(['administrator'], undefined)).toBe(false)
expect(isValidAnnouncementRole(['administrator'], [])).toBe(false)
})
test("returns true if the user's role is in the audienceRole", () => {
expect(isValidAnnouncementRole(['administrator'], userRoles)).toBe(true)
})
test("returns true if the user's role is in the audienceRole", () => {
expect(isValidAnnouncementRole(['developer', 'custom'], userRoles)).toBe(true)
})
test("returns false if the user's role is not in the audienceRole", () => {
expect(isValidAnnouncementRole(['editor'], userRoles)).toBe(false)
})
test("returns false if the user's role is not in the audienceRole", () => {
expect(isValidAnnouncementRole(['editor'], [{name: 'foo', title: 'Custom foo role'}])).toBe(
false,
)
})
test('returns false if the user has a custom role and we aim custom roles', () => {
expect(
isValidAnnouncementRole(
['custom'],
[...userRoles, {name: 'foo', title: 'A custom foo role'}],
),
).toBe(true)
})
})

describe('isValidAnnouncementAudience', () => {
test('should return true when audience is "everyone"', () => {
const announcement = {audience: 'everyone', studioVersion: undefined} as const
const sanityVersion = '3.55.0'
expect(isValidAnnouncementAudience(announcement, sanityVersion)).toBe(true)
})

describe('when audience is "specific-version"', () => {
const document = {audience: 'specific-version', studioVersion: '3.55.0'} as const
test('should return true when versions match', () => {
const sanityVersion = '3.55.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(true)
})

test('should return false when versions do not match', () => {
const sanityVersion = '3.56.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(false)
})
})

describe('when audience is "above-version"', () => {
const document = {audience: 'above-version', studioVersion: '3.55.0'} as const
test('should return true when sanityVersion is above document.studioVersion', () => {
const sanityVersion = '3.56.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(true)
})

test('should return false when sanityVersion is equal to document.studioVersion', () => {
const sanityVersion = '3.55.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(false)
})

test('should return false when sanityVersion is below document.studioVersion', () => {
const sanityVersion = '3.54.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(false)
})
})
describe('when audience is "below-version"', () => {
const document = {audience: 'below-version', studioVersion: '3.55.0'} as const
test('should return false when sanityVersion is above document.studioVersion', () => {
const sanityVersion = '3.56.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(false)
})

test('should return false when sanityVersion is equal to document.studioVersion', () => {
const sanityVersion = '3.55.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(false)
})

test('should return true when sanityVersion is below document.studioVersion', () => {
const sanityVersion = '3.54.0'
expect(isValidAnnouncementAudience(document, sanityVersion)).toBe(true)
})
})
})
12 changes: 12 additions & 0 deletions packages/sanity/src/core/studio/studioAnnouncements/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import {type PortableTextBlock} from 'sanity'

export const audienceRoles = [
'administrator',
'editor',
'viewer',
'contributor',
'developer',
'custom',
] as const

export type AudienceRole = (typeof audienceRoles)[number]

export interface StudioAnnouncementDocument {
_id: string
_type: 'productAnnouncement'
Expand All @@ -12,6 +23,7 @@ export interface StudioAnnouncementDocument {
publishedDate: string
expiryDate?: string
audience: 'everyone' | 'specific-version' | 'above-version' | 'below-version'
audienceRole?: AudienceRole[] | undefined
studioVersion?: string
}

Expand Down
38 changes: 31 additions & 7 deletions packages/sanity/src/core/studio/studioAnnouncements/utils.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,55 @@
import {type Role} from 'sanity'
import {satisfies} from 'semver'

import {type StudioAnnouncementDocument} from './types'
import {type AudienceRole, audienceRoles, type StudioAnnouncementDocument} from './types'

/**
* @internal
* @hidden
*/
export function isValidAudience(
document: StudioAnnouncementDocument,
studioVersion: string,
export function isValidAnnouncementAudience(
document: {
audience: StudioAnnouncementDocument['audience']
studioVersion: StudioAnnouncementDocument['studioVersion']
},
sanityVersion: string,
): boolean {
switch (document.audience) {
case 'everyone':
return true
case 'specific-version':
return satisfies(studioVersion, `= ${document.studioVersion}`, {
return satisfies(sanityVersion, `= ${document.studioVersion}`, {
includePrerelease: true,
})
case 'above-version':
return satisfies(studioVersion, `> ${document.studioVersion}`, {
return satisfies(sanityVersion, `> ${document.studioVersion}`, {
includePrerelease: true,
})
case 'below-version':
return satisfies(studioVersion, `< ${document.studioVersion}`, {
return satisfies(sanityVersion, `< ${document.studioVersion}`, {
includePrerelease: true,
})
default:
return true
}
}

/**
* @internal
* @hidden
*/
export function isValidAnnouncementRole(
audience: StudioAnnouncementDocument['audienceRole'] | undefined,
userRoles: Role[] | undefined,
): boolean {
if (!audience || !audience.length) return true
if (!userRoles || !userRoles.length) return false

if (userRoles.some((role) => audience.includes(role.name as AudienceRole))) return true

// Check if the user has a custom role
if (userRoles.some((role) => !audienceRoles.includes(role.name as AudienceRole))) {
return audience.includes('custom')
}
return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ const InlineIcon = styled(Icon)<InlineIconProps>`
}
`

const Link = styled.a<{useTextColor: boolean}>`
const Link = styled.a<{$useTextColor: boolean}>`
font-weight: 600;
color: ${(props) => (props.useTextColor ? 'var(--card-muted-fg-color) !important' : '')};
color: ${(props) => (props.$useTextColor ? 'var(--card-muted-fg-color) !important' : '')};
`

const DynamicIconContainer = styled.span<{$inline: boolean}>`
Expand Down Expand Up @@ -225,7 +225,7 @@ const createComponents = ({
href={props.value.href}
rel="noopener noreferrer"
target="_blank"
useTextColor={props.value.useTextColor}
$useTextColor={props.value.useTextColor}
// eslint-disable-next-line react/jsx-no-bind
onClick={
onLinkClick
Expand Down

0 comments on commit d4dbf7c

Please sign in to comment.