Skip to content

Commit

Permalink
WIP: test-studio
Browse files Browse the repository at this point in the history
  • Loading branch information
mariuslundgard committed Aug 16, 2022
1 parent 3f1a4d9 commit c7f9127
Show file tree
Hide file tree
Showing 11 changed files with 444 additions and 0 deletions.
132 changes: 132 additions & 0 deletions dev/test-studio/plugins/language-filter/LanguageFilterMenuButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import {TranslateIcon} from '@sanity/icons'
import {Box, Button, Card, Checkbox, Flex, Popover, Stack, Text, useClickOutside} from '@sanity/ui'
import React, {FormEvent, useCallback, useState} from 'react'
import {ObjectSchemaType} from 'sanity'
import {LanguageFilterPluginOptions} from './types'
import {usePaneLanguages} from './usePaneLanguages'

export interface LanguageFilterMenuButtonProps {
options: LanguageFilterPluginOptions

// eslint-disable-next-line react/no-unused-prop-types
schemaType: ObjectSchemaType
}

export function LanguageFilterMenuButton(props: LanguageFilterMenuButtonProps) {
const {options} = props
const defaultLanguages = options.supportedLanguages.filter((l) =>
options.defaultLanguages?.includes(l.id)
)
const languageOptions = options.supportedLanguages.filter(
(l) => !options.defaultLanguages?.includes(l.id)
)
const [open, setOpen] = useState(false)
const {selectableLanguages, selectedLanguages, selectAll, selectNone, toggleLanguage} =
usePaneLanguages({options})
const [button, setButton] = useState<HTMLElement | null>(null)
const [popover, setPopover] = useState<HTMLElement | null>(null)

const handleToggleAll = useCallback(
(event: FormEvent<HTMLInputElement>) => {
const checked = event.currentTarget.checked

if (checked) {
selectAll()
} else {
selectNone()
}
},
[selectAll, selectNone]
)

const handleClick = useCallback(() => setOpen((o) => !o), [])

const handleClickOutside = useCallback(() => setOpen(false), [])

useClickOutside(handleClickOutside, [button, popover])

const allSelected = selectedLanguages.length === selectableLanguages.length

const content = (
<Box overflow="auto" padding={1}>
{defaultLanguages.length > 0 && (
<Card radius={2} tone="primary">
<Stack padding={2} space={3}>
<Text size={1} weight="semibold">
Default language{defaultLanguages.length > 1 && <>s</>}
</Text>

{defaultLanguages.map((l) => (
<Text key={l.id}>{l.title}</Text>
))}
</Stack>
</Card>
)}

<Stack marginTop={3} padding={2} space={2}>
<Box paddingBottom={1}>
<Text size={1} weight="semibold">
Show translations
</Text>
</Box>

<Card as="label">
<Flex align="center" gap={2}>
<Checkbox checked={allSelected} name="_allSelected" onChange={handleToggleAll} />
<Box flex={1}>
<Text muted={!allSelected} weight="semibold">
All translations
</Text>
</Box>
</Flex>
</Card>

{languageOptions.map((lang) => (
<LanguageFilterOption
id={lang.id}
key={lang.id}
onToggle={toggleLanguage}
selected={selectedLanguages.includes(lang.id)}
title={lang.title}
/>
))}
</Stack>
</Box>
)

return (
<Popover constrainSize content={content} open={open} portal ref={setPopover}>
<Button
icon={TranslateIcon}
mode="bleed"
onClick={handleClick}
ref={setButton}
selected={open}
/>
</Popover>
)
}

