@sanity/dashboard 3.1.5 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":["../src/components/DashboardWidgetContainer.tsx","../src/versionedClient.ts","../src/containers/DashboardContext.tsx","../src/containers/WidgetContainer.tsx","../src/widgets/projectInfo/ProjectInfo.tsx","../src/widgets/projectInfo/index.ts","../src/widgets/projectUsers/ProjectUser.tsx","../src/widgets/projectUsers/ProjectUsers.tsx","../src/widgets/projectUsers/index.ts","../src/widgets/sanityTutorials/Tutorial.tsx","../src/widgets/sanityTutorials/dataAdapter.ts","../src/widgets/sanityTutorials/SanityTutorials.tsx","../src/widgets/sanityTutorials/index.ts","../src/components/DashboardLayout.tsx","../src/components/WidgetGroup.tsx","../src/containers/Dashboard.tsx","../src/plugin.tsx"],"sourcesContent":["import React, {forwardRef} from 'react'\nimport {Card, Box, Heading} from '@sanity/ui'\nimport {styled} from 'styled-components'\n\nconst Root = styled(Card)`\n display: flex;\n flex-direction: column;\n justify-content: stretch;\n height: 100%;\n box-sizing: border-box;\n position: relative;\n`\n\nconst Header = styled(Card)`\n position: sticky;\n top: 0;\n z-index: 2;\n border-top-left-radius: inherit;\n border-top-right-radius: inherit;\n`\n\nconst Footer = styled(Card)`\n position: sticky;\n overflow: hidden;\n bottom: 0;\n z-index: 2;\n border-bottom-right-radius: inherit;\n border-bottom-left-radius: inherit;\n margin-top: auto;\n`\n\nconst Content = styled(Box)`\n position: relative;\n z-index: 1;\n height: stretch;\n min-height: 21.5em;\n\n @media (min-width: ${({theme}) => theme.sanity.media[0]}px) {\n overflow-y: auto;\n outline: none;\n }\n`\n\ninterface DashboardWidgetProps {\n header?: string\n children: React.ReactNode\n footer?: React.ReactNode\n}\n\nexport const DashboardWidgetContainer = forwardRef(function DashboardWidgetContainer(\n props: DashboardWidgetProps,\n ref: React.Ref<HTMLDivElement>,\n) {\n const {header, children, footer} = props\n\n return (\n <Root radius={3} display=\"flex\" ref={ref}>\n {header && (\n <Header borderBottom paddingX={3} paddingY={4}>\n <Heading size={1} textOverflow=\"ellipsis\">\n {header}\n </Heading>\n </Header>\n )}\n {children && <Content>{children}</Content>}\n {footer && <Footer borderTop>{footer}</Footer>}\n </Root>\n )\n})\n","import {useClient} from 'sanity'\n\nexport function useVersionedClient() {\n return useClient({apiVersion: '2024-06-03'})\n}\n","import {createContext, useContext} from 'react'\nimport {DashboardConfig} from '../types'\n\nexport const DashboardContext = createContext<DashboardConfig>({widgets: []})\n\nexport function useDashboardConfig(): DashboardConfig {\n return useContext(DashboardContext)\n}\n","import React, {createElement, useMemo} from 'react'\nimport {useDashboardConfig} from './DashboardContext'\nimport {Card} from '@sanity/ui'\nimport {DashboardWidget} from '../types'\n\nexport function WidgetContainer(props: DashboardWidget) {\n const config = useDashboardConfig()\n const layout = useMemo(\n () => ({\n ...(props.layout || {}),\n ...(config.layout || {}),\n }),\n [props.layout, config.layout],\n )\n\n return (\n <Card shadow={1} data-width={layout.width} data-height={layout.height}>\n {createElement(props.component, {})}\n </Card>\n )\n}\n","import React, {useEffect, useMemo, useState} from 'react'\nimport {Box, Card, Stack, Heading, Grid, Label, Text, Code, Button} from '@sanity/ui'\nimport {useVersionedClient} from '../../versionedClient'\nimport {Subscription} from 'rxjs'\nimport {WidgetContainer} from '../../containers/WidgetContainer'\nimport {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'\nimport {DashboardWidget} from '../../types'\n\nexport interface ProjectInfoProps {\n __experimental_before?: DashboardWidget[]\n data: ProjectData[]\n}\n\ninterface App {\n title: string\n rows?: App[]\n value?: string | {error: string}\n}\n\ninterface ProjectData {\n title: string\n category?: string\n}\n\nfunction isUrl(url?: string) {\n return url && /^https?:\\/\\//.test(`${url}`)\n}\n\nfunction getGraphQlUrl(projectId: string, dataset: string) {\n return `https://${projectId}.api.sanity.io/v1/graphql/${dataset}/default`\n}\n\nfunction getGroqUrl(projectId: string, dataset: string) {\n return `https://${projectId}.api.sanity.io/v1/groq/${dataset}`\n}\n\nfunction getManageUrl(projectId: string) {\n return `https://manage.sanity.io/projects/${projectId}`\n}\n\nconst NO_EXPERIMENTAL: DashboardWidget[] = []\nconst NO_DATA: ProjectData[] = []\n\nexport function ProjectInfo(props: ProjectInfoProps) {\n const {__experimental_before = NO_EXPERIMENTAL, data = NO_DATA} = props\n const [studioHost, setStudioHost] = useState<string | {error: string} | undefined>()\n const [graphqlApi, setGraphQlApi] = useState<string | {error: string} | undefined>()\n const versionedClient = useVersionedClient()\n const {projectId = 'unknown', dataset = 'unknown'} = versionedClient.config()\n\n useEffect(() => {\n const subscriptions: Subscription[] = []\n\n subscriptions.push(\n versionedClient.observable\n .request<{\n studioHost: string\n metadata?: {externalStudioHost?: string}\n }>({uri: `/projects/${projectId}`, tag: 'dashboard.project-info.studio-host'})\n .subscribe({\n next: (result) => {\n if (result.metadata?.externalStudioHost) {\n setStudioHost(result.metadata.externalStudioHost)\n return\n }\n\n setStudioHost(\n result.studioHost ? `https://${result.studioHost}.sanity.studio` : undefined,\n )\n },\n error: (error) => {\n console.error('Error while looking for studioHost', error)\n setStudioHost({\n error: 'Something went wrong while looking up studioHost. See console.',\n })\n },\n }),\n )\n\n // ping assumed graphql endpoint\n subscriptions.push(\n versionedClient.observable\n .request({\n method: 'HEAD',\n uri: `/graphql/${dataset}/default`,\n tag: 'dashboard.project-info.graphql-api',\n })\n .subscribe({\n next: () => setGraphQlApi(getGraphQlUrl(projectId, dataset)),\n error: (error) => {\n if (error.statusCode === 404) {\n setGraphQlApi(undefined)\n } else {\n console.error('Error while looking for graphqlApi', error)\n setGraphQlApi({\n error: 'Something went wrong while looking up graphqlApi. See console.',\n })\n }\n },\n }),\n )\n\n return () => {\n subscriptions.forEach((s) => s.unsubscribe())\n }\n }, [dataset, projectId, versionedClient, setGraphQlApi, setStudioHost])\n\n const assembleTableRows = useMemo(() => {\n let result: App[] = [\n {\n title: 'Sanity project',\n rows: [\n {title: 'Project ID', value: projectId},\n {title: 'Dataset', value: dataset},\n ],\n },\n ]\n\n // Handle any apps\n const apps: App[] = [\n studioHost ? {title: 'Studio', value: studioHost} : null,\n ...data.filter((item) => item.category === 'apps'),\n ].filter((a): a is App => !!a)\n if (apps.length > 0) {\n result = result.concat([{title: 'Apps', rows: apps}])\n }\n\n // Handle APIs\n result = result.concat(\n [\n {\n title: 'APIs',\n rows: [\n {title: 'GROQ', value: getGroqUrl(projectId, dataset)},\n {\n title: 'GraphQL',\n value: (typeof graphqlApi === 'object' ? 'Error' : graphqlApi) ?? 'Not deployed',\n },\n ],\n },\n ],\n data.filter((item) => item.category === 'apis'),\n )\n\n // Handle whatever else there might be\n const otherStuff: Record<string, ProjectData[]> = {}\n data.forEach((item) => {\n if (item.category && item.category !== 'apps' && item.category !== 'apis') {\n if (!otherStuff[item.category]) {\n otherStuff[item.category] = []\n }\n otherStuff[item.category].push(item)\n }\n })\n Object.keys(otherStuff).forEach((category) => {\n result.push({title: category, rows: otherStuff[category]})\n })\n\n return result\n }, [graphqlApi, studioHost, projectId, dataset, data])\n\n return (\n <>\n {__experimental_before.map((widgetConfig, idx) => (\n <WidgetContainer key={idx} {...widgetConfig} />\n ))}\n <Box height=\"fill\" marginTop={__experimental_before?.length > 0 ? 4 : 0}>\n <DashboardWidgetContainer\n footer={\n <Button\n style={{width: '100%'}}\n paddingX={2}\n paddingY={4}\n mode=\"bleed\"\n tone=\"primary\"\n text=\"Manage project\"\n as=\"a\"\n href={getManageUrl(projectId)}\n />\n }\n >\n <Card\n paddingY={4}\n radius={2}\n role=\"table\"\n aria-label=\"Project info\"\n aria-describedby=\"project_info_table\"\n >\n <Stack space={4}>\n <Box paddingX={3} as=\"header\">\n <Heading size={1} as=\"h2\" id=\"project_info_table\">\n Project info\n </Heading>\n </Box>\n {assembleTableRows.map((item) => {\n if (!item || !item.rows) {\n return null\n }\n\n return (\n <Stack key={item.title} space={3}>\n <Card borderBottom padding={3}>\n <Label size={0} muted role=\"columnheader\">\n {item.title}\n </Label>\n </Card>\n <Stack space={4} paddingX={3} role=\"rowgroup\">\n {item.rows.map((row) => {\n return (\n <Grid key={row.title} columns={2} role=\"row\">\n <Text weight=\"medium\" role=\"rowheader\">\n {row.title}\n </Text>\n {typeof row.value === 'object' && (\n <Text size={1}>{row.value?.error}</Text>\n )}\n {typeof row.value === 'string' && (\n <>\n {isUrl(row.value) ? (\n <Text size={1} role=\"cell\" style={{wordBreak: 'break-word'}}>\n <a href={row.value}>{row.value}</a>\n </Text>\n ) : (\n <Code size={1} role=\"cell\" style={{wordBreak: 'break-word'}}>\n {row.value}\n </Code>\n )}\n </>\n )}\n </Grid>\n )\n })}\n </Stack>\n </Stack>\n )\n })}\n </Stack>\n </Card>\n </DashboardWidgetContainer>\n </Box>\n </>\n )\n}\n","import {ProjectInfo} from './ProjectInfo'\nimport {LayoutConfig, DashboardWidget} from '../../types'\n\nexport function projectInfoWidget(config?: {layout?: LayoutConfig}): DashboardWidget {\n return {\n name: 'project-info',\n component: ProjectInfo,\n layout: config?.layout ?? {width: 'medium'},\n }\n}\n","import React from 'react'\nimport {Box, Flex, rem, Stack, Text} from '@sanity/ui'\nimport {styled} from 'styled-components'\nimport {useListFormat, type User, UserAvatar} from 'sanity'\nimport {RobotIcon} from '@sanity/icons'\n\nconst Root = styled(Flex)`\n height: ${rem(33)}; // 33 = PREVIEW_SIZES.default.media.height\n box-sizing: content-box;\n`\n\nexport interface ProjectUserProps {\n user: User\n isRobot: boolean\n roles: string[]\n}\n\nexport function ProjectUser({user, isRobot, roles}: ProjectUserProps) {\n const listFormat = useListFormat({style: 'narrow'})\n return (\n <Root align=\"center\">\n <Flex align=\"center\" flex={1} gap={2}>\n <Box flex=\"none\">\n {isRobot ? (\n <Text size={2}>\n <RobotIcon />\n </Text>\n ) : (\n <UserAvatar user={user} />\n )}\n </Box>\n\n <Stack flex={1} space={2}>\n <Text size={1} style={{color: 'inherit'}} textOverflow=\"ellipsis\" weight=\"medium\">\n {user.displayName}\n </Text>\n\n <Text muted size={1} textOverflow=\"ellipsis\">\n {listFormat.format(roles)}\n </Text>\n </Stack>\n </Flex>\n </Root>\n )\n}\n","import React, {useCallback, useEffect, useState} from 'react'\nimport {from} from 'rxjs'\nimport {map, switchMap} from 'rxjs/operators'\nimport {Stack, Spinner, Box, Text, Button} from '@sanity/ui'\nimport {Role, useUserStore} from 'sanity'\nimport {useVersionedClient} from '../../versionedClient'\nimport {User} from 'sanity'\nimport {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'\nimport {ProjectUser} from './ProjectUser'\n\nfunction getInviteUrl(projectId: string) {\n return `https://manage.sanity.io/projects/${projectId}/members`\n}\n\ninterface Member {\n id: string\n roles: Role[]\n isRobot: boolean\n isCurrentUser: boolean\n createdAt: string\n}\n\ninterface Project {\n id: string\n members: Member[]\n}\n\nexport function ProjectUsers() {\n const [project, setProject] = useState<Project | undefined>()\n const [users, setUsers] = useState<User[] | undefined>()\n const [error, setError] = useState<Error | undefined>()\n\n const userStore = useUserStore()\n const versionedClient = useVersionedClient()\n\n const fetchData = useCallback(() => {\n const {projectId} = versionedClient.config()\n const subscription = versionedClient.observable\n .request<Project>({\n uri: `/projects/${projectId}`,\n tag: 'dashboard.project-users',\n })\n .pipe(\n switchMap((_project) =>\n from(userStore.getUsers(_project.members.map((mem) => mem.id))).pipe(\n map((_users) => ({project: _project, users: _users})),\n ),\n ),\n )\n .subscribe({\n next: ({users: _users, project: _project}) => {\n setProject(_project)\n setUsers(\n (Array.isArray(_users) ? _users : [_users]).sort((userA, userB) =>\n sortUsersByRobotStatus(userA, userB, _project),\n ),\n )\n },\n error: (e: Error) => setError(e),\n })\n\n return () => subscription.unsubscribe()\n }, [userStore, versionedClient])\n\n useEffect(() => fetchData(), [fetchData])\n\n const handleRetryFetch = useCallback(() => fetchData(), [fetchData])\n\n const isLoading = !users || !project\n\n if (error) {\n return (\n <DashboardWidgetContainer header=\"Project users\">\n <Box padding={4}>\n <Text>\n Something went wrong while fetching data. You could{' '}\n <a onClick={handleRetryFetch} title=\"Retry users fetch\" style={{cursor: 'pointer'}}>\n retry\n </a>\n ..?\n </Text>\n </Box>\n </DashboardWidgetContainer>\n )\n }\n\n return (\n <DashboardWidgetContainer\n header=\"Project users\"\n footer={\n <Button\n style={{width: '100%'}}\n paddingX={2}\n paddingY={4}\n mode=\"bleed\"\n tone=\"primary\"\n text=\"Manage members\"\n as=\"a\"\n loading={isLoading}\n href={isLoading ? undefined : getInviteUrl(project.id)}\n />\n }\n >\n {isLoading && (\n <Box paddingY={5} paddingX={2}>\n <Stack space={4}>\n <Text align=\"center\" muted size={1}>\n <Spinner />\n </Text>\n <Text align=\"center\" size={1} muted>\n Loading items…\n </Text>\n </Stack>\n </Box>\n )}\n\n {!isLoading && (\n <Stack space={3} padding={3}>\n {users?.map((user) => {\n const membership = project.members.find((member) => member.id === user.id)\n return (\n <ProjectUser\n key={user.id}\n user={user}\n isRobot={membership?.isRobot ?? false}\n roles={membership?.roles.map((role) => role.title) || []}\n />\n )\n })}\n </Stack>\n )}\n </DashboardWidgetContainer>\n )\n}\n\nfunction sortUsersByRobotStatus(userA: User, userB: User, project: Project) {\n const {members} = project\n const membershipA = members.find((member) => member.id === userA?.id)\n const membershipB = members.find((member) => member.id === userB?.id)\n\n // On ties, sort by when the user was added\n if (membershipA?.isRobot === membershipB?.isRobot) {\n return (membershipA?.createdAt || '') > (membershipB?.createdAt || '') ? 1 : -1\n }\n\n // Robots go to the bottom\n return membershipA?.isRobot ? 1 : -1\n}\n","import {ProjectUsers} from './ProjectUsers'\nimport {LayoutConfig, DashboardWidget} from '../../types'\n\nexport function projectUsersWidget(config?: {layout?: LayoutConfig}): DashboardWidget {\n return {\n name: 'project-info',\n component: ProjectUsers,\n layout: config?.layout,\n }\n}\n","import React from 'react'\nimport {Card, Box, Heading, Flex, Text, Stack} from '@sanity/ui'\nimport {PlayIcon} from '@sanity/icons'\nimport {styled} from 'styled-components'\n\nconst PlayIconBox = styled(Box)`\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n\n &:before {\n content: '';\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 2.75em;\n height: 2.75em;\n border-radius: 50%;\n background: ${({theme}) => theme.sanity.color.card.enabled.bg};\n opacity: 0.75;\n }\n`\n\nconst Root = styled(Flex)`\n &:hover {\n ${PlayIconBox} {\n &:before {\n opacity: 1;\n }\n }\n }\n`\n\nconst PosterCard = styled(Card)`\n width: 100%;\n padding-bottom: calc(9 / 16 * 100%);\n position: relative;\n`\n\nconst Poster = styled.img`\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n object-fit: cover;\n display: block;\n\n &:not([src]) {\n display: none;\n }\n`\n\nexport interface TutorialProps {\n title: string\n posterURL?: string\n href: string\n showPlayIcon?: boolean\n presenterName?: string\n presenterSubtitle?: string\n}\n\nexport function Tutorial(props: TutorialProps) {\n const {title, posterURL, showPlayIcon, href, presenterName, presenterSubtitle} = props\n\n return (\n <Root flex={1}>\n <Card\n sizing=\"border\"\n flex={1}\n padding={2}\n radius={2}\n as=\"a\"\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n style={{position: 'relative'}}\n >\n <Flex direction=\"column\" style={{height: '100%'}}>\n {posterURL && (\n <PosterCard marginBottom={1}>\n <Poster src={posterURL} />\n {showPlayIcon && (\n <PlayIconBox display=\"flex\">\n <Text align=\"center\">\n <PlayIcon />\n </Text>\n </PlayIconBox>\n )}\n </PosterCard>\n )}\n <Flex direction=\"column\" justify=\"space-between\" paddingY={2} flex={1}>\n <Heading as=\"h3\" size={1}>\n {title}\n </Heading>\n <Box marginTop={4}>\n <Stack space={2} flex={1}>\n <Text size={1}>{presenterName}</Text>\n <Text size={0} style={{opacity: 0.7}}>\n {presenterSubtitle}\n </Text>\n </Stack>\n </Box>\n </Flex>\n </Flex>\n </Card>\n </Root>\n )\n}\n","import {useMemo} from 'react'\nimport {useVersionedClient} from '../../versionedClient'\nimport imageUrlBuilder from '@sanity/image-url'\n\nconst tutorialsProjectConfig = {\n projectId: '3do82whm',\n dataset: 'next',\n}\n\nexport interface Guide {\n _type?: string\n slug?: {current: string}\n presenter?: {\n name?: string\n }\n}\n\nexport interface FeedItem {\n _id: string\n title?: string\n poster?: string\n category?: string\n guideOrTutorial?: Guide\n externalLink?: string\n presenter?: {\n name?: string\n }\n hasVideo?: boolean\n}\n\nexport function useDataAdapter() {\n const versionedClient = useVersionedClient()\n return useMemo(\n () => ({\n getFeed: (templateRepoId: string) => {\n const uri = templateRepoId\n ? `/addons/dashboard?templateRepoId=${templateRepoId}`\n : '/addons/dashboard'\n return versionedClient.observable.request<{items: FeedItem[]}>({\n uri,\n tag: 'dashboard.sanity-tutorials',\n withCredentials: false,\n })\n },\n urlBuilder: imageUrlBuilder(tutorialsProjectConfig),\n }),\n [versionedClient],\n )\n}\n","import React, {useEffect, useState} from 'react'\nimport {Flex} from '@sanity/ui'\nimport {Tutorial} from './Tutorial'\nimport {FeedItem, Guide, useDataAdapter} from './dataAdapter'\nimport {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'\n\nfunction createUrl(slug: {current: string}, type?: string) {\n if (type === 'tutorial') {\n return `https://www.sanity.io/docs/tutorials/${slug.current}`\n } else if (type === 'guide') {\n return `https://www.sanity.io/docs/guides/${slug.current}`\n }\n return false\n}\n\nexport interface SanityTutorialsProps {\n templateRepoId: string\n}\n\nexport function SanityTutorials(props: SanityTutorialsProps) {\n const {templateRepoId} = props\n const [feedItems, setFeedItems] = useState<FeedItem[]>([])\n\n const {getFeed, urlBuilder} = useDataAdapter()\n\n useEffect(() => {\n const subscription = getFeed(templateRepoId).subscribe((response) => {\n setFeedItems(response.items)\n })\n return () => {\n subscription.unsubscribe()\n }\n }, [setFeedItems, getFeed, templateRepoId])\n\n const title = 'Learn about Sanity'\n\n return (\n <DashboardWidgetContainer header={title}>\n <Flex as=\"ul\" overflow=\"auto\" align=\"stretch\" paddingY={2}>\n {feedItems?.map((feedItem, index) => {\n if (!feedItem.title || (!feedItem.guideOrTutorial && !feedItem.externalLink)) {\n return null\n }\n const presenter = feedItem.presenter || feedItem.guideOrTutorial?.presenter || {}\n const subtitle = feedItem.category\n const {guideOrTutorial = {} as Guide} = feedItem\n const href =\n (guideOrTutorial.slug\n ? createUrl(guideOrTutorial.slug, guideOrTutorial._type)\n : feedItem.externalLink) || feedItem.externalLink\n\n return (\n <Flex\n as=\"li\"\n key={feedItem._id}\n paddingRight={index < feedItems?.length - 1 ? 1 : 3}\n paddingLeft={index === 0 ? 3 : 0}\n align=\"stretch\"\n style={{minWidth: 272, width: '30%'}}\n >\n <Tutorial\n title={feedItem.title}\n href={href ?? ''}\n presenterName={presenter.name}\n presenterSubtitle={subtitle}\n showPlayIcon={feedItem.hasVideo}\n posterURL={\n feedItem.poster ? urlBuilder.image(feedItem.poster).height(360).url() : undefined\n }\n />\n </Flex>\n )\n })}\n </Flex>\n </DashboardWidgetContainer>\n )\n}\n","import {SanityTutorials} from './SanityTutorials'\nimport {LayoutConfig, DashboardWidget} from '../../types'\n\nexport function sanityTutorialsWidget(config?: {layout?: LayoutConfig}): DashboardWidget {\n return {\n name: 'sanity-tutorials',\n component: SanityTutorials,\n layout: config?.layout ?? {width: 'full'},\n }\n}\n","import React, {PropsWithChildren} from 'react'\nimport {Container} from '@sanity/ui'\n\nexport function DashboardLayout(props: PropsWithChildren<{}>) {\n return (\n <Container width={4} padding={4} sizing=\"border\" style={{height: '100%', overflowY: 'auto'}}>\n {props.children}\n </Container>\n )\n}\n","import React from 'react'\nimport {styled, css} from 'styled-components'\nimport {Box, Card, Grid, Text} from '@sanity/ui'\nimport {WidgetContainer} from '../containers/WidgetContainer'\nimport {DashboardConfig, LayoutConfig, DashboardWidget} from '../types'\n\nconst media = {\n small: (...args: Parameters<typeof css>) => css`\n @media (min-width: ${({theme}) => theme.sanity.media[0]}px) {\n ${css(...args)}\n }\n `,\n medium: (...args: Parameters<typeof css>) => css`\n @media (min-width: ${({theme}) => theme.sanity.media[2]}px) {\n ${css(...args)}\n }\n `,\n}\n\nconst Root = styled(Grid)`\n grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));\n\n & > div {\n overflow: hidden;\n }\n\n & > div[data-width='medium'] {\n ${media.small`\n grid-column: span 2;\n `}\n }\n\n & > div[data-width='large'] {\n ${media.small`\n grid-column: span 2;\n `}\n\n ${media.medium`\n grid-column: span 3;\n `}\n }\n\n & > div[data-width='full'] {\n ${media.small`\n grid-column: 1 / -1;\n `}\n }\n\n & > div[data-height='medium'] {\n ${media.small`\n grid-row: span 2;\n `}\n }\n\n & > div[data-height='large'] {\n ${media.small`\n grid-row: span 2;\n `}\n\n ${media.medium`\n grid-row: span 3;\n `}\n }\n\n & > div[data-height='full'] {\n ${media.medium`\n grid-row: 1 / -1;\n `}\n }\n`\n\nexport interface WidgetGroupProps {\n config: Partial<DashboardConfig>\n}\n\nconst NO_WIDGETS: DashboardWidget[] = []\nconst NO_LAYOUT: LayoutConfig = {}\n\nexport function WidgetGroup(props: WidgetGroupProps) {\n const {\n config: {layout = NO_LAYOUT, widgets = NO_WIDGETS},\n } = props\n return (\n <Root\n autoFlow=\"row dense\"\n data-width={layout.width || 'auto'}\n data-height={layout.height || 'auto'}\n gap={4}\n >\n {widgets.length ? null : (\n <Card padding={4} shadow={1} tone=\"primary\">\n <Text align=\"center\">Add some widgets to populate this space.</Text>\n </Card>\n )}\n {widgets.map((widgetConfig, index) => {\n if (widgetConfig.type === '__experimental_group') {\n return <WidgetGroup key={index} config={widgetConfig} />\n }\n if (widgetConfig.component) {\n return <WidgetContainer key={index} {...widgetConfig} />\n }\n return <Box key={index}>{widgetConfig.name} is missing widget component</Box>\n })}\n </Root>\n )\n}\n","import React from 'react'\nimport {DashboardLayout} from '../components/DashboardLayout'\nimport {WidgetGroup} from '../components/WidgetGroup'\nimport {DashboardContext} from './DashboardContext'\nimport {DashboardConfig} from '../types'\n\nexport function Dashboard({config}: {config: DashboardConfig}) {\n if (!config) {\n return null\n }\n\n return (\n <DashboardContext.Provider value={config}>\n <DashboardLayout>\n <WidgetGroup config={config} />\n </DashboardLayout>\n </DashboardContext.Provider>\n )\n}\n","import React, {ComponentType, CSSProperties} from 'react'\nimport {Dashboard} from './containers/Dashboard'\nimport {definePlugin} from 'sanity'\nimport {DashboardConfig, DashboardWidget, LayoutConfig} from './types'\n\nconst strokeStyle: CSSProperties = {\n stroke: 'currentColor',\n strokeWidth: 1.2,\n}\n\nconst DashboardIcon = () => (\n <svg\n data-sanity-icon\n viewBox=\"0 0 25 25\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n preserveAspectRatio=\"xMidYMid\"\n width=\"1em\"\n height=\"1em\"\n >\n <path d=\"M19.5 19.5H5.5V5.5H19.5V19.5Z\" style={strokeStyle} />\n <path d=\"M5.5 12.5H19.5\" style={strokeStyle} />\n <path d=\"M14.5 19.5V12.5M10.5 12.5V5.5\" style={strokeStyle} />\n </svg>\n)\n\nexport interface DashboardPluginConfig {\n /**\n * Dashboard tool title\n */\n title?: string\n /**\n * Dashboard tool name (used in url path)\n */\n name?: string\n /**\n * Dashboard tool icon\n */\n icon?: ComponentType\n widgets?: DashboardWidget[]\n\n /**\n * Will be used for widgets that do not define a layout directly.\n */\n defaultLayout?: LayoutConfig\n}\n\nexport const dashboardTool = definePlugin<DashboardPluginConfig>((config = {}) => {\n const pluginConfig: DashboardConfig = {\n layout: config.defaultLayout ?? {},\n widgets: config.widgets ?? [],\n }\n\n const title = config.title ?? 'Dashboard'\n const name = config.name ?? 'dashboard'\n const icon = config.icon ?? DashboardIcon\n\n return {\n name: 'dashboard',\n tools: (prev, context) => {\n return [\n ...prev,\n {\n title,\n name,\n icon,\n component: () => <Dashboard config={pluginConfig} />,\n },\n ]\n },\n }\n})\n"],"names":["Root"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/dashboard",
3
- "version": "3.1.5",
3
+ "version": "4.0.0",
4
4
  "description": "Tool for rendering dashboard widgets",
5
5
  "keywords": [
6
6
  "sanity",
@@ -22,19 +22,18 @@
22
22
  },
23
23
  "license": "MIT",
24
24
  "author": "Sanity.io <hello@sanity.io>",
25
+ "sideEffects": false,
25
26
  "exports": {
26
27
  ".": {
27
- "types": "./lib/index.d.ts",
28
28
  "source": "./src/index.ts",
29
- "import": "./lib/index.esm.js",
29
+ "import": "./lib/index.mjs",
30
30
  "require": "./lib/index.js",
31
- "default": "./lib/index.esm.js"
31
+ "default": "./lib/index.mjs"
32
32
  },
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "main": "./lib/index.js",
36
36
  "module": "./lib/index.esm.js",
37
- "source": "./src/index.ts",
38
37
  "types": "./lib/index.d.ts",
39
38
  "files": [
40
39
  "lib",
@@ -44,60 +43,62 @@
44
43
  ],
45
44
  "scripts": {
46
45
  "prebuild": "npm run clean && plugin-kit verify-package --silent && pkg-utils",
47
- "build": "run-s clean && plugin-kit verify-package --silent && pkg-utils build --strict && pkg-utils --strict",
46
+ "build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
48
47
  "clean": "rimraf lib",
49
48
  "compile": "tsc --noEmit",
49
+ "dev": "sanity dev",
50
50
  "format": "prettier --write --cache --ignore-unknown .",
51
51
  "link-watch": "plugin-kit link-watch",
52
52
  "lint": "eslint .",
53
53
  "prepare": "husky install",
54
- "prepublishOnly": "run-s build",
54
+ "prepublishOnly": "npm run build",
55
55
  "watch": "pkg-utils watch --strict"
56
56
  },
57
+ "browserslist": "extends @sanity/browserslist-config",
57
58
  "dependencies": {
58
- "@sanity/icons": "^2.0.0",
59
- "@sanity/image-url": "^1.0.1",
59
+ "@sanity/icons": "^3.3.0",
60
+ "@sanity/image-url": "^1.0.2",
60
61
  "@sanity/incompatible-plugin": "^1.0.4",
61
- "@sanity/ui": "^1.0.0",
62
+ "@sanity/ui": "^2.6.8",
62
63
  "lodash": "^4.17.21",
63
- "rxjs": "^7.0.0"
64
+ "rxjs": "^7.8.1"
64
65
  },
65
66
  "devDependencies": {
66
- "@commitlint/cli": "^17.2.0",
67
- "@commitlint/config-conventional": "^17.2.0",
68
- "@sanity/pkg-utils": "^2.1.0",
69
- "@sanity/plugin-kit": "^3.1.1",
70
- "@sanity/semantic-release-preset": "^2.0.2",
71
- "@types/react": "^18.0.26",
72
- "@types/styled-components": "^5.1.26",
73
- "@typescript-eslint/eslint-plugin": "^5.48.0",
74
- "@typescript-eslint/parser": "^5.48.0",
75
- "eslint": "^8.31.0",
76
- "eslint-config-prettier": "^8.6.0",
67
+ "@commitlint/cli": "^17.8.1",
68
+ "@commitlint/config-conventional": "^17.8.1",
69
+ "@sanity/pkg-utils": "^6.10.3",
70
+ "@sanity/plugin-kit": "^4.0.17",
71
+ "@sanity/semantic-release-preset": "^5.0.0",
72
+ "@types/react": "^18.3.3",
73
+ "@typescript-eslint/eslint-plugin": "^7.16.0",
74
+ "@typescript-eslint/parser": "^7.16.0",
75
+ "eslint": "^8.57.0",
76
+ "eslint-config-prettier": "^9.1.0",
77
77
  "eslint-config-sanity": "^6.0.0",
78
- "eslint-plugin-prettier": "^4.2.1",
79
- "eslint-plugin-react": "^7.31.11",
80
- "eslint-plugin-react-hooks": "^4.6.0",
81
- "husky": "^8.0.1",
82
- "lint-staged": "^13.0.3",
78
+ "eslint-plugin-prettier": "^5.1.3",
79
+ "eslint-plugin-react": "^7.34.3",
80
+ "eslint-plugin-react-hooks": "^4.6.2",
81
+ "husky": "^8.0.3",
82
+ "lint-staged": "^13.3.0",
83
83
  "npm-run-all": "^4.1.5",
84
- "prettier": "^2.8.1",
85
- "prettier-plugin-packagejson": "^2.3.0",
86
- "react": "^18.2.0",
87
- "react-dom": "^18.2.0",
88
- "react-is": "^18.2.0",
89
- "rimraf": "^3.0.2",
90
- "sanity": "^3.14.4",
91
- "styled-components": "^5.3.6",
92
- "typescript": "^4.9.4"
84
+ "prettier": "^3.3.2",
85
+ "prettier-plugin-packagejson": "^2.5.0",
86
+ "semantic-release": "^24.0.0",
87
+ "react": "^18.3.1",
88
+ "react-dom": "^18.3.1",
89
+ "react-is": "^18.3.1",
90
+ "rimraf": "^6.0.0",
91
+ "sanity": "^3.50.0",
92
+ "styled-components": "^6.1.11",
93
+ "typescript": "^5.5.3"
93
94
  },
94
95
  "peerDependencies": {
95
96
  "react": "^18",
96
97
  "sanity": "^3",
97
- "styled-components": "^5.2"
98
+ "styled-components": "^6.1"
98
99
  },
99
100
  "engines": {
100
- "node": ">=14"
101
+ "node": ">=18"
101
102
  },
102
103
  "sanityExchangeUrl": "https://www.sanity.io/exchange/dashboard"
103
104
  }
@@ -1,6 +1,6 @@
1
1
  import React, {forwardRef} from 'react'
2
2
  import {Card, Box, Heading} from '@sanity/ui'
3
- import styled from 'styled-components'
3
+ import {styled} from 'styled-components'
4
4
 
5
5
  const Root = styled(Card)`
6
6
  display: flex;
@@ -49,7 +49,7 @@ interface DashboardWidgetProps {
49
49
 
50
50
  export const DashboardWidgetContainer = forwardRef(function DashboardWidgetContainer(
51
51
  props: DashboardWidgetProps,
52
- ref: React.Ref<HTMLDivElement>
52
+ ref: React.Ref<HTMLDivElement>,
53
53
  ) {
54
54
  const {header, children, footer} = props
55
55
 
@@ -1,6 +1,6 @@
1
1
  import React, {PropsWithChildren, ReactNode} from 'react'
2
2
  import {Card, Stack, Heading, Box} from '@sanity/ui'
3
- import styled from 'styled-components'
3
+ import {styled} from 'styled-components'
4
4
 
5
5
  const Root = styled(Card)`
6
6
  display: flex;
@@ -1,22 +1,20 @@
1
1
  import React from 'react'
2
- import styled, {css} from 'styled-components'
2
+ import {styled, css} from 'styled-components'
3
3
  import {Box, Card, Grid, Text} from '@sanity/ui'
4
4
  import {WidgetContainer} from '../containers/WidgetContainer'
5
5
  import {DashboardConfig, LayoutConfig, DashboardWidget} from '../types'
6
6
 
7
7
  const media = {
8
- small: (...args: Parameters<typeof css>) =>
9
- css`
10
- @media (min-width: ${({theme}) => theme.sanity.media[0]}px) {
11
- ${css(...args)}
12
- }
13
- `,
14
- medium: (...args: Parameters<typeof css>) =>
15
- css`
16
- @media (min-width: ${({theme}) => theme.sanity.media[2]}px) {
17
- ${css(...args)}
18
- }
19
- `,
8
+ small: (...args: Parameters<typeof css>) => css`
9
+ @media (min-width: ${({theme}) => theme.sanity.media[0]}px) {
10
+ ${css(...args)}
11
+ }
12
+ `,
13
+ medium: (...args: Parameters<typeof css>) => css`
14
+ @media (min-width: ${({theme}) => theme.sanity.media[2]}px) {
15
+ ${css(...args)}
16
+ }
17
+ `,
20
18
  }
21
19
 
22
20
  const Root = styled(Grid)`
@@ -10,7 +10,7 @@ export function WidgetContainer(props: DashboardWidget) {
10
10
  ...(props.layout || {}),
11
11
  ...(config.layout || {}),
12
12
  }),
13
- [props.layout, config.layout]
13
+ [props.layout, config.layout],
14
14
  )
15
15
 
16
16
  return (
@@ -1,5 +1,5 @@
1
1
  import {useClient} from 'sanity'
2
2
 
3
3
  export function useVersionedClient() {
4
- return useClient({apiVersion: '1'})
4
+ return useClient({apiVersion: '2024-06-03'})
5
5
  }
@@ -53,11 +53,20 @@ export function ProjectInfo(props: ProjectInfoProps) {
53
53
 
54
54
  subscriptions.push(
55
55
  versionedClient.observable
56
- .request<{studioHost: string}>({uri: `/projects/${projectId}`})
56
+ .request<{
57
+ studioHost: string
58
+ metadata?: {externalStudioHost?: string}
59
+ }>({uri: `/projects/${projectId}`, tag: 'dashboard.project-info.studio-host'})
57
60
  .subscribe({
58
61
  next: (result) => {
59
- const {studioHost: host} = result
60
- setStudioHost(host ? `https://${host}.sanity.studio` : undefined)
62
+ if (result.metadata?.externalStudioHost) {
63
+ setStudioHost(result.metadata.externalStudioHost)
64
+ return
65
+ }
66
+
67
+ setStudioHost(
68
+ result.studioHost ? `https://${result.studioHost}.sanity.studio` : undefined,
69
+ )
61
70
  },
62
71
  error: (error) => {
63
72
  console.error('Error while looking for studioHost', error)
@@ -65,7 +74,7 @@ export function ProjectInfo(props: ProjectInfoProps) {
65
74
  error: 'Something went wrong while looking up studioHost. See console.',
66
75
  })
67
76
  },
68
- })
77
+ }),
69
78
  )
