@adminforth/i18n 1.2.4 → 1.3.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/build.log CHANGED
@@ -16,5 +16,5 @@ custom/package-lock.json
16
16
  custom/package.json
17
17
  custom/tsconfig.json
18
18
 
19
- sent 24,836 bytes received 229 bytes 50,130.00 bytes/sec
20
- total size is 23,969 speedup is 0.96
19
+ sent 31,064 bytes received 229 bytes 62,586.00 bytes/sec
20
+ total size is 30,197 speedup is 0.96
@@ -1,25 +1,205 @@
1
1
  <template>
2
- <div class="text-sm text-gray-900 dark:text-white min-w-32">
3
- {{ limitedText }}
2
+ <div class="relative group flex items-center">
3
+ <!-- Normal value display -->
4
+ <div v-if="column.editReadonly" class="flex items-center" :class="limitedText?.length > 50 ? 'min-w-48 max-w-full' : 'min-w-32'">
5
+ {{ limitedText? limitedText : '-' }}
6
+ </div>
7
+
8
+ <div @click.stop="startEdit()"
9
+ v-else-if="!isEditing" class="flex items-center" :class="limitedText?.length > 50 ? 'min-w-48 max-w-full' : 'min-w-32'">
10
+ {{ limitedText? limitedText : '-' }}
11
+
12
+ <span v-if="meta?.reviewedCheckboxesFieldName && limitedText" class="flex items-center ml-2">
13
+ <Tooltip
14
+ >
15
+ <template #tooltip>
16
+ {{ record[meta?.reviewedCheckboxesFieldName]?.[props.column.name] ? t('Translation is reviewed') : t('Translation is not reviewed') }}
17
+ </template>
18
+ <IconCheckOutline
19
+ v-if="record[meta?.reviewedCheckboxesFieldName]?.[props.column.name]"
20
+ class="w-5 h-5 text-green-500"
21
+ />
22
+ <IconQuestionCircleSolid
23
+ v-else
24
+ class="w-5 h-5 text-yellow-500"
25
+ />
26
+ </Tooltip>
27
+ </span>
28
+
29
+ <button
30
+ v-if="!column.editReadonly"
31
+ @click="startEdit"
32
+ class="ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
33
+ >
34
+ <IconPenSolid class="w-5 h-5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"/>
35
+ </button>
36
+ </div>
37
+
38
+ <!-- Edit mode -->
39
+ <div v-else class="flex flex-col gap-2" @click.stop>
40
+ <div class="flex items-center max-w-full gap-2"
41
+ :class="limitedText?.length > 50 ? 'min-w-72' : 'min-w-64'"
42
+ ref="inputHolder"
43
+ >
44
+ <ColumnValueInputWrapper
45
+ class="flex-grow"
46
+ ref="input"
47
+ :source="'edit'"
48
+ :column="column"
49
+ :currentValues="currentValues"
50
+ :mode="mode"
51
+ :columnOptions="columnOptions"
52
+ :unmasked="unmasked"
53
+ :setCurrentValue="setCurrentValue"
54
+ />
55
+ <div class="flex gap-1">
56
+ <button
57
+ @click="saveEdit"
58
+ :disabled="saving || (originalReviewed === reviewed && originalValue === currentValues[props.column.name])"
59
+ class="text-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400 disabled:opacity-50"
60
+
61
+ >
62
+ <IconCheckOutline class="w-5 h-5" />
63
+ </button>
64
+ <button
65
+ @click="cancelEdit"
66
+ :disabled="saving"
67
+ class="text-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400"
68
+ >
69
+ <IconXOutline class="w-5 h-5" />
70
+ </button>
71
+ </div>
72
+ </div>
73
+ <div v-if="meta?.reviewedCheckboxesFieldName">
74
+ <label class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-0 cursor-pointer select-none">
75
+ <Checkbox
76
+ v-model="reviewed"
77
+ :disabled="saving"
78
+ />
79
+ {{ t('Translation is reviewed') }}
80
+ </label>
81
+
82
+ </div>
83
+ </div>
84
+
4
85
  </div>
5
86
  </template>
6
87
 
7
88
  <script setup lang="ts">
