diff --git a/packages/sanity/src/_singletons/context/VersionContext.tsx b/packages/sanity/src/_singletons/context/VersionContext.tsx new file mode 100644 index 00000000000..0c5347a1286 --- /dev/null +++ b/packages/sanity/src/_singletons/context/VersionContext.tsx @@ -0,0 +1,58 @@ +// eslint-disable-next-line no-warning-comments +/* TODO DO WE STILL NEED THIS AFTER THE STORES ARE SET UP? */ + +// eslint-disable-next-line import/consistent-type-specifier-style +import {createContext, type ReactElement} from 'react' + +import type {Version} from '../../../core/versions/types' +import {BUNDLES, LATEST} from '../../../core/versions/util/const' +import {useRouter} from '../../../router' + +export interface VersionContextValue { + currentVersion: Version + isDraft: boolean + setCurrentVersion: (version: Version) => void +} + +export const VersionContext = createContext({ + currentVersion: LATEST, + isDraft: true, + // eslint-disable-next-line no-empty-function + setCurrentVersion: () => {}, +}) + +interface VersionProviderProps { + children: ReactElement +} + +export function VersionProvider({children}: VersionProviderProps): JSX.Element { + const router = useRouter() + const setCurrentVersion = (version: Version) => { + const {name} = version + if (name === 'drafts') { + router.navigateStickyParam('perspective', '') + } else { + router.navigateStickyParam('perspective', `bundle.${name}`) + } + } + const selectedVersion = router.stickyParams?.perspective + ? BUNDLES.find((bundle) => { + return ( + `bundle.${bundle.name}`.toLocaleLowerCase() === + router.stickyParams.perspective?.toLocaleLowerCase() + ) + }) + : LATEST + + const currentVersion = selectedVersion || LATEST + + const isDraft = currentVersion.name === 'drafts' + + const contextValue: VersionContextValue = { + isDraft, + setCurrentVersion, + currentVersion, + } + + return {children} +} diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/newVersion.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/newVersion.ts index ac362c6f8bf..c250ef5f6f8 100644 --- a/packages/sanity/src/core/store/_legacy/document/document-pair/operations/newVersion.ts +++ b/packages/sanity/src/core/store/_legacy/document/document-pair/operations/newVersion.ts @@ -1,7 +1,5 @@ import {omit} from 'lodash' -import {getDraftId} from '../../../../../util' -import {isLiveEditEnabled} from '../utils/isLiveEditEnabled' import {type OperationImpl} from './types' const omitProps = ['_createdAt', '_updatedAt'] @@ -20,7 +18,9 @@ export const newVersion: OperationImpl<[baseDocumentId: string], 'NO_NEW_VERSION return client.observable.create( { ...omit(source, omitProps), - _id: isLiveEditEnabled(schema, typeName) ? dupeId : getDraftId(dupeId), + // we don't need to get a draft id or check live editing, we'll always want to create a new version based on the dupeId + // we have guardrails for this on the front + _id: dupeId, _type: source._type, _version: {}, }, diff --git a/packages/sanity/src/core/studio/StudioProvider.tsx b/packages/sanity/src/core/studio/StudioProvider.tsx index 73420f54c48..c2f3e6c15cf 100644 --- a/packages/sanity/src/core/studio/StudioProvider.tsx +++ b/packages/sanity/src/core/studio/StudioProvider.tsx @@ -7,6 +7,7 @@ import json from 'refractor/lang/json.js' import jsx from 'refractor/lang/jsx.js' import typescript from 'refractor/lang/typescript.js' +import {VersionProvider} from '../../_singletons/core/form/VersionContext' import {LoadingBlock} from '../components/loadingBlock' import {ErrorLogger} from '../error/ErrorLogger' import {errorReporter} from '../error/errorReporter' @@ -62,15 +63,20 @@ export function StudioProvider({ // mounted React component that is shared across embedded and standalone studios. errorReporter.initialize() + // eslint-disable-next-line no-warning-comments + /* TODO REMOVE VERSION PROVIDER ONCE STORES ARE SET UP */ + const _children = useMemo( () => ( - - - {children} - + + + + {children} + + diff --git a/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx b/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx index b9e51a7576f..f74a7056823 100644 --- a/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx +++ b/packages/sanity/src/core/studio/components/navbar/StudioNavbar.tsx @@ -8,18 +8,9 @@ import { Layer, LayerProvider, PortalProvider, - Select, useMediaIndex, } from '@sanity/ui' -import { - type ChangeEvent, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react' +import {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react' import {NavbarContext} from 'sanity/_singletons' import {type RouterState, useRouter, useRouterState} from 'sanity/router' import {styled} from 'styled-components' @@ -28,6 +19,7 @@ import {Button, TooltipDelayGroupProvider} from '../../../../ui-components' import {type NavbarProps} from '../../../config/studio/types' import {isDev} from '../../../environment' import {useTranslation} from '../../../i18n' +import {GlobalBundleMenu} from '../../../versions/components/GlobalBundleMenu' import {useToolMenuComponent} from '../../studio-components-hooks' import {useWorkspace} from '../../workspace' import {ConfigIssuesButton} from './configIssues/ConfigIssuesButton' @@ -170,13 +162,6 @@ export function StudioNavbar(props: Omit) { const perspective = useMemo(() => router.stickyParams.perspective, [router.stickyParams]) - const handleReleaseChange = useCallback( - (element: ChangeEvent) => { - router.navigateStickyParam('perspective', element.currentTarget.value || '') - }, - [router], - ) - const actionNodes = useMemo(() => { if (!shouldRender.tools) return null @@ -228,29 +213,18 @@ export function StudioNavbar(props: Omit) { - {/* New document button */} - - {/* Search button (desktop) */} - {!shouldRender.searchFullscreen && ( - - )} - - {/* TODO: Fix */} - + {/* Versions button */} + + + {/* New document button */} + + {/* Search button (desktop) */} + {!shouldRender.searchFullscreen && ( + + )} diff --git a/packages/sanity/src/core/studio/components/navbar/new-document/NewDocumentButton.tsx b/packages/sanity/src/core/studio/components/navbar/new-document/NewDocumentButton.tsx index cd41303bd74..c90490e7600 100644 --- a/packages/sanity/src/core/studio/components/navbar/new-document/NewDocumentButton.tsx +++ b/packages/sanity/src/core/studio/components/navbar/new-document/NewDocumentButton.tsx @@ -163,13 +163,13 @@ export function NewDocumentButton(props: NewDocumentButtonProps) { 'data-testid': 'new-document-button', 'disabled': disabled || loading, 'icon': AddIcon, - 'text': t('new-document.button'), - 'mode': 'ghost', + 'text': '', + 'mode': 'bleed', 'onClick': handleToggleOpen, 'ref': setButtonElement, 'selected': open, }), - [disabled, handleToggleOpen, loading, open, openDialogAriaLabel, t], + [disabled, handleToggleOpen, loading, open, openDialogAriaLabel], ) // Tooltip content for the open button diff --git a/packages/sanity/src/core/util/draftUtils.ts b/packages/sanity/src/core/util/draftUtils.ts index 9661e89d9da..2eb756d9aee 100644 --- a/packages/sanity/src/core/util/draftUtils.ts +++ b/packages/sanity/src/core/util/draftUtils.ts @@ -56,11 +56,6 @@ export function isDraftId(id: string): id is DraftId { return id.startsWith(DRAFTS_PREFIX) } -/** @internal */ -export function isVersion(id: string): id is DraftId { - return id.includes('.') -} - /** @internal */ export function getIdPair(id: string): {draftId: DraftId; publishedId: PublishedId} { return { @@ -76,7 +71,7 @@ export function isPublishedId(id: string): id is PublishedId { /** @internal */ export function getDraftId(id: string): DraftId { - return isDraftId(id) || isVersion(id) ? id : ((DRAFTS_PREFIX + id) as DraftId) + return isDraftId(id) ? id : ((DRAFTS_PREFIX + id) as DraftId) } /** @internal */ diff --git a/packages/sanity/src/core/util/versions/util.ts b/packages/sanity/src/core/util/versions/util.ts deleted file mode 100644 index 96e31dc7182..00000000000 --- a/packages/sanity/src/core/util/versions/util.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {type ColorHueKey} from '@sanity/color' -import {type IconSymbol} from '@sanity/icons' -import {type SanityClient, type SanityDocument} from 'sanity' - -/* MOSTLY TEMPORARY FUNCTIONS / DUMMY DATA */ - -const RANDOM_TONES: ColorHueKey[] = [ - 'green', - 'yellow', - 'red', - 'purple', - 'blue', - 'cyan', - 'magenta', - 'orange', -] -const RANDOM_SYMBOLS = [ - 'archive', - 'edit', - 'eye-open', - 'heart', - 'info-filled', - 'circle', - 'search', - 'sun', - 'star', - 'trash', - 'user', -] - -export interface SanityReleaseIcon { - hue: ColorHueKey - icon: IconSymbol -} - -// move out of here and make it right -export interface Version { - name: string - title: string - icon: IconSymbol - hue: ColorHueKey - publishAt: Date | number -} - -// dummy data -export const BUNDLES: Version[] = [ - {name: 'draft', title: 'Published + Drafts', icon: 'archive', hue: 'green', publishAt: 0}, - {name: 'previewDrafts', title: 'Preview drafts', icon: 'edit', hue: 'yellow', publishAt: 0}, - {name: 'published', title: 'Published', icon: 'eye-open', hue: 'blue', publishAt: 0}, - {name: 'summerDrop', title: 'Summer Drop', icon: 'sun', hue: 'orange', publishAt: 0}, - {name: 'autumnDrop', title: 'Autumn Drop', icon: 'star', hue: 'red', publishAt: 0}, -] - -/** - * Returns all versions of a document - * - * @param documentId - document id - * @param client - sanity client - * @returns array of SanityDocuments versions from a specific doc - */ -export async function getAllVersionsOfDocument( - client: SanityClient, - documentId: string, -): Promise { - // remove all versions, get just id (anything before .) - const id = documentId.replace(/^[^.]*\./, '') - - const query = `*[_id match "*${id}*"]` - - return await client.fetch(query, {}, {tag: 'document.list-versions'}).then((documents) => { - return documents.map((doc: SanityDocument, index: number) => ({ - name: getVersionName(doc._id), - title: getVersionName(doc._id), - hue: RANDOM_TONES[index % RANDOM_SYMBOLS.length], - icon: RANDOM_SYMBOLS[index % RANDOM_SYMBOLS.length], - })) - }) -} - -export function getVersionName(documentId: string): string { - const version = documentId.slice(0, documentId.indexOf('.')) - return version -} diff --git a/packages/sanity/src/core/versions/components/GlobalBundleMenu.tsx b/packages/sanity/src/core/versions/components/GlobalBundleMenu.tsx new file mode 100644 index 00000000000..d6c1b8183b5 --- /dev/null +++ b/packages/sanity/src/core/versions/components/GlobalBundleMenu.tsx @@ -0,0 +1,136 @@ +import {AddIcon, CheckmarkIcon} from '@sanity/icons' +import {Box, Button, Flex, Menu, MenuButton, MenuDivider, MenuItem, Text} from '@sanity/ui' +import {useCallback, useContext, useState} from 'react' +import {useRouter} from 'sanity/router' + +import { + VersionContext, + type VersionContextValue, +} from '../../../_singletons/core/form/VersionContext' +import {type Bundle, type Version} from '../types' +import {BUNDLES, LATEST} from '../util/const' +import {isDraftOrPublished} from '../util/dummyGetters' +import {CreateBundleDialog} from './dialog/CreateBundleDialog' +import {VersionBadge} from './VersionBadge' + +export function GlobalBundleMenu(): JSX.Element { + const router = useRouter() + + // eslint-disable-next-line no-warning-comments + // FIXME REPLACE WHEN WE HAVE REAL DATA + const bundles = BUNDLES + + // eslint-disable-next-line no-warning-comments + // FIXME REPLACE WHEN WE HAVE REAL DATA + const {currentVersion, setCurrentVersion, isDraft} = + useContext(VersionContext) + const [createBundleDialogOpen, setCreateBundleDialogOpen] = useState(false) + + const handleBundleChange = useCallback( + (bundle: Version) => () => { + const {name} = bundle + + if (isDraftOrPublished(name)) { + router.navigateStickyParam('perspective', '') + } else { + router.navigateStickyParam('perspective', `bundle.${name}`) + } + + setCurrentVersion(bundle) + }, + [router, setCurrentVersion], + ) + + /* create new bundle */ + + const handleCreateBundleClick = useCallback(() => { + setCreateBundleDialogOpen(true) + }, []) + + const handleCancel = useCallback(() => { + setCreateBundleDialogOpen(false) + }, []) + + const handleSubmit = useCallback( + () => (value: Bundle) => { + // eslint-disable-next-line no-console + console.log('create new bundle', value.name) + + setCreateBundleDialogOpen(false) + }, + [], + ) + + return ( + <> + + + + } + id="global-version-menu" + menu={ + + : undefined} + onClick={handleBundleChange(LATEST)} + pressed={false} + text={LATEST.title} + /> + {bundles.length > 0 && ( + <> + + + )} + {bundles + .filter((b) => !isDraftOrPublished(b.name)) + .map((b) => ( + + + + + + + {b.title} + + + + + + {b.publishAt ? `a date will be here ${b.publishAt}` : 'No target date'} + + + + + + + + + + + ))} + + {/* eslint-disable-next-line @sanity/i18n/no-attribute-string-literals */} + + + } + popover={{ + placement: 'bottom-start', + portal: true, + zOffset: 3000, + }} + /> + + {createBundleDialogOpen && ( + + )} + + ) +} diff --git a/packages/sanity/src/structure/panes/document/versions/ReleaseIcon.tsx b/packages/sanity/src/core/versions/components/VersionBadge.tsx similarity index 50% rename from packages/sanity/src/structure/panes/document/versions/ReleaseIcon.tsx rename to packages/sanity/src/core/versions/components/VersionBadge.tsx index 1e921811851..cceed5542c4 100644 --- a/packages/sanity/src/structure/panes/document/versions/ReleaseIcon.tsx +++ b/packages/sanity/src/core/versions/components/VersionBadge.tsx @@ -1,17 +1,13 @@ -import {hues} from '@sanity/color' import {ChevronDownIcon, Icon} from '@sanity/icons' -// eslint-disable-next-line camelcase -import {Box, Flex, Text, useTheme_v2} from '@sanity/ui' -import {rgba} from '@sanity/ui/theme' +import {Box, Flex, Text} from '@sanity/ui' import {type CSSProperties} from 'react' -import {type SanityReleaseIcon} from '../../../../core/util/versions/util' +import {type SanityVersionIcon} from '../types' -export function ReleaseIcon( - props: Partial & {openButton?: boolean; padding?: number; title?: string}, -) { - const {hue = 'gray', icon, openButton, padding = 3, title} = props - const {color} = useTheme_v2() +export function VersionBadge( + props: Partial & {openButton?: boolean; padding?: number; title?: string}, +): JSX.Element { + const {tone, icon, openButton, padding = 3, title} = props return ( void + value: Bundle +}): JSX.Element { + const {onChange, value} = props + const [showTitleValidation, setShowTitleValidation] = useState(false) + + const handleBundleTitleChange = useCallback( + (event: React.ChangeEvent) => { + const title = event.target.value + + if (isDraftOrPublished(title)) { + setShowTitleValidation(true) + } else { + setShowTitleValidation(false) + } + + onChange({...value, title: title, name: speakingurl(title)}) + }, + [onChange, value], + ) + + const handleBundleDescriptionChange = useCallback( + (event: React.ChangeEvent) => { + const v = event.target.value + + onChange({...value, description: v || undefined}) + }, + [onChange, value], + ) + + const handleBundleToneChange = useCallback( + (event: React.ChangeEvent) => { + onChange({...value, tone: (event.target.value || undefined) as ButtonTone | undefined}) + }, + [onChange, value], + ) + + const handleBundlePublishAtChange = useCallback( + (event: React.ChangeEvent) => { + const v = event.target.value + + onChange({...value, publishAt: v}) + }, + [onChange, value], + ) + + return ( + + + {showTitleValidation && ( + + + {/* localize text */} + Title cannot be "drafts" or "published" + + + )} + + {/* localize text */} + Title + + + + + + + {/* localize text */} + Description + +