70
79
 
71
80
  // ping assumed graphql endpoint
@@ -74,6 +83,7 @@ export function ProjectInfo(props: ProjectInfoProps) {
74
83
  .request({
75
84
  method: 'HEAD',
76
85
  uri: `/graphql/${dataset}/default`,
86
+ tag: 'dashboard.project-info.graphql-api',
77
87
  })
78
88
  .subscribe({
79
89
  next: () => setGraphQlApi(getGraphQlUrl(projectId, dataset)),
@@ -87,7 +97,7 @@ export function ProjectInfo(props: ProjectInfoProps) {
87
97
  })
88
98
  }
89
99
  },
90
- })
100
+ }),
91
101
  )
92
102
 
93
103
  return () => {
@@ -129,7 +139,7 @@ export function ProjectInfo(props: ProjectInfoProps) {
129
139
  ],
130
140
  },
131
141
  ],
132
- data.filter((item) => item.category === 'apis')
142
+ data.filter((item) => item.category === 'apis'),
133
143
  )
134
144
 
135
145
  // Handle whatever else there might be
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+ import {Box, Flex, rem, Stack, Text} from '@sanity/ui'
3
+ import {styled} from 'styled-components'
4
+ import {useListFormat, type User, UserAvatar} from 'sanity'
5
+ import {RobotIcon} from '@sanity/icons'
6
+
7
+ const Root = styled(Flex)`
8
+ height: ${rem(33)}; // 33 = PREVIEW_SIZES.default.media.height
9
+ box-sizing: content-box;
10
+ `
11
+
12
+ export interface ProjectUserProps {
13
+ user: User
14
+ isRobot: boolean
15
+ roles: string[]
16
+ }
17
+
18
+ export function ProjectUser({user, isRobot, roles}: ProjectUserProps) {
19
+ const listFormat = useListFormat({style: 'narrow'})
20
+ return (
21
+ <Root align="center">
22
+ <Flex align="center" flex={1} gap={2}>
23
+ <Box flex="none">
24
+ {isRobot ? (
25
+ <Text size={2}>
26
+ <RobotIcon />
27
+ </Text>
28
+ ) : (
29
+ <UserAvatar user={user} />
30
+ )}
31
+ </Box>
32
+
33
+ <Stack flex={1} space={2}>
34
+ <Text size={1} style={{color: 'inherit'}} textOverflow="ellipsis" weight="medium">
35
+ {user.displayName}
36
+ </Text>
37
+
38
+ <Text muted size={1} textOverflow="ellipsis">
39
+ {listFormat.format(roles)}
40
+ </Text>
41
+ </Stack>
42
+ </Flex>
43
+ </Root>
44
+ )
45
+ }
@@ -1,36 +1,23 @@
1
1
  import React, {useCallback, useEffect, useState} from 'react'