8
- import { computed } from 'vue';
9
- import { AdminForthResourceColumnCommon, AdminForthResourceCommon, AdminUser } from '@/types/Common';
89
+ import { ref, Ref, computed, nextTick } from 'vue';
90
+ import { IconPenSolid, IconCheckOutline, IconXOutline, IconQuestionCircleSolid } from '@iconify-prerendered/vue-flowbite';
91
+ import { callAdminForthApi } from '@/utils';
92
+ import { showErrorTost, showSuccesTost } from '@/composables/useFrontendApi';
93
+ import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
94
+ import { useI18n } from 'vue-i18n';
95
+ import Tooltip from '@/afcl/Tooltip.vue';
96
+ import Checkbox from '@/afcl/Checkbox.vue';
97
+
98
+ const { t } = useI18n();
99
+ const props = defineProps(['column', 'record', 'resource', 'adminUser', 'meta']);
100
+ const isEditing = ref(false);
101
+ const editValue = ref(null);
102
+ const saving = ref(false);
103
+ const input = ref(null);
104
+ const columnOptions = ref({});
105
+ const mode = ref('edit');
106
+ const currentValues = ref({});
107
+ const unmasked = ref({});
10
108
 
109
+ const inputHolder = ref(null);
11
110
 
12
111
  const limitedText = computed(() => {
13
112
  const text = props.record[props.column.name];
14
113
  return text?.length > 50 ? text.slice(0, 50) + '...' : text;
15
114
  });
16
115
 
17
- const props = defineProps<{
18
- column: AdminForthResourceColumnCommon;
19
- record: any;
20
- meta: any;
21
- resource: AdminForthResourceCommon;
22
- adminUser: AdminUser;
23
- }>();
116
+ const reviewed: Ref<boolean> = ref(false);
117
+
118
+
119
+ const originalReviewed = ref(false);
120
+ const originalValue = ref(null);
121
+
122
+ async function startEdit() {
123
+ const value = props.record[props.column.name];
124
+ currentValues.value = {
125
+ [props.column.name]: props.column.isArray?.enabled
126
+ ? (Array.isArray(value) ? value : [value]).filter(v => v !== null && v !== undefined)
127
+ : value,
128
+ };
129
+ reviewed.value = props.record[props.meta?.reviewedCheckboxesFieldName]?.[props.column.name] || false;
130
+ originalReviewed.value = reviewed.value;
131
+ originalValue.value = value;
132
+ isEditing.value = true;
133
+ await nextTick();
134
+ if (inputHolder.value) {
135
+ inputHolder.value.querySelector('input, textarea, select')?.focus();
136
+ }
137
+ }
138
+
139
+ function cancelEdit() {
140
+ isEditing.value = false;
141
+ editValue.value = null;
142
+ }
143
+
144
+ function setCurrentValue(field, value, arrayIndex = undefined) {
145
+ if (arrayIndex !== undefined && props.column.isArray?.enabled) {
146
+ // Handle array updates
147
+ if (!Array.isArray(currentValues.value[field])) {
148
+ currentValues.value[field] = [];
149
+ }
150
+
151
+ const newArray = [...currentValues.value[field]];
152
+
153
+ if (arrayIndex >= newArray.length) {
154
+ // When adding a new item, always add null
155
+ newArray.push(null);
156
+ } else {
157
+ // For existing items, handle type conversion
158
+ if (props.column.isArray?.itemType && ['integer', 'float', 'decimal'].includes(props.column.isArray.itemType)) {
159
+ newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
160
+ } else {
161
+ newArray[arrayIndex] = value;
162
+ }
163
+ }
164
+
165
+ // Assign the new array
166
+ currentValues.value[field] = newArray;
167
+ editValue.value = newArray;
168
+ } else {
169
+ // Handle non-array updates
170
+ currentValues.value[field] = value;
171
+ editValue.value = value;
172
+ }
173
+ }
174
+
175
+ async function saveEdit() {
176
+ saving.value = true;
177
+ try {
178
+ const result = await callAdminForthApi({
179
+ method: 'POST',
180
+ path: `/plugin/${props.meta.pluginInstanceId}/update-field`,
181
+ body: {
182
+ resourceId: props.resource.resourceId,
183
+ recordId: props.record._primaryKeyValue,
184
+ field: props.column.name,
185
+ value: currentValues.value[props.column.name],
186
+ reviewed: reviewed.value,
187
+ }
188
+ });
189
+
190
+ if (result.error) {
191
+ showErrorTost(result.error);
192
+ return;
193
+ }
24
194
 
25
- </script>
195
+ showSuccesTost(t('Field updated successfully'));
196
+ props.record[props.column.name] = result.record[props.column.name];
197
+ if (props.meta?.reviewedCheckboxesFieldName) {
198
+ props.record[props.meta?.reviewedCheckboxesFieldName] = result.record[props.meta?.reviewedCheckboxesFieldName];
199
+ }
200
+ isEditing.value = false;
201
+ } finally {
202
+ saving.value = false;
203
+ }
204
+ }
205
+ </script>
@@ -1,25 +1,205 @@
1
1
  <template>
