-
Notifications
You must be signed in to change notification settings - Fork 394
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3f1a4d9
commit c7f9127
Showing
11 changed files
with
444 additions
and
0 deletions.
There are no files selected for viewing
132 changes: 132 additions & 0 deletions
132
dev/test-studio/plugins/language-filter/LanguageFilterMenuButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
80 changes: 80 additions & 0 deletions
80
dev/test-studio/plugins/language-filter/LanguageFilterObjectInput.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} /> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './plugin' | ||
export * from './types' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}, | ||
}, | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
67
dev/test-studio/plugins/language-filter/usePaneLanguages.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.