@sanity/document-internationalization 1.0.0 → 1.0.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.
Files changed (114) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +89 -47
  3. package/lib/index.esm.js +2 -0
  4. package/lib/index.esm.js.map +1 -0
  5. package/lib/index.js +2 -0
  6. package/lib/index.js.map +1 -0
  7. package/lib/src/index.d.ts +200 -0
  8. package/package.json +99 -43
  9. package/src/actions/DeleteWithi18nAction.tsx +118 -0
  10. package/src/actions/DuplicateWithi18nAction.tsx +77 -0
  11. package/src/actions/PublishWithi18nAction.ts +118 -0
  12. package/src/actions/index.ts +42 -0
  13. package/src/badges/LanguageBadge.tsx +22 -0
  14. package/src/constants/I18nDelimiter.ts +1 -0
  15. package/src/constants/I18nPrefix.ts +1 -0
  16. package/src/constants/IdStructure.ts +4 -0
  17. package/src/constants/LanguageCultures.ts +902 -0
  18. package/src/constants/ReferenceBehavior.ts +5 -0
  19. package/src/constants/SupportedEmojiFlagCodes.ts +251 -0
  20. package/src/constants/UiMessages.ts +56 -0
  21. package/src/constants/index.ts +7 -0
  22. package/src/documentI18n.tsx +33 -0
  23. package/src/hooks/index.ts +2 -0
  24. package/src/hooks/useConfig.ts +22 -0
  25. package/src/hooks/useDelayedFlag.ts +31 -0
  26. package/src/index.ts +6 -1
  27. package/src/language-select/components/LanguageSelect/LanguageConfigContext.tsx +4 -0
  28. package/src/language-select/components/LanguageSelect/LanguageSelect.tsx +188 -0
  29. package/src/language-select/components/LanguageSelect/LanguageSelectContext.ts +16 -0
  30. package/src/language-select/components/LanguageSelect/LanguageSelectLabel.tsx +12 -0
  31. package/src/language-select/components/LanguageSelect/LanguageSelectList.tsx +84 -0
  32. package/src/language-select/components/LanguageSelect/LanguageSelectListItem.tsx +251 -0
  33. package/src/language-select/components/LanguageSelect/index.tsx +25 -0
  34. package/src/language-select/components/SingleFlag/SingleFlag.tsx +44 -0
  35. package/src/language-select/components/SingleFlag/allEmojiFlagCodes.ts +251 -0
  36. package/src/language-select/components/SingleFlag/index.ts +1 -0
  37. package/src/language-select/components/SplitPaneIcon/SplitPaneIcon.tsx +25 -0
  38. package/src/language-select/components/SplitPaneIcon/index.ts +1 -0
  39. package/src/language-select/components/index.ts +2 -0
  40. package/src/language-select/hooks/index.ts +1 -0
  41. package/src/language-select/hooks/useLanguages.ts +37 -0
  42. package/src/language-select/hooks/useListeningQuery.ts +66 -0
  43. package/src/structure/IDefaultDocumentNodeStructureProps.ts +4 -0
  44. package/src/structure/components/MaintenanceTab/MaintenanceTab.tsx +13 -0
  45. package/src/structure/components/MaintenanceTab/MaintenanceTabContent.tsx +259 -0
  46. package/src/structure/components/MaintenanceTab/index.ts +1 -0
  47. package/src/structure/components/MaintenanceTabResult/MaintenanceTabResult.tsx +44 -0
  48. package/src/structure/components/MaintenanceTabResult/index.ts +1 -0
  49. package/src/structure/components/MaintenanceTabTypeSelector/MaintenanceTabTypeSelector.tsx +56 -0
  50. package/src/structure/components/MaintenanceTabTypeSelector/index.ts +1 -0
  51. package/src/structure/hooks/index.ts +1 -0
  52. package/src/structure/hooks/useDocumentsInformation.ts +110 -0
  53. package/src/structure/index.tsx +120 -0
  54. package/src/structure/utils/fixBaseDocumentRefs.ts +30 -0
  55. package/src/structure/utils/fixBaseLanguageMismatch.ts +21 -0
  56. package/src/structure/utils/fixIdStructureMismatchDocuments.ts +72 -0
  57. package/src/structure/utils/fixLanguageFields.ts +32 -0
  58. package/src/structure/utils/fixOrphanedDocuments.ts +22 -0
  59. package/src/structure/utils/fixTranslationRefs.ts +50 -0
  60. package/src/structure/utils/index.ts +6 -0
  61. package/src/types/IEditState.ts +6 -0
  62. package/src/types/IExtendedLanguageObject.ts +6 -0
  63. package/src/types/ILanguageObject.ts +4 -0
  64. package/src/types/ILanguageQuery.ts +13 -0
  65. package/src/types/IResolverProps.ts +10 -0
  66. package/src/types/ITranslationRef.ts +6 -0
  67. package/src/types/IType.ts +40 -0
  68. package/src/types/IUseDocumentOperationResult.ts +11 -0
  69. package/src/types/TFieldNamesConfig.ts +5 -0
  70. package/src/types/TLanguage.ts +3 -0
  71. package/src/types/TLanguagesOption.ts +4 -0
  72. package/src/types/TSchema.ts +6 -0
  73. package/src/types/Ti18nConfig.ts +33 -0
  74. package/src/types/Ti18nDocument.ts +14 -0
  75. package/src/types/Ti18nSchema.ts +13 -0
  76. package/src/types/index.ts +15 -0
  77. package/src/utils/applyConfig.ts +43 -0
  78. package/src/utils/baseToTop.ts +5 -0
  79. package/src/utils/buildDocId.ts +8 -0
  80. package/src/utils/createSanityReference.ts +11 -0
  81. package/src/utils/getBaseIdFromId.ts +20 -0
  82. package/src/utils/getBaseLanguage.ts +6 -0
  83. package/src/utils/getFlag.ts +38 -0
  84. package/src/utils/getLanguageFromDocument.ts +9 -0
  85. package/src/utils/getLanguageFromId.ts +13 -0
  86. package/src/utils/getLanguagesFromOption.ts +40 -0
  87. package/src/utils/getTranslationsForId.ts +39 -0
  88. package/src/utils/index.ts +14 -0
  89. package/src/utils/makeObjectKey.ts +3 -0
  90. package/src/utils/normalizeLanguageList.ts +26 -0
  91. package/src/utils/serializePath.ts +11 -0
  92. package/src/utils/updateIntlFieldsForDocument.ts +79 -0
  93. package/src/utils/useSanityClient.ts +6 -0
  94. package/src/validators/index.ts +1 -0
  95. package/src/validators/isSlugUnique.ts +56 -0
  96. package/src/withDocumentI18nPlugin.ts +24 -0
  97. package/v2-incompatible.js +1 -1
  98. package/lib/cjs/index.js +0 -539
  99. package/lib/cjs/index.js.map +0 -1
  100. package/lib/esm/index.js +0 -532
  101. package/lib/esm/index.js.map +0 -1
  102. package/lib/types/index.d.ts +0 -12
  103. package/lib/types/index.d.ts.map +0 -1
  104. package/src/LanguageManage.tsx +0 -25
  105. package/src/LanguageOption.tsx +0 -158
  106. package/src/LanguagePatch.tsx +0 -61
  107. package/src/MenuButton.tsx +0 -101
  108. package/src/badges/index.tsx +0 -23
  109. package/src/constants.ts +0 -1
  110. package/src/hooks/useLanguageMetadata.tsx +0 -21
  111. package/src/hooks/useOpenInNewPane.tsx +0 -27
  112. package/src/plugin.tsx +0 -104
  113. package/src/schema/translation/metadata.ts +0 -42
  114. package/src/types.ts +0 -24