2
- <div class="text-sm text-gray-900 dark:text-white min-w-32">
3
- {{ limitedText }}
2
+ <div class="relative group flex items-center">
3
+ <!-- Normal value display -->
4
+ <div v-if="column.editReadonly" class="flex items-center" :class="limitedText?.length > 50 ? 'min-w-48 max-w-full' : 'min-w-32'">
5
+ {{ limitedText? limitedText : '-' }}
6
+ </div>
7
+
8
+ <div @click.stop="startEdit()"
9
+ v-else-if="!isEditing" class="flex items-center" :class="limitedText?.length > 50 ? 'min-w-48 max-w-full' : 'min-w-32'">
10
+ {{ limitedText? limitedText : '-' }}
11
+
12
+ <span v-if="meta?.reviewedCheckboxesFieldName && limitedText" class="flex items-center ml-2">
13
+ <Tooltip
14
+ >
15
+ <template #tooltip>
16
+ {{ record[meta?.reviewedCheckboxesFieldName]?.[props.column.name] ? t('Translation is reviewed') : t('Translation is not reviewed') }}
17
+ </template>
18
+ <IconCheckOutline
19
+ v-if="record[meta?.reviewedCheckboxesFieldName]?.[props.column.name]"
20
+ class="w-5 h-5 text-green-500"
21
+ />
22
+ <IconQuestionCircleSolid
23
+ v-else
24
+ class="w-5 h-5 text-yellow-500"
25
+ />
26
+ </Tooltip>
27
+ </span>
28
+
29
+ <button
30
+ v-if="!column.editReadonly"
31
+ @click="startEdit"
32
+ class="ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
33
+ >
34
+ <IconPenSolid class="w-5 h-5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"/>
35
+ </button>
36
+ </div>
37
+
38
+ <!-- Edit mode -->
39
+ <div v-else class="flex flex-col gap-2" @click.stop>
40
+ <div class="flex items-center max-w-full gap-2"
41
+ :class="limitedText?.length > 50 ? 'min-w-72' : 'min-w-64'"
42
+ ref="inputHolder"
43
+ >
44
+ <ColumnValueInputWrapper
45
+ class="flex-grow"
46
+ ref="input"
47
+ :source="'edit'"
48
+ :column="column"
49
+ :currentValues="currentValues"
50
+ :mode="mode"
51
+ :columnOptions="columnOptions"
52
+ :unmasked="unmasked"
53
+ :setCurrentValue="setCurrentValue"
54
+ />
55
+ <div class="flex gap-1">
56
+ <button
57
+ @click="saveEdit"
58
+ :disabled="saving || (originalReviewed === reviewed && originalValue === currentValues[props.column.name])"
59
+ class="text-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400 disabled:opacity-50"
60
+
61
+ >
62
+ <IconCheckOutline class="w-5 h-5" />
63
+ </button>
64
+ <button
65
+ @click="cancelEdit"
66
+ :disabled="saving"
67
+ class="text-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400"
68
+ >
69
+ <IconXOutline class="w-5 h-5" />
70
+ </button>
71
+ </div>
72
+ </div>
73
+ <div v-if="meta?.reviewedCheckboxesFieldName">
74
+ <label class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-0 cursor-pointer select-none">
75
+ <Checkbox
76
+ v-model="reviewed"
77
+ :disabled="saving"
78
+ />
79
+ {{ t('Translation is reviewed') }}
80
+ </label>
81
+
82
+ </div>
83
+ </div>
84
+
4
85
  </div>
5
86
  </template>
6
87
 
7
88
  <script setup lang="ts">
8
- import { computed } from 'vue';
9
- import { AdminForthResourceColumnCommon, AdminForthResourceCommon, AdminUser } from '@/types/Common';
89
+ import { ref, Ref, computed, nextTick } from 'vue';
90
+ import { IconPenSolid, IconCheckOutline, IconXOutline, IconQuestionCircleSolid } from '@iconify-prerendered/vue-flowbite';
91
+ import { callAdminForthApi } from '@/utils';
92
+ import { showErrorTost, showSuccesTost } from '@/composables/useFrontendApi';
93
+ import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
94
+ import { useI18n } from 'vue-i18n';
95
+ import Tooltip from '@/afcl/Tooltip.vue';
96
+ import Checkbox from '@/afcl/Checkbox.vue';
97
+
98
+ const { t } = useI18n();
99
+ const props = defineProps(['column', 'record', 'resource', 'adminUser', 'meta']);
100
+ const isEditing = ref(false);
101
+ const editValue = ref(null);
102
+ const saving = ref(false);
103
+ const input = ref(null);
104
+ const columnOptions = ref({});
105
+ const mode = ref('edit');
106
+ const currentValues = ref({});
107
+ const unmasked = ref({});
10
108
 
