@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.
Files changed (27) hide show
  1. package/README.md +15 -8
  2. package/dist/collections/translationExclusions.js +101 -4
  3. package/dist/collections/translationExclusions.js.map +1 -1
  4. package/dist/components/LockTranslation/actions/lockTranslations.d.ts +1 -1
  5. package/dist/components/LockTranslation/actions/lockTranslations.js +2 -2
  6. package/dist/components/LockTranslation/actions/lockTranslations.js.map +1 -1
  7. package/dist/components/LockTranslation/index.js +6 -3
  8. package/dist/components/LockTranslation/index.js.map +1 -1
  9. package/dist/components/OpenAiModelField.js +7 -4
  10. package/dist/components/OpenAiModelField.js.map +1 -1
  11. package/dist/components/TranslationControl.js +45 -51
  12. package/dist/components/TranslationControl.js.map +1 -1
  13. package/dist/globals/translationSettings.js +10 -10
  14. package/dist/globals/translationSettings.js.map +1 -1
  15. package/dist/services/translationService.js +4 -7
  16. package/dist/services/translationService.js.map +1 -1
  17. package/dist/utilities/fieldHelpers.d.ts +0 -29
  18. package/dist/utilities/fieldHelpers.js +0 -152
  19. package/dist/utilities/fieldHelpers.js.map +1 -1
  20. package/dist/utilities/injectTranslationControls.d.ts +1 -1
  21. package/dist/utilities/injectTranslationControls.js +16 -11
  22. package/dist/utilities/injectTranslationControls.js.map +1 -1
  23. package/package.json +36 -65
  24. package/dist/components/TranslationSettingsLock.css +0 -87
  25. package/dist/components/TranslationSettingsLock.d.ts +0 -9
  26. package/dist/components/TranslationSettingsLock.js +0 -155
  27. 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
- └── translationExclusionsEndpoint.ts
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
- For more detailed information, check out these guides:
404
+ More detailed guides (architecture, integration examples, advanced usage) will be published on the project's GitHub Wiki.
405
405
 
406
- - **[QUICKSTART.md](./docs/QUICKSTART.md)** - Quick setup and basic usage
407
- - **[USAGE_GUIDE.md](./docs/USAGE_GUIDE.md)** - Detailed usage examples and best practices
408
- - **[ARCHITECTURE.md](./docs/ARCHITECTURE.md)** - Technical architecture and implementation details
409
- - **[INTEGRATION_EXAMPLES.md](./docs/INTEGRATION_EXAMPLES.md)** - Real-world integration examples
410
- - **[TRANSLATION_SETTINGS_LOCK.md](./docs/TRANSLATION_SETTINGS_LOCK.md)** - Lock/unlock feature for translation settings
411
- - **[FEATURE_SUMMARY.md](./docs/FEATURE_SUMMARY.md)** - Complete feature overview
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: { user } })=>Boolean(user),
5
- delete: ({ req: { user } })=>Boolean(user),
6
- read: ({ req: { user } })=>Boolean(user),
7
- update: ({ req: { user } })=>Boolean(user)
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: { user } }) => Boolean(user),\n delete: ({ req: { user } }) => Boolean(user),\n read: ({ req: { user } }) => Boolean(user),\n update: ({ req: { user } }) => Boolean(user),\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","req","user","Boolean","delete","read","update","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,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKC,QAAQD;YACvCE,QAAQ,CAAC,EAAEH,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKC,QAAQD;YACvCG,MAAM,CAAC,EAAEJ,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKC,QAAQD;YACrCI,QAAQ,CAAC,EAAEL,KAAK,EAAEC,IAAI,EAAE,EAAE,GAAKC,QAAQD;QACzC;QACAK,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"}
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":"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"}
@@ -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('/api/globals/translation-settings', {
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('/api/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","response","fetch","body","JSON","stringify","lockTranslationSettings","credentials","headers","method","ok","Error","statusText","success","error","console"],"mappings":"AAAA;;;CAGC,GACD,OAAO,eAAeA,mCACpBC,QAAiB;IAEjB,IAAI;QACF,MAAMC,WAAW,MAAMC,MAAM,qCAAqC;YAChEC,MAAMC,KAAKC,SAAS,CAAC;gBACnBC,yBAAyBN;YAC3B;YACAO,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;YACLZ;YACAa,SAAS;QACX;IACF,EAAE,OAAOC,OAAO;QACd,sCAAsC;QACtCC,QAAQD,KAAK,CAAC,gDAAgDA;QAC9D,OAAO;YACLd,UAAU,CAACA;YACXa,SAAS;QACX;IACF;AACF"}
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;AACvD,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,MAAMY,WAAWC,QAAQL,WAAWM,SAAS;IAC7C,MAAM,CAACC,WAAWC,aAAa,GAAGX,SAAS;IAE3C,wEAAwE;IACxE,MAAMY,yBAAyBd,YAAY,CAACe;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;IACvEvB,UAAU;QACR,iDAAiD;QACjD,MAAMwB,QAAQC,WAAW;YACvBZ,uBAAuBL;QACzB,GAAG;QAEH,OAAO,IAAMkB,aAAaF;IAC5B,GAAG;QAAChB;QAAUK;KAAuB;IAErC,MAAMc,aAAa5B,YAAY;QAC7B,MAAM6B,eAAe,CAACpB;QACtBI,aAAa;QAEb,IAAI;YACF,MAAMiB,SAAS,MAAM3B,mCAAmC0B;YAExD,IAAIC,OAAOC,OAAO,EAAE;gBAClB,iDAAiD;gBACjDvB,eAAe;oBACbwB,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;QAAUD;QAAgBM;KAAuB;IAErD,qBACE,KAACsB;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
+ {"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(MODELS_URL, {
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 { FieldDescription, FieldLabel, SelectInput, useField, useFormFields } from '@payloadcms/ui'\nimport React, { useEffect, useMemo, useState } from 'react'\n\nconst MODELS_URL = '/payload/api/globals/translation-settings/openai-models'\n\nexport const OpenAiModelField: TextFieldClientComponent = ({ field, path, readOnly }) => {\n const { value, setValue, showError, errorMessage } = useField<string>({ path })\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(MODELS_URL, {\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 }, [])\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","useField","useFormFields","React","useEffect","useMemo","useState","MODELS_URL","OpenAiModelField","field","path","readOnly","value","setValue","showError","errorMessage","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
+ {"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 or when there's no ID (new document)
35
- if (!id || !effectiveCollectionSlug) {
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
- collection: {
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 = `/api/translation-exclusions?${queryString}`;
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
- fieldPath
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
- collection: {
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 = `/api/translation-exclusions?${queryString}`;
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
- collection: effectiveCollectionSlug,
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
- if (existingId) {
205
- const updateResponse = await fetch(`/api/translation-exclusions/${existingId}`, {
206
- body: JSON.stringify(exclusionsData),
207
- headers: {
208
- 'Content-Type': 'application/json'
209
- },
210
- method: 'PATCH'
211
- });
212
- if (updateResponse.ok) {
213
- const result = await updateResponse.json();
214
- console.log('[TranslationControl] Updated exclusions:', result.doc);
215
- }
216
- } else {
217
- const createResponse = await fetch('/api/translation-exclusions', {
218
- body: JSON.stringify(exclusionsData),
219
- headers: {
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 create (no id yet)
243
- if (!id) {
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", {