├── .prettierignore ├── src ├── index.ts ├── demo.gif ├── useSlugContext.ts ├── SlugInput.tsx └── usePrefixLogic.ts ├── .eslintignore ├── .npmignore ├── .prettierrc ├── sanity.json ├── .editorconfig ├── .eslintrc ├── v2-incompatible.js ├── package.config.ts ├── tsconfig.json ├── .gitignore ├── README.md ├── package.json └── LICENSE /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | lib -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SlugInput } from './SlugInput' 2 | -------------------------------------------------------------------------------- /src/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hdoro/sanity-plugin-prefixed-slug/HEAD/src/demo.gif -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | commitlint.config.js 3 | lib 4 | lint-staged.config.js 5 | package.config.ts 6 | *.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | src/* 3 | !src/previewTypes.d.ts 4 | clearLib.js 5 | .prettierignore 6 | .prettierrc 7 | tsconfig.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /sanity.json: -------------------------------------------------------------------------------- 1 | { 2 | "parts": [ 3 | { 4 | "implements": "part:@sanity/base/sanity-root", 5 | "path": "./v2-incompatible.js" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; editorconfig.org 2 | root = true 3 | charset= utf8 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true 6 | }, 7 | "extends": [ 8 | "sanity", 9 | "sanity/typescript", 10 | "sanity/react", 11 | "plugin:react-hooks/recommended", 12 | "plugin:prettier/recommended" 13 | ] 14 | } -------------------------------------------------------------------------------- /v2-incompatible.js: -------------------------------------------------------------------------------- 1 | const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin') 2 | const {name, version, sanityExchangeUrl} = require('./package.json') 3 | 4 | export default showIncompatiblePluginDialog({ 5 | name: name, 6 | versions: { 7 | v3: version, 8 | v2: undefined, 9 | }, 10 | sanityExchangeUrl, 11 | }) 12 | -------------------------------------------------------------------------------- /package.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from '@sanity/pkg-utils' 2 | 3 | export default defineConfig({ 4 | dist: 'lib', 5 | minify: true, 6 | legacyExports: true, 7 | // Remove this block to enable strict export validation 8 | extract: { 9 | rules: { 10 | 'ae-forgotten-export': 'off', 11 | 'ae-incompatible-release-tags': 'off', 12 | 'ae-internal-missing-underscore': 'off', 13 | 'ae-missing-release-tag': 'off', 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "moduleResolution": "node", 5 | "target": "esnext", 6 | "module": "esnext", 7 | "esModuleInterop": true, 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "strict": true, 10 | "sourceMap": false, 11 | "inlineSourceMap": false, 12 | "downlevelIteration": true, 13 | "declaration": true, 14 | "allowSyntheticDefaultImports": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "outDir": "lib", 18 | "skipLibCheck": true, 19 | "isolatedModules": true, 20 | "checkJs": false, 21 | "rootDir": "src" 22 | }, 23 | "include": ["src/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # macOS finder cache file 40 | .DS_Store 41 | 42 | # VS Code settings 43 | .vscode 44 | 45 | # IntelliJ 46 | .idea 47 | *.iml 48 | 49 | # Cache 50 | .cache 51 | 52 | # Yalc 53 | .yalc 54 | yalc.lock 55 | 56 | ##npm package zips 57 | *.tgz 58 | 59 | # Compiled plugin 60 | lib 61 | 62 | -------------------------------------------------------------------------------- /src/useSlugContext.ts: -------------------------------------------------------------------------------- 1 | // Provides the context needed for usePrefixLogic. 2 | // Adapted from: 3 | // https://github.com/sanity-io/sanity/blob/next/packages/sanity/src/core/form/inputs/Slug/utils/useSlugContext.ts 4 | 5 | import { useMemo } from 'react' 6 | import { 7 | useCurrentUser, 8 | useDataset, 9 | useProjectId, 10 | useSchema, 11 | useSource, 12 | SlugSourceContext, 13 | } from 'sanity' 14 | 15 | /** 16 | * @internal 17 | */ 18 | export type SlugContext = Omit 19 | 20 | /** 21 | * @internal 22 | */ 23 | export function useSlugContext(): SlugContext { 24 | const { getClient } = useSource() 25 | const schema = useSchema() 26 | const currentUser = useCurrentUser() 27 | const projectId = useProjectId() 28 | const dataset = useDataset() 29 | 30 | return useMemo(() => { 31 | return { 32 | projectId, 33 | dataset, 34 | getClient, 35 | schema, 36 | currentUser, 37 | } 38 | }, [getClient, schema, currentUser, projectId, dataset]) 39 | } 40 | -------------------------------------------------------------------------------- /src/SlugInput.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Card, Code, Flex, Text, TextInput, Tooltip } from '@sanity/ui' 2 | import React, { useCallback } from 'react' 3 | import { SlugInputProps } from 'sanity' 4 | import styled from 'styled-components' 5 | import { usePrefixLogic } from './usePrefixLogic' 6 | 7 | const UrlPrefix = styled(Card)` 8 | flex: 0 1 min-content; 9 | 10 | pre { 11 | padding: 1em 0; 12 | } 13 | 14 | pre, 15 | code { 16 | overflow: hidden; 17 | white-space: nowrap; 18 | max-width: 30ch; 19 | text-overflow: ellipsis; 20 | } 21 | 22 | // When no generate button is available, make it bigger 23 | &[data-no-generate='true'] { 24 | pre, 25 | code { 26 | max-width: 35ch; 27 | } 28 | } 29 | ` 30 | 31 | /** 32 | * Custom slug component for better UX & safer slugs: 33 | * - shows the final URL for the relative address (adds the BASE.PATH/ at the start) 34 | * - removes special characters and startin/trailing slashes 35 | */ 36 | const PrefixedSlugInput = (props: SlugInputProps) => { 37 | const { value, schemaType } = props 38 | const { prefix, generateSlug, updateValue, formatSlug } = usePrefixLogic(props) 39 | 40 | const onChange = useCallback( 41 | (event: React.FormEvent) => updateValue(event.currentTarget.value), 42 | [updateValue], 43 | ) 44 | 45 | const onBlur = useCallback( 46 | (event: React.FocusEvent) => { 47 | formatSlug(event.currentTarget.value) 48 | props.elementProps.onBlur(event) 49 | }, 50 | // eslint-disable-next-line 51 | [formatSlug, props.elementProps.onBlur], 52 | ) 53 | 54 | return ( 55 | 56 | {prefix && ( 57 | 60 | {prefix} 61 | 62 | } 63 | > 64 | 65 | {prefix} 66 | 67 | 68 | )} 69 | 70 | 77 | 78 | {schemaType.options?.source && ( 79 |