109
+ const inputHolder = ref(null);
11
110
 
12
111
  const limitedText = computed(() => {
13
112
  const text = props.record[props.column.name];
14
113
  return text?.length > 50 ? text.slice(0, 50) + '...' : text;
15
114
  });
16
115
 
17
- const props = defineProps<{
18
- column: AdminForthResourceColumnCommon;
19
- record: any;
20
- meta: any;
21
- resource: AdminForthResourceCommon;
22
- adminUser: AdminUser;
23
- }>();
116
+ const reviewed: Ref<boolean> = ref(false);
117
+
118
+
119
+ const originalReviewed = ref(false);
120
+ const originalValue = ref(null);
121
+
122
+ async function startEdit() {
123
+ const value = props.record[props.column.name];
124
+ currentValues.value = {
125
+ [props.column.name]: props.column.isArray?.enabled
126
+ ? (Array.isArray(value) ? value : [value]).filter(v => v !== null && v !== undefined)
127
+ : value,
128
+ };
129
+ reviewed.value = props.record[props.meta?.reviewedCheckboxesFieldName]?.[props.column.name] || false;
130
+ originalReviewed.value = reviewed.value;
131
+ originalValue.value = value;
132
+ isEditing.value = true;
133
+ await nextTick();
134
+ if (inputHolder.value) {
135
+ inputHolder.value.querySelector('input, textarea, select')?.focus();
136
+ }
137
+ }
138
+
139
+ function cancelEdit() {
140
+ isEditing.value = false;
141
+ editValue.value = null;
142
+ }
143
+
144
+ function setCurrentValue(field, value, arrayIndex = undefined) {
145
+ if (arrayIndex !== undefined && props.column.isArray?.enabled) {
146
+ // Handle array updates
147
+ if (!Array.isArray(currentValues.value[field])) {
148
+ currentValues.value[field] = [];
149
+ }
150
+
151
+ const newArray = [...currentValues.value[field]];
152
+
153
+ if (arrayIndex >= newArray.length) {
154
+ // When adding a new item, always add null
155
+ newArray.push(null);
156
+ } else {
157
+ // For existing items, handle type conversion
158
+ if (props.column.isArray?.itemType && ['integer', 'float', 'decimal'].includes(props.column.isArray.itemType)) {
159
+ newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
160
+ } else {
161
+ newArray[arrayIndex] = value;
162
+ }
163
+ }
164
+
165
+ // Assign the new array
166
+ currentValues.value[field] = newArray;
167
+ editValue.value = newArray;
168
+ } else {
169
+ // Handle non-array updates
170
+ currentValues.value[field] = value;
171
+ editValue.value = value;
172
+ }
173
+ }
174
+
175
+ async function saveEdit() {
176
+ saving.value = true;
177
+ try {
178
+ const result = await callAdminForthApi({
179
+ method: 'POST',
180
+ path: `/plugin/${props.meta.pluginInstanceId}/update-field`,
181
+ body: {
182
+ resourceId: props.resource.resourceId,
183
+ recordId: props.record._primaryKeyValue,
184
+ field: props.column.name,
185
+ value: currentValues.value[props.column.name],
186
+ reviewed: reviewed.value,
187
+ }
188
+ });
189
+
190
+ if (result.error) {
191
+ showErrorTost(result.error);
192
+ return;
193
+ }
24
194
 
25
- </script>
195
+ showSuccesTost(t('Field updated successfully'));
196
+ props.record[props.column.name] = result.record[props.column.name];
197
+ if (props.meta?.reviewedCheckboxesFieldName) {
198
+ props.record[props.meta?.reviewedCheckboxesFieldName] = result.record[props.meta?.reviewedCheckboxesFieldName];
199
+ }
200
+ isEditing.value = false;
201
+ } finally {
202
+ saving.value = false;
203
+ }
204
+ }
205
+ </script>
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
10
+ import { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
11
11
  import iso6391 from 'iso-639-1';
12
12
  import path from 'path';
13
13
  import fs from 'fs-extra';
@@ -162,6 +162,11 @@ export default class I18nPlugin extends AdminForthPlugin {
162
162
  // set ListCell for list
163
163
  column.components.list = {
164
164
  file: this.componentPath('ListCell.vue'),
165
+ meta: {
166
+ pluginInstanceId: this.pluginInstanceId,
167
+ lang,
168
+ reviewedCheckboxesFieldName: this.options.reviewedCheckboxesFieldName,
169
+ },
165
170
  };
166
171
  }
167
172
  this.enFieldName = this.trFieldNames['en'] || 'en_string';
@@ -616,6 +621,17 @@ ${JSON.stringify(strings.reduce((acc, s) => {
616
621
  throw new Error(`Field ${this.trFieldNames[lang]} should be not required in resource ${resourceConfig.resourceId}`);
617
622
  }
618
623
  }
624
+ if (this.options.reviewedCheckboxesFieldName) {
625
+ // ensure type is JSON
626
+ const column = resourceConfig.columns.find(c => c.name === this.options.reviewedCheckboxesFieldName);
627
+ if (!column) {
628
+ const similar = suggestIfTypo(resourceConfig.columns.map((col) => col.name), this.options.reviewedCheckboxesFieldName);
629
+ throw new Error(`Field ${this.options.reviewedCheckboxesFieldName} not found in resource ${resourceConfig.resourceId}${similar ? `Did you mean '${similar}'?` : ''}`);
630
+ }
631
+ if (column.type !== AdminForthDataTypes.JSON) {
632
+ throw new Error(`Field ${this.options.reviewedCheckboxesFieldName} should be of type JSON in resource ${resourceConfig.resourceId}, but it is ${column.type}`);
633
+ }
634
+ }
619
635
  // ensure categoryFieldName defined and is string
620
636
  if (!this.options.categoryFieldName) {
621
637
  throw new Error(`categoryFieldName option is not defined. It is used to categorize translations and return only specific category e.g. to frontend`);
@@ -797,5 +813,47 @@ ${JSON.stringify(strings.reduce((acc, s) => {
797
813
  return translations;
798
814
  })
799
815
  });
816
+ const lock = new RAMLock();
817
+ server.endpoint({
818
+ method: 'POST',
819
+ path: `/plugin/${this.pluginInstanceId}/update-field`,
820
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, headers }) {
821
+ const { resourceId, recordId, field, value, reviewed } = body;
822
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === resourceId);
823
+ // Create update object with just the single field
824
+ const updateRecord = { [field]: value };
825
+ // Use AdminForth's built-in update method
826
+ const connector = this.adminforth.connectors[resource.dataSource];
827
+ let oldRecord;
828
+ let result;
829
+ yield lock.run(`edit-trans-${recordId}`, () => __awaiter(this, void 0, void 0, function* () {
830
+ // put into lock so 2 editors will not update the same record at the same time
831
+ oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
832
+ if (this.options.reviewedCheckboxesFieldName) {
833
+ let oldValue;
834
+ if (!oldRecord[this.options.reviewedCheckboxesFieldName]) {
835
+ oldValue = {};
836
+ }
837
+ else {
838
+ oldValue = Object.assign({}, oldRecord[this.options.reviewedCheckboxesFieldName]);
839
+ }
840
+ oldValue[field] = reviewed;
841
+ updateRecord[this.options.reviewedCheckboxesFieldName] = Object.assign({}, oldValue);
842
+ }
843
+ result = yield this.adminforth.updateResourceRecord({
844
+ resource,
845
+ recordId,
846
+ record: updateRecord,
847
+ oldRecord,
848
+ adminUser
849
+ });
850
+ }));
851
+ if (result.error) {
852
+ return { error: result.error };
853
+ }
854
+ const updatedRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
855
+ return { record: updatedRecord };
856
+ })
857
+ });
800
858
  }
