@pigment/auto-translate 1.6.0 → 1.6.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 +15 -8
- package/dist/collections/translationExclusions.js +101 -4
- package/dist/collections/translationExclusions.js.map +1 -1
- package/dist/components/LockTranslation/actions/lockTranslations.d.ts +1 -1
- package/dist/components/LockTranslation/actions/lockTranslations.js +2 -2
- package/dist/components/LockTranslation/actions/lockTranslations.js.map +1 -1
- package/dist/components/LockTranslation/index.js +6 -3
- package/dist/components/LockTranslation/index.js.map +1 -1
- package/dist/components/OpenAiModelField.js +7 -4
- package/dist/components/OpenAiModelField.js.map +1 -1
- package/dist/components/TranslationControl.js +45 -51
- package/dist/components/TranslationControl.js.map +1 -1
- package/dist/globals/translationSettings.js +10 -10
- package/dist/globals/translationSettings.js.map +1 -1
- package/dist/services/translationService.js +4 -7
- package/dist/services/translationService.js.map +1 -1
- package/dist/utilities/fieldHelpers.d.ts +0 -29
- package/dist/utilities/fieldHelpers.js +0 -152
- package/dist/utilities/fieldHelpers.js.map +1 -1
- package/dist/utilities/injectTranslationControls.d.ts +1 -1
- package/dist/utilities/injectTranslationControls.js +16 -11
- package/dist/utilities/injectTranslationControls.js.map +1 -1
- package/package.json +36 -65
- package/dist/components/TranslationSettingsLock.css +0 -87
- package/dist/components/TranslationSettingsLock.d.ts +0 -9
- package/dist/components/TranslationSettingsLock.js +0 -155
- package/dist/components/TranslationSettingsLock.js.map +0 -1
package/README.md
CHANGED
|
@@ -340,7 +340,7 @@ src/
|
|
|
340
340
|
├── components/ # UI components
|
|
341
341
|
│ └── TranslationControl.tsx # Field-level control button
|
|
342
342
|
└── endpoints/ # Custom API endpoints
|
|
343
|
-
└──
|
|
343
|
+
└── listOpenAiModels.ts # Live OpenAI model list for Translation Settings
|
|
344
344
|
```
|
|
345
345
|
|
|
346
346
|
---
|
|
@@ -401,14 +401,21 @@ If you need custom access control for translation features, you can add hooks or
|
|
|
401
401
|
|
|
402
402
|
## 📖 Additional Documentation
|
|
403
403
|
|
|
404
|
-
|
|
404
|
+
More detailed guides (architecture, integration examples, advanced usage) will be published on the project's GitHub Wiki.
|
|
405
405
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 🗄️ Database Compatibility
|
|
409
|
+
|
|
410
|
+
The plugin is tested against both officially-supported Payload database adapters:
|
|
411
|
+
|
|
412
|
+
- **MongoDB** (`@payloadcms/db-mongodb`) — no extra setup required.
|
|
413
|
+
- **Postgres** (`@payloadcms/db-postgres`) — schema changes (e.g. adding localized fields, or fields from
|
|
414
|
+
companion plugins like `@payloadcms/plugin-nested-docs`) require running `payload migrate` before those
|
|
415
|
+
fields are usable. Unlike MongoDB, Postgres will reject writes to columns that don't exist in the schema yet.
|
|
416
|
+
|
|
417
|
+
Both translation and field-level locking behave identically across adapters; the only adapter-specific
|
|
418
|
+
behavior is the migration step required by Postgres.
|
|
412
419
|
|
|
413
420
|
---
|
|
414
421
|
|
|
@@ -1,10 +1,107 @@
|
|
|
1
1
|
export const getTranslationExclusionsCollection = (slug = 'translation-exclusions')=>({
|
|
2
2
|
slug,
|
|
3
3
|
access: {
|
|
4
|
-
create: ({ req
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
create: async ({ data, req })=>{
|
|
5
|
+
if (!req.user || !data?.collectionSlug || !data?.documentId) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
await req.payload.findByID({
|
|
10
|
+
id: data.documentId,
|
|
11
|
+
collection: data.collectionSlug,
|
|
12
|
+
overrideAccess: false,
|
|
13
|
+
req
|
|
14
|
+
});
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
delete: async ({ id, req })=>{
|
|
21
|
+
if (!req.user || !id) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const exclusion = await req.payload.findByID({
|
|
26
|
+
id,
|
|
27
|
+
collection: slug,
|
|
28
|
+
req
|
|
29
|
+
});
|
|
30
|
+
await req.payload.findByID({
|
|
31
|
+
id: exclusion.documentId,
|
|
32
|
+
collection: exclusion.collectionSlug,
|
|
33
|
+
overrideAccess: false,
|
|
34
|
+
req
|
|
35
|
+
});
|
|
36
|
+
return true;
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
read: async ({ req })=>{
|
|
42
|
+
if (!req.user) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const allowedSlugs = [];
|
|
46
|
+
for (const collectionConfig of req.payload.config.collections){
|
|
47
|
+
// Skip this collection itself — its `read` access is the function
|
|
48
|
+
// currently executing, so calling it here would recurse forever.
|
|
49
|
+
if (collectionConfig.slug === slug) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const readAccess = collectionConfig.access?.read;
|
|
54
|
+
const allowed = typeof readAccess === 'function' ? await readAccess({
|
|
55
|
+
req
|
|
56
|
+
}) : true;
|
|
57
|
+
if (allowed) {
|
|
58
|
+
allowedSlugs.push(collectionConfig.slug);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Deny collections whose own access check throws
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
collectionSlug: {
|
|
66
|
+
in: allowedSlugs
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
update: async ({ id, data, req })=>{
|
|
71
|
+
if (!req.user || !id) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const exclusion = await req.payload.findByID({
|
|
76
|
+
id,
|
|
77
|
+
collection: slug,
|
|
78
|
+
req
|
|
79
|
+
});
|
|
80
|
+
// Check the persisted target and, if the request moves it, the new target too —
|
|
81
|
+
// otherwise a record for a doc you can't access could be repointed and taken over.
|
|
82
|
+
const targets = [
|
|
83
|
+
[
|
|
84
|
+
exclusion.collectionSlug,
|
|
85
|
+
exclusion.documentId
|
|
86
|
+
],
|
|
87
|
+
[
|
|
88
|
+
data?.collectionSlug ?? exclusion.collectionSlug,
|
|
89
|
+
data?.documentId ?? exclusion.documentId
|
|
90
|
+
]
|
|
91
|
+
];
|
|
92
|
+
for (const [collection, documentId] of targets){
|
|
93
|
+
await req.payload.findByID({
|
|
94
|
+
id: documentId,
|
|
95
|
+
collection,
|
|
96
|
+
overrideAccess: false,
|
|
97
|
+
req
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
8
105
|
},
|
|
9
106
|
admin: {
|
|
10
107
|
defaultColumns: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/collections/translationExclusions.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport const getTranslationExclusionsCollection = (\n slug: string = 'translation-exclusions',\n): CollectionConfig => ({\n slug,\n access: {\n create: ({ req
|
|
1
|
+
{"version":3,"sources":["../../src/collections/translationExclusions.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport const getTranslationExclusionsCollection = (\n slug: string = 'translation-exclusions',\n): CollectionConfig => ({\n slug,\n access: {\n create: async ({ data, req }) => {\n if (!req.user || !data?.collectionSlug || !data?.documentId) {\n return false\n }\n try {\n await req.payload.findByID({\n id: data.documentId,\n collection: data.collectionSlug,\n overrideAccess: false,\n req,\n })\n return true\n } catch {\n return false\n }\n },\n delete: async ({ id, req }) => {\n if (!req.user || !id) {\n return false\n }\n try {\n const exclusion = await req.payload.findByID({\n id,\n collection: slug,\n req,\n })\n await req.payload.findByID({\n id: exclusion.documentId,\n collection: exclusion.collectionSlug,\n overrideAccess: false,\n req,\n })\n return true\n } catch {\n return false\n }\n },\n read: async ({ req }) => {\n if (!req.user) {\n return false\n }\n const allowedSlugs: string[] = []\n for (const collectionConfig of req.payload.config.collections) {\n // Skip this collection itself — its `read` access is the function\n // currently executing, so calling it here would recurse forever.\n if (collectionConfig.slug === slug) {\n continue\n }\n try {\n const readAccess = collectionConfig.access?.read\n const allowed = typeof readAccess === 'function' ? await readAccess({ req }) : true\n if (allowed) {\n allowedSlugs.push(collectionConfig.slug)\n }\n } catch {\n // Deny collections whose own access check throws\n }\n }\n return { collectionSlug: { in: allowedSlugs } }\n },\n update: async ({ id, data, req }) => {\n if (!req.user || !id) {\n return false\n }\n try {\n const exclusion = await req.payload.findByID({\n id,\n collection: slug,\n req,\n })\n // Check the persisted target and, if the request moves it, the new target too —\n // otherwise a record for a doc you can't access could be repointed and taken over.\n const targets = [\n [exclusion.collectionSlug, exclusion.documentId],\n [data?.collectionSlug ?? exclusion.collectionSlug, data?.documentId ?? exclusion.documentId],\n ]\n for (const [collection, documentId] of targets) {\n await req.payload.findByID({\n id: documentId,\n collection,\n overrideAccess: false,\n req,\n })\n }\n return true\n } catch {\n return false\n }\n },\n },\n admin: {\n defaultColumns: ['collectionSlug', 'documentId', 'locale', 'excludedPaths'],\n description:\n 'Stores field-level translation exclusions per document and locale. Each locale can have its own set of excluded fields. There should only be ONE record per (collectionSlug, documentId, locale) combination.',\n group: 'Auto-Translate Settings',\n useAsTitle: 'collectionSlug',\n },\n fields: [\n {\n name: 'collectionSlug',\n type: 'text',\n admin: {\n description: 'The collection this exclusion belongs to',\n position: 'sidebar',\n readOnly: true,\n },\n index: true,\n required: true,\n },\n {\n name: 'documentId',\n type: 'text',\n admin: {\n description: 'The ID of the document',\n position: 'sidebar',\n readOnly: true,\n },\n index: true,\n required: true,\n },\n {\n name: 'locale',\n type: 'text',\n admin: {\n description: 'The locale these exclusions apply to (e.g., \"en\", \"de\", \"fr\")',\n position: 'sidebar',\n readOnly: true,\n },\n index: true,\n label: 'Locale',\n required: true,\n },\n {\n name: 'excludedPaths',\n type: 'array',\n admin: {\n description:\n 'Fields that should NOT be auto-translated in this specific locale. Each locale has its own independent set of exclusions.',\n },\n fields: [\n {\n name: 'path',\n type: 'text',\n admin: {\n description: 'Field path (e.g., \"title\", \"content.0.description\")',\n },\n label: 'Field Path',\n required: true,\n },\n ],\n label: `Excluded Fields for this Locale`,\n required: true,\n },\n ],\n})\n"],"names":["getTranslationExclusionsCollection","slug","access","create","data","req","user","collectionSlug","documentId","payload","findByID","id","collection","overrideAccess","delete","exclusion","read","allowedSlugs","collectionConfig","config","collections","readAccess","allowed","push","in","update","targets","admin","defaultColumns","description","group","useAsTitle","fields","name","type","position","readOnly","index","required","label"],"mappings":"AAEA,OAAO,MAAMA,qCAAqC,CAChDC,OAAe,wBAAwB,GACjB,CAAA;QACtBA;QACAC,QAAQ;YACNC,QAAQ,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;gBAC1B,IAAI,CAACA,IAAIC,IAAI,IAAI,CAACF,MAAMG,kBAAkB,CAACH,MAAMI,YAAY;oBAC3D,OAAO;gBACT;gBACA,IAAI;oBACF,MAAMH,IAAII,OAAO,CAACC,QAAQ,CAAC;wBACzBC,IAAIP,KAAKI,UAAU;wBACnBI,YAAYR,KAAKG,cAAc;wBAC/BM,gBAAgB;wBAChBR;oBACF;oBACA,OAAO;gBACT,EAAE,OAAM;oBACN,OAAO;gBACT;YACF;YACAS,QAAQ,OAAO,EAAEH,EAAE,EAAEN,GAAG,EAAE;gBACxB,IAAI,CAACA,IAAIC,IAAI,IAAI,CAACK,IAAI;oBACpB,OAAO;gBACT;gBACA,IAAI;oBACF,MAAMI,YAAY,MAAMV,IAAII,OAAO,CAACC,QAAQ,CAAC;wBAC3CC;wBACAC,YAAYX;wBACZI;oBACF;oBACA,MAAMA,IAAII,OAAO,CAACC,QAAQ,CAAC;wBACzBC,IAAII,UAAUP,UAAU;wBACxBI,YAAYG,UAAUR,cAAc;wBACpCM,gBAAgB;wBAChBR;oBACF;oBACA,OAAO;gBACT,EAAE,OAAM;oBACN,OAAO;gBACT;YACF;YACAW,MAAM,OAAO,EAAEX,GAAG,EAAE;gBAClB,IAAI,CAACA,IAAIC,IAAI,EAAE;oBACb,OAAO;gBACT;gBACA,MAAMW,eAAyB,EAAE;gBACjC,KAAK,MAAMC,oBAAoBb,IAAII,OAAO,CAACU,MAAM,CAACC,WAAW,CAAE;oBAC7D,kEAAkE;oBAClE,iEAAiE;oBACjE,IAAIF,iBAAiBjB,IAAI,KAAKA,MAAM;wBAClC;oBACF;oBACA,IAAI;wBACF,MAAMoB,aAAaH,iBAAiBhB,MAAM,EAAEc;wBAC5C,MAAMM,UAAU,OAAOD,eAAe,aAAa,MAAMA,WAAW;4BAAEhB;wBAAI,KAAK;wBAC/E,IAAIiB,SAAS;4BACXL,aAAaM,IAAI,CAACL,iBAAiBjB,IAAI;wBACzC;oBACF,EAAE,OAAM;oBACN,iDAAiD;oBACnD;gBACF;gBACA,OAAO;oBAAEM,gBAAgB;wBAAEiB,IAAIP;oBAAa;gBAAE;YAChD;YACAQ,QAAQ,OAAO,EAAEd,EAAE,EAAEP,IAAI,EAAEC,GAAG,EAAE;gBAC9B,IAAI,CAACA,IAAIC,IAAI,IAAI,CAACK,IAAI;oBACpB,OAAO;gBACT;gBACA,IAAI;oBACF,MAAMI,YAAY,MAAMV,IAAII,OAAO,CAACC,QAAQ,CAAC;wBAC3CC;wBACAC,YAAYX;wBACZI;oBACF;oBACA,gFAAgF;oBAChF,mFAAmF;oBACnF,MAAMqB,UAAU;wBACd;4BAACX,UAAUR,cAAc;4BAAEQ,UAAUP,UAAU;yBAAC;wBAChD;4BAACJ,MAAMG,kBAAkBQ,UAAUR,cAAc;4BAAEH,MAAMI,cAAcO,UAAUP,UAAU;yBAAC;qBAC7F;oBACD,KAAK,MAAM,CAACI,YAAYJ,WAAW,IAAIkB,QAAS;wBAC9C,MAAMrB,IAAII,OAAO,CAACC,QAAQ,CAAC;4BACzBC,IAAIH;4BACJI;4BACAC,gBAAgB;4BAChBR;wBACF;oBACF;oBACA,OAAO;gBACT,EAAE,OAAM;oBACN,OAAO;gBACT;YACF;QACF;QACAsB,OAAO;YACLC,gBAAgB;gBAAC;gBAAkB;gBAAc;gBAAU;aAAgB;YAC3EC,aACE;YACFC,OAAO;YACPC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aAAa;oBACbM,UAAU;oBACVC,UAAU;gBACZ;gBACAC,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aAAa;oBACbM,UAAU;oBACVC,UAAU;gBACZ;gBACAC,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aAAa;oBACbM,UAAU;oBACVC,UAAU;gBACZ;gBACAC,OAAO;gBACPE,OAAO;gBACPD,UAAU;YACZ;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aACE;gBACJ;gBACAG,QAAQ;oBACN;wBACEC,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLE,aAAa;wBACf;wBACAU,OAAO;wBACPD,UAAU;oBACZ;iBACD;gBACDC,OAAO,CAAC,+BAA+B,CAAC;gBACxCD,UAAU;YACZ;SACD;IACH,CAAA,EAAE"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Client-side function to update the lockTranslationSettings field
|
|
3
3
|
* Makes a fetch request to update the global
|
|
4
4
|
*/
|
|
5
|
-
export declare function updateLockTranslationSettingsField(isLocked: boolean): Promise<{
|
|
5
|
+
export declare function updateLockTranslationSettingsField(isLocked: boolean, apiURL: string): Promise<{
|
|
6
6
|
isLocked: boolean;
|
|
7
7
|
success: boolean;
|
|
8
8
|
}>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side function to update the lockTranslationSettings field
|
|
3
3
|
* Makes a fetch request to update the global
|
|
4
|
-
*/ export async function updateLockTranslationSettingsField(isLocked) {
|
|
4
|
+
*/ export async function updateLockTranslationSettingsField(isLocked, apiURL) {
|
|
5
5
|
try {
|
|
6
|
-
const response = await fetch(
|
|
6
|
+
const response = await fetch(`${apiURL}/globals/translation-settings`, {
|
|
7
7
|
body: JSON.stringify({
|
|
8
8
|
lockTranslationSettings: isLocked
|
|
9
9
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/components/LockTranslation/actions/lockTranslations.ts"],"sourcesContent":["/**\n * Client-side function to update the lockTranslationSettings field\n * Makes a fetch request to update the global\n */\nexport async function updateLockTranslationSettingsField(\n isLocked: boolean,\n): Promise<{ isLocked: boolean; success: boolean }> {\n try {\n const response = await fetch(
|
|
1
|
+
{"version":3,"sources":["../../../../src/components/LockTranslation/actions/lockTranslations.ts"],"sourcesContent":["/**\n * Client-side function to update the lockTranslationSettings field\n * Makes a fetch request to update the global\n */\nexport async function updateLockTranslationSettingsField(\n isLocked: boolean,\n apiURL: string,\n): Promise<{ isLocked: boolean; success: boolean }> {\n try {\n const response = await fetch(`${apiURL}/globals/translation-settings`, {\n body: JSON.stringify({\n lockTranslationSettings: isLocked,\n }),\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n throw new Error(`Failed to update: ${response.statusText}`)\n }\n\n return {\n isLocked,\n success: true,\n }\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('[LockTranslation] Error updating lock state:', error)\n return {\n isLocked: !isLocked,\n success: false,\n }\n }\n}\n"],"names":["updateLockTranslationSettingsField","isLocked","apiURL","response","fetch","body","JSON","stringify","lockTranslationSettings","credentials","headers","method","ok","Error","statusText","success","error","console"],"mappings":"AAAA;;;CAGC,GACD,OAAO,eAAeA,mCACpBC,QAAiB,EACjBC,MAAc;IAEd,IAAI;QACF,MAAMC,WAAW,MAAMC,MAAM,GAAGF,OAAO,6BAA6B,CAAC,EAAE;YACrEG,MAAMC,KAAKC,SAAS,CAAC;gBACnBC,yBAAyBP;YAC3B;YACAQ,aAAa;YACbC,SAAS;gBACP,gBAAgB;YAClB;YACAC,QAAQ;QACV;QAEA,IAAI,CAACR,SAASS,EAAE,EAAE;YAChB,MAAM,IAAIC,MAAM,CAAC,kBAAkB,EAAEV,SAASW,UAAU,EAAE;QAC5D;QAEA,OAAO;YACLb;YACAc,SAAS;QACX;IACF,EAAE,OAAOC,OAAO;QACd,sCAAsC;QACtCC,QAAQD,KAAK,CAAC,gDAAgDA;QAC9D,OAAO;YACLf,UAAU,CAACA;YACXc,SAAS;QACX;IACF;AACF"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useForm, useFormFields } from '@payloadcms/ui';
|
|
3
|
+
import { useConfig, useForm, useFormFields } from '@payloadcms/ui';
|
|
4
4
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
5
|
import { updateLockTranslationSettingsField } from './actions/lockTranslations.js';
|
|
6
6
|
import './style.css';
|
|
@@ -13,6 +13,8 @@ import './style.css';
|
|
|
13
13
|
*/ export const LockTranslation = ()=>{
|
|
14
14
|
const lockField = useFormFields(([fields])=>fields?.lockTranslationSettings);
|
|
15
15
|
const { dispatchFields } = useForm();
|
|
16
|
+
const { config: { routes, serverURL } } = useConfig();
|
|
17
|
+
const apiURL = `${serverURL}${routes.api}`;
|
|
16
18
|
const isLocked = Boolean(lockField?.value ?? true);
|
|
17
19
|
const [isLoading, setIsLoading] = useState(false);
|
|
18
20
|
// Model select uses form lock state in OpenAiModelField (react-select).
|
|
@@ -49,7 +51,7 @@ import './style.css';
|
|
|
49
51
|
const newLockState = !isLocked;
|
|
50
52
|
setIsLoading(true);
|
|
51
53
|
try {
|
|
52
|
-
const result = await updateLockTranslationSettingsField(newLockState);
|
|
54
|
+
const result = await updateLockTranslationSettingsField(newLockState, apiURL);
|
|
53
55
|
if (result.success) {
|
|
54
56
|
// Update the lockTranslationSettings field value
|
|
55
57
|
dispatchFields({
|
|
@@ -69,7 +71,8 @@ import './style.css';
|
|
|
69
71
|
}, [
|
|
70
72
|
isLocked,
|
|
71
73
|
dispatchFields,
|
|
72
|
-
applyLockStateToFields
|
|
74
|
+
applyLockStateToFields,
|
|
75
|
+
apiURL
|
|
73
76
|
]);
|
|
74
77
|
return /*#__PURE__*/ _jsx("div", {
|
|
75
78
|
className: "translation-settings-lock-container",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/components/LockTranslation/index.tsx"],"sourcesContent":["'use client'\n\nimport { useForm, useFormFields } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useState } from 'react'\n\nimport { updateLockTranslationSettingsField } from './actions/lockTranslations.js'\nimport './style.css'\n\n/**\n * Component that provides lock/unlock functionality for translation settings\n * - Fields are locked by default (read-only)\n * - User must click \"Unlock\" to edit\n * - After saving, fields automatically lock again\n * - Updates the hidden lockTranslationSettings checkbox field to enable server-side access control\n */\nexport const LockTranslation: React.FC = () => {\n const lockField = useFormFields(([fields]) => fields?.lockTranslationSettings)\n const { dispatchFields } = useForm()\n const isLocked = Boolean(lockField?.value ?? true)\n const [isLoading, setIsLoading] = useState(false)\n\n // Model select uses form lock state in OpenAiModelField (react-select).\n const applyLockStateToFields = useCallback((locked: boolean) => {\n const fieldsToLock = ['systemPrompt', 'translationRules', 'temperature', 'maxTokens']\n\n fieldsToLock.forEach((fieldPath) => {\n const inputs = document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(\n `[name=\"${fieldPath}\"], textarea[id*=\"${fieldPath}\"], input[id*=\"${fieldPath}\"]`,\n )\n\n inputs.forEach((input) => {\n if (locked) {\n input.setAttribute('disabled', 'true')\n } else {\n input.removeAttribute('disabled')\n }\n })\n })\n }, [])\n\n // Apply initial lock state when component mounts or lock state changes\n useEffect(() => {\n // Small delay to ensure form fields are rendered\n const timer = setTimeout(() => {\n applyLockStateToFields(isLocked)\n }, 100)\n\n return () => clearTimeout(timer)\n }, [isLocked, applyLockStateToFields])\n\n const toggleLock = useCallback(async () => {\n const newLockState = !isLocked\n setIsLoading(true)\n\n try {\n const result = await updateLockTranslationSettingsField(newLockState)\n\n if (result.success) {\n // Update the lockTranslationSettings field value\n dispatchFields({\n type: 'UPDATE',\n path: 'lockTranslationSettings',\n value: result.isLocked,\n })\n\n // Apply lock state to fields\n applyLockStateToFields(result.isLocked)\n }\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('[LockTranslation] Error:', error)\n } finally {\n setIsLoading(false)\n }\n }, [isLocked, dispatchFields, applyLockStateToFields])\n\n return (\n <div className=\"translation-settings-lock-container\">\n <button\n className={`translation-settings-lock-button ${isLocked ? 'locked' : 'unlocked'}`}\n disabled={isLoading}\n onClick={toggleLock}\n title={\n isLocked\n ? '🔒 Settings are locked to prevent accidental changes. Click to unlock and edit.'\n : '🔓 Settings are unlocked. Click to lock after saving your changes.'\n }\n type=\"button\"\n >\n <span className=\"translation-settings-lock-icon\">\n {isLoading ? '⏳' : isLocked ? '🔒' : '🔓'}\n </span>\n <span className=\"translation-settings-lock-label\">\n {isLoading ? 'Updating...' : isLocked ? 'Unlock Settings' : 'Lock Settings'}\n </span>\n </button>\n </div>\n )\n}\n"],"names":["useForm","useFormFields","React","useCallback","useEffect","useState","updateLockTranslationSettingsField","LockTranslation","lockField","fields","lockTranslationSettings","dispatchFields","isLocked","Boolean","value","isLoading","setIsLoading","applyLockStateToFields","locked","fieldsToLock","forEach","fieldPath","inputs","document","querySelectorAll","input","setAttribute","removeAttribute","timer","setTimeout","clearTimeout","toggleLock","newLockState","result","success","type","path","error","console","div","className","button","disabled","onClick","title","span"],"mappings":"AAAA;;AAEA,SAASA,OAAO,EAAEC,aAAa,QAAQ,iBAAgB;
|
|
1
|
+
{"version":3,"sources":["../../../src/components/LockTranslation/index.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useForm, useFormFields } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useState } from 'react'\n\nimport { updateLockTranslationSettingsField } from './actions/lockTranslations.js'\nimport './style.css'\n\n/**\n * Component that provides lock/unlock functionality for translation settings\n * - Fields are locked by default (read-only)\n * - User must click \"Unlock\" to edit\n * - After saving, fields automatically lock again\n * - Updates the hidden lockTranslationSettings checkbox field to enable server-side access control\n */\nexport const LockTranslation: React.FC = () => {\n const lockField = useFormFields(([fields]) => fields?.lockTranslationSettings)\n const { dispatchFields } = useForm()\n const {\n config: { routes, serverURL },\n } = useConfig()\n const apiURL = `${serverURL}${routes.api}`\n const isLocked = Boolean(lockField?.value ?? true)\n const [isLoading, setIsLoading] = useState(false)\n\n // Model select uses form lock state in OpenAiModelField (react-select).\n const applyLockStateToFields = useCallback((locked: boolean) => {\n const fieldsToLock = ['systemPrompt', 'translationRules', 'temperature', 'maxTokens']\n\n fieldsToLock.forEach((fieldPath) => {\n const inputs = document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>(\n `[name=\"${fieldPath}\"], textarea[id*=\"${fieldPath}\"], input[id*=\"${fieldPath}\"]`,\n )\n\n inputs.forEach((input) => {\n if (locked) {\n input.setAttribute('disabled', 'true')\n } else {\n input.removeAttribute('disabled')\n }\n })\n })\n }, [])\n\n // Apply initial lock state when component mounts or lock state changes\n useEffect(() => {\n // Small delay to ensure form fields are rendered\n const timer = setTimeout(() => {\n applyLockStateToFields(isLocked)\n }, 100)\n\n return () => clearTimeout(timer)\n }, [isLocked, applyLockStateToFields])\n\n const toggleLock = useCallback(async () => {\n const newLockState = !isLocked\n setIsLoading(true)\n\n try {\n const result = await updateLockTranslationSettingsField(newLockState, apiURL)\n\n if (result.success) {\n // Update the lockTranslationSettings field value\n dispatchFields({\n type: 'UPDATE',\n path: 'lockTranslationSettings',\n value: result.isLocked,\n })\n\n // Apply lock state to fields\n applyLockStateToFields(result.isLocked)\n }\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('[LockTranslation] Error:', error)\n } finally {\n setIsLoading(false)\n }\n }, [isLocked, dispatchFields, applyLockStateToFields, apiURL])\n\n return (\n <div className=\"translation-settings-lock-container\">\n <button\n className={`translation-settings-lock-button ${isLocked ? 'locked' : 'unlocked'}`}\n disabled={isLoading}\n onClick={toggleLock}\n title={\n isLocked\n ? '🔒 Settings are locked to prevent accidental changes. Click to unlock and edit.'\n : '🔓 Settings are unlocked. Click to lock after saving your changes.'\n }\n type=\"button\"\n >\n <span className=\"translation-settings-lock-icon\">\n {isLoading ? '⏳' : isLocked ? '🔒' : '🔓'}\n </span>\n <span className=\"translation-settings-lock-label\">\n {isLoading ? 'Updating...' : isLocked ? 'Unlock Settings' : 'Lock Settings'}\n </span>\n </button>\n </div>\n )\n}\n"],"names":["useConfig","useForm","useFormFields","React","useCallback","useEffect","useState","updateLockTranslationSettingsField","LockTranslation","lockField","fields","lockTranslationSettings","dispatchFields","config","routes","serverURL","apiURL","api","isLocked","Boolean","value","isLoading","setIsLoading","applyLockStateToFields","locked","fieldsToLock","forEach","fieldPath","inputs","document","querySelectorAll","input","setAttribute","removeAttribute","timer","setTimeout","clearTimeout","toggleLock","newLockState","result","success","type","path","error","console","div","className","button","disabled","onClick","title","span"],"mappings":"AAAA;;AAEA,SAASA,SAAS,EAAEC,OAAO,EAAEC,aAAa,QAAQ,iBAAgB;AAClE,OAAOC,SAASC,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAE/D,SAASC,kCAAkC,QAAQ,gCAA+B;AAClF,OAAO,cAAa;AAEpB;;;;;;CAMC,GACD,OAAO,MAAMC,kBAA4B;IACvC,MAAMC,YAAYP,cAAc,CAAC,CAACQ,OAAO,GAAKA,QAAQC;IACtD,MAAM,EAAEC,cAAc,EAAE,GAAGX;IAC3B,MAAM,EACJY,QAAQ,EAAEC,MAAM,EAAEC,SAAS,EAAE,EAC9B,GAAGf;IACJ,MAAMgB,SAAS,GAAGD,YAAYD,OAAOG,GAAG,EAAE;IAC1C,MAAMC,WAAWC,QAAQV,WAAWW,SAAS;IAC7C,MAAM,CAACC,WAAWC,aAAa,GAAGhB,SAAS;IAE3C,wEAAwE;IACxE,MAAMiB,yBAAyBnB,YAAY,CAACoB;QAC1C,MAAMC,eAAe;YAAC;YAAgB;YAAoB;YAAe;SAAY;QAErFA,aAAaC,OAAO,CAAC,CAACC;YACpB,MAAMC,SAASC,SAASC,gBAAgB,CACtC,CAAC,OAAO,EAAEH,UAAU,kBAAkB,EAAEA,UAAU,eAAe,EAAEA,UAAU,EAAE,CAAC;YAGlFC,OAAOF,OAAO,CAAC,CAACK;gBACd,IAAIP,QAAQ;oBACVO,MAAMC,YAAY,CAAC,YAAY;gBACjC,OAAO;oBACLD,MAAME,eAAe,CAAC;gBACxB;YACF;QACF;IACF,GAAG,EAAE;IAEL,uEAAuE;IACvE5B,UAAU;QACR,iDAAiD;QACjD,MAAM6B,QAAQC,WAAW;YACvBZ,uBAAuBL;QACzB,GAAG;QAEH,OAAO,IAAMkB,aAAaF;IAC5B,GAAG;QAAChB;QAAUK;KAAuB;IAErC,MAAMc,aAAajC,YAAY;QAC7B,MAAMkC,eAAe,CAACpB;QACtBI,aAAa;QAEb,IAAI;YACF,MAAMiB,SAAS,MAAMhC,mCAAmC+B,cAActB;YAEtE,IAAIuB,OAAOC,OAAO,EAAE;gBAClB,iDAAiD;gBACjD5B,eAAe;oBACb6B,MAAM;oBACNC,MAAM;oBACNtB,OAAOmB,OAAOrB,QAAQ;gBACxB;gBAEA,6BAA6B;gBAC7BK,uBAAuBgB,OAAOrB,QAAQ;YACxC;QACF,EAAE,OAAOyB,OAAO;YACd,sCAAsC;YACtCC,QAAQD,KAAK,CAAC,4BAA4BA;QAC5C,SAAU;YACRrB,aAAa;QACf;IACF,GAAG;QAACJ;QAAUN;QAAgBW;QAAwBP;KAAO;IAE7D,qBACE,KAAC6B;QAAIC,WAAU;kBACb,cAAA,MAACC;YACCD,WAAW,CAAC,iCAAiC,EAAE5B,WAAW,WAAW,YAAY;YACjF8B,UAAU3B;YACV4B,SAASZ;YACTa,OACEhC,WACI,oFACA;YAENuB,MAAK;;8BAEL,KAACU;oBAAKL,WAAU;8BACbzB,YAAY,MAAMH,WAAW,OAAO;;8BAEvC,KAACiC;oBAAKL,WAAU;8BACbzB,YAAY,gBAAgBH,WAAW,oBAAoB;;;;;AAKtE,EAAC"}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { FieldDescription, FieldLabel, SelectInput, useField, useFormFields } from '@payloadcms/ui';
|
|
3
|
+
import { FieldDescription, FieldLabel, SelectInput, useConfig, useField, useFormFields } from '@payloadcms/ui';
|
|
4
4
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
5
|
-
const MODELS_URL = '/payload/api/globals/translation-settings/openai-models';
|
|
6
5
|
export const OpenAiModelField = ({ field, path, readOnly })=>{
|
|
7
6
|
const { value, setValue, showError, errorMessage } = useField({
|
|
8
7
|
path
|
|
9
8
|
});
|
|
9
|
+
const { config: { routes, serverURL } } = useConfig();
|
|
10
|
+
const modelsURL = `${serverURL}${routes.api}/globals/translation-settings/openai-models`;
|
|
10
11
|
const lockField = useFormFields(([fields])=>fields?.lockTranslationSettings);
|
|
11
12
|
const isLocked = lockField === undefined ? Boolean(readOnly) : Boolean(lockField.value);
|
|
12
13
|
const [options, setOptions] = useState([]);
|
|
@@ -18,7 +19,7 @@ export const OpenAiModelField = ({ field, path, readOnly })=>{
|
|
|
18
19
|
setLoading(true);
|
|
19
20
|
setLoadError(null);
|
|
20
21
|
try {
|
|
21
|
-
const res = await fetch(
|
|
22
|
+
const res = await fetch(modelsURL, {
|
|
22
23
|
credentials: 'include',
|
|
23
24
|
signal: controller.signal
|
|
24
25
|
});
|
|
@@ -36,7 +37,9 @@ export const OpenAiModelField = ({ field, path, readOnly })=>{
|
|
|
36
37
|
};
|
|
37
38
|
void load();
|
|
38
39
|
return ()=>controller.abort();
|
|
39
|
-
}, [
|
|
40
|
+
}, [
|
|
41
|
+
modelsURL
|
|
42
|
+
]);
|
|
40
43
|
const optionsWithCurrent = useMemo(()=>{
|
|
41
44
|
if (!value || options.some((option)=>option.value === value)) return options;
|
|
42
45
|
return [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/OpenAiModelField.tsx"],"sourcesContent":["'use client'\n\nimport type { TextFieldClientComponent, OptionObject } from 'payload'\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/components/OpenAiModelField.tsx"],"sourcesContent":["'use client'\n\nimport type { TextFieldClientComponent, OptionObject } from 'payload'\nimport {\n FieldDescription,\n FieldLabel,\n SelectInput,\n useConfig,\n useField,\n useFormFields,\n} from '@payloadcms/ui'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nexport const OpenAiModelField: TextFieldClientComponent = ({ field, path, readOnly }) => {\n const { value, setValue, showError, errorMessage } = useField<string>({ path })\n const {\n config: { routes, serverURL },\n } = useConfig()\n const modelsURL = `${serverURL}${routes.api}/globals/translation-settings/openai-models`\n const lockField = useFormFields(([fields]) => fields?.lockTranslationSettings)\n const isLocked = lockField === undefined ? Boolean(readOnly) : Boolean(lockField.value)\n const [options, setOptions] = useState<OptionObject[]>([])\n const [loading, setLoading] = useState(true)\n const [loadError, setLoadError] = useState<string | null>(null)\n\n useEffect(() => {\n const controller = new AbortController()\n\n const load = async () => {\n setLoading(true)\n setLoadError(null)\n try {\n const res = await fetch(modelsURL, {\n credentials: 'include',\n signal: controller.signal,\n })\n const data = (await res.json()) as { models?: OptionObject[]; error?: string }\n if (!res.ok) {\n throw new Error(data.error || `Failed to load models (${res.status})`)\n }\n setOptions(Array.isArray(data.models) ? data.models : [])\n } catch (err) {\n if (err instanceof Error && err.name === 'AbortError') return\n setLoadError(err instanceof Error ? err.message : 'Failed to load models')\n } finally {\n if (!controller.signal.aborted) setLoading(false)\n }\n }\n\n void load()\n return () => controller.abort()\n }, [modelsURL])\n\n const optionsWithCurrent = useMemo(() => {\n if (!value || options.some((option) => option.value === value)) return options\n return [{ label: value, value }, ...options]\n }, [options, value])\n\n const description =\n typeof field.admin?.description === 'string' ? field.admin.description : undefined\n\n const disabled = isLocked || loading\n\n return (\n <div className=\"field-type select\">\n <FieldLabel label={field.label} path={path} required={field.required} />\n <SelectInput\n description={description}\n Error={showError && errorMessage ? <div className=\"field-error\">{errorMessage}</div> : null}\n isClearable={false}\n name={path}\n onChange={(option) => {\n if (disabled || !option || Array.isArray(option)) return\n if ('value' in option && typeof option.value === 'string') {\n setValue(option.value)\n }\n }}\n options={optionsWithCurrent}\n path={path}\n placeholder={loading ? 'Loading models…' : 'Select a model'}\n readOnly={disabled}\n required={field.required}\n showError={showError}\n value={value ?? ''}\n />\n {loadError ? (\n <FieldDescription description={`Could not load OpenAI models: ${loadError}`} path={path} />\n ) : null}\n </div>\n )\n}\n"],"names":["FieldDescription","FieldLabel","SelectInput","useConfig","useField","useFormFields","React","useEffect","useMemo","useState","OpenAiModelField","field","path","readOnly","value","setValue","showError","errorMessage","config","routes","serverURL","modelsURL","api","lockField","fields","lockTranslationSettings","isLocked","undefined","Boolean","options","setOptions","loading","setLoading","loadError","setLoadError","controller","AbortController","load","res","fetch","credentials","signal","data","json","ok","Error","error","status","Array","isArray","models","err","name","message","aborted","abort","optionsWithCurrent","some","option","label","description","admin","disabled","div","className","required","isClearable","onChange","placeholder"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useDocumentInfo, useLocale } from '@payloadcms/ui';
|
|
3
|
+
import { useConfig, useDocumentInfo, useLocale } from '@payloadcms/ui';
|
|
4
4
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
5
|
import './TranslationControl.css';
|
|
6
6
|
/**
|
|
@@ -16,23 +16,17 @@ import './TranslationControl.css';
|
|
|
16
16
|
const fieldPath = payloadPath || clientFieldPath;
|
|
17
17
|
const { id, collectionSlug: docCollectionSlug } = useDocumentInfo();
|
|
18
18
|
const { code: currentLocale } = useLocale();
|
|
19
|
+
const { config: { routes, serverURL } } = useConfig();
|
|
20
|
+
const exclusionsURL = `${serverURL}${routes.api}/translation-exclusions`;
|
|
19
21
|
const [isExcluded, setIsExcluded] = useState(false);
|
|
20
22
|
const [isLoading, setIsLoading] = useState(false);
|
|
21
23
|
// Use collectionSlug from props or from document context
|
|
22
24
|
const effectiveCollectionSlug = collectionSlug || docCollectionSlug;
|
|
23
|
-
// Don't show on default locale - you can only lock fields in secondary locales
|
|
24
|
-
if (currentLocale === defaultLocale) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
// Don't show if we don't have a valid field path
|
|
28
|
-
if (!fieldPath) {
|
|
29
|
-
console.warn('[TranslationControl] No field path available');
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
25
|
// Load exclusion state on mount and when locale changes
|
|
33
26
|
useEffect(()=>{
|
|
34
|
-
// Reset state when switching documents
|
|
35
|
-
|
|
27
|
+
// Reset state when switching documents, on create (no ID), or on the default
|
|
28
|
+
// locale where the control is hidden anyway
|
|
29
|
+
if (!id || !effectiveCollectionSlug || currentLocale === defaultLocale) {
|
|
36
30
|
setIsExcluded(false); // Reset to default state
|
|
37
31
|
return;
|
|
38
32
|
}
|
|
@@ -48,13 +42,13 @@ import './TranslationControl.css';
|
|
|
48
42
|
const whereQuery = {
|
|
49
43
|
and: [
|
|
50
44
|
{
|
|
51
|
-
|
|
45
|
+
collectionSlug: {
|
|
52
46
|
equals: effectiveCollectionSlug
|
|
53
47
|
}
|
|
54
48
|
},
|
|
55
49
|
{
|
|
56
50
|
documentId: {
|
|
57
|
-
equals: id
|
|
51
|
+
equals: String(id)
|
|
58
52
|
}
|
|
59
53
|
},
|
|
60
54
|
{
|
|
@@ -68,7 +62,7 @@ import './TranslationControl.css';
|
|
|
68
62
|
limit: '1',
|
|
69
63
|
where: JSON.stringify(whereQuery)
|
|
70
64
|
}).toString();
|
|
71
|
-
const fullUrl =
|
|
65
|
+
const fullUrl = `${exclusionsURL}?${queryString}`;
|
|
72
66
|
console.log('[TranslationControl] Query URL:', fullUrl);
|
|
73
67
|
console.log('[TranslationControl] Where clause:', whereQuery);
|
|
74
68
|
const response = await fetch(fullUrl);
|
|
@@ -77,7 +71,7 @@ import './TranslationControl.css';
|
|
|
77
71
|
if (data.docs && data.docs.length > 0) {
|
|
78
72
|
const exclusion = data.docs[0];
|
|
79
73
|
// CRITICAL: Verify this exclusion belongs to THIS document AND locale
|
|
80
|
-
if (exclusion.locale === currentLocale && exclusion.documentId === id) {
|
|
74
|
+
if (exclusion.locale === currentLocale && exclusion.documentId === String(id)) {
|
|
81
75
|
const excludedPaths = exclusion.excludedPaths?.map((item)=>item.path) || [];
|
|
82
76
|
const isFieldExcluded = excludedPaths.includes(fieldPath);
|
|
83
77
|
console.log('[TranslationControl] Loaded exclusions for document', id, 'locale', currentLocale, ':', {
|
|
@@ -111,10 +105,12 @@ import './TranslationControl.css';
|
|
|
111
105
|
id,
|
|
112
106
|
effectiveCollectionSlug,
|
|
113
107
|
currentLocale,
|
|
114
|
-
|
|
108
|
+
defaultLocale,
|
|
109
|
+
fieldPath,
|
|
110
|
+
exclusionsURL
|
|
115
111
|
]);
|
|
116
112
|
const toggleExclusion = useCallback(async ()=>{
|
|
117
|
-
if (!id || !effectiveCollectionSlug) {
|
|
113
|
+
if (!id || !effectiveCollectionSlug || !fieldPath) {
|
|
118
114
|
return;
|
|
119
115
|
}
|
|
120
116
|
setIsLoading(true);
|
|
@@ -123,13 +119,13 @@ import './TranslationControl.css';
|
|
|
123
119
|
const whereQuery = {
|
|
124
120
|
and: [
|
|
125
121
|
{
|
|
126
|
-
|
|
122
|
+
collectionSlug: {
|
|
127
123
|
equals: effectiveCollectionSlug
|
|
128
124
|
}
|
|
129
125
|
},
|
|
130
126
|
{
|
|
131
127
|
documentId: {
|
|
132
|
-
equals: id
|
|
128
|
+
equals: String(id)
|
|
133
129
|
}
|
|
134
130
|
},
|
|
135
131
|
{
|
|
@@ -151,7 +147,7 @@ import './TranslationControl.css';
|
|
|
151
147
|
limit: '1',
|
|
152
148
|
where: JSON.stringify(whereQuery)
|
|
153
149
|
}).toString();
|
|
154
|
-
const fullUrl =
|
|
150
|
+
const fullUrl = `${exclusionsURL}?${queryString}`;
|
|
155
151
|
console.log('[TranslationControl] Toggle - Query URL:', fullUrl);
|
|
156
152
|
console.log('[TranslationControl] Toggle - Where clause:', whereQuery);
|
|
157
153
|
const findResponse = await fetch(fullUrl);
|
|
@@ -163,7 +159,7 @@ import './TranslationControl.css';
|
|
|
163
159
|
if (data.docs && data.docs.length > 0) {
|
|
164
160
|
const exclusion = data.docs[0];
|
|
165
161
|
// CRITICAL: Verify this exclusion belongs to THIS document AND locale
|
|
166
|
-
if (exclusion.locale === currentLocale && exclusion.documentId === id) {
|
|
162
|
+
if (exclusion.locale === currentLocale && exclusion.documentId === String(id)) {
|
|
167
163
|
existingId = exclusion.id;
|
|
168
164
|
currentExcludedPaths = exclusion.excludedPaths?.map((item)=>item.path) || [];
|
|
169
165
|
console.log('[TranslationControl] Current excluded paths for document', id, 'locale', currentLocale, ':', currentExcludedPaths);
|
|
@@ -192,8 +188,8 @@ import './TranslationControl.css';
|
|
|
192
188
|
}
|
|
193
189
|
// Create the exclusion data - ALWAYS include the current locale
|
|
194
190
|
const exclusionsData = {
|
|
195
|
-
|
|
196
|
-
documentId: id,
|
|
191
|
+
collectionSlug: effectiveCollectionSlug,
|
|
192
|
+
documentId: String(id),
|
|
197
193
|
excludedPaths: currentExcludedPaths.map((path)=>({
|
|
198
194
|
path
|
|
199
195
|
})),
|
|
@@ -201,31 +197,27 @@ import './TranslationControl.css';
|
|
|
201
197
|
};
|
|
202
198
|
console.log('[TranslationControl] Saving exclusions:', exclusionsData);
|
|
203
199
|
// Update or create record using Payload's REST API
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
'Content-Type': 'application/json'
|
|
221
|
-
},
|
|
222
|
-
method: 'POST'
|
|
223
|
-
});
|
|
224
|
-
if (createResponse.ok) {
|
|
225
|
-
const result = await createResponse.json();
|
|
226
|
-
console.log('[TranslationControl] Created exclusions:', result.doc);
|
|
227
|
-
}
|
|
200
|
+
const saveResponse = existingId ? await fetch(`${exclusionsURL}/${existingId}`, {
|
|
201
|
+
body: JSON.stringify(exclusionsData),
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json'
|
|
204
|
+
},
|
|
205
|
+
method: 'PATCH'
|
|
206
|
+
}) : await fetch(exclusionsURL, {
|
|
207
|
+
body: JSON.stringify(exclusionsData),
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': 'application/json'
|
|
210
|
+
},
|
|
211
|
+
method: 'POST'
|
|
212
|
+
});
|
|
213
|
+
if (!saveResponse.ok) {
|
|
214
|
+
const errorBody = await saveResponse.text();
|
|
215
|
+
throw new Error(`Save failed (${saveResponse.status}): ${errorBody}`);
|
|
228
216
|
}
|
|
217
|
+
const result = await saveResponse.json();
|
|
218
|
+
console.log('[TranslationControl] Saved exclusions:', result.doc);
|
|
219
|
+
// Only flip the displayed state once the save is confirmed — otherwise
|
|
220
|
+
// the button would show "Locked" for a field that was never persisted.
|
|
229
221
|
setIsExcluded(!isExcluded);
|
|
230
222
|
} catch (error) {
|
|
231
223
|
console.error('[TranslationControl] Error toggling exclusion:', error);
|
|
@@ -237,10 +229,12 @@ import './TranslationControl.css';
|
|
|
237
229
|
effectiveCollectionSlug,
|
|
238
230
|
currentLocale,
|
|
239
231
|
fieldPath,
|
|
240
|
-
isExcluded
|
|
232
|
+
isExcluded,
|
|
233
|
+
exclusionsURL
|
|
241
234
|
]);
|
|
242
|
-
// Don't show on
|
|
243
|
-
|
|
235
|
+
// Don't show on default locale (you can only lock fields in secondary locales),
|
|
236
|
+
// without a valid field path, or on create (no id yet)
|
|
237
|
+
if (currentLocale === defaultLocale || !fieldPath || !id) {
|
|
244
238
|
return null;
|
|
245
239
|
}
|
|
246
240
|
return /*#__PURE__*/ _jsxs("div", {
|