package/package.json CHANGED
@@ -1,19 +1,41 @@
1
1
  {
2
2
  "name": "@sanity/document-internationalization",
3
- "version": "1.0.0",
4
- "description": "Create unique translations of a document based on its language, joined by a shared reference document.",
5
- "author": "Simeon Griggs <simeon@sanity.io>",
3
+ "version": "1.0.2",
4
+ "description": "Provides document level translations for Sanity Studio",
5
+ "keywords": [
6
+ "sanity",
7
+ "cms",
8
+ "headless",
9
+ "realtime",
10
+ "content",
11
+ "sanity-plugin",
12
+ "i18n",
13
+ "localization"
14
+ ],
15
+ "homepage": "https://github.com/sanity-io/document-internationalization#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/sanity-io/document-internationalization/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git@github.com:sanity-io/document-internationalization.git"
22
+ },
6
23
  "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",
24
+ "author": "Sanity.io <hello@sanity.io>",
11
25
  "exports": {
12
26
  ".": {
13
- "require": "./lib/cjs/index.js",
14
- "default": "./lib/esm/index.js"
15
- }
27
+ "types": "./lib/src/index.d.ts",
28
+ "source": "./src/index.ts",
29
+ "import": "./lib/index.esm.js",
30
+ "require": "./lib/index.js",
31
+ "default": "./lib/index.esm.js"
32
+ },
33
+ "./package.json": "./package.json"
16
34
  },