801
859
  }
package/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes } from "adminforth";
1
+ import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
2
2
  import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem } from "adminforth";
3
3
  import type { PluginOptions } from './types.js';
4
4
  import iso6391, { LanguageCode } from 'iso-639-1';
@@ -6,6 +6,8 @@ import path from 'path';
6
6
  import fs from 'fs-extra';
7
7
  import chokidar from 'chokidar';
8
8
  import { AsyncQueue } from '@sapphire/async-queue';
9
+
10
+
9
11
  console.log = (...args) => {
10
12
  process.stdout.write(args.join(" ") + "\n");
11
13
  };
@@ -182,6 +184,11 @@ export default class I18nPlugin extends AdminForthPlugin {
182
184
  // set ListCell for list
183
185
  column.components.list = {
184
186
  file: this.componentPath('ListCell.vue'),
187
+ meta: {
188
+ pluginInstanceId: this.pluginInstanceId,
189
+ lang,
190
+ reviewedCheckboxesFieldName: this.options.reviewedCheckboxesFieldName,
191
+ },
185
192
  };
186
193
  }
187
194
 
@@ -732,6 +739,19 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
732
739
  }
733
740
  }
734
741
 
742
+ if (this.options.reviewedCheckboxesFieldName) {
743
+ // ensure type is JSON
744
+ const column = resourceConfig.columns.find(c => c.name === this.options.reviewedCheckboxesFieldName);
745
+ if (!column) {
746
+ const similar = suggestIfTypo(resourceConfig.columns.map((col) => col.name), this.options.reviewedCheckboxesFieldName);
747
+ throw new Error(`Field ${this.options.reviewedCheckboxesFieldName} not found in resource ${resourceConfig.resourceId}${similar ? `Did you mean '${similar}'?` : ''}`);
748
+ }
749
+ if (column.type !== AdminForthDataTypes.JSON) {
750
+ throw new Error(`Field ${this.options.reviewedCheckboxesFieldName} should be of type JSON in resource ${resourceConfig.resourceId}, but it is ${column.type}`);
751
+ }
752
+ }
753
+
754
+
735
755
  // ensure categoryFieldName defined and is string
