Skip to content

Commit 4a95ece

Browse files
codercatdevclaude
andcommitted
chore: upgrade Sanity Studio to v6.9.2
sanity 5.17.1 -> 6.9.2, @sanity/ui 3 -> 4, @sanity/vision 6.9.2, @sanity/icons 3 -> 5, @sanity/assist 6.1.19, @sanity/code-input 7.3.5, sanity-plugin-media 4 -> 6, @portabletext/react 6 -> 7 (matching sanity@6.9.2's own dep so toPlainText resolves a single copy). Four blockers surfaced during the upgrade: 1. @sanity/icons v5 removed every icon from the root entry (they remain as `declare const X: never` with a deprecation naming the fix). All eight imports moved to subpaths, e.g. @sanity/icons/User. 2. The workspace override "@portabletext/sanity-bridge": "^1.2.14" starved sanity@6.9.2, which needs ^3.2.5 for getSanitySubSchema. Removed; pnpm now resolves 1.2.14 for apps/web's sanity@5 and 3.2.5 for the Studio. 3. @codingcatdev/sanity-plugin-podcast-rss could not simply be dropped: schemas/documents/podcast.ts declares a field of type "podcastRssEpisode", which only that plugin registers, so removing it broke schema extraction. Vendored into plugins/podcast-rss/ (see its README) migrated to @sanity/ui v4 — Menu/MenuButton/MenuItem from the /menu subpath, `space` -> `gap` — and off xml2js onto the native DOMParser, since xml2js only resolved via a Vite polyfill that Vite 8 no longer provides. Field shapes are kept byte-identical so existing podcast documents are unaffected. 4. @sanity/ui v4: Stack `space` -> `gap` (2 call sites) and Avatar `alt` -> `title`. Also fixed pre-existing issues the upgrade exposed: react-inlinesvg was imported by the Studio but declared only in apps/web; SharePreviewActionButton imported five symbols from "@/sanity/lib/api" despite the Studio tsconfig having no such path alias (all unused); CodeSandboxPreview imported an unused RenderPreviewCallbackProps. Removed the stray unused podcast-rss dependency from apps/web. tsconfig target ES2020 -> ES2022 for Array.prototype.at. Verified: builds, typechecks clean, dev server boots on Vite 8.2.1, and schema extract succeeds with zero types removed (two added by sanity-plugin-media v6). Still needs a human click-test of markdown paste in the Portable Text editor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HZ4S1uTJwFdbv3A6PHWfw2
1 parent 7196305 commit 4a95ece

27 files changed

Lines changed: 657 additions & 28 deletions

‎apps/sanity/components/CodeSandboxPreview.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from "react";
2-
import { RenderPreviewCallbackProps } from "sanity";
32
// TODO: Add stronger typing
43

54
const CodePenPreview = (props: any) => {

‎apps/sanity/components/YouTubePreview.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function YoutubeInputComponent(props: ObjectInputProps<string>) {
105105
);
106106

107107
return (
108-
<Stack space={3}>
108+
<Stack gap={3}>
109109
<TextInput {...elementProps} onChange={handleChange} value={value} />{" "}
110110
{!!props.value && (
111111
<Box style={{ position: "relative" }}>

‎apps/sanity/components/documentActions/SharePreviewActionButton.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import React, { useEffect, useState } from "react";
2-
import { useClient } from "sanity";
3-
import { CopyIcon } from "@sanity/icons";
2+
import { CopyIcon } from "@sanity/icons/Copy";
43
import { Button, Dialog, Text, Stack, Card } from "@sanity/ui";
54
import { RecycleIcon } from "lucide-react";
6-
import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api";
75

86
interface SharePreviewActionButtonProps {
97
id: string;
@@ -60,7 +58,7 @@ const SharePreviewActionButton: React.FC<SharePreviewActionButtonProps> = ({
6058
onClose={onClose}
6159
>
6260
<Card padding={4}>
63-
<Stack space={3}>
61+
<Stack gap={3}>
6462
{loading && <Text>Generating link...</Text>}
6563
{!loading && shareUrl && (
6664
<>

‎apps/sanity/components/documentActions/sharePreviewAction.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { DocumentActionComponent, DocumentActionProps } from "sanity";
22
import React, { useState } from "react";
33
import SharePreviewActionButton from "./SharePreviewActionButton";
4-
import { ShareIcon } from "@sanity/icons";
4+
import { ShareIcon } from "@sanity/icons/Share";
55

66
export const sharePreviewAction: DocumentActionComponent = (
77
props: DocumentActionProps,

‎apps/sanity/extract.json‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13510,6 +13510,88 @@
1351013510
}
1351113511
}
1351213512
},
13513+
{
13514+
"type": "type",
13515+
"name": "media.folder.reference",
13516+
"value": {
13517+
"type": "object",
13518+
"attributes": {
13519+
"_ref": {
13520+
"type": "objectAttribute",
13521+
"value": {
13522+
"type": "string"
13523+
}
13524+
},
13525+
"_type": {
13526+
"type": "objectAttribute",
13527+
"value": {
13528+
"type": "string",
13529+
"value": "reference"
13530+
}
13531+
},
13532+
"_weak": {
13533+
"type": "objectAttribute",
13534+
"value": {
13535+
"type": "boolean"
13536+
},
13537+
"optional": true
13538+
}
13539+
},
13540+
"dereferencesTo": "media.folder"
13541+
}
13542+
},
13543+
{
13544+
"name": "media.folder",
13545+
"type": "document",
13546+
"attributes": {
13547+
"_id": {
13548+
"type": "objectAttribute",
13549+
"value": {
13550+
"type": "string"
13551+
}
13552+
},
13553+
"_type": {
13554+
"type": "objectAttribute",
13555+
"value": {
13556+
"type": "string",
13557+
"value": "media.folder"
13558+
}
13559+
},
13560+
"_createdAt": {
13561+
"type": "objectAttribute",
13562+
"value": {
13563+
"type": "string"
13564+
}
13565+
},
13566+
"_updatedAt": {
13567+
"type": "objectAttribute",
13568+
"value": {
13569+
"type": "string"
13570+
}
13571+
},
13572+
"_rev": {
13573+
"type": "objectAttribute",
13574+
"value": {
13575+
"type": "string"
13576+
}
13577+
},
13578+
"name": {
13579+
"type": "objectAttribute",
13580+
"value": {
13581+
"type": "string"
13582+
},
13583+
"optional": true
13584+
},
13585+
"parent": {
13586+
"type": "objectAttribute",
13587+
"value": {
13588+
"type": "inline",
13589+
"name": "media.folder.reference"
13590+
},
13591+
"optional": true
13592+
}
13593+
}
13594+
},
1351313595
{
1351413596
"name": "media.tag",
1351513597
"type": "document",

‎apps/sanity/package.json‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,33 +6,36 @@
66
"dev": "sanity dev",
77
"build": "sanity build",
88
"deploy": "sanity deploy",
9-
"typegen:extract": "sanity schema extract --path=./extract.json --workspace production",
9+
"typegen:extract": "sanity schema extract --path=./extract.json --workspace production --force",
1010
"typegen": "pnpm run typegen:extract"
1111
},
1212
"dependencies": {
13-
"@codingcatdev/sanity-plugin-podcast-rss": "^1.0.0",
14-
"@portabletext/block-tools": "^5.1.0",
15-
"@portabletext/react": "^6.0.3",
16-
"@sanity/assist": "^6.0.3",
17-
"@sanity/code-input": "^7.0.11",
18-
"@sanity/icons": "^3.7.4",
19-
"@sanity/ui": "^3.1.14",
20-
"@sanity/vision": "^5.17.1",
13+
"@portabletext/block-tools": "^5.2.0",
14+
"@portabletext/react": "^7.0.1",
15+
"@sanity/assist": "^6.1.19",
16+
"@sanity/code-input": "^7.3.5",
17+
"@sanity/icons": "^5.2.1",
18+
"@sanity/ui": "^4.0.3",
19+
"@sanity/vision": "^6.9.2",
2120
"date-fns": "^4.1.0",
2221
"lucide-react": "^0.577.0",
2322
"micromark": "^4.0.2",
2423
"micromark-extension-gfm-table": "^2.1.1",
2524
"react": "^19.2.4",
2625
"react-dom": "^19.2.4",
2726
"react-icons": "^5.6.0",
27+
"react-inlinesvg": "^4.2.0",
2828
"react-twitter-embed": "^4.0.4",
29-
"sanity": "^5.17.1",
30-
"sanity-plugin-media": "^4.1.1",
29+
"sanity": "^6.9.2",
30+
"sanity-plugin-media": "^6.1.4",
3131
"styled-components": "^6.3.11"
3232
},
3333
"devDependencies": {
3434
"@types/react": "^19.2.14",
3535
"@types/react-dom": "^19.2.3",
3636
"typescript": "^5.9.3"
37+
},
38+
"engines": {
39+
"node": ">=22.12"
3740
}
3841
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# podcast-rss (vendored)
2+
3+
Vendored from `@codingcatdev/sanity-plugin-podcast-rss@1.0.0`.
4+
5+
Vendored rather than consumed from npm because the published package declares
6+
`peerDependencies: { react: "^18", sanity: "^3" }` and carries `@sanity/ui@2`
7+
as a **direct** dependency. Under Studio v6 that would render v2 components
8+
inside the v4 `ThemeProvider`, handing them theme tokens they don't understand.
9+
10+
This copy is not optional: `schemas/documents/podcast.ts` declares a field of
11+
`type: "podcastRssEpisode"`, which only this plugin registers. Disabling the
12+
plugin breaks schema extraction.
13+
14+
Changes from upstream:
15+
- `Menu` / `MenuButton` / `MenuItem` import from `@sanity/ui/menu` (v4 moved them off the root entry)
16+
- `Stack`/`Select` `space=` -> `gap=` (v4 rename)
17+
- `xml2js` -> native `DOMParser` (xml2js is a Node lib that only worked via a Vite polyfill; Vite 8 drops it)
18+
- Controlled `<Select value=...>` instead of `<option selected>` (React 19 warns on the latter)
19+
- Dropped the unused `useMedia` hook and the unused `useRef` in SearchResults
20+
21+
Upstream should be republished with widened peers; this directory can then go away.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Avatar, Button, Card, Flex, Select, Stack, Text } from "@sanity/ui";
2+
// v4 moved Menu/MenuButton/MenuItem off the root entry onto the /menu subpath.
3+
import { Menu, MenuButton, MenuItem } from "@sanity/ui/menu";
4+
import { useCallback, useState } from "react";
5+
import { set, setIfMissing, unset } from "sanity";
6+
7+
import { useQuery } from "../hooks/useQuery";
8+
import type { EpisodePickerParams } from "../schema/podcastRssEpisode";
9+
import type { Episode } from "../types";
10+
import Popup from "./Popup";
11+
import SearchBar, { type SearchBarOnChange } from "./SearchBar";
12+
import SearchResults, {
13+
type SearchResultsOnSelectCallback,
14+
} from "./SearchResults";
15+
16+
const EpisodePicker = (params: EpisodePickerParams) => {
17+
const { podcasts = [], schemaType, onChange, value } = params;
18+
const defaultEpisode = value as Episode | undefined;
19+
20+
const [selectedPodcast, setSelectedPodcast] = useState(podcasts.at(0));
21+
const [isPopupOpen, setOpen] = useState(false);
22+
const { query, loading, error, results, setQuery } = useQuery(selectedPodcast);
23+
const [selectedEpisode, setSelectedEpisode] = useState<Episode | undefined>(
24+
defaultEpisode,
25+
);
26+
27+
const onClose = useCallback(() => setOpen(false), []);
28+
const onOpen = useCallback(() => setOpen(true), []);
29+
const onQueryChange: SearchBarOnChange = (e) => setQuery(e.target.value);
30+
31+
const unsetEpisode = () => {
32+
onChange(unset());
33+
setSelectedEpisode(undefined);
34+
};
35+
36+
const setEpisode: SearchResultsOnSelectCallback = (episode: Episode) => {
37+
if (selectedEpisode && episode.guid.id === selectedEpisode.guid.id) {
38+
return unsetEpisode();
39+
}
40+
onChange([setIfMissing({ _type: schemaType.name }), set(episode)]);
41+
setOpen(false);
42+
setSelectedEpisode(episode);
43+
};
44+
45+
return (
46+
<Card>
47+
<Stack padding={2} gap={[3, 3, 4, 5]}>
48+
{selectedEpisode && (
49+
<Card padding={[3, 3, 4]} radius={2} shadow={1}>
50+
<Flex align="center" justify="space-between" gap={3}>
51+
<Avatar
52+
title={selectedEpisode.title}
53+
src={selectedEpisode.itunes?.image?.href}
54+
size={2}
55+
/>
56+
<Text size={[2, 2, 3]}>
57+
<a
58+
href={selectedEpisode.link}
59+
target="_blank"
60+
rel="noopener noreferrer"
61+
>
62+
{selectedEpisode.title}
63+
</a>
64+
</Text>
65+
<MenuButton
66+
button={<Button text="..." mode="ghost" />}
67+
id="podcast-rss-menu-button"
68+
menu={
69+
<Menu>
70+
<MenuItem text="Delete" onClick={unsetEpisode} />
71+
</Menu>
72+
}
73+
popover={{ portal: true, placement: "bottom-start" }}
74+
/>
75+
</Flex>
76+
</Card>
77+
)}
78+
79+
<Button onClick={onOpen} mode="ghost" text="Search for Episodes" />
80+
81+
<Popup onClose={onClose} isOpen={isPopupOpen}>
82+
<Stack padding={4} gap={[3, 3, 4, 5]}>
83+
<Select
84+
fontSize={[2, 2, 3, 4]}
85+
padding={[3, 3, 4]}
86+
value={selectedPodcast?.url ?? ""}
87+
onChange={(e) =>
88+
setSelectedPodcast(
89+
podcasts.find(
90+
(p) => p.url === (e.target as HTMLSelectElement).value,
91+
),
92+
)
93+
}
94+
>
95+
{podcasts.map((podcast) => (
96+
<option key={podcast.url} value={podcast.url}>
97+
{podcast.title}
98+
</option>
99+
))}
100+
</Select>
101+
<SearchBar value={query} onChange={onQueryChange} />
102+
<SearchResults
103+
onSelect={setEpisode}
104+
results={results}
105+
loading={loading}
106+
error={error}
107+
selectedEpisode={selectedEpisode}
108+
/>
109+
</Stack>
110+
</Popup>
111+
</Stack>
112+
</Card>
113+
);
114+
};
115+
116+
export default EpisodePicker;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Box, Dialog } from "@sanity/ui";
2+
import type { ReactNode } from "react";
3+
4+
interface IPopup {
5+
onClose: () => void;
6+
children: ReactNode;
7+
isOpen: boolean;
8+
}
9+
10+
const Popup = ({ onClose, children, isOpen }: IPopup) =>
11+
isOpen ? (
12+
<Dialog
13+
header="Episode Picker"
14+
id="podcast-rss-episode-picker"
15+
onClose={onClose}
16+
zOffset={1000}
17+
width={1}
18+
>
19+
<Box padding={4}>{children}</Box>
20+
</Dialog>
21+
) : null;
22+
23+
export default Popup;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { TextInput } from "@sanity/ui";
2+
import type { ChangeEvent } from "react";
3+
4+
export type SearchBarOnChange = (e: ChangeEvent<HTMLInputElement>) => void;
5+
6+
interface ISearchBar {
7+
value: string;
8+
onChange: SearchBarOnChange;
9+
}
10+
11+
const SearchBar = ({ value, onChange }: ISearchBar) => (
12+
<TextInput
13+
fontSize={2}
14+
padding={4}
15+
value={value}
16+
placeholder="Episode Search"
17+
onChange={onChange}
18+
/>
19+
);
20+
21+
export default SearchBar;

0 commit comments

Comments
 (0)