35
+ "main": "./lib/index.js",
36
+ "module": "./lib/index.esm.js",
37
+ "source": "./src/index.ts",
38
+ "types": "./lib/src/index.d.ts",
17
39
  "files": [
18
40
  "src",
19
41
  "lib",
@@ -21,54 +43,88 @@
21
43
  "sanity.json"
22
44
  ],
23
45
  "scripts": {
46
+ "prebuild": "npm run clean && plugin-kit verify-package --silent && pkg-utils",
47
+ "build": "pkg-utils build --strict",
24
48
  "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",
49
+ "compile": "tsc --noEmit",
50
+ "eslint": "eslint --ext=.js,.jsx,.mjs,.ts,.tsx --quiet",
29
51
  "link-watch": "plugin-kit link-watch",
30
- "prepublishOnly": "npm run build"
31
- },
32
- "repository": {
33
- "type": "git",
34
- "url": "https://github.com/sanity-io/document-internationalization"
52
+ "lint": "eslint .",
53
+ "lint:fix": "eslint . --quiet --fix",
54
+ "prepare": "husky install",
55
+ "prepublishOnly": "npm run build",
56
+ "watch": "pkg-utils watch"
35
57
  },
36
- "engines": {
37
- "node": ">=14.0.0"
58
+ "husky": {
59
+ "hooks": {
60
+ "pre-commit": "npm run lint:fix"
61
+ }
38
62
  },
39
63
  "dependencies": {
40
- "@sanity/icons": "^1.3.6",
41
- "@sanity/incompatible-plugin": "^1.0.1",
42
- "@sanity/ui": "^0.38.2",
64
+ "@cprecioso/country-flag-emoji": "^1.0.0",
65
+ "@sanity/color": "^2.1.20",
66
+ "@sanity/icons": "^2.0.0",
67
+ "@sanity/incompatible-plugin": "^1.0.4",
43
68
  "@sanity/uuid": "^3.0.1",
44
- "sanity-plugin-internationalized-array": "^1.1.0",
45
- "sanity-plugin-utils": "^0.0.1"
69
+ "@sanity/ui": "^1.0.0",
70
+ "html-react-parser": "^1.4.12",
71
+ "just-omit": "^2.0.1",
72
+ "just-safe-get": "^4.0.1",
73
+ "just-split": "^3.0.1",
74
+ "pinst": "^2.1.6",
75
+ "react-fast-compare": "^3.2.0",
76
+ "react-twemoji": "^0.5.0",
77
+ "twemoji": "^14.0.2"
46
78
  },
47
79
  "devDependencies": {
48
- "@parcel/packager-ts": "^2.7.0",
49
- "@parcel/transformer-typescript-types": "^2.7.0",
50
- "@sanity/plugin-kit": "^1.0.1",
51
- "@typescript-eslint/eslint-plugin": "^5.36.2",
52
- "@typescript-eslint/parser": "^5.36.2",
53
- "eslint": "^8.23.0",
80
+ "@commitlint/cli": "^17.2.0",
81
+ "@commitlint/config-conventional": "^17.2.0",
82
+ "@sanity/pkg-utils": "^1.17.2",
83
+ "@sanity/plugin-kit": "^2.1.5",
84
+ "@sanity/semantic-release-preset": "^2.0.2",
85
+ "@types/lodash": "^4.14.179",
86
+ "@types/react": "^18",
87
+ "@types/react-router": "^5.1.11",
88
+ "@types/react-router-dom": "^5.1.7",
89
+ "@types/resize-observer-browser": "^0.1.5",
90
+ "@types/styled-components": "^5.1.20",
91
+ "@typescript-eslint/eslint-plugin": "^5.42.0",
92
+ "@typescript-eslint/parser": "^5.42.0",
93
+ "eslint": "^8.26.0",
54
94
  "eslint-config-prettier": "^8.5.0",
55
95
  "eslint-config-sanity": "^6.0.0",
96
+ "eslint-plugin-cypress": "^2.12.1",
97
+ "eslint-plugin-es5": "^1.5.0",
98
+ "eslint-plugin-import": "^2.25.3",
56
99
  "eslint-plugin-prettier": "^4.2.1",
57
- "eslint-plugin-react": "^7.31.8",
100
+ "eslint-plugin-react": "^7.31.10",
58
101
  "eslint-plugin-react-hooks": "^4.6.0",
59
- "parcel": "^2.7.0",
102
+ "husky": "^8.0.1",
103
+ "lint-staged": "^13.0.3",
104
+ "lodash": "^4.17.21",
60
105
  "prettier": "^2.7.1",
61
- "react": "^17.0.0 || ^18.0.0",
106
+ "prettier-plugin-packagejson": "^2.3.0",
107
+ "react": "^18",
108
+ "react-dom": "^18",
109
+ "react-router": "^5.1.0",
110
+ "react-router-dom": "^5.1.0",
62
111
  "rimraf": "^3.0.2",
63
- "sanity": "^3.0.0-dev-preview.15",
64
- "typescript": "^4.8.3"
112
+ "rxjs": "^6.6.0",
113
+ "sanity": "^3.0.0",
114
+ "styled-components": "^5.3.1",
115
+ "typescript": "^4.8.4"
65
116
  },
66
117
  "peerDependencies": {
67
- "react": "^17.0.0 || ^18.0.0",
68
- "sanity": "dev-preview"
118
+ "lodash": "^4.0.0",
119
+ "react": "^18",
120
+ "react-dom": "^18",
121
+ "react-router": "^5.0.0",
122
+ "react-router-dom": "^5.0.0",
123
+ "rxjs": "^6.0.0",
124
+ "sanity": "^3.0.0",
125
+ "styled-components": "^5.0.0"
69
126
  },
70
- "bugs": {
71
- "url": "https://github.com/sanity-io/document-internationalization/issues"
72
- },
73
- "homepage": "https://github.com/sanity-io/document-internationalization#readme"
127
+ "engines": {
128
+ "node": ">=14"
129
+ }
74
130
  }