736
756
  if (!this.options.categoryFieldName) {
737
757
  throw new Error(`categoryFieldName option is not defined. It is used to categorize translations and return only specific category e.g. to frontend`);
@@ -940,6 +960,57 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
940
960
  }
941
961
  });
942
962
 
963
+ const lock = new RAMLock();
964
+
965
+ server.endpoint({
966
+ method: 'POST',
967
+ path: `/plugin/${this.pluginInstanceId}/update-field`,
968
+ handler: async ({ body, adminUser, headers }) => {
969
+ const { resourceId, recordId, field, value, reviewed } = body;
970
+
971
+ const resource = this.adminforth.config.resources.find(r => r.resourceId === resourceId);
972
+ // Create update object with just the single field
973
+ const updateRecord = { [field]: value };
974
+
975
+ // Use AdminForth's built-in update method
976
+ const connector = this.adminforth.connectors[resource.dataSource];
977
+
978
+ let oldRecord;
979
+ let result;
980
+ await lock.run(`edit-trans-${recordId}`, async () => {
981
+ // put into lock so 2 editors will not update the same record at the same time
982
+ oldRecord = await connector.getRecordByPrimaryKey(resource, recordId)
983
+
984
+ if (this.options.reviewedCheckboxesFieldName) {
985
+ let oldValue;
986
+ if (!oldRecord[this.options.reviewedCheckboxesFieldName]) {
987
+ oldValue = {}
988
+ } else {
989
+ oldValue = {...oldRecord[this.options.reviewedCheckboxesFieldName]};
990
+ }
991
+ oldValue[field] = reviewed;
992
+ updateRecord[this.options.reviewedCheckboxesFieldName] = { ...oldValue };
993
+ }
994
+
995
+ result = await this.adminforth.updateResourceRecord({
996
+ resource,
997
+ recordId,
998
+ record: updateRecord,
999
+ oldRecord,
1000
+ adminUser
1001
+ });
1002
+ });
1003
+
1004
+ if (result.error) {
1005
+ return { error: result.error };
1006
+ }
1007
+
1008
+ const updatedRecord = await connector.getRecordByPrimaryKey(resource, recordId);
1009
+
1010
+ return { record: updatedRecord };
1011
+ }
1012
+ });
1013
+
943
1014
  }
944
1015
 
945
1016
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/i18n",
3
- "version": "1.2.4",
3
+ "version": "1.3.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -42,4 +42,11 @@ export interface PluginOptions {
42
42
  * not AdminForth applications
43
43
  */
44
44
  externalAppOnly?: boolean;
45
+
46
+
47
+ /**
48
+ * You can enable "Reviewed" checkbox for each translation string by defing this field,
49
+ * it should be a JSON field (underlyng database type should be TEXT or JSON)
50
+ */
51
+ reviewedCheckboxesFieldName?: string;
45
52
  }