2
2
  import {from} from 'rxjs'
3
3
  import {map, switchMap} from 'rxjs/operators'
4
- import {Stack, Spinner, Card, Box, Text, Button} from '@sanity/ui'
5
- import {RobotIcon} from '@sanity/icons'
6
- import styled from 'styled-components'
7
- import {DefaultPreview, useUserStore} from 'sanity'
4
+ import {Stack, Spinner, Box, Text, Button} from '@sanity/ui'
5
+ import {Role, useUserStore} from 'sanity'
8
6
  import {useVersionedClient} from '../../versionedClient'
9
7
  import {User} from 'sanity'
10
8
  import {DashboardWidgetContainer} from '../../components/DashboardWidgetContainer'
11
-
12
- const AvatarWrapper = styled(Card)`
13
- box-sizing: border-box;
14
- border-radius: 50%;
15
- border-color: transparent;
16
- overflow: hidden;
17
- width: 100%;
18
- height: 100%;
19
-
20
- & > img {
21
- width: 100%;
22
- height: auto;
23
- }
24
- `
9
+ import {ProjectUser} from './ProjectUser'
25
10
 
26
11
  function getInviteUrl(projectId: string) {
27
- return `https://manage.sanity.io/projects/${projectId}/team/invite`
12
+ return `https://manage.sanity.io/projects/${projectId}/members`
28
13
  }