function LanguageFilterOption(props: {
id: string
onToggle: (id: string) => void
selected: boolean
title: string
}) {
const {id, onToggle, selected, title} = props

const handleChange = useCallback(() => {
onToggle(id)
}, [id, onToggle])

return (
<Card as="label">
<Flex align="center" gap={2}>
<Checkbox checked={selected} name={`language-${id}`} onChange={handleChange} />
<Box flex={1}>
<Text muted={!selected}>{title}</Text>
</Box>
</Flex>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {pathFor} from '@sanity/util/paths'
import React, {useMemo} from 'react'
import {FieldError, FieldMember, FieldSetMember, ObjectInputProps, ObjectMember} from 'sanity'
import {ObjectInput, useFormBuilder} from 'sanity/form'
import {LanguageFilterPluginOptions} from './types'
import {usePaneLanguages} from './usePaneLanguages'
import {_isPathCollapsed} from './_helpers'

export function LanguageFilterObjectInput(
props: {options: LanguageFilterPluginOptions} & ObjectInputProps
) {
const {members: membersProp, level, options, path, ...restProps} = props
const {collapsedFieldSets} = useFormBuilder()
const {selectedLanguages} = usePaneLanguages({options})
const translationsFieldSetPath = pathFor(path.concat(['translationsFieldSet']))
const translationsFieldSetCollapsed = _isPathCollapsed(
translationsFieldSetPath,
collapsedFieldSets
)

const defaultMembers = useMemo(
() =>
membersProp.filter(
(member) => member.kind === 'field' && options.defaultLanguages?.includes(member.name)
),
[membersProp, options]
)

const members: ObjectMember[] = useMemo(() => {
const translationsFieldSetMembers = membersProp
.filter(
(member) =>
member.kind === 'field' &&
selectedLanguages.includes(member.name) &&
!options.defaultLanguages?.includes(member.name)
)
.map((member): FieldMember | FieldError => {
if (member.kind === 'fieldSet') {
return {
kind: 'error',
key: member.key,
fieldName: member.fieldSet.name,
error: new Error('test') as any, // @todo
}
}

return member
})

if (translationsFieldSetMembers.length === 0) {
return defaultMembers
}

const translationsFieldSet: FieldSetMember = {
kind: 'fieldSet',
key: 'translationsFieldSet',
fieldSet: {
path: translationsFieldSetPath,
name: 'translations',
level: level + 1,
title: 'Translations',
collapsible: true,
collapsed: translationsFieldSetCollapsed ?? true,
members: translationsFieldSetMembers,
},
}

return defaultMembers.concat([translationsFieldSet])
}, [
defaultMembers,
translationsFieldSetCollapsed,
level,
membersProp,
options,
selectedLanguages,
translationsFieldSetPath,
])

return <ObjectInput {...restProps} level={level} members={members} path={path} />
}
29 changes: 29 additions & 0 deletions dev/test-studio/plugins/language-filter/_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {Path} from 'sanity'
import {StateTree} from 'sanity/form'

export function _isPathCollapsed(
path: Path,
state: StateTree<boolean> | undefined
): boolean | undefined {
if (!state) return undefined

let node: StateTree<boolean> | undefined = state

for (const segment of path) {
if (!node) {
return undefined
}

if (typeof segment === 'string') {
node = node.children?.[segment]
} else if (typeof segment === 'number') {
node = node.children?.[segment]
} else if (Array.isArray(segment)) {
node = node.children?.[String(segment[0])]
} else {
node = node.children?.[segment._key]
}
}

return node?.value
}
2 changes: 2 additions & 0 deletions dev/test-studio/plugins/language-filter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './plugin'
export * from './types'
42 changes: 42 additions & 0 deletions dev/test-studio/plugins/language-filter/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import {createPlugin, ObjectInputProps, _DocumentLanguageFilterComponent} from 'sanity'
import {LanguageFilterMenuButton} from './LanguageFilterMenuButton'
import {LanguageFilterObjectInput} from './LanguageFilterObjectInput'
import {LanguageFilterPluginOptions} from './types'

/**
* Language filter plugin for Sanity
*/
export const languageFilter = createPlugin<LanguageFilterPluginOptions>((options) => {
const RenderLanguageFilter: _DocumentLanguageFilterComponent = (props) => {
return <LanguageFilterMenuButton options={options} schemaType={props.schemaType} />
}

return {
name: '@sanity/language-filter',

document: {
unstable_languageFilter: (prev, {schemaType}) => {
if (!options.types || options.types?.includes(schemaType)) {
return [...prev, RenderLanguageFilter]
}

return prev
},
},

form: {
renderInput(props, next) {
if (props.schemaType.name === 'object') {
const segment = props.path[props.path.length - 1]

if (typeof segment === 'string' && segment.startsWith('locale')) {
return <LanguageFilterObjectInput {...(props as ObjectInputProps)} options={options} />
}
}

return next(props)
},
},
}
})
5 changes: 5 additions & 0 deletions dev/test-studio/plugins/language-filter/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface LanguageFilterPluginOptions {
defaultLanguages?: string[]
supportedLanguages: {id: string; title: string}[]
types?: string[]
}
67 changes: 67 additions & 0 deletions dev/test-studio/plugins/language-filter/usePaneLanguages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {useCallback, useMemo} from 'react'
import {usePaneRouter} from 'sanity/desk'
import {LanguageFilterPluginOptions} from './types'

// NOTE: use `+` instead of `,` since the desk tool URL format already uses comma
const LANG_ID_SEPARATOR = '+'

export function usePaneLanguages(props: {options: LanguageFilterPluginOptions}): {
selectableLanguages: {id: string; title: string}[]
selectedLanguages: string[]
selectAll: () => void
selectNone: () => void
toggleLanguage: (languageId: string) => void
} {
const {options} = props
const {params, setParams} = usePaneRouter()
const selectableLanguages = options.supportedLanguages.filter(
(lang) => !options.defaultLanguages?.includes(lang.id)
)

const selectedLanguages: string[] = useMemo(() => {
if (params?.langs === '$none') {
return []
}

if (params?.langs === '$all') {
return selectableLanguages.map((lang) => lang.id)
}

return params?.langs?.split(LANG_ID_SEPARATOR) || selectableLanguages.map((lang) => lang.id)
}, [params, selectableLanguages])

const selectAll = useCallback(() => {
setParams({...params, langs: '$all'})
}, [params, setParams])

const selectNone = useCallback(() => {
setParams({...params, langs: '$none'})
}, [params, setParams])

const toggleLanguage = useCallback(
(languageId: string) => {
let lang = selectedLanguages

if (lang.includes(languageId)) {
lang = lang.filter((l) => l !== languageId)
} else {
lang = [...lang, languageId]
}

if (lang.length === 0) {
setParams({...params, langs: '$none'}) // none
return
}

if (lang.length === selectableLanguages.length) {
setParams({...params, langs: '$all'})
return
}

setParams({...params, langs: lang.join(LANG_ID_SEPARATOR)})
},
[params, selectableLanguages, selectedLanguages, setParams]
)

return {selectableLanguages, selectedLanguages, selectAll, selectNone, toggleLanguage}
}
13 changes: 13 additions & 0 deletions dev/test-studio/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {CustomMarkers} from './components/formBuilder/CustomMarkers'
import {Markers} from './components/formBuilder/Markers'
import {resolveDocumentActions as documentActions} from './documentActions'
import {resolveInitialValueTemplates} from './initialValueTemplates'
import {languageFilter} from './plugins/language-filter'
import {schemaTypes} from './schema'
import {defaultDocumentNode, structure, newDocumentOptions} from './structure'
import {workshopTool} from './workshop'
Expand Down Expand Up @@ -50,6 +51,18 @@ const sharedSettings = createPlugin({
structure,
defaultDocumentNode,
}),
languageFilter({
defaultLanguages: ['nb'],
supportedLanguages: [
{id: 'ar', title: 'Arabic'},
{id: 'en', title: 'English'},
{id: 'nb', title: 'Norwegian (bokmål)'},
{id: 'nn', title: 'Norwegian (nynorsk)'},
{id: 'pt', title: 'Portuguese'},
{id: 'es', title: 'Spanish'},
],
types: ['languageFilterDebug'],
}),
workshopTool({
collections: [
{name: 'sanity', title: 'sanity'},
Expand Down

0 comments on commit c7f9127

Please sign in to comment.