@sanity/language-filter 2.31.2-performance-opts.9 → 3.0.0-v3-studio.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +95 -32
- package/lib/cjs/index.js +438 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/esm/index.js +432 -0
- package/lib/esm/index.js.map +1 -0
- package/lib/types/index.d.ts +56 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +77 -31
- package/sanity.json +2 -10
- package/src/LanguageFilterContext.tsx +30 -0
- package/src/LanguageFilterMenuButton.tsx +147 -0
- package/src/LanguageFilterObjectInput.tsx +91 -0
- package/src/filterField.test.ts +91 -0
- package/src/filterField.ts +34 -0
- package/src/index.ts +14 -0
- package/src/languageSubscription.ts +32 -0
- package/src/plugin.tsx +86 -0
- package/src/types.ts +27 -0
- package/src/usePaneLanguages.ts +78 -0
- package/src/useSelectedLanguageIds.ts +40 -0
- package/v2-incompatible.js +11 -0
- package/config.dist.json +0 -3
- package/dist/dts/SelectLanguage.d.ts +0 -16
- package/dist/dts/SelectLanguage.d.ts.map +0 -1
- package/dist/dts/SelectLanguageProvider.d.ts +0 -8
- package/dist/dts/SelectLanguageProvider.d.ts.map +0 -1
- package/dist/dts/datastore.d.ts +0 -7
- package/dist/dts/datastore.d.ts.map +0 -1
- package/lib/SelectLanguage.js +0 -191
- package/lib/SelectLanguageProvider.js +0 -98
- package/lib/datastore.js +0 -62
- package/lib/filter-fields.js +0 -13
- package/src/SelectLanguage.tsx +0 -178
- package/src/SelectLanguageProvider.tsx +0 -70
- package/src/datastore.ts +0 -70
- package/src/filter-fields.js +0 -1
- package/tsconfig.json +0 -13
package/package.json
CHANGED
|
@@ -1,43 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/language-filter",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-v3-studio.2",
|
|
4
4
|
"description": "A Sanity plugin that supports filtering localized fields by language",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
"author": "Sanity.io <hello@sanity.io>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"source": "./src/index.ts",
|
|
8
|
+
"main": "./lib/cjs/index.js",
|
|
9
|
+
"module": "./lib/esm/index.js",
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"require": "./lib/cjs/index.js",
|
|
14
|
+
"default": "./lib/esm/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"lib",
|
|
20
|
+
"v2-incompatible.js",
|
|
21
|
+
"sanity.json"
|
|
16
22
|
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"clean": "rimraf lib",
|
|
25
|
+
"lint": "eslint .",
|
|
26
|
+
"prebuild": "npm run clean && plugin-kit verify-package --silent",
|
|
27
|
+
"build": "parcel build --no-cache",
|
|
28
|
+
"watch": "parcel watch",
|
|
29
|
+
"test": "jest",
|
|
30
|
+
"link-watch": "plugin-kit link-watch",
|
|
31
|
+
"prepublishOnly": "npm run build",
|
|
32
|
+
"prepare": "husky install"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+ssh://git@github.com/sanity-io/language-filter.git"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=14.0.0"
|
|
40
|
+
},
|
|
17
41
|
"dependencies": {
|
|
18
42
|
"@sanity/icons": "^1.3.4",
|
|
19
|
-
"@sanity/
|
|
20
|
-
"@sanity/ui": "^0.
|
|
21
|
-
"
|
|
22
|
-
|
|
43
|
+
"@sanity/incompatible-plugin": "^1.0.0",
|
|
44
|
+
"@sanity/ui": "^0.38.0",
|
|
45
|
+
"@sanity/util": "3.0.0-dev-preview.15"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@babel/preset-env": "^7.18.10",
|
|
49
|
+
"@babel/preset-react": "^7.18.6",
|
|
50
|
+
"@commitlint/cli": "^17.1.2",
|
|
51
|
+
"@commitlint/config-conventional": "^17.1.0",
|
|
52
|
+
"@parcel/packager-ts": "^2.7.0",
|
|
53
|
+
"@parcel/transformer-typescript-types": "^2.7.0",
|
|
54
|
+
"@sanity/plugin-kit": "^1.0.1",
|
|
55
|
+
"@sanity/semantic-release-preset": "^2.0.1",
|
|
56
|
+
"@types/jest": "^28.1.8",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^5.35.1",
|
|
58
|
+
"@typescript-eslint/parser": "^5.35.1",
|
|
59
|
+
"eslint": "8.22.0",
|
|
60
|
+
"eslint-config-prettier": "^8.5.0",
|
|
61
|
+
"eslint-config-sanity": "^6.0.0",
|
|
62
|
+
"eslint-plugin-prettier": "^4.2.1",
|
|
63
|
+
"eslint-plugin-react": "^7.31.1",
|
|
64
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
65
|
+
"husky": "^8.0.1",
|
|
66
|
+
"jest": "^28.1.3",
|
|
67
|
+
"lint-staged": "^13.0.3",
|
|
68
|
+
"parcel": "^2.7.0",
|
|
69
|
+
"prettier": "^2.7.1",
|
|
70
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
71
|
+
"rimraf": "^3.0.2",
|
|
72
|
+
"sanity": "3.0.0-dev-preview.15",
|
|
73
|
+
"ts-jest": "^28.0.8",
|
|
74
|
+
"typescript": "4.7.4"
|
|
23
75
|
},
|
|
24
76
|
"peerDependencies": {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
77
|
+
"react": "^17.0.0 || ^18.0.0",
|
|
78
|
+
"sanity": "dev-preview"
|
|
27
79
|
},
|
|
28
|
-
"author": "Sanity.io <hello@sanity.io>",
|
|
29
|
-
"license": "MIT",
|
|
30
80
|
"bugs": {
|
|
31
|
-
"url": "https://github.com/sanity-io/
|
|
32
|
-
},
|
|
33
|
-
"homepage": "https://www.sanity.io/",
|
|
34
|
-
"repository": {
|
|
35
|
-
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/sanity-io/sanity.git",
|
|
37
|
-
"directory": "packages/@sanity/language-filter"
|
|
38
|
-
},
|
|
39
|
-
"publishConfig": {
|
|
40
|
-
"access": "public"
|
|
81
|
+
"url": "https://github.com/sanity-io/language-filter/issues"
|
|
41
82
|
},
|
|
42
|
-
"
|
|
83
|
+
"homepage": "https://github.com/sanity-io/language-filter#readme",
|
|
84
|
+
"sanityPlugin": {
|
|
85
|
+
"verifyPackage": {
|
|
86
|
+
"babelConfig": false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
43
89
|
}
|
package/sanity.json
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"paths": {
|
|
3
|
-
"source": "./src",
|
|
4
|
-
"compiled": "./lib"
|
|
5
|
-
},
|
|
6
2
|
"parts": [
|
|
7
3
|
{
|
|
8
|
-
"implements": "part:@sanity/
|
|
9
|
-
"path": "
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"implements": "part:@sanity/desk-tool/language-select-component",
|
|
13
|
-
"path": "SelectLanguageProvider"
|
|
4
|
+
"implements": "part:@sanity/base/sanity-root",
|
|
5
|
+
"path": "./v2-incompatible.js"
|
|
14
6
|
}
|
|
15
7
|
]
|
|
16
8
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, {createContext, PropsWithChildren, useContext, useMemo} from 'react'
|
|
2
|
+
import {LanguageFilterConfig} from './types'
|
|
3
|
+
|
|
4
|
+
export interface LanguageFilterContextValue {
|
|
5
|
+
// eslint-disable-next-line react/require-default-props
|
|
6
|
+
options: LanguageFilterConfig
|
|
7
|
+
// eslint-disable-next-line react/require-default-props
|
|
8
|
+
enabled: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const LanguageFilterContext = createContext<LanguageFilterContextValue | undefined>(undefined)
|
|
12
|
+
|
|
13
|
+
export function LanguageFilterProvider({
|
|
14
|
+
options,
|
|
15
|
+
enabled,
|
|
16
|
+
children,
|
|
17
|
+
}: PropsWithChildren<
|
|
18
|
+
Omit<LanguageFilterContextValue, 'selectedLanguageIds' | 'setSelectedLanguageIds'>
|
|
19
|
+
>) {
|
|
20
|
+
const value = useMemo(() => ({options, enabled}), [options, enabled])
|
|
21
|
+
return <LanguageFilterContext.Provider value={value}>{children}</LanguageFilterContext.Provider>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useLanguageFilterContext() {
|
|
25
|
+
const value = useContext(LanguageFilterContext)
|
|
26
|
+
if (!value) {
|
|
27
|
+
throw new Error('LanguageFilterContext is missing')
|
|
28
|
+
}
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {Box, Button, Card, Checkbox, Flex, Popover, Stack, Text, useClickOutside} from '@sanity/ui'
|
|
2
|
+
import React, {FormEvent, useCallback, useState} from 'react'
|
|
3
|
+
import {usePaneLanguages} from './usePaneLanguages'
|
|
4
|
+
import {LanguageFilterConfig} from './types'
|
|
5
|
+
|
|
6
|
+
export interface LanguageFilterMenuButtonProps {
|
|
7
|
+
options: LanguageFilterConfig
|
|
8
|
+
onSelectedIdsChange: (ids: string[]) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function LanguageFilterMenuButton(props: LanguageFilterMenuButtonProps) {
|
|
12
|
+
const {options, onSelectedIdsChange} = props
|
|
13
|
+
|
|
14
|
+
const defaultLanguages = options.supportedLanguages.filter((l) =>
|
|
15
|
+
options.defaultLanguages?.includes(l.id)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const languageOptions = options.supportedLanguages.filter(
|
|
19
|
+
(l) => !options.defaultLanguages?.includes(l.id)
|
|
20
|
+
)
|
|
21
|
+
const [open, setOpen] = useState(false)
|
|
22
|
+
const {activeLanguages, allSelected, selectAll, selectNone, toggleLanguage} = usePaneLanguages({
|
|
23
|
+
options,
|
|
24
|
+
onSelectedIdsChange,
|
|
25
|
+
})
|
|
26
|
+
const [button, setButton] = useState<HTMLElement | null>(null)
|
|
27
|
+
const [popover, setPopover] = useState<HTMLElement | null>(null)
|
|
28
|
+
|
|
29
|
+
const handleToggleAll = useCallback(
|
|
30
|
+
(event: FormEvent<HTMLInputElement>) => {
|
|
31
|
+
const checked = event.currentTarget.checked
|
|
32
|
+
|
|
33
|
+
if (checked) {
|
|
34
|
+
selectAll()
|
|
35
|
+
} else {
|
|
36
|
+
selectNone()
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[selectAll, selectNone]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const handleClick = useCallback(() => setOpen((o) => !o), [])
|
|
43
|
+
|
|
44
|
+
const handleClickOutside = useCallback(() => setOpen(false), [])
|
|
45
|
+
|
|
46
|
+
useClickOutside(handleClickOutside, [button, popover])
|
|
47
|
+
|
|
48
|
+
const content = (
|
|
49
|
+
<Box overflow="auto" padding={1}>
|
|
50
|
+
{defaultLanguages.length > 0 && (
|
|
51
|
+
<Card radius={2}>
|
|
52
|
+
<Stack padding={2} space={3}>
|
|
53
|
+
<Box paddingBottom={2}>
|
|
54
|
+
<Text size={1} weight="semibold">
|
|
55
|
+
Default language{defaultLanguages.length > 1 && <>s</>}
|
|
56
|
+
</Text>
|
|
57
|
+
</Box>
|
|
58
|
+
|
|
59
|
+
{defaultLanguages.map((l) => (
|
|
60
|
+
<Text key={l.id}>{l.title}</Text>
|
|
61
|
+
))}
|
|
62
|
+
</Stack>
|
|
63
|
+
</Card>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
<Stack marginTop={3} padding={2} space={2}>
|
|
67
|
+
<Box paddingBottom={2}>
|
|
68
|
+
<Text size={1} weight="semibold">
|
|
69
|
+
Show translations
|
|
70
|
+
</Text>
|
|
71
|
+
</Box>
|
|
72
|
+
|
|
73
|
+
<Card as="label">
|
|
74
|
+
<Flex align="center" gap={2}>
|
|
75
|
+
<Checkbox checked={allSelected} name="_allSelected" onChange={handleToggleAll} />
|
|
76
|
+
<Box flex={1}>
|
|
77
|
+
<Text muted={!allSelected} weight="semibold">
|
|
78
|
+
All translations
|
|
79
|
+
</Text>
|
|
80
|
+
</Box>
|
|
81
|
+
</Flex>
|
|
82
|
+
</Card>
|
|
83
|
+
|
|
84
|
+
{languageOptions.map((lang) => (
|
|
85
|
+
<LanguageFilterOption
|
|
86
|
+
id={lang.id}
|
|
87
|
+
key={lang.id}
|
|
88
|
+
onToggle={toggleLanguage}
|
|
89
|
+
selected={activeLanguages.includes(lang.id)}
|
|
90
|
+
title={lang.title}
|
|
91
|
+
/>
|
|
92
|
+
))}
|
|
93
|
+
</Stack>
|
|
94
|
+
</Box>
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const langCount = options.supportedLanguages.length
|
|
98
|
+
return (
|
|
99
|
+
<Popover content={content} open={open} portal ref={setPopover}>
|
|
100
|
+
<Button
|
|
101
|
+
text={
|
|
102
|
+
<Flex gap={1}>
|
|
103
|
+
<Box>Filter languages:</Box>
|
|
104
|
+
<Flex gap={1} justify="space-around">
|
|
105
|
+
<Flex
|
|
106
|
+
style={{width: `${Math.floor(Math.log10(langCount) + 1)}ch`}}
|
|
107
|
+
justify="flex-end"
|
|
108
|
+
>
|
|
109
|
+
{activeLanguages.length}
|
|
110
|
+
</Flex>
|
|
111
|
+
<Box>/</Box>
|
|
112
|
+
<Box>{langCount}</Box>
|
|
113
|
+
</Flex>
|
|
114
|
+
</Flex>
|
|
115
|
+
}
|
|
116
|
+
mode="bleed"
|
|
117
|
+
onClick={handleClick}
|
|
118
|
+
ref={setButton}
|
|
119
|
+
selected={open}
|
|
120
|
+
/>
|
|
121
|
+
</Popover>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function LanguageFilterOption(props: {
|
|
126
|
+
id: string
|
|
127
|
+
onToggle: (id: string) => void
|
|
128
|
+
selected: boolean
|
|
129
|
+
title: string
|
|
130
|
+
}) {
|
|
131
|
+
const {id, onToggle, selected, title} = props
|
|
132
|
+
|
|
133
|
+
const handleChange = useCallback(() => {
|
|
134
|
+
onToggle(id)
|
|
135
|
+
}, [id, onToggle])
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Card as="label">
|
|
139
|
+
<Flex align="center" gap={2}>
|
|
140
|
+
<Checkbox checked={selected} name={`language-${id}`} onChange={handleChange} />
|
|
141
|
+
<Box flex={1}>
|
|
142
|
+
<Text muted={!selected}>{title}</Text>
|
|
143
|
+
</Box>
|
|
144
|
+
</Flex>
|
|
145
|
+
</Card>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, {useEffect, useMemo} from 'react'
|
|
2
|
+
import {ObjectInputProps, ObjectMember, RenderInputCallback} from 'sanity'
|
|
3
|
+
import {LanguageFilterConfig} from './types'
|
|
4
|
+
import {defaultFilterField} from './filterField'
|
|
5
|
+
import {useLanguageFilterContext} from './LanguageFilterContext'
|
|
6
|
+
import {useSelectedLanguageIds} from './useSelectedLanguageIds'
|
|
7
|
+
|
|
8
|
+
export type LanguageFilterObjectInputProps = {
|
|
9
|
+
options: LanguageFilterConfig
|
|
10
|
+
next: RenderInputCallback
|
|
11
|
+
/**
|
|
12
|
+
* We need a way to communicate state changes between the pane menu and input components.
|
|
13
|
+
* LanguageFilter button lives outside the input-render tree, so Context is out.
|
|
14
|
+
* This is a workaround for that.
|
|
15
|
+
*/
|
|
16
|
+
subscribeSelectedIds: (callback: (ids: string[]) => void) => () => void
|
|
17
|
+
} & ObjectInputProps
|
|
18
|
+
|
|
19
|
+
export function LanguageFilterObjectInput(
|
|
20
|
+
props: ObjectInputProps & {
|
|
21
|
+
next: RenderInputCallback
|
|
22
|
+
subscribeSelectedIds: (callback: (ids: string[]) => void) => () => void
|
|
23
|
+
}
|
|
24
|
+
) {
|
|
25
|
+
const {options, enabled} = useLanguageFilterContext()
|
|
26
|
+
const {next, subscribeSelectedIds, ...restProps} = props
|
|
27
|
+
if (!enabled || !options) {
|
|
28
|
+
return <>{next(restProps)}</>
|
|
29
|
+
}
|
|
30
|
+
return (
|
|
31
|
+
<FilteredObjectInput
|
|
32
|
+
{...restProps}
|
|
33
|
+
next={next}
|
|
34
|
+
options={options}
|
|
35
|
+
subscribeSelectedIds={subscribeSelectedIds}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function FilteredObjectInput(props: LanguageFilterObjectInputProps) {
|
|
41
|
+
const {
|
|
42
|
+
members: membersProp,
|
|
43
|
+
options,
|
|
44
|
+
schemaType,
|
|
45
|
+
next,
|
|
46
|
+
subscribeSelectedIds,
|
|
47
|
+
...restProps
|
|
48
|
+
} = props
|
|
49
|
+
const [selectedIds, setSelectedIds] = useSelectedLanguageIds(options)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const unsubscribe = subscribeSelectedIds(setSelectedIds)
|
|
53
|
+
return () => unsubscribe()
|
|
54
|
+
}, [subscribeSelectedIds, setSelectedIds])
|
|
55
|
+
|
|
56
|
+
const activeLanguages = useMemo(
|
|
57
|
+
() => [...(options.defaultLanguages ?? []), ...selectedIds],
|
|
58
|
+
[options.defaultLanguages, selectedIds]
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const filterField = options.filterField ?? defaultFilterField
|
|
62
|
+
|
|
63
|
+
const members: ObjectMember[] = useMemo(() => {
|
|
64
|
+
return membersProp
|
|
65
|
+
.filter((member) => {
|
|
66
|
+
return (
|
|
67
|
+
(member.kind === 'field' && filterField(schemaType, member, activeLanguages)) ||
|
|
68
|
+
member.kind === 'fieldSet'
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
.map((member) => {
|
|
72
|
+
if (member.kind === 'fieldSet') {
|
|
73
|
+
return {
|
|
74
|
+
...member,
|
|
75
|
+
fieldSet: {
|
|
76
|
+
...member.fieldSet,
|
|
77
|
+
members: member.fieldSet.members.filter((fieldsetMember) => {
|
|
78
|
+
return (
|
|
79
|
+
fieldsetMember.kind === 'field' &&
|
|
80
|
+
filterField(schemaType, fieldsetMember, activeLanguages)
|
|
81
|
+
)
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return member
|
|
87
|
+
})
|
|
88
|
+
}, [schemaType, membersProp, filterField, activeLanguages])
|
|
89
|
+
|
|
90
|
+
return <>{next({...restProps, members, schemaType})}</>
|
|
91
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {defaultFilterField, isLanguageFilterEnabled} from './filterField'
|
|
2
|
+
import {FieldMember, ObjectSchemaType} from 'sanity'
|
|
3
|
+
|
|
4
|
+
describe('filterField', () => {
|
|
5
|
+
describe('isLanguageFilterEnabled', () => {
|
|
6
|
+
const docType: ObjectSchemaType = {
|
|
7
|
+
name: 'some-doc',
|
|
8
|
+
jsonType: 'object',
|
|
9
|
+
fields: [],
|
|
10
|
+
type: {
|
|
11
|
+
name: 'document',
|
|
12
|
+
jsonType: 'object',
|
|
13
|
+
fields: [],
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
it('should be enabled when documentTypes is missing', () => {
|
|
17
|
+
const enabled = isLanguageFilterEnabled(docType, {supportedLanguages: []})
|
|
18
|
+
expect(enabled).toBeTruthy()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should be disabled when documentTypes is missing and options.languageFilter: false', () => {
|
|
22
|
+
const enabled = isLanguageFilterEnabled(
|
|
23
|
+
{...docType, options: {languageFilter: false}},
|
|
24
|
+
{supportedLanguages: []}
|
|
25
|
+
)
|
|
26
|
+
expect(enabled).toBeFalsy()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should be enabled when documentTypes is contains doc-type name', () => {
|
|
30
|
+
const enabled = isLanguageFilterEnabled(
|
|
31
|
+
{...docType, options: {languageFilter: false}},
|
|
32
|
+
{supportedLanguages: [], documentTypes: [docType.name]}
|
|
33
|
+
)
|
|
34
|
+
expect(enabled).toBeTruthy()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should be enabled when documentTypes does not contain doc-type name, but options.languageFilter: true', () => {
|
|
38
|
+
const enabled = isLanguageFilterEnabled(
|
|
39
|
+
{...docType, options: {languageFilter: true}},
|
|
40
|
+
{supportedLanguages: [], documentTypes: []}
|
|
41
|
+
)
|
|
42
|
+
expect(enabled).toBeTruthy()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('defaultFilterField', () => {
|
|
47
|
+
const localePrefixedObject: ObjectSchemaType = {
|
|
48
|
+
name: 'locale_parent',
|
|
49
|
+
jsonType: 'object',
|
|
50
|
+
fields: [],
|
|
51
|
+
}
|
|
52
|
+
const member: FieldMember = {
|
|
53
|
+
name: 'nb',
|
|
54
|
+
key: 'nb',
|
|
55
|
+
collapsed: undefined,
|
|
56
|
+
collapsible: undefined,
|
|
57
|
+
kind: 'field',
|
|
58
|
+
open: true,
|
|
59
|
+
index: 0,
|
|
60
|
+
field: {
|
|
61
|
+
schemaType: {name: 'string', jsonType: 'string'},
|
|
62
|
+
level: 1,
|
|
63
|
+
id: 'nb',
|
|
64
|
+
path: [],
|
|
65
|
+
validation: [],
|
|
66
|
+
presence: [],
|
|
67
|
+
changed: false,
|
|
68
|
+
value: undefined,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
it('should filter -> true for nb field inside local-prefixed object', () => {
|
|
73
|
+
const result = defaultFilterField(localePrefixedObject, member, ['nb'])
|
|
74
|
+
expect(result).toBeTruthy()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should filter -> false for unselected field inside local-prefixed object', () => {
|
|
78
|
+
const result = defaultFilterField(localePrefixedObject, member, ['other'])
|
|
79
|
+
expect(result).toBeFalsy()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should filter -> true for nb field inside non-prefixed object', () => {
|
|
83
|
+
const result = defaultFilterField(
|
|
84
|
+
{...localePrefixedObject, name: 'not-start-with-locale-field'},
|
|
85
|
+
member,
|
|
86
|
+
['nb']
|
|
87
|
+
)
|
|
88
|
+
expect(result).toBeTruthy()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {SchemaType} from 'sanity'
|
|
2
|
+
import {FilterFieldFunction, LanguageFilterConfig, LanguageFilterSchema} from './types'
|
|
3
|
+
|
|
4
|
+
export const defaultFilterField: FilterFieldFunction = (
|
|
5
|
+
enclosingType,
|
|
6
|
+
field,
|
|
7
|
+
selectedLanguageIds
|
|
8
|
+
) => !enclosingType.name.startsWith('locale') || selectedLanguageIds.includes(field.name)
|
|
9
|
+
|
|
10
|
+
export function isLanguageFilterEnabled(
|
|
11
|
+
schemaType: SchemaType | undefined,
|
|
12
|
+
options: LanguageFilterConfig
|
|
13
|
+
): boolean {
|
|
14
|
+
const schemaFilter =
|
|
15
|
+
isDocument(schemaType) && (schemaType as LanguageFilterSchema)?.options?.languageFilter
|
|
16
|
+
const defaultEnabled = !options.documentTypes
|
|
17
|
+
|
|
18
|
+
return !!(
|
|
19
|
+
(defaultEnabled && schemaFilter !== false) ||
|
|
20
|
+
(!defaultEnabled && schemaFilter) ||
|
|
21
|
+
(schemaType && options.documentTypes?.includes(schemaType.name))
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isDocument(schemaType?: SchemaType) {
|
|
26
|
+
return schemaType?.jsonType === 'object' && getRootType(schemaType).name === 'document'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRootType(schema: SchemaType): SchemaType {
|
|
30
|
+
if (schema.type) {
|
|
31
|
+
return getRootType(schema.type)
|
|
32
|
+
}
|
|
33
|
+
return schema
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin function
|
|
3
|
+
*/
|
|
4
|
+
export {languageFilter} from './plugin'
|
|
5
|
+
|
|
6
|
+
export {defaultFilterField, isLanguageFilterEnabled} from './filterField'
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
LanguageFilterConfig,
|
|
10
|
+
LanguageFilterSchema,
|
|
11
|
+
LanguageFilterOptions,
|
|
12
|
+
FilterFieldFunction,
|
|
13
|
+
Language,
|
|
14
|
+
} from './types'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type LanguageSubscription = (ids: string[]) => void
|
|
2
|
+
export type Unsubscribe = () => void
|
|
3
|
+
export type LanguageSubscribe = (subscription: LanguageSubscription) => Unsubscribe
|
|
4
|
+
|
|
5
|
+
export interface SelectedLanguageIdsBus {
|
|
6
|
+
onSelectedIdsChange: (ids: string[]) => void
|
|
7
|
+
subscribeSelectedIds: LanguageSubscribe
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* We need a way to communicate state changes between the pane menu and input components.
|
|
12
|
+
* LanguageFilter button lives outside the input-render tree, so Context is out.
|
|
13
|
+
* This is a workaround for that.
|
|
14
|
+
*/
|
|
15
|
+
export function createSelectedLanguageIdsBus(): SelectedLanguageIdsBus {
|
|
16
|
+
const subs: LanguageSubscription[] = []
|
|
17
|
+
|
|
18
|
+
const onSelectedIdsChange = (ids: string[]) => {
|
|
19
|
+
subs.forEach((s) => s(ids))
|
|
20
|
+
}
|
|
21
|
+
const subscribeSelectedIds = (subscription: LanguageSubscription) => {
|
|
22
|
+
subs.push(subscription)
|
|
23
|
+
return () => {
|
|
24
|
+
subs.splice(subs.indexOf(subscription), 1)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
onSelectedIdsChange,
|
|
30
|
+
subscribeSelectedIds,
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/plugin.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {_DocumentLanguageFilterComponent, createPlugin, ObjectInputProps} from 'sanity'
|
|
3
|
+
import {LanguageFilterObjectInput} from './LanguageFilterObjectInput'
|
|
4
|
+
import {LanguageFilterMenuButton} from './LanguageFilterMenuButton'
|
|
5
|
+
import {LanguageFilterConfig} from './types'
|
|
6
|
+
import {isLanguageFilterEnabled} from './filterField'
|
|
7
|
+
import {LanguageFilterProvider} from './LanguageFilterContext'
|
|
8
|
+
import {createSelectedLanguageIdsBus} from './languageSubscription'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ## Usage in sanity.config.ts (or .js)
|
|
12
|
+
*
|
|
13
|
+
* ```
|
|
14
|
+
* import {createConfig} from 'sanity'
|
|
15
|
+
* import {languageFilter} from '@sanity/language-filter'
|
|
16
|
+
*
|
|
17
|
+
* export const createConfig({
|
|
18
|
+
* /...
|
|
19
|
+
* plugins: [
|
|
20
|
+
* languageFilter({
|
|
21
|
+
* supportedLanguages: [
|
|
22
|
+
* {id: 'nb', title: 'Norwegian (Bokmål)'},
|
|
23
|
+
* {id: 'nn', title: 'Norwegian (Nynorsk)'},
|
|
24
|
+
* {id: 'en', title: 'English'},
|
|
25
|
+
* {id: 'es', title: 'Spanish'},
|
|
26
|
+
* {id: 'arb', title: 'Arabic'},
|
|
27
|
+
* {id: 'pt', title: 'Portuguese'},
|
|
28
|
+
* //...
|
|
29
|
+
* ],
|
|
30
|
+
* // Select Norwegian (Bokmål) by default
|
|
31
|
+
* defaultLanguages: ['nb'],
|
|
32
|
+
* // Only show language filter for document type `page` (schemaType.name)
|
|
33
|
+
* // Can also enable via document-options: options.languageFilter: true
|
|
34
|
+
* documentTypes: ['page'],
|
|
35
|
+
* // default filter function shown
|
|
36
|
+
* filterField: (enclosingType, field, selectedLanguageIds) =>
|
|
37
|
+
* !enclosingType.name.startsWith('locale') || selectedLanguageIds.includes(field.name),
|
|
38
|
+
* })
|
|
39
|
+
* ]
|
|
40
|
+
* })
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export const languageFilter = createPlugin<LanguageFilterConfig>((options) => {
|
|
44
|
+
const {onSelectedIdsChange, subscribeSelectedIds} = createSelectedLanguageIdsBus()
|
|
45
|
+
|
|
46
|
+
const RenderLanguageFilter: _DocumentLanguageFilterComponent = () => {
|
|
47
|
+
return <LanguageFilterMenuButton options={options} onSelectedIdsChange={onSelectedIdsChange} />
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: '@sanity/language-filter',
|
|
52
|
+
document: {
|
|
53
|
+
unstable_languageFilter: (prev, {schemaType, schema}) => {
|
|
54
|
+
if (isLanguageFilterEnabled(schema.get(schemaType), options)) {
|
|
55
|
+
return [...prev, RenderLanguageFilter]
|
|
56
|
+
}
|
|
57
|
+
return prev
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
form: {
|
|
62
|
+
renderInput(props, next) {
|
|
63
|
+
const enabled = isLanguageFilterEnabled(props.schemaType, options)
|
|
64
|
+
// will only be considered enabled for document, so this is only done once
|
|
65
|
+
if (enabled) {
|
|
66
|
+
return (
|
|
67
|
+
<LanguageFilterProvider enabled={enabled} options={options}>
|
|
68
|
+
{next(props)}
|
|
69
|
+
</LanguageFilterProvider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
if (props.schemaType.jsonType === 'object') {
|
|
73
|
+
return (
|
|
74
|
+
<LanguageFilterObjectInput
|
|
75
|
+
{...(props as ObjectInputProps)}
|
|
76
|
+
next={next}
|
|
77
|
+
subscribeSelectedIds={subscribeSelectedIds}
|
|
78
|
+
/>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return undefined
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
})
|