29
14
 
30
15
  interface Member {
31
16
  id: string
32
- role: string
17
+ roles: Role[]
33
18
  isRobot: boolean
19
+ isCurrentUser: boolean
20
+ createdAt: string
34
21
  }
35
22
 
36
23
  interface Project {
@@ -51,21 +38,22 @@ export function ProjectUsers() {
51
38
  const subscription = versionedClient.observable
52
39
  .request<Project>({
53
40
  uri: `/projects/${projectId}`,
41
+ tag: 'dashboard.project-users',
54
42
  })
55
43
  .pipe(
56
44
  switchMap((_project) =>
57
45
  from(userStore.getUsers(_project.members.map((mem) => mem.id))).pipe(
58
- map((_users) => ({project: _project, users: _users}))
59
- )
60
- )
46
+ map((_users) => ({project: _project, users: _users})),
47
+ ),
48
+ ),
61
49
  )
62
50
  .subscribe({
63
51
  next: ({users: _users, project: _project}) => {
64
52
  setProject(_project)
65
53
  setUsers(
66
54
  (Array.isArray(_users) ? _users : [_users]).sort((userA, userB) =>
67
- sortUsersByRobotStatus(userA, userB, _project)
68
- )
55
+ sortUsersByRobotStatus(userA, userB, _project),
56
+ ),
69
57
  )
70
58
  },
71
59
  error: (e: Error) => setError(e),
@@ -106,7 +94,7 @@ export function ProjectUsers() {
106
94
  paddingY={4}
107
95
  mode="bleed"
108
96
  tone="primary"
109
- text="Invite members"
97
+ text="Manage members"
110
98
  as="a"
111
99
  loading={isLoading}
112
100
  href={isLoading ? undefined : getInviteUrl(project.id)}
@@ -120,7 +108,7 @@ export function ProjectUsers() {
120
108
  <Spinner />
121
109
  </Text>
122
110
  <Text align="center" size={1} muted>
123
- Loading items...
111
+ Loading items
124
112
  </Text>
125
113
  </Stack>
126
114
  </Box>
@@ -130,23 +118,13 @@ export function ProjectUsers() {
130
118
  <Stack space={3} padding={3}>
131
119
  {users?.map((user) => {
132
120
  const membership = project.members.find((member) => member.id === user.id)
133
- const media = membership?.isRobot ? (
134
- <Text size={3}>
135
- <RobotIcon />
136
- </Text>
137
- ) : (
138
- <AvatarWrapper tone="transparent">
139
- {user?.imageUrl && <img src={user.imageUrl} alt={user?.displayName} />}
140
- </AvatarWrapper>
141
- )
142
121
  return (
143
- <Box key={user.id}>
144
- <DefaultPreview
145
- title={user.displayName}
146
- subtitle={membership?.role}
147
- media={media}
148
- />
149
- </Box>
122
+ <ProjectUser
123
+ key={user.id}
124
+ user={user}
125
+ isRobot={membership?.isRobot ?? false}
126
+ roles={membership?.roles.map((role) => role.title) || []}
127
+ />
150
128
  )
151
129
  })}
152
130
  </Stack>
@@ -159,11 +137,12 @@ function sortUsersByRobotStatus(userA: User, userB: User, project: Project) {
159
137
  const {members} = project
160
138
  const membershipA = members.find((member) => member.id === userA?.id)
161
139
  const membershipB = members.find((member) => member.id === userB?.id)
162
- if (membershipA?.isRobot) {
163
- return 1
164
- }
165
- if (membershipB?.isRobot) {
166
- return -1
140
+
141
+ // On ties, sort by when the user was added
142
+ if (membershipA?.isRobot === membershipB?.isRobot) {
143
+ return (membershipA?.createdAt || '') > (membershipB?.createdAt || '') ? 1 : -1
167
144
  }
168
- return 0
145
+
146
+ // Robots go to the bottom
147
+ return membershipA?.isRobot ? 1 : -1
169
148
  }
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import {Card, Box, Heading, Flex, Text, Stack} from '@sanity/ui'
3
3
  import {PlayIcon} from '@sanity/icons'
4
- import styled from 'styled-components'
4
+ import {styled} from 'styled-components'
5
5
 
6
6
  const PlayIconBox = styled(Box)`
7
7
  position: absolute;
@@ -38,11 +38,12 @@ export function useDataAdapter() {
38
38
  : '/addons/dashboard'
39
39
  return versionedClient.observable.request<{items: FeedItem[]}>({
40
40
  uri,
41
+ tag: 'dashboard.sanity-tutorials',
41
42
  withCredentials: false,
42
43
  })
43
44
  },
44
45
  urlBuilder: imageUrlBuilder(tutorialsProjectConfig),
45
46
  }),
46
- [versionedClient]
47
+ [versionedClient],
47
48
  )
48
49
  }