@@ -0,0 +1,118 @@
1
+ import React from 'react'
2
+ import {TrashIcon} from '@sanity/icons'
3
+ import {useDocumentOperation, useEditState, useSyncState} from 'sanity'
4
+ import {ConfirmDeleteDialog} from 'sanity/desk'
5
+ import {useToast} from '@sanity/ui'
6
+ import type {DocumentActionComponent, DocumentActionDescription} from 'sanity'
7
+ import {IEditState, IUseDocumentOperationResult, Ti18nConfig} from '../types'
8
+ import {getBaseIdFromId, getTranslationsFor, useSanityClient} from '../utils'
9
+ import {UiMessages} from '../constants'
10
+ import {useConfig} from '../hooks'
11
+
12
+ /**
13
+ * This code is mostly taken from the defualt DeleteAction provided by Sanity
14
+ */
15
+ const DISABLED_REASON_TITLE = {
16
+ NOTHING_TO_DELETE: "This document doesn't yet exist or is already deleted",
17
+ }
18
+
19
+ export function createDeleteAction(pluginConfig: Ti18nConfig): DocumentActionComponent {
20
+ return function DeleteWith18nAction({id, type, onComplete}): DocumentActionDescription {
21
+ const toast = useToast()
22
+ const client = useSanityClient()
23
+ const config = useConfig(pluginConfig, type)
24
+ const baseDocumentId = React.useMemo(() => getBaseIdFromId(id), [id])
25
+ const baseDocumentEditState = useEditState(baseDocumentId, type) as IEditState
26
+ const syncState = useSyncState(id, type)
27
+ const baseDocumentSyncState = useSyncState(baseDocumentId, type)
28
+ const {delete: deleteOp} = useDocumentOperation(id, type) as IUseDocumentOperationResult
29
+ const [isDeleting, setIsDeleting] = React.useState(false)
30
+ const [isConfirmDialogOpen, setConfirmDialogOpen] = React.useState(false)
31
+
32
+ const onHandle = React.useCallback(() => {
33
+ setConfirmDialogOpen(true)
34
+ }, [])
35
+
36
+ const onDialogCancel = React.useCallback(() => {
37
+ setConfirmDialogOpen(false)
38
+ if (onComplete) onComplete()
39
+ }, [onComplete])
40
+
41
+ const onDialogConfirm = React.useCallback(async () => {
42
+ try {
43
+ setIsDeleting(true)
44
+ setConfirmDialogOpen(false)
45
+
46
+ if (baseDocumentEditState.draft || baseDocumentEditState.published) {
47
+ const baseTransaction = client.transaction()
48
+ if (baseDocumentEditState.draft) {
49
+ baseTransaction.delete(`drafts.${baseDocumentId}`)
50
+ }
51
+ if (baseDocumentEditState.published) {
52
+ baseTransaction.patch(baseDocumentId, {
53
+ unset: [config.fieldNames.references],
54
+ })
55
+ }
56
+ await baseTransaction.commit()
57
+ }
58
+
59
+ const translatedDocuments = await getTranslationsFor(client, config, baseDocumentId, true)
60
+ const translationsTransaction = client.transaction()
61
+ translatedDocuments.forEach((doc) => {
62
+ translationsTransaction.delete(doc._id)
63
+ })
64
+ await translationsTransaction.commit()
65
+
66
+ deleteOp.execute()
67
+ if (onComplete) onComplete()
68
+ } catch (err) {
69
+ toast.push({
70
+ closable: true,
71
+ status: 'error',
72
+ title: (err as Error).toString(),
73
+ })
74
+ } finally {
75
+ setIsDeleting(true)
76
+ }
77
+ }, [
78
+ toast,
79
+ config,
80
+ client,
81
+ baseDocumentId,
82
+ baseDocumentEditState.draft,
83
+ baseDocumentEditState.published,
84
+ deleteOp,
85
+ onComplete,
86
+ ])
87
+
88
+ return {
89
+ onHandle,
90
+ tone: 'critical',
91
+ icon: TrashIcon,
92
+ disabled:
93
+ isDeleting ||
94
+ Boolean(deleteOp.disabled) ||
95
+ syncState.isSyncing ||
96
+ baseDocumentSyncState.isSyncing,
97
+ shortcut: 'CTRL+ALT+D',
98
+ title:
99
+ (deleteOp.disabled &&
100
+ DISABLED_REASON_TITLE[deleteOp.disabled as keyof typeof DISABLED_REASON_TITLE]) ||
101
+ '',
102
+ label: isDeleting ? UiMessages.deleteAll.deleting : UiMessages.deleteAll.buttonTitle,
103
+ dialog: isConfirmDialogOpen && {
104
+ type: 'dialog',
105
+ onClose: onComplete,
106
+ content: (
107
+ <ConfirmDeleteDialog
108
+ action="delete"
109
+ id={id}
110
+ type={type}
111
+ onCancel={onDialogCancel}
112
+ onConfirm={onDialogConfirm}
113
+ />
114
+ ),
115
+ },
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,77 @@
1
+ import React from 'react'
2
+ import {CopyIcon} from '@sanity/icons'
3
+ import {useDocumentOperation} from 'sanity'
4
+ import {useToast} from '@sanity/ui'
5
+ import {uuid} from '@sanity/uuid'
6
+ import type {DocumentActionComponent, DocumentActionDescription} from 'sanity'
7
+ import {
8
+ buildDocId,
9
+ getBaseIdFromId,
10
+ getLanguageFromDocument,
11
+ getTranslationsFor,
12
+ useSanityClient,
13
+ } from '../utils'
14
+ import {IUseDocumentOperationResult, Ti18nConfig} from '../types'
15
+ import {UiMessages} from '../constants'
16
+ import {useConfig} from '../hooks'
17
+
18
+ /**
19
+ * This code is mostly taken from the default DuplicateAction provided by Sanity
20
+ */
21
+
22
+ const DISABLED_REASON_TITLE = {
23
+ NOTHING_TO_DUPLICATE: "This document doesn't yet exist so there's nothing to duplicate",
24
+ }
25
+
26
+ export function createDuplicateAction(pluginConfig: Ti18nConfig): DocumentActionComponent {
27
+ return ({id, type, draft, published}): DocumentActionDescription => {
28
+ const toast = useToast()
29
+ const client = useSanityClient()
30
+ const config = useConfig(pluginConfig, type)
31
+ const baseDocumentId = getBaseIdFromId(id)
32
+ const {duplicate: duplicateOp} = useDocumentOperation(id, type) as IUseDocumentOperationResult
33
+ const [isDuplicating, setDuplicating] = React.useState(false)
34
+
35
+ const onDuplicate = React.useCallback(async () => {
36
+ setDuplicating(true)
37
+ try {
38
+ const dupeId = uuid()
39
+ const translations = await getTranslationsFor(client, config, baseDocumentId)
40
+ const transaction = client.transaction()
41
+ transaction.create({
42
+ ...(draft ?? published),
43
+ _id: dupeId,
44
+ _type: type,
45
+ })
46
+ translations.forEach((t) => {
47
+ const isDraft = t._id.startsWith('drafts.')
48
+ const newId = buildDocId(config, dupeId, getLanguageFromDocument(t, config))
49
+ transaction.create({
50
+ ...t,
51
+ _id: isDraft ? `drafts.${newId}` : newId,
52
+ })
53
+ })
54
+ await transaction.commit()
55
+ } catch (err) {
56
+ console.error(err)
57
+ toast.push({
58
+ description: (err as Error).message,
59
+ })
60
+ }
61
+ setDuplicating(false)
62
+ }, [client, config, toast, baseDocumentId, type, draft, published])
63
+
64
+ return {
65
+ icon: CopyIcon,
66
+ disabled: Boolean(duplicateOp.disabled) || isDuplicating,
67
+ title:
68
+ (duplicateOp.disabled &&
69
+ DISABLED_REASON_TITLE[duplicateOp.disabled as keyof typeof DISABLED_REASON_TITLE]) ||
70
+ '',
71
+ label: isDuplicating
72
+ ? UiMessages.duplicateAll.duplicating
73
+ : UiMessages.duplicateAll.buttonTitle,
74
+ onHandle: onDuplicate,
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,118 @@
1
+ import * as React from 'react'
2
+
3
+ import {useToast} from '@sanity/ui'
4
+ import {CheckmarkIcon, PublishIcon} from '@sanity/icons'
5
+ import {useDocumentOperation, useEditState, useSyncState, useValidationStatus} from 'sanity'
6
+ import type {DocumentActionComponent, DocumentActionDescription} from 'sanity'
7
+ import {getBaseIdFromId, updateIntlFieldsForDocument, useSanityClient} from '../utils'
8
+ import {ReferenceBehavior, UiMessages} from '../constants'
9
+ import {IEditState, IUseDocumentOperationResult, Ti18nConfig} from '../types'
10
+ import {useConfig, useDelayedFlag} from '../hooks'
11
+
12
+ export function createPublishAction(pluginConfig: Ti18nConfig): DocumentActionComponent {
13
+ return ({type, id, onComplete}): DocumentActionDescription => {
14
+ const toast = useToast()
15
+ const client = useSanityClient()
16
+ const config = useConfig(pluginConfig, type)
17
+ const baseDocumentId = getBaseIdFromId(id)
18
+ const updatingIntlFieldsPromiseRef = React.useRef<Promise<void> | null>(null)
19
+ const [publishState, setPublishState] = React.useState<'publishing' | 'published' | null>(null)
20
+ const [updatingIntlFields, setUpdatingIntlFields] = React.useState(false)
21
+ const {draft, published} = useEditState(id, type) as IEditState
22
+ const baseDocumentEditState = useEditState(baseDocumentId, type) as IEditState
23
+ const {publish} = useDocumentOperation(id, type) as IUseDocumentOperationResult
24
+ const {isValidating, validation} = useValidationStatus(id, type)
25
+ const syncState = useSyncState(id, type)
26
+ const baseDocumentSyncState = useSyncState(baseDocumentId, type)
27
+ const disabled = useDelayedFlag(
28
+ !!(
29
+ publishState === 'published' ||
30
+ publishState === 'publishing' ||
31
+ updatingIntlFields ||
32
+ publish.disabled ||
33
+ syncState.isSyncing ||
34
+ baseDocumentSyncState.isSyncing ||
35
+ isValidating ||
36
+ validation.some((marker) => marker.level === 'error')
37
+ )
38
+ )
39
+
40
+ const label = React.useMemo(() => {
41
+ if (publishState === 'publishing') return UiMessages.publishing
42
+ if (updatingIntlFields) return UiMessages.updatingIntlFields
43
+ return UiMessages.publish
44
+ }, [publishState, updatingIntlFields])
45
+
46
+ const doUpdateIntlFields = React.useCallback(async () => {
47
+ setUpdatingIntlFields(true)
48
+ try {
49
+ const document = draft || published
50
+ if (document) {
51
+ await updateIntlFieldsForDocument(
52
+ client,
53
+ config,
54
+ document,
55
+ baseDocumentEditState.published
56
+ )
57
+ }
58
+
59
+ toast.push({
60
+ closable: true,
61
+ status: 'success',
62
+ title: UiMessages.intlFieldsUpdated,
63
+ })
64
+ } catch (err) {
65
+ console.error(err)
66
+ toast.push({
67
+ closable: true,
68
+ status: 'error',
69
+ title: (err as Error).toString(),
70
+ })
71
+ }
72
+ setUpdatingIntlFields(false)
73
+ }, [toast, draft, published, client, baseDocumentEditState.published, config])
74
+
75
+ const onHandle = React.useCallback(() => {
76
+ setPublishState('publishing')
77
+ const isTranslation = id !== baseDocumentId
78
+ if (
79
+ isTranslation &&
80
+ !baseDocumentEditState.published &&
81
+ config.referenceBehavior === ReferenceBehavior.STRONG
82
+ ) {
83
+ throw new Error(UiMessages.errors.baseDocumentNotPublished)
84
+ } else {
85
+ publish.execute()
86
+ }
87
+ }, [id, baseDocumentId, publish, config, baseDocumentEditState.published])
88
+
89
+ React.useEffect(() => {
90
+ const didPublish = publishState === 'publishing' && !draft
91
+
92
+ const nextState = didPublish ? 'published' : null
93
+ const delay = didPublish ? 200 : 4000
94
+ const timer = setTimeout(() => {
95
+ setPublishState(nextState)
96
+ }, delay)
97
+ return () => clearTimeout(timer)
98
+ }, [publishState, draft])
99
+
100
+ React.useEffect(() => {
101
+ if (publishState === 'published') {
102
+ if (!updatingIntlFieldsPromiseRef.current) {
103
+ updatingIntlFieldsPromiseRef.current = doUpdateIntlFields()
104
+ .then(() => onComplete && onComplete())
105
+ .finally(() => (updatingIntlFieldsPromiseRef.current = null))
106
+ }
107
+ }
108
+ }, [publishState, onComplete, doUpdateIntlFields])
109
+
110
+ return {
111
+ disabled,
112
+ label,
113
+ icon: publishState === 'published' ? CheckmarkIcon : PublishIcon,
114
+ shortcut: disabled ? null : 'Ctrl+Alt+P',
115
+ onHandle,
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,42 @@
1
+ import {type DocumentActionsContext, type DocumentActionComponent} from 'sanity'
2
+ import {Ti18nConfig, Ti18nSchema} from '../types'
3
+ import {getBaseIdFromId} from '../utils'
4
+ import {createPublishAction} from './PublishWithi18nAction'
5
+ import {createDeleteAction} from './DeleteWithi18nAction'
6
+ import {createDuplicateAction} from './DuplicateWithi18nAction'
7
+
8
+ export {createPublishAction, createDeleteAction, createDuplicateAction}
9
+
10
+ export const resolveActions = (
11
+ prev: DocumentActionComponent[],
12
+ {schema, schemaType, documentId}: DocumentActionsContext,
13
+ pluginConfig: Ti18nConfig
14
+ ): DocumentActionComponent[] => {
15
+ const type: Ti18nSchema = schema.get(schemaType) as Ti18nSchema
16
+ const isI18n = type && type.i18n
17
+ const isBase = documentId === getBaseIdFromId(documentId)
18
+ let actions = prev
19
+
20
+ if (isI18n) {
21
+ actions = actions.map((Action) => {
22
+ const isPublishAction = Action.action === 'publish'
23
+ const isDeleteAction = Action.action === 'delete'
24
+
25
+ if (isPublishAction) {
26
+ return createPublishAction(pluginConfig)
27
+ }
28
+
29
+ if (isDeleteAction && isBase) {
30
+ return createDeleteAction(pluginConfig)
31
+ }
32
+
33
+ return Action
34
+ })
35
+
36
+ if (isBase) {
37
+ actions.push(createDuplicateAction(pluginConfig))
38
+ }
39
+ }
40
+
41
+ return actions
42
+ }
@@ -0,0 +1,22 @@
1
+ import {DocumentBadgeComponent} from 'sanity'
2
+ import {useConfig} from '../hooks'
3
+ import {getLanguageFromDocument} from '../utils'
4
+ import {Ti18nConfig} from '../types'
5
+
6
+ export function createLanguageBadge(pluginConfig: Ti18nConfig): DocumentBadgeComponent {
7
+ const LanguageBadge: DocumentBadgeComponent = (props) => {
8
+ const config = useConfig(pluginConfig, props.type)
9
+ const doc = props.draft || props.published
10
+ const lang = doc ? getLanguageFromDocument(doc, config) : null
11
+ if (lang) {
12
+ return {
13
+ label: lang,
14
+ color: 'success',
15
+ }
16
+ }
17
+
18
+ return null
19
+ }
20
+
21
+ return LanguageBadge
22
+ }
@@ -0,0 +1 @@
1
+ export const I18nDelimiter = '__i18n_'
@@ -0,0 +1 @@
1
+ export const I18nPrefix = 'i18n'
@@ -0,0 +1,4 @@
1
+ export enum IdStructure {
2
+ SUBPATH = 'subpath',
3
+ DELIMITER = 'delimiter',
4
+ }