├── .eslintignore ├── .prettierignore ├── .prettierrc.js ├── src ├── index.html ├── index.css └── index.tsx ├── tsconfig.json ├── .eslintrc.js ├── .gitignore ├── .babelrc ├── LICENSE ├── docs ├── src.a8b20bb6.css └── src.abfa7487.js ├── package.json ├── extension.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .coverage/ 3 | dist/ 4 | build/ 5 | .cache/ 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .coverage/ 3 | dist/ 4 | build/ 5 | .cache/ 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | useTabs: false, 5 | singleQuote: true, 6 | jsxBracketSameLine: true 7 | }; 8 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "strict": true, 6 | "jsx": "react", 7 | "rootDir": "./src", 8 | "experimentalDecorators": true, 9 | "esModuleInterop": true, 10 | "lib": ["dom", "es2015"], 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | require.resolve('@contentful/eslint-config-extension/typescript'), 4 | require.resolve('@contentful/eslint-config-extension/jest'), 5 | require.resolve('@contentful/eslint-config-extension/jsx-a11y'), 6 | require.resolve('@contentful/eslint-config-extension/react') 7 | ] 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # dotenv environment variables file 9 | .env 10 | .contentfulrc.json 11 | 12 | # Parcel-bundler cache 13 | .cache 14 | 15 | # Dependency directories 16 | node_modules/ 17 | 18 | # Coverage 19 | .coverage 20 | 21 | # Build 22 | build/* 23 | !/build/index.html 24 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": false 7 | } 8 | ], 9 | [ 10 | "@babel/preset-react", 11 | { 12 | "useBuiltIns": true 13 | } 14 | ] 15 | ], 16 | "plugins": [ 17 | [ 18 | "@babel/plugin-proposal-class-properties", 19 | { 20 | "loose": true 21 | } 22 | ], 23 | [ 24 | "@babel/plugin-transform-runtime", 25 | { 26 | "corejs": false, 27 | "helpers": false, 28 | "regenerator": true 29 | } 30 | ] 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Paulo Gomes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/src.a8b20bb6.css: -------------------------------------------------------------------------------- 1 | body,div,html{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol}.iframe{height:50px,}.container{display:flex;flex-wrap:nowrap;align-items:center;margin:3px}.container input{-webkit-appearance:textfield;background-color:#fff;border:1px solid #cfd9e0;border-radius:6px;-webkit-box-shadow:inset 0 2px 0 rgba(225,228,232,.2);box-shadow:inset 0 2px 0 rgba(225,228,232,.2);color:#414d63;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:.875rem;margin:0;max-height:2.5rem;outline:none;padding:.65625rem;width:100%}.container input:focus{border:1px solid #036fe3;-webkit-box-shadow:0 0 0 3px #98cbff;box-shadow:0 0 0 3px #98cbff;outline:none}.container button{position:absolute;right:7px;margin-left:4px;-webkit-box-sizing:border-box;box-sizing:border-box;height:2rem;display:inline-block;padding:0 8px;border:1px solid #c3cfd5;border-radius:.125rem;font-size:.875rem;overflow:hidden;background-size:100% 200%;background-color:#e5ebed;color:#536171;vertical-align:middle;text-decoration:none;cursor:pointer;transition-duration:.1s;transition-timing-function:ease-in-out;transition-property:all} -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div { 4 | margin: 0; 5 | padding: 0; 6 | border: 0; 7 | font-size: 100%; 8 | font: inherit; 9 | vertical-align: baseline; 10 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, 11 | Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 12 | } 13 | 14 | .iframe { 15 | height: 50px, 16 | } 17 | .container { 18 | display: flex; 19 | flex-wrap: nowrap; 20 | align-items: center; 21 | margin: 3px; 22 | } 23 | .container input { 24 | -webkit-appearance: textfield; 25 | background-color: #fff; 26 | border: 1px solid #cfd9e0; 27 | border-radius: 6px; 28 | -webkit-box-shadow: inset 0 2px 0 #e1e4e833; 29 | box-shadow: inset 0 2px 0 #e1e4e833; 30 | color: #414d63; 31 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, 32 | Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; 33 | font-size: 0.875rem; 34 | margin: 0; 35 | max-height: 2.5rem; 36 | outline: none; 37 | padding: 0.65625rem; 38 | width: 100%; 39 | } 40 | .container input:focus { 41 | border: 1px solid #036fe3; 42 | -webkit-box-shadow: 0 0 0 3px #98cbff; 43 | box-shadow: 0 0 0 3px #98cbff; 44 | outline: none; 45 | } 46 | .container button { 47 | position: absolute; 48 | right: 7px; 49 | margin-left: 4px; 50 | -webkit-box-sizing: border-box; 51 | box-sizing: border-box; 52 | height: 2rem; 53 | display: inline-block; 54 | padding: 0 8px; 55 | border: 1px solid #c3cfd5; 56 | border-radius: 0.125rem; 57 | font-size: 0.875rem; 58 | overflow: hidden; 59 | background-size: 100% 200%; 60 | background-color: #e5ebed; 61 | color: #536171; 62 | vertical-align: middle; 63 | text-decoration: none; 64 | cursor: pointer; 65 | transition-duration: 0.1s; 66 | transition-timing-function: ease-in-out; 67 | transition-property: all; 68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-slugs", 3 | "version": "1.0.0", 4 | "author": "Paulo Gomes", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/pauloamgomes/contentful-better-slugs.git" 9 | }, 10 | "devDependencies": { 11 | "@babel/core": "7.3.4", 12 | "@babel/plugin-proposal-class-properties": "7.3.4", 13 | "@babel/plugin-transform-runtime": "7.3.4", 14 | "@babel/preset-env": "7.3.4", 15 | "@babel/preset-react": "7.0.0", 16 | "@contentful/contentful-extension-scripts": "^0.20.3", 17 | "@contentful/eslint-config-extension": "0.3.1", 18 | "@types/jest": "24.0.15", 19 | "@types/react": "^16.8.17", 20 | "@types/react-dom": "^16.8.4", 21 | "@types/speakingurl": "^13.0.2", 22 | "@types/webpack-env": "1.13.9", 23 | "contentful-cli": "^1.4.51", 24 | "cssnano": "4.1.10", 25 | "eslint": "^6.0.1", 26 | "typescript": "3.5.2" 27 | }, 28 | "scripts": { 29 | "start": "contentful-extension-scripts start", 30 | "build": "contentful-extension-scripts build", 31 | "dist": "rm -rf ./build/* && yarn build && rm -rf ./docs/* && cp ./build/* ./docs/", 32 | "lint": "eslint ./ --ext .js,.jsx,.ts,.tsx && tsc -p ./ --noEmit", 33 | "deploy": "npm run build && contentful extension update --force", 34 | "configure": "contentful space use && contentful space environment use", 35 | "login": "contentful login", 36 | "logout": "contentful logout", 37 | "help": "contentful-extension-scripts help" 38 | }, 39 | "dependencies": { 40 | "@contentful/forma-36-fcss": "^0.0.20", 41 | "@contentful/forma-36-react-components": "^3.11.3", 42 | "@contentful/forma-36-tokens": "^0.3.0", 43 | "contentful-ui-extensions-sdk": "^3.13.0", 44 | "react": "^16.8.6", 45 | "react-dom": "^16.8.6", 46 | "speakingurl": "^14.0.1" 47 | }, 48 | "browserslist": [ 49 | "last 5 Chrome version", 50 | "> 1%", 51 | "not ie <= 11" 52 | ], 53 | "resolutions": { 54 | "node-forge": "0.10.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "better-slugs", 3 | "name": "Better Slugs", 4 | "srcdoc": "./build/index.html", 5 | "fieldTypes": ["Symbol"], 6 | "parameters": { 7 | "instance": [ 8 | { 9 | "id": "pattern", 10 | "name": "Slug Pattern", 11 | "description": "Use replacement tokens: [locale], [field:name], [field:reference:name], e.g. [locale]/[field:category:title]/[field:title]", 12 | "type": "Symbol", 13 | "required": true, 14 | "default": "[field:title]" 15 | }, 16 | { 17 | "id": "translations1", 18 | "name": "String translations 1", 19 | "description": "To provide translations from a string, for example having a pattern like /products/[field:name] and nl, fr and de locales, you can use products=nl:producten,fr:produits,de:produkte", 20 | "type": "Symbol", 21 | "required": false, 22 | "default": "" 23 | }, 24 | { 25 | "id": "translations2", 26 | "name": "String translations 2", 27 | "description": "Optional translations for a second string (if it exists)", 28 | "type": "Symbol", 29 | "required": false, 30 | "default": "" 31 | }, 32 | { 33 | "id": "translations3", 34 | "name": "String translations 3", 35 | "description": "Optional translations for a third string (if it exists)", 36 | "type": "Symbol", 37 | "required": false, 38 | "default": "" 39 | }, 40 | { 41 | "id": "displayDefaultLocale", 42 | "name": "Display the default locale", 43 | "description": "When using the [locale] token, display or hide the locale in the slug for the default locale.", 44 | "type": "Boolean", 45 | "required": true, 46 | "default": true 47 | }, 48 | { 49 | "id": "lockWhenPublished", 50 | "name": "Do not update slug if entry is published", 51 | "description": "If the entry is published sometimes is desirable to not change the slug", 52 | "type": "Boolean", 53 | "required": true, 54 | "default": false 55 | }, 56 | { 57 | "id": "hideReset", 58 | "name": "Hide the reset button", 59 | "description": "If enabled it will hide the reset button", 60 | "type": "Boolean", 61 | "required": false, 62 | "default": false 63 | }, 64 | { 65 | "id": "caseOption", 66 | "name": "Case Option", 67 | "description": "Set the case mode (if not defined uses lowercase)", 68 | "type": "Enum", 69 | "required": false, 70 | "options": [{"maintainCase": "Maintain Case"}, {"titleCase": "Title Case"}] 71 | } 72 | ] 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: 2 | This UI Extension is deprecated and superseded by https://github.com/pauloamgomes/contentful-better-slugs-app 3 | 4 | # Contentful Better Slugs 5 | 6 | The Better Slugs is a simple UI extension for Contentful CMS that provides a more enhanced way to deal with slug fields. 7 | The existing slug functionality in Contentful is limited to one field (title) and therefore a bit limited for most of cases like: 8 | 9 | - Work with Gatsby Preview 10 | - Ability to automatically generate the slug with information from other fields (including referenced fields) 11 | - Autogeneration of slug doesn't end when the entry is published 12 | 13 | ## Overview 14 | 15 | The extension has the following features: 16 | 17 | - Generate a dynamic slug based on a pattern defined for each Content Model 18 | 19 |  20 | 21 | ## Requirements 22 | 23 | - Contentful CMS account with permissions to manage extensions 24 | 25 | ## Instalation (UI - using this repo) 26 | 27 | The UI Extension can be installed manually from the Contentful UI following the below steps: 28 | 29 | 1. Navigate to Settings > Extensions 30 | 2. Click on "Add extension > Install from Github" 31 | 3. Use `https://raw.githubusercontent.com/pauloamgomes/contentful-better-slugs/master/extension.json` in the url 32 | 4. On the extension settings screen change Hosting to Self-hosted using the url `https://pauloamgomes.github.io/contentful-better-slugs/` 33 | 34 | ## Usage 35 | 36 | 1. Add a new text field to your content model, it can be localized. 37 | 2. On the Appearance tab ensure that Better Slugs is selected 38 | 3. Provide your slug pattern, the pattern can use the following tokens: 39 | 40 | - **`[locale]`** - Will replace the token with the node locale 41 | - **`[field:your-field-name]`** - Will replace the token with the value of the field 42 | - **`[field:your-reference-field-name:field-name]`** - Will replace the token with the value of field that belongs to the reference. 43 | 44 | Example patterns: 45 | 46 | ``` 47 | [field:title] 48 | ``` 49 | 50 | Thats the default pattern, and probably for most of the cases would be enough. 51 | 52 | ``` 53 | [locale]/[field:category:title]/[field:title] 54 | ``` 55 | 56 | Assuming your locale is English, you have a reference field named category with title value `Computers` and your entry title is `Laptop 15"` the slug will be: `en/computers/laptop-15` 57 | 58 | ``` 59 | [field:date]/[field:title] 60 | ``` 61 | 62 | Assuming your locale is English, you have a date field named date with value `Friday, April 10th 2020` and your entry title is `London Event` the slug will be: `2020-04-10/london-event` 63 | 64 | If you have non dynamic strings in the path and you want to localize them, that would be possible (until a max of 3) using the translations fields, for example having a pattern like `/products/[field:name]` and `nl`, `fr` and `de` locales, you can use `products=nl:producten,fr:produits,de:produkte` and `products` will be `produkte` on the slug field for `de` locale. 65 | 66 | Other options: 67 | 68 | Ability to translate strings from the pattern: 69 | 70 |  71 | 72 | Option to not update the slug automatically if entry is published, hide the reset button and set the case mode (lowercase, maintain case, title case): 73 | 74 |  75 | 76 | ## Optional Usage for Development 77 | 78 | After cloning, install the dependencies 79 | 80 | ```bash 81 | yarn install 82 | ``` 83 | 84 | To bundle the extension 85 | 86 | ```bash 87 | yarn build 88 | ``` 89 | 90 | To host the extension for development on `http://localhost:1234` 91 | 92 | ```bash 93 | yarn start 94 | ``` 95 | 96 | To install the extension: 97 | 98 | ```bash 99 | contentful extension update --force 100 | ``` 101 | 102 | ## Limitations 103 | 104 | Tested only with text and date fields, so not sure how it can behave with custom fields, it will depend on the value stored in the field. 105 | 106 | The slug generation is based on the speakingurl library - https://github.com/pid/speakingurl 107 | 108 | ## Todo 109 | 110 | - Improve the handling of reference fields 111 | - Improve the handling of date fields by providing a date format. 112 | 113 | ## Copyright and license 114 | 115 | Copyright 2020 pauloamgomes under the MIT license. 116 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/no-use-before-define */ 3 | import React, { useState, useRef, useEffect } from 'react'; 4 | import { render } from 'react-dom'; 5 | import { init, FieldExtensionSDK } from 'contentful-ui-extensions-sdk'; 6 | import getSlug from 'speakingurl'; 7 | //== 8 | import './index.css'; 9 | 10 | interface BetterSlugsProps { 11 | sdk: FieldExtensionSDK; 12 | } 13 | 14 | const languages: any = { 15 | ar: 'ar', 16 | az: 'az', 17 | cs: 'cs', 18 | de: 'de', 19 | dv: 'dv', 20 | en: 'en', 21 | es: 'es', 22 | fa: 'fa', 23 | fi: 'fi', 24 | fr: 'fr', 25 | ge: 'ge', 26 | gr: 'gr', 27 | hu: 'hu', 28 | it: 'it', 29 | lt: 'lt', 30 | lv: 'lv', 31 | my: 'my', 32 | mk: 'mk', 33 | nl: 'nl', 34 | pl: 'pl', 35 | pt: 'pt', 36 | ro: 'ro', 37 | ru: 'ru', 38 | sk: 'sk', 39 | sr: 'sr', 40 | tr: 'tr', 41 | uk: 'uk', 42 | vn: 'vn', 43 | }; 44 | 45 | const BetterSlugs = ({ sdk }: BetterSlugsProps) => { 46 | const debounceInterval: any = useRef(false); 47 | const detachExternalChangeHandler: any = useRef(null); 48 | const parameters: any = sdk.parameters; 49 | const pattern: string = parameters.instance.pattern || ''; 50 | const displayDefaultLocale: boolean = parameters.instance.displayDefaultLocale; 51 | const lockWhenPublished: boolean = parameters.instance.lockWhenPublished; 52 | const translations1: string = parameters.instance.translations1 || ''; 53 | const translations2: string = parameters.instance.translations2 || ''; 54 | const translations3: string = parameters.instance.translations3 || ''; 55 | const hideReset: boolean = parameters.instance.hideReset || false; 56 | const caseOption: string = parameters.instance.caseOption; 57 | const slugOptions: any = {}; 58 | if (caseOption) { 59 | slugOptions[caseOption] = true; 60 | } 61 | 62 | const parts = pattern.split('/').map((part: string) => part.replace(/(\[|\])/gi, '').trim()); 63 | 64 | const [value, setValue] = useState(''); 65 | const fields: string[] = []; 66 | 67 | useEffect(() => { 68 | sdk.window.startAutoResizer(); 69 | 70 | // Extract fields used in slug parts. 71 | parts.forEach((part: string) => { 72 | if (part.startsWith('field:')) { 73 | fields.push(part.replace('field:', '')); 74 | } 75 | }); 76 | 77 | // Create a listener for each field and matching locales. 78 | fields.forEach((field: string) => { 79 | const fieldParts = field.split(':'); 80 | const fieldName = fieldParts.length === 1 ? field : fieldParts[0]; 81 | if (Object.prototype.hasOwnProperty.call(sdk.entry.fields, fieldName)) { 82 | const locales = sdk.entry.fields[fieldName].locales; 83 | 84 | locales.forEach((locale: string) => { 85 | sdk.entry.fields[fieldName].onValueChanged(locale, () => { 86 | if (debounceInterval.current) { 87 | clearInterval(debounceInterval.current); 88 | } 89 | debounceInterval.current = setTimeout(() => { 90 | updateSlug(locale); 91 | }, 500); 92 | }); 93 | }); 94 | } 95 | }); 96 | 97 | // Handler for external field value changes (e.g. when multiple authors are working on the same entry). 98 | if (sdk.field) { 99 | detachExternalChangeHandler.current = sdk.field.onValueChanged(onExternalChange); 100 | } 101 | 102 | return () => { 103 | if (detachExternalChangeHandler.current) { 104 | detachExternalChangeHandler.current(); 105 | } 106 | }; 107 | // eslint-disable-next-line react-hooks/exhaustive-deps 108 | }, []); 109 | 110 | /** 111 | * Retrieves the raw value from a referenced field. 112 | */ 113 | const getReferenceFieldValue = async ( 114 | fieldName: string, 115 | subFieldName: string, 116 | locale: string 117 | ) => { 118 | const defaultLocale = sdk.locales.default; 119 | const referenceLocale = sdk.entry.fields[fieldName].locales.includes(locale) 120 | ? locale 121 | : defaultLocale; 122 | 123 | const reference = sdk.entry.fields[fieldName].getValue(referenceLocale); 124 | if (!reference || !reference.sys || !reference.sys.id) { 125 | return ''; 126 | } 127 | 128 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 129 | const result: any = await sdk.space.getEntry(reference.sys.id); 130 | const { fields } = result; 131 | 132 | if (!fields) { 133 | return ''; 134 | } 135 | 136 | if (!Object.prototype.hasOwnProperty.call(fields, subFieldName)) { 137 | return ''; 138 | } 139 | 140 | if (Object.prototype.hasOwnProperty.call(fields[subFieldName], locale)) { 141 | return fields[subFieldName][locale]; 142 | } 143 | 144 | if (Object.prototype.hasOwnProperty.call(fields[subFieldName], defaultLocale)) { 145 | return fields[subFieldName][defaultLocale]; 146 | } 147 | 148 | return ''; 149 | }; 150 | 151 | const isLocked = () => { 152 | const sys: any = sdk.entry.getSys(); 153 | 154 | const published = !!sys.publishedVersion && sys.version == sys.publishedVersion + 1; 155 | const changed = !!sys.publishedVersion && sys.version >= sys.publishedVersion + 2; 156 | 157 | return published || changed; 158 | }; 159 | 160 | const translatePart = (part: string, locale: string) => { 161 | const regex = new RegExp(`^${part}=`); 162 | let translationConfig = ''; 163 | let translation = ''; 164 | 165 | if (regex.test(translations1)) { 166 | translationConfig = translations1; 167 | } else if (regex.test(translations2)) { 168 | translationConfig = translations2; 169 | } else if (regex.test(translations3)) { 170 | translationConfig = translations3; 171 | } 172 | 173 | translationConfig 174 | .replace(`${part}=`, '') 175 | .split(',') 176 | .find((val) => { 177 | const [transKey, transValue] = val.split(':'); 178 | if (transKey === locale) { 179 | translation = transValue; 180 | return true; 181 | } 182 | }); 183 | 184 | return translation || part; 185 | }; 186 | 187 | const partIsTranslatable = (part: string) => { 188 | const regex = new RegExp(`^${part}=`); 189 | return regex.test(translations1) || regex.test(translations2) || regex.test(translations3); 190 | }; 191 | 192 | /** 193 | * Updates the slug based on the defined pattern. 194 | */ 195 | const updateSlug = async (locale: string, force = false) => { 196 | if (sdk.field.locale !== locale || (!force && lockWhenPublished && isLocked())) { 197 | return; 198 | } 199 | 200 | const defaultLocale = sdk.locales.default; 201 | const slugParts: string[] = []; 202 | 203 | for (const part of parts) { 204 | if (part.startsWith('field:')) { 205 | const fieldParts = part.split(':'); 206 | let raw = ''; 207 | let slug = ''; 208 | 209 | const lang: string = languages[locale.slice(0, 2).toLowerCase()] || 'en'; 210 | 211 | if (fieldParts.length === 2) { 212 | if (sdk.entry.fields[fieldParts[1]] !== undefined) { 213 | if (sdk.entry.fields[fieldParts[1]].locales.includes(locale)) { 214 | raw = sdk.entry.fields[fieldParts[1]].getValue(locale); 215 | } else { 216 | raw = sdk.entry.fields[fieldParts[1]].getValue(defaultLocale); 217 | } 218 | } 219 | // eslint-disable-next-line no-misleading-character-class 220 | slug = getSlug(raw, { ...slugOptions, lang }).replace(/[-\ufe0f]+$/gu, ''); 221 | } else { 222 | raw = (await getReferenceFieldValue(fieldParts[1], fieldParts[2], locale)) || ''; 223 | slug = getSlug(raw, { ...slugOptions, lang, custom: { '/': '/' } }) 224 | // eslint-disable-next-line no-misleading-character-class 225 | .replace(/[-\ufe0f]+$/gu, ''); 226 | } 227 | 228 | slugParts.push(slug); 229 | } else if (part === 'locale') { 230 | if (locale !== defaultLocale || (locale === defaultLocale && displayDefaultLocale)) { 231 | slugParts.push(locale); 232 | } 233 | } else if (partIsTranslatable(part)) { 234 | slugParts.push(translatePart(part, locale)); 235 | } else { 236 | slugParts.push(part); 237 | } 238 | } 239 | 240 | sdk.entry.fields[sdk.field.id].setValue( 241 | slugParts.join('/').replace('//', '/').replace(/\/$/, ''), 242 | locale 243 | ); 244 | }; 245 | 246 | const onExternalChange = (value: string) => { 247 | setValue(value); 248 | }; 249 | 250 | const onChange = async (e: React.ChangeEventP(l,t))void 0!==u&&0>P(u,l)?(e[r]=u,e[i]=t,r=i):(e[r]=l,e[a]=t,r=a);else{if(!(void 0!==u&&0>P(u,t)))break e;e[r]=u,e[i]=t,r=i}}}return n}return null}function P(e,n){var t=e.sortIndex-n.sortIndex;return 0!==t?t:e.id-n.id}var F=[],I=[],M=1,C=null,A=3,L=!1,q=!1,D=!1;function R(e){for(var n=T(I);null!==n;){if(null===n.callback)g(I);else{if(!(n.startTime<=e))break;g(I),n.sortIndex=n.expirationTime,k(F,n)}n=T(I)}}function j(t){if(D=!1,R(t),!q)if(null!==T(F))q=!0,e(E);else{var r=T(I);null!==r&&n(j,r.startTime-t)}}function E(e,o){q=!1,D&&(D=!1,t()),L=!0;var a=A;try{for(R(o),C=T(F);null!==C&&(!(C.expirationTime>o)||e&&!r());){var l=C.callback;if(null!==l){C.callback=null,A=C.priorityLevel;var i=l(C.expirationTime<=o);o=exports.unstable_now(),"function"==typeof i?C.callback=i:C===T(F)&&g(F),R(o)}else g(F);C=T(F)}if(null!==C)var u=!0;else{var s=T(I);null!==s&&n(j,s.startTime-o),u=!1}return u}finally{C=null,A=a,L=!1}}function N(e){switch(e){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1e4;default:return 5e3}}var B=o;exports.unstable_IdlePriority=5,exports.unstable_ImmediatePriority=1,exports.unstable_LowPriority=4,exports.unstable_NormalPriority=3,exports.unstable_Profiling=null,exports.unstable_UserBlockingPriority=2,exports.unstable_cancelCallback=function(e){e.callback=null},exports.unstable_continueExecution=function(){q||L||(q=!0,e(E))},exports.unstable_getCurrentPriorityLevel=function(){return A},exports.unstable_getFirstCallbackNode=function(){return T(F)},exports.unstable_next=function(e){switch(A){case 1:case 2:case 3:var n=3;break;default:n=A}var t=A;A=n;try{return e()}finally{A=t}},exports.unstable_pauseExecution=function(){},exports.unstable_requestPaint=B,exports.unstable_runWithPriority=function(e,n){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var t=A;A=e;try{return n()}finally{A=t}},exports.unstable_scheduleCallback=function(r,o,a){var l=exports.unstable_now();if("object"==typeof a&&null!==a){var i=a.delay;i="number"==typeof i&&0l?(r.sortIndex=i,k(I,r),null===T(F)&&r===T(I)&&(D?t():D=!0,n(j,i-l))):(r.sortIndex=a,k(F,r),q||L||(q=!0,e(E))),r},exports.unstable_shouldYield=function(){var e=exports.unstable_now();R(e);var n=T(F);return n!==C&&null!==C&&null!==n&&null!==n.callback&&n.startTime<=e&&n.expirationTime