@smartcat/sanity-plugin 1.0.0 → 1.0.1
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/README.md +294 -0
- package/dist/_chunks-cjs/constants.cjs +13 -0
- package/dist/_chunks-cjs/constants.cjs.map +1 -0
- package/dist/_chunks-cjs/index.cjs +168 -0
- package/dist/_chunks-cjs/index.cjs.map +1 -0
- package/dist/_chunks-cjs/index2.cjs +311 -0
- package/dist/_chunks-cjs/index2.cjs.map +1 -0
- package/dist/_chunks-cjs/workflow.cjs +16 -0
- package/dist/_chunks-cjs/workflow.cjs.map +1 -0
- package/dist/_chunks-es/constants.js +14 -0
- package/dist/_chunks-es/constants.js.map +1 -0
- package/dist/_chunks-es/index.js +169 -0
- package/dist/_chunks-es/index.js.map +1 -0
- package/dist/_chunks-es/index2.js +313 -0
- package/dist/_chunks-es/index2.js.map +1 -0
- package/dist/_chunks-es/workflow.js +17 -0
- package/dist/_chunks-es/workflow.js.map +1 -0
- package/dist/export.cjs +160 -0
- package/dist/export.cjs.map +1 -0
- package/dist/export.d.cts +162 -0
- package/dist/export.d.ts +162 -0
- package/dist/export.js +162 -0
- package/dist/export.js.map +1 -0
- package/dist/import.cjs +169 -0
- package/dist/import.cjs.map +1 -0
- package/dist/import.d.cts +155 -0
- package/dist/import.d.ts +155 -0
- package/dist/import.js +169 -0
- package/dist/import.js.map +1 -0
- package/dist/index.cjs +2103 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +122 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2110 -0
- package/dist/index.js.map +1 -0
- package/dist/locjson.cjs +12 -0
- package/dist/locjson.cjs.map +1 -0
- package/dist/locjson.d.cts +287 -0
- package/dist/locjson.d.ts +287 -0
- package/dist/locjson.js +12 -0
- package/dist/locjson.js.map +1 -0
- package/dist/progress.cjs +16 -0
- package/dist/progress.cjs.map +1 -0
- package/dist/progress.d.cts +262 -0
- package/dist/progress.d.ts +262 -0
- package/dist/progress.js +16 -0
- package/dist/progress.js.map +1 -0
- package/dist/smartcat.cjs +287 -0
- package/dist/smartcat.cjs.map +1 -0
- package/dist/smartcat.d.cts +234 -0
- package/dist/smartcat.d.ts +234 -0
- package/dist/smartcat.js +287 -0
- package/dist/smartcat.js.map +1 -0
- package/dist/templates.cjs +12 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +52 -0
- package/dist/templates.d.ts +52 -0
- package/dist/templates.js +12 -0
- package/dist/templates.js.map +1 -0
- package/package.json +101 -15
- package/src/actions/AddToProjectAction.tsx +258 -0
- package/src/export/export.test.ts +537 -0
- package/src/export/index.ts +393 -0
- package/src/form/TranslationStatusInput.tsx +212 -0
- package/src/import/import.test.ts +346 -0
- package/src/import/index.ts +418 -0
- package/src/index.ts +63 -0
- package/src/lib/constants.ts +23 -0
- package/src/lib/documentTitle.test.ts +43 -0
- package/src/lib/documentTitle.ts +30 -0
- package/src/lib/languageMap.test.ts +56 -0
- package/src/lib/languageMap.ts +71 -0
- package/src/lib/linkedDocuments.test.ts +56 -0
- package/src/lib/linkedDocuments.ts +100 -0
- package/src/lib/locjson/deserialize.ts +60 -0
- package/src/lib/locjson/fields.ts +355 -0
- package/src/lib/locjson/filename.ts +27 -0
- package/src/lib/locjson/index.ts +10 -0
- package/src/lib/locjson/locjson.test.ts +545 -0
- package/src/lib/locjson/paths.test.ts +41 -0
- package/src/lib/locjson/paths.ts +73 -0
- package/src/lib/locjson/portableText.ts +98 -0
- package/src/lib/locjson/serialize.ts +124 -0
- package/src/lib/locjson/types.ts +106 -0
- package/src/lib/log.ts +33 -0
- package/src/lib/projectItems.ts +17 -0
- package/src/lib/resolveConfig.test.ts +62 -0
- package/src/lib/resolveConfig.ts +47 -0
- package/src/lib/workflow.test.ts +37 -0
- package/src/lib/workflow.ts +48 -0
- package/src/lib/zip.fixtures.ts +75 -0
- package/src/lib/zip.test.ts +110 -0
- package/src/lib/zip.ts +164 -0
- package/src/progress/core.ts +387 -0
- package/src/progress/index.ts +7 -0
- package/src/progress/progress.test.ts +268 -0
- package/src/schema/settings.ts +55 -0
- package/src/schema/translatableOptions.ts +25 -0
- package/src/schema/translationProject.ts +264 -0
- package/src/smartcat/client.test.ts +222 -0
- package/src/smartcat/client.ts +351 -0
- package/src/smartcat/index.ts +10 -0
- package/src/smartcat/types.ts +99 -0
- package/src/templates/core.ts +55 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/templates.test.ts +50 -0
- package/src/tool/Dashboard.tsx +44 -0
- package/src/tool/ItemProgress.tsx +176 -0
- package/src/tool/LogPanel.tsx +207 -0
- package/src/tool/ProjectView.tsx +975 -0
- package/src/tool/ProjectsView.tsx +208 -0
- package/src/tool/StatusBadge.tsx +33 -0
- package/src/tool/StudioInit.tsx +42 -0
- package/src/tool/WorkflowSelect.tsx +41 -0
- package/src/tool/data.ts +79 -0
- package/src/tool/index.ts +19 -0
- package/src/tool/processing.test.ts +623 -0
- package/src/tool/processing.ts +766 -0
- package/src/tool/useTemplates.ts +84 -0
- package/src/tool/useWorkflowSelection.ts +65 -0
- package/src/types.ts +77 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches templates from Smartcat and writes them (sorted by name) to the
|
|
3
|
+
* settings singleton, stamping `templatesSyncedAt`. Always writes the timestamp —
|
|
4
|
+
* even for an empty list — so the cache reads as initialized.
|
|
5
|
+
*/
|
|
6
|
+
export declare function runTemplatesSync(
|
|
7
|
+
options: RunTemplatesSyncOptions,
|
|
8
|
+
): Promise<{
|
|
9
|
+
count: number;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export declare interface RunTemplatesSyncOptions {
|
|
13
|
+
sanity: SanityLikeClient;
|
|
14
|
+
smartcat: SmartcatTemplatesClient;
|
|
15
|
+
/** `_id` of the settings singleton (defaults to the fixed id). */
|
|
16
|
+
settingsId?: string;
|
|
17
|
+
now?: () => string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Refreshes the cached Smartcat project-template list onto the `smartcat.settings`
|
|
22
|
+
* singleton. Run by the thin `smartcat-templates` Function when the browser bumps
|
|
23
|
+
* `templatesRequestedAt` (on Studio init or when a create dialog opens).
|
|
24
|
+
*
|
|
25
|
+
* Dependency-free (no DOM, no @sanity/* runtime) so it bundles into the Function.
|
|
26
|
+
*/
|
|
27
|
+
/** Minimal structural subset of @sanity/client used by the templates refresh. */
|
|
28
|
+
export declare interface SanityLikeClient {
|
|
29
|
+
patch(id: string): {
|
|
30
|
+
set(attrs: Record<string, unknown>): {
|
|
31
|
+
commit(): Promise<unknown>;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A Smartcat project template (from `GET /template`). */
|
|
37
|
+
declare interface SmartcatTemplate {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Structural subset of SmartcatClient the templates refresh needs. */
|
|
43
|
+
export declare interface SmartcatTemplatesClient {
|
|
44
|
+
getTemplates(): Promise<SmartcatTemplatesResponse>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Response shape of `GET /template`. */
|
|
48
|
+
declare interface SmartcatTemplatesResponse {
|
|
49
|
+
templates: SmartcatTemplate[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SETTINGS_DOC_ID } from "./_chunks-es/constants.js";
|
|
2
|
+
function keyify(value) {
|
|
3
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "-") || "x";
|
|
4
|
+
}
|
|
5
|
+
async function runTemplatesSync(options) {
|
|
6
|
+
const { sanity, smartcat, settingsId = SETTINGS_DOC_ID, now = () => (/* @__PURE__ */ new Date()).toISOString() } = options, { templates } = await smartcat.getTemplates(), value = [...templates ?? []].sort((a, b) => a.name.localeCompare(b.name)).map((t) => ({ _key: keyify(t.id), id: t.id, name: t.name }));
|
|
7
|
+
return await sanity.patch(settingsId).set({ templates: value, templatesSyncedAt: now() }).commit(), { count: value.length };
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
runTemplatesSync
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sources":["../src/templates/core.ts"],"sourcesContent":["import type {SmartcatTemplatesResponse} from '../smartcat/types'\nimport {SETTINGS_DOC_ID} from '../lib/constants'\n\n/**\n * Refreshes the cached Smartcat project-template list onto the `smartcat.settings`\n * singleton. Run by the thin `smartcat-templates` Function when the browser bumps\n * `templatesRequestedAt` (on Studio init or when a create dialog opens).\n *\n * Dependency-free (no DOM, no @sanity/* runtime) so it bundles into the Function.\n */\n\n/** Minimal structural subset of @sanity/client used by the templates refresh. */\nexport interface SanityLikeClient {\n patch(id: string): {\n set(attrs: Record<string, unknown>): {commit(): Promise<unknown>}\n }\n}\n\n/** Structural subset of SmartcatClient the templates refresh needs. */\nexport interface SmartcatTemplatesClient {\n getTemplates(): Promise<SmartcatTemplatesResponse>\n}\n\nexport interface RunTemplatesSyncOptions {\n sanity: SanityLikeClient\n smartcat: SmartcatTemplatesClient\n /** `_id` of the settings singleton (defaults to the fixed id). */\n settingsId?: string\n now?: () => string\n}\n\n/** Sanitizes a template id into a stable, Sanity-safe array `_key`. */\nfunction keyify(value: string): string {\n return value.replace(/[^a-zA-Z0-9_-]/g, '-') || 'x'\n}\n\n/**\n * Fetches templates from Smartcat and writes them (sorted by name) to the\n * settings singleton, stamping `templatesSyncedAt`. Always writes the timestamp —\n * even for an empty list — so the cache reads as initialized.\n */\nexport async function runTemplatesSync(\n options: RunTemplatesSyncOptions,\n): Promise<{count: number}> {\n const {sanity, smartcat, settingsId = SETTINGS_DOC_ID, now = () => new Date().toISOString()} = options\n\n const {templates} = await smartcat.getTemplates()\n const value = [...(templates ?? [])]\n .sort((a, b) => a.name.localeCompare(b.name))\n .map((t) => ({_key: keyify(t.id), id: t.id, name: t.name}))\n\n await sanity.patch(settingsId).set({templates: value, templatesSyncedAt: now()}).commit()\n\n return {count: value.length}\n}\n"],"names":[],"mappings":";AAgCA,SAAS,OAAO,OAAuB;AACrC,SAAO,MAAM,QAAQ,mBAAmB,GAAG,KAAK;AAClD;AAOA,eAAsB,iBACpB,SAC0B;AAC1B,QAAM,EAAC,QAAQ,UAAU,aAAa,iBAAiB,MAAM,OAAM,oBAAI,KAAA,GAAO,YAAA,EAAY,IAAK,SAEzF,EAAC,UAAA,IAAa,MAAM,SAAS,aAAA,GAC7B,QAAQ,CAAC,GAAI,aAAa,CAAA,CAAG,EAChC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,EAC3C,IAAI,CAAC,OAAO,EAAC,MAAM,OAAO,EAAE,EAAE,GAAG,IAAI,EAAE,IAAI,MAAM,EAAE,KAAA,EAAM;AAE5D,SAAA,MAAM,OAAO,MAAM,UAAU,EAAE,IAAI,EAAC,WAAW,OAAO,mBAAmB,IAAA,EAAI,CAAE,EAAE,OAAA,GAE1E,EAAC,OAAO,MAAM,OAAA;AACvB;"}
|
package/package.json
CHANGED
|
@@ -1,23 +1,109 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartcat/sanity-plugin",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"homepage": "https://github.com/smartcatai/sanity-plugin#readme",
|
|
6
|
-
"bugs": {
|
|
7
|
-
"url": "https://github.com/smartcatai/sanity-plugin/issues"
|
|
8
|
-
},
|
|
3
|
+
"version": "1.0.1",
|
|
9
4
|
"publishConfig": {
|
|
10
|
-
"access": "public"
|
|
5
|
+
"access": "public",
|
|
6
|
+
"registry": "https://registry.npmjs.org/"
|
|
11
7
|
},
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
"
|
|
8
|
+
"description": "Group Sanity content into translation projects, send them to Smartcat, and import localized variants.",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"sanity",
|
|
11
|
+
"sanity-plugin",
|
|
12
|
+
"smartcat",
|
|
13
|
+
"translation",
|
|
14
|
+
"localization",
|
|
15
|
+
"i18n"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Smartcat",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"browserslist": "extends @sanity/browserslist-config",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"source": "./src/index.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"require": "./dist/index.cjs",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./locjson": {
|
|
30
|
+
"source": "./src/lib/locjson/index.ts",
|
|
31
|
+
"import": "./dist/locjson.js",
|
|
32
|
+
"require": "./dist/locjson.cjs",
|
|
33
|
+
"default": "./dist/locjson.js"
|
|
34
|
+
},
|
|
35
|
+
"./smartcat": {
|
|
36
|
+
"source": "./src/smartcat/index.ts",
|
|
37
|
+
"import": "./dist/smartcat.js",
|
|
38
|
+
"require": "./dist/smartcat.cjs",
|
|
39
|
+
"default": "./dist/smartcat.js"
|
|
40
|
+
},
|
|
41
|
+
"./export": {
|
|
42
|
+
"source": "./src/export/index.ts",
|
|
43
|
+
"import": "./dist/export.js",
|
|
44
|
+
"require": "./dist/export.cjs",
|
|
45
|
+
"default": "./dist/export.js"
|
|
46
|
+
},
|
|
47
|
+
"./import": {
|
|
48
|
+
"source": "./src/import/index.ts",
|
|
49
|
+
"import": "./dist/import.js",
|
|
50
|
+
"require": "./dist/import.cjs",
|
|
51
|
+
"default": "./dist/import.js"
|
|
52
|
+
},
|
|
53
|
+
"./progress": {
|
|
54
|
+
"source": "./src/progress/index.ts",
|
|
55
|
+
"import": "./dist/progress.js",
|
|
56
|
+
"require": "./dist/progress.cjs",
|
|
57
|
+
"default": "./dist/progress.js"
|
|
58
|
+
},
|
|
59
|
+
"./templates": {
|
|
60
|
+
"source": "./src/templates/index.ts",
|
|
61
|
+
"import": "./dist/templates.js",
|
|
62
|
+
"require": "./dist/templates.cjs",
|
|
63
|
+
"default": "./dist/templates.js"
|
|
64
|
+
},
|
|
65
|
+
"./package.json": "./package.json"
|
|
15
66
|
},
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
67
|
+
"main": "./dist/index.cjs",
|
|
68
|
+
"module": "./dist/index.js",
|
|
69
|
+
"types": "./dist/index.d.ts",
|
|
70
|
+
"files": [
|
|
71
|
+
"dist",
|
|
72
|
+
"src",
|
|
73
|
+
"README.md"
|
|
74
|
+
],
|
|
20
75
|
"scripts": {
|
|
21
|
-
"
|
|
76
|
+
"build": "pkg-utils build --check --clean",
|
|
77
|
+
"watch": "pkg-utils watch",
|
|
78
|
+
"test": "vitest run",
|
|
79
|
+
"prepublishOnly": "npm run build"
|
|
80
|
+
},
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"@portabletext/to-html": "^5.0.2",
|
|
83
|
+
"@sanity/block-tools": "^3.70.0",
|
|
84
|
+
"@sanity/icons": "^3.7.0",
|
|
85
|
+
"@sanity/uuid": "^3.0.2"
|
|
86
|
+
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"@sanity/ui": "^2 || ^3",
|
|
89
|
+
"react": "^18 || ^19",
|
|
90
|
+
"sanity": "^3 || ^4 || ^5 || ^6",
|
|
91
|
+
"styled-components": "^6"
|
|
92
|
+
},
|
|
93
|
+
"devDependencies": {
|
|
94
|
+
"@sanity/pkg-utils": "^7.2.2",
|
|
95
|
+
"@sanity/schema": "^6.2.0",
|
|
96
|
+
"@sanity/ui": "^3.0.0",
|
|
97
|
+
"@types/jsdom": "^28.0.3",
|
|
98
|
+
"@types/node": "^25.9.3",
|
|
99
|
+
"jsdom": "^29.1.1",
|
|
100
|
+
"react": "^19.2.4",
|
|
101
|
+
"sanity": "^6.2.0",
|
|
102
|
+
"styled-components": "^6.1.18",
|
|
103
|
+
"typescript": "^5.8.0",
|
|
104
|
+
"vitest": "^4.1.8"
|
|
105
|
+
},
|
|
106
|
+
"engines": {
|
|
107
|
+
"node": ">=18"
|
|
22
108
|
}
|
|
23
109
|
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
2
|
+
import {useClient, useSchema, type DocumentActionComponent, type DocumentActionProps} from 'sanity'
|
|
3
|
+
import {Box, Button, Card, Flex, Inline, Label, Select, Spinner, Stack, Switch, Text, TextInput, useToast} from '@sanity/ui'
|
|
4
|
+
import {AddIcon, TranslateIcon} from '@sanity/icons'
|
|
5
|
+
import {uuid} from '@sanity/uuid'
|
|
6
|
+
import {API_VERSION, TRANSLATION_PROJECT_TYPE} from '../lib/constants'
|
|
7
|
+
import {WorkflowSelect} from '../tool/WorkflowSelect'
|
|
8
|
+
import {useTemplates} from '../tool/useTemplates'
|
|
9
|
+
import {useWorkflowSelection} from '../tool/useWorkflowSelection'
|
|
10
|
+
import {resolveDocumentTitle, UNTITLED} from '../lib/documentTitle'
|
|
11
|
+
import {gatherLinkedDocuments} from '../lib/linkedDocuments'
|
|
12
|
+
import {ITEM_ID} from '../lib/projectItems'
|
|
13
|
+
import {getTranslatableFields} from '../lib/locjson/fields'
|
|
14
|
+
import type {ResolvedSmartcatConfig} from '../lib/resolveConfig'
|
|
15
|
+
|
|
16
|
+
interface ProjectOption {
|
|
17
|
+
_id: string
|
|
18
|
+
name: string
|
|
19
|
+
count: number
|
|
20
|
+
status: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PROJECTS_QUERY = `*[_type == $type] | order(name asc){
|
|
24
|
+
_id,
|
|
25
|
+
name,
|
|
26
|
+
status,
|
|
27
|
+
"count": count(items)
|
|
28
|
+
}`
|
|
29
|
+
|
|
30
|
+
const CREATE_NEW = '__create_new__'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds the "Add to translation project" document action, shown on the
|
|
34
|
+
* configured translatable types. Lets the editor add the current document to an
|
|
35
|
+
* existing project or create a new one — appending a weak reference to the
|
|
36
|
+
* project's `items`.
|
|
37
|
+
*/
|
|
38
|
+
export function createAddToProjectAction(config: ResolvedSmartcatConfig): DocumentActionComponent {
|
|
39
|
+
const sourceLanguage = config.sourceLanguage
|
|
40
|
+
|
|
41
|
+
const AddToProjectAction: DocumentActionComponent = (props: DocumentActionProps) => {
|
|
42
|
+
const {id, draft, published, onComplete} = props
|
|
43
|
+
const client = useClient({apiVersion: API_VERSION})
|
|
44
|
+
const schema = useSchema()
|
|
45
|
+
const toast = useToast()
|
|
46
|
+
|
|
47
|
+
const [open, setOpen] = useState(false)
|
|
48
|
+
const [projects, setProjects] = useState<ProjectOption[] | null>(null)
|
|
49
|
+
const [selection, setSelection] = useState<string>(CREATE_NEW)
|
|
50
|
+
const [newName, setNewName] = useState('')
|
|
51
|
+
const [busy, setBusy] = useState(false)
|
|
52
|
+
const [includeLinked, setIncludeLinked] = useState(true)
|
|
53
|
+
const {templates, loaded, requestRefresh} = useTemplates()
|
|
54
|
+
const [workflow, setWorkflow] = useWorkflowSelection(open, templates, loaded)
|
|
55
|
+
|
|
56
|
+
const publishedId = id.replace(/^drafts\./, '')
|
|
57
|
+
const doc = draft ?? published
|
|
58
|
+
// Resolve the title natively (preview.prepare) so field-level i18n types yield
|
|
59
|
+
// a string, never the raw internationalized-array object (renders as React #31).
|
|
60
|
+
const docTitle = (doc && resolveDocumentTitle(schema, doc)) || UNTITLED
|
|
61
|
+
|
|
62
|
+
// Load existing projects when the dialog opens.
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!open) return
|
|
65
|
+
// Refresh the template list each time the dialog opens.
|
|
66
|
+
requestRefresh()
|
|
67
|
+
let cancelled = false
|
|
68
|
+
setProjects(null)
|
|
69
|
+
client
|
|
70
|
+
.fetch<ProjectOption[]>(PROJECTS_QUERY, {type: TRANSLATION_PROJECT_TYPE})
|
|
71
|
+
.then((res) => {
|
|
72
|
+
if (cancelled) return
|
|
73
|
+
setProjects(res)
|
|
74
|
+
// Default to the first existing project if there is one.
|
|
75
|
+
setSelection(res.length > 0 ? res[0]._id : CREATE_NEW)
|
|
76
|
+
})
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
if (!cancelled) toast.push({status: 'error', title: 'Failed to load projects', description: String(err)})
|
|
79
|
+
})
|
|
80
|
+
return () => {
|
|
81
|
+
cancelled = true
|
|
82
|
+
}
|
|
83
|
+
}, [open, client, toast, requestRefresh])
|
|
84
|
+
|
|
85
|
+
const close = useCallback(() => {
|
|
86
|
+
setOpen(false)
|
|
87
|
+
setNewName('')
|
|
88
|
+
onComplete()
|
|
89
|
+
}, [onComplete])
|
|
90
|
+
|
|
91
|
+
const handleConfirm = useCallback(async () => {
|
|
92
|
+
setBusy(true)
|
|
93
|
+
try {
|
|
94
|
+
const makeItem = (docId: string, docType?: string) => ({
|
|
95
|
+
_type: 'smartcat.projectItem',
|
|
96
|
+
_key: uuid(),
|
|
97
|
+
docId,
|
|
98
|
+
docType,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// The root document plus, optionally, everything it links to — de-duped by id.
|
|
102
|
+
const collected = new Map<string, string | undefined>([[publishedId, doc?._type]])
|
|
103
|
+
if (includeLinked) {
|
|
104
|
+
const isRoot = (type: string) => config.rootTypes.includes(type)
|
|
105
|
+
const isTranslatable = (type: string) =>
|
|
106
|
+
getTranslatableFields(schema.get(type), {exclude: [config.documentI18nLanguageField]}).length > 0
|
|
107
|
+
const linked = await gatherLinkedDocuments({client, rootId: publishedId, isRoot, isTranslatable})
|
|
108
|
+
for (const {docId, docType} of linked) if (!collected.has(docId)) collected.set(docId, docType)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (selection === CREATE_NEW) {
|
|
112
|
+
const name = newName.trim() || `Translation project ${new Date().toISOString().slice(0, 10)}`
|
|
113
|
+
const items = [...collected].map(([docId, docType]) => makeItem(docId, docType))
|
|
114
|
+
await client.create({
|
|
115
|
+
_type: TRANSLATION_PROJECT_TYPE,
|
|
116
|
+
name,
|
|
117
|
+
status: 'draft',
|
|
118
|
+
sourceLanguage,
|
|
119
|
+
workflow,
|
|
120
|
+
items,
|
|
121
|
+
})
|
|
122
|
+
toast.push({
|
|
123
|
+
status: 'success',
|
|
124
|
+
title: `Created “${name}” with ${items.length} document${items.length === 1 ? '' : 's'}`,
|
|
125
|
+
})
|
|
126
|
+
} else {
|
|
127
|
+
// Skip ids already in the project (normalized across old/new item shapes).
|
|
128
|
+
const existing = await client.fetch<{id: string}[]>(
|
|
129
|
+
`*[_id == $pid][0].items[]{"id": ${ITEM_ID}}`,
|
|
130
|
+
{pid: selection},
|
|
131
|
+
)
|
|
132
|
+
const existingIds = new Set((existing ?? []).map((e) => e.id))
|
|
133
|
+
const newItems = [...collected]
|
|
134
|
+
.filter(([docId]) => !existingIds.has(docId))
|
|
135
|
+
.map(([docId, docType]) => makeItem(docId, docType))
|
|
136
|
+
const projName = projects?.find((p) => p._id === selection)?.name ?? 'project'
|
|
137
|
+
if (newItems.length === 0) {
|
|
138
|
+
toast.push({status: 'info', title: `Already in “${projName}”`})
|
|
139
|
+
} else {
|
|
140
|
+
await client.patch(selection).setIfMissing({items: []}).insert('after', 'items[-1]', newItems).commit()
|
|
141
|
+
const skipped = collected.size - newItems.length
|
|
142
|
+
toast.push({
|
|
143
|
+
status: 'success',
|
|
144
|
+
title: `Added ${newItems.length} document${newItems.length === 1 ? '' : 's'} to “${projName}”`,
|
|
145
|
+
description: skipped > 0 ? `${skipped} already in the project` : undefined,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
close()
|
|
150
|
+
} catch (err) {
|
|
151
|
+
toast.push({status: 'error', title: 'Failed to add to project', description: String(err)})
|
|
152
|
+
} finally {
|
|
153
|
+
setBusy(false)
|
|
154
|
+
}
|
|
155
|
+
}, [selection, newName, workflow, publishedId, doc, includeLinked, schema, client, projects, close])
|
|
156
|
+
|
|
157
|
+
const content = useMemo(() => {
|
|
158
|
+
if (projects === null) {
|
|
159
|
+
return (
|
|
160
|
+
<Flex align="center" justify="center" padding={4}>
|
|
161
|
+
<Spinner muted />
|
|
162
|
+
</Flex>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
return (
|
|
166
|
+
<Stack space={4}>
|
|
167
|
+
<Stack space={3}>
|
|
168
|
+
<Label size={1}>Project</Label>
|
|
169
|
+
<Select
|
|
170
|
+
value={selection}
|
|
171
|
+
onChange={(e) => setSelection(e.currentTarget.value)}
|
|
172
|
+
disabled={busy}
|
|
173
|
+
>
|
|
174
|
+
{projects.map((p) => (
|
|
175
|
+
<option key={p._id} value={p._id}>
|
|
176
|
+
{p.name} ({p.count} item{p.count === 1 ? '' : 's'})
|
|
177
|
+
</option>
|
|
178
|
+
))}
|
|
179
|
+
<option value={CREATE_NEW}>➕ Create new project…</option>
|
|
180
|
+
</Select>
|
|
181
|
+
</Stack>
|
|
182
|
+
|
|
183
|
+
{selection === CREATE_NEW && (
|
|
184
|
+
<>
|
|
185
|
+
<Stack space={3}>
|
|
186
|
+
<Label size={1}>New project name</Label>
|
|
187
|
+
<TextInput
|
|
188
|
+
value={newName}
|
|
189
|
+
placeholder="e.g. Spring campaign"
|
|
190
|
+
onChange={(e) => setNewName(e.currentTarget.value)}
|
|
191
|
+
disabled={busy}
|
|
192
|
+
/>
|
|
193
|
+
</Stack>
|
|
194
|
+
<Stack space={3}>
|
|
195
|
+
<Label size={1}>Workflow</Label>
|
|
196
|
+
<WorkflowSelect
|
|
197
|
+
value={workflow}
|
|
198
|
+
onChange={setWorkflow}
|
|
199
|
+
templates={templates}
|
|
200
|
+
disabled={busy}
|
|
201
|
+
/>
|
|
202
|
+
</Stack>
|
|
203
|
+
</>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<Card padding={3} radius={2} tone="transparent" border>
|
|
207
|
+
<Stack space={4}>
|
|
208
|
+
<Text size={1} muted>
|
|
209
|
+
Adding <strong>{docTitle}</strong> to the selected project.
|
|
210
|
+
</Text>
|
|
211
|
+
<Flex align="flex-start" gap={3}>
|
|
212
|
+
<Switch
|
|
213
|
+
checked={includeLinked}
|
|
214
|
+
onChange={(e) => setIncludeLinked(e.currentTarget.checked)}
|
|
215
|
+
disabled={busy}
|
|
216
|
+
/>
|
|
217
|
+
<Stack space={2}>
|
|
218
|
+
<Text size={1} weight="medium">
|
|
219
|
+
Include linked content
|
|
220
|
+
</Text>
|
|
221
|
+
<Text size={1} muted>
|
|
222
|
+
Also add documents this one references, recursively — skipping any already in the project.
|
|
223
|
+
</Text>
|
|
224
|
+
</Stack>
|
|
225
|
+
</Flex>
|
|
226
|
+
</Stack>
|
|
227
|
+
</Card>
|
|
228
|
+
|
|
229
|
+
<Flex justify="flex-end" gap={2}>
|
|
230
|
+
<Button mode="bleed" text="Cancel" onClick={close} disabled={busy} />
|
|
231
|
+
<Button
|
|
232
|
+
tone="primary"
|
|
233
|
+
text="Add to project"
|
|
234
|
+
icon={AddIcon}
|
|
235
|
+
onClick={handleConfirm}
|
|
236
|
+
loading={busy}
|
|
237
|
+
disabled={selection === CREATE_NEW && !workflow}
|
|
238
|
+
/>
|
|
239
|
+
</Flex>
|
|
240
|
+
</Stack>
|
|
241
|
+
)
|
|
242
|
+
}, [projects, selection, newName, workflow, templates, busy, includeLinked, docTitle, close, handleConfirm])
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
label: 'Add to translation project',
|
|
246
|
+
icon: TranslateIcon,
|
|
247
|
+
onHandle: () => setOpen(true),
|
|
248
|
+
dialog: open && {
|
|
249
|
+
type: 'dialog',
|
|
250
|
+
header: 'Add to translation project',
|
|
251
|
+
onClose: close,
|
|
252
|
+
content,
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return AddToProjectAction
|
|
258
|
+
}
|