diff --git a/studio/lib/interfaces/global.ts b/studio/lib/interfaces/global.ts index eb11d8418..dbd856aaa 100644 --- a/studio/lib/interfaces/global.ts +++ b/studio/lib/interfaces/global.ts @@ -1,16 +1,15 @@ +import { PortableTextBlock } from "sanity"; + +import { isRichText } from "./richText"; + export interface Slug { _type: string; current: string; } -export interface DocumentWithSlug { - slug: Slug; - _updatedAt: string; -} - -export function isInternationalizedString( +export function isInternationalizedValue( value: unknown, -): value is InternationalizedString { +): value is InternationalizedValue { return ( Array.isArray(value) && value.every( @@ -19,15 +18,34 @@ export function isInternationalizedString( item !== null && "_key" in item && typeof item._key === "string" && - "value" in item && - typeof item.value === "string", + "value" in item, ) ); } -export type InternationalizedString = InternationalizedStringRecord[]; +export function isInternationalizedString( + value: unknown, +): value is InternationalizedString { + return ( + isInternationalizedValue(value) && + value.every((item) => typeof item.value === "string") + ); +} -export interface InternationalizedStringRecord { +export function isInternationalizedRichText( + value: unknown, +): value is InternationalizedValue { + return ( + isInternationalizedValue(value) && + value.every((record) => isRichText(record.value)) + ); +} + +export type InternationalizedValue = InternationalizedValueRecord[]; + +export interface InternationalizedValueRecord { _key: string; - value: string; + value: T; } + +export type InternationalizedString = InternationalizedValueRecord[]; diff --git a/studio/lib/interfaces/richText.ts b/studio/lib/interfaces/richText.ts new file mode 100644 index 000000000..29ed1ad8f --- /dev/null +++ b/studio/lib/interfaces/richText.ts @@ -0,0 +1,37 @@ +import { + PortableTextBlock, + PortableTextObject, + isPortableTextTextBlock, +} from "sanity"; + +export function isRichText(value: unknown): value is PortableTextBlock[] { + return ( + Array.isArray(value) && value.every((item) => isPortableTextBlock(item)) + ); +} + +export function isPortableTextBlock( + value: unknown, +): value is PortableTextBlock { + return isPortableTextTextBlock(value) || isPortableTextObject(value); +} + +export function isPortableTextObject( + value: unknown, +): value is PortableTextObject { + return isSanityKeyTypeObject(value); +} + +function isSanityKeyTypeObject(value: unknown): value is { + _key: string; + _type: string; +} { + return ( + typeof value === "object" && + value !== null && + "_key" in value && + typeof value._key === "string" && + "_type" in value && + typeof value._type === "string" + ); +} diff --git a/studio/utils/i18n.ts b/studio/utils/i18n.ts index 7b63d5bbf..a67ec211b 100644 --- a/studio/utils/i18n.ts +++ b/studio/utils/i18n.ts @@ -1,10 +1,10 @@ -import { InternationalizedString } from "studio/lib/interfaces/global"; +import { InternationalizedValue } from "studio/lib/interfaces/global"; -export function firstTranslation( - translatedString: InternationalizedString, -): string | null { - if (translatedString.length === 0) { +export function firstTranslation( + internationalizedValue: InternationalizedValue, +): T | null { + if (internationalizedValue.length === 0) { return null; } - return translatedString[0].value; + return internationalizedValue[0].value; } diff --git a/studio/utils/preview.ts b/studio/utils/preview.ts new file mode 100644 index 000000000..ae701d939 --- /dev/null +++ b/studio/utils/preview.ts @@ -0,0 +1,20 @@ +import { + PortableTextBlock, + isPortableTextSpan, + isPortableTextTextBlock, +} from "sanity"; + +// Convert rich text content to plaintext preview string +// (inspired by https://www.sanity.io/docs/previewing-block-content) +export function richTextPreview( + richText: PortableTextBlock[], +): string | undefined { + const block = richText.find((block) => block._type === "block"); + if (!isPortableTextTextBlock(block)) { + return undefined; + } + return block.children + .filter((child) => isPortableTextSpan(child)) + .map((span) => span.text) + .join(""); +} diff --git a/studioShared/lib/queries/customerCases.ts b/studioShared/lib/queries/customerCases.ts index e50e0058f..2ead1314f 100644 --- a/studioShared/lib/queries/customerCases.ts +++ b/studioShared/lib/queries/customerCases.ts @@ -28,6 +28,13 @@ export const CUSTOMER_CASE_QUERY = groq` "sector": ${translatedFieldFragment("sector")}, "delivery": ${translatedFieldFragment("delivery")}, consultants + }, + "sections": sections[] { + _type, + _type == "richTextBlock" => { + ..., + "richText": ${translatedFieldFragment("richText")}, + } } } `; diff --git a/studioShared/schemas/documents/customerCase.ts b/studioShared/schemas/documents/customerCase.ts index 3f4e8df7e..c86997b15 100644 --- a/studioShared/schemas/documents/customerCase.ts +++ b/studioShared/schemas/documents/customerCase.ts @@ -5,6 +5,7 @@ import { richTextID, titleID } from "studio/schemas/fields/text"; import { titleSlug } from "studio/schemas/schemaTypes/slug"; import { firstTranslation } from "studio/utils/i18n"; import { customerCaseProjectInfo } from "studioShared/schemas/fields/customerCaseProjectInfo"; +import richTextBlock from "studioShared/schemas/objects/richTextBlock"; export const customerCaseID = "customerCase"; @@ -30,7 +31,13 @@ const customerCase = defineType({ "Short paragraph displayed at the top of the customer case page", }), customerCaseProjectInfo, - //TODO: Block section + defineField({ + name: "sections", + title: "Sections", + description: "Add sections here", + type: "array", + of: [richTextBlock], + }), defineField({ name: richTextID, title: "Body", diff --git a/studioShared/schemas/objects/richTextBlock.ts b/studioShared/schemas/objects/richTextBlock.ts new file mode 100644 index 000000000..74ce0164a --- /dev/null +++ b/studioShared/schemas/objects/richTextBlock.ts @@ -0,0 +1,39 @@ +import { defineField } from "sanity"; + +import { isInternationalizedRichText } from "studio/lib/interfaces/global"; +import { firstTranslation } from "studio/utils/i18n"; +import { richTextPreview } from "studio/utils/preview"; + +const richTextBlock = defineField({ + name: "richTextBlock", + title: "Rich Text Block", + type: "object", + fields: [ + { + name: "richText", + title: "Rich Text", + type: "internationalizedArrayRichText", + }, + ], + preview: { + select: { + richText: "richText", + }, + prepare({ richText }) { + if (!isInternationalizedRichText(richText)) { + throw new TypeError( + `Expected 'richText' to be InternationalizedRichText, was ${typeof richText}`, + ); + } + const translatedRichText = firstTranslation(richText); + return { + title: + translatedRichText !== null + ? richTextPreview(translatedRichText) + : undefined, + }; + }, + }, +}); + +export default richTextBlock;