@adminforth/i18n 1.2.3 → 1.3.0

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 30,826 bytes received 229 bytes 62,110.00 bytes/sec
20
+ total size is 29,971 speedup is 0.97
@@ -1,25 +1,200 @@
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" @click.stop>
3
+ <!-- Normal value display -->
4
+ <div v-if="!isEditing" class="flex items-center" :class="limitedText?.length > 50 ? 'min-w-48 max-w-full' : 'min-w-32'">
5
+ {{ limitedText? limitedText : '-' }}
6
+
7
+ <span v-if="meta?.reviewedCheckboxesFieldName && limitedText" class="flex items-center ml-2">
8
+ <Tooltip
9
+ >
10
+ <template #tooltip>
11
+ {{ record[meta?.reviewedCheckboxesFieldName]?.[props.column.name] ? t('Translation is reviewed') : t('Translation is not reviewed') }}
12
+ </template>
13
+ <IconCheckOutline
14
+ v-if="record[meta?.reviewedCheckboxesFieldName]?.[props.column.name]"
15
+ class="w-5 h-5 text-green-500"
16
+ />
17
+ <IconQuestionCircleSolid
18
+ v-else
19
+ class="w-5 h-5 text-yellow-500"
20
+ />
21
+ </Tooltip>
22
+ </span>
23
+
24
+ <button
25
+ v-if="!column.editReadonly"
26
+ @click="startEdit"
27
+ class="ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
28
+ >
29
+ <IconPenSolid class="w-5 h-5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"/>
30
+ </button>
31
+ </div>
32
+
33
+ <!-- Edit mode -->
34
+ <div v-else class="flex flex-col gap-2">
35
+ <div class="flex items-center max-w-full gap-2"
36
+ :class="limitedText?.length > 50 ? 'min-w-72' : 'min-w-64'"
37
+ ref="inputHolder"
38
+ >
39
+ <ColumnValueInputWrapper
40
+ class="flex-grow"
41
+ ref="input"
42
+ :source="'edit'"
43
+ :column="column"
44
+ :currentValues="currentValues"
45
+ :mode="mode"
46
+ :columnOptions="columnOptions"
47
+ :unmasked="unmasked"
48
+ :setCurrentValue="setCurrentValue"
49
+ />
50
+ <div class="flex gap-1">
51
+ <button
52
+ @click="saveEdit"
53
+ :disabled="saving || (originalReviewed === reviewed && originalValue === currentValues[props.column.name])"
54
+ class="text-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400 disabled:opacity-50"
55
+
56
+ >
57
+ <IconCheckOutline class="w-5 h-5" />
58
+ </button>
59
+ <button
60
+ @click="cancelEdit"
61
+ :disabled="saving"
62
+ class="text-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400"
63
+ >
64
+ <IconXOutline class="w-5 h-5" />
65
+ </button>
66
+ </div>
67
+ </div>
68
+ <div v-if="meta?.reviewedCheckboxesFieldName">
69
+ <label class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-0 cursor-pointer select-none">
70
+ <Checkbox
71
+ v-model="reviewed"
72
+ :disabled="saving"
73
+ />
74
+ {{ t('Translation is reviewed') }}
75
+ </label>
76
+
77
+ </div>
78
+ </div>
79
+
4
80
  </div>
5
81
  </template>
6
82
 
7
83
  <script setup lang="ts">
8
- import { computed } from 'vue';
9
- import { AdminForthResourceColumnCommon, AdminForthResourceCommon, AdminUser } from '@/types/Common';
84
+ import { ref, Ref, computed, nextTick } from 'vue';
85
+ import { IconPenSolid, IconCheckOutline, IconXOutline, IconQuestionCircleSolid } from '@iconify-prerendered/vue-flowbite';
86
+ import { callAdminForthApi } from '@/utils';
87
+ import { showErrorTost, showSuccesTost } from '@/composables/useFrontendApi';
88
+ import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
89
+ import { useI18n } from 'vue-i18n';
90
+ import Tooltip from '@/afcl/Tooltip.vue';
91
+ import Checkbox from '@/afcl/Checkbox.vue';
10
92
 
93
+ const { t } = useI18n();
94
+ const props = defineProps(['column', 'record', 'resource', 'adminUser', 'meta']);
95
+ const isEditing = ref(false);
96
+ const editValue = ref(null);
97
+ const saving = ref(false);
98
+ const input = ref(null);
99
+ const columnOptions = ref({});
100
+ const mode = ref('edit');
101
+ const currentValues = ref({});
102
+ const unmasked = ref({});
103
+
104
+ const inputHolder = ref(null);
11
105
 
12
106
  const limitedText = computed(() => {
13
107
  const text = props.record[props.column.name];
14
108
  return text?.length > 50 ? text.slice(0, 50) + '...' : text;
15
109
  });
16
110
 
17
- const props = defineProps<{
18
- column: AdminForthResourceColumnCommon;
19
- record: any;
20
- meta: any;
21
- resource: AdminForthResourceCommon;
22
- adminUser: AdminUser;
23
- }>();
111
+ const reviewed: Ref<boolean> = ref(false);
112
+
113
+
114
+ const originalReviewed = ref(false);
115
+ const originalValue = ref(null);
116
+
117
+ async function startEdit() {
118
+ const value = props.record[props.column.name];
119
+ currentValues.value = {
120
+ [props.column.name]: props.column.isArray?.enabled
121
+ ? (Array.isArray(value) ? value : [value]).filter(v => v !== null && v !== undefined)
122
+ : value,
123
+ };
124
+ reviewed.value = props.record[props.meta?.reviewedCheckboxesFieldName]?.[props.column.name] || false;
125
+ originalReviewed.value = reviewed.value;
126
+ originalValue.value = value;
127
+ isEditing.value = true;
128
+ await nextTick();
129
+ if (inputHolder.value) {
130
+ inputHolder.value.querySelector('input, textarea, select')?.focus();
131
+ }
132
+ }
133
+
134
+ function cancelEdit() {
135
+ isEditing.value = false;
136
+ editValue.value = null;
137
+ }
138
+
139
+ function setCurrentValue(field, value, arrayIndex = undefined) {
140
+ if (arrayIndex !== undefined && props.column.isArray?.enabled) {
141
+ // Handle array updates
142
+ if (!Array.isArray(currentValues.value[field])) {
143
+ currentValues.value[field] = [];
144
+ }
145
+
146
+ const newArray = [...currentValues.value[field]];
147
+
148
+ if (arrayIndex >= newArray.length) {
149
+ // When adding a new item, always add null
150
+ newArray.push(null);
151
+ } else {
152
+ // For existing items, handle type conversion
153
+ if (props.column.isArray?.itemType && ['integer', 'float', 'decimal'].includes(props.column.isArray.itemType)) {
154
+ newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
155
+ } else {
156
+ newArray[arrayIndex] = value;
157
+ }
158
+ }
159
+
160
+ // Assign the new array
161
+ currentValues.value[field] = newArray;
162
+ editValue.value = newArray;
163
+ } else {
164
+ // Handle non-array updates
165
+ currentValues.value[field] = value;
166
+ editValue.value = value;
167
+ }
168
+ }
169
+
170
+ async function saveEdit() {
171
+ saving.value = true;
172
+ try {
173
+ const result = await callAdminForthApi({
174
+ method: 'POST',
175
+ path: `/plugin/${props.meta.pluginInstanceId}/update-field`,
176
+ body: {
177
+ resourceId: props.resource.resourceId,
178
+ recordId: props.record._primaryKeyValue,
179
+ field: props.column.name,
180
+ value: currentValues.value[props.column.name],
181
+ reviewed: reviewed.value,
182
+ }
183
+ });
184
+
185
+ if (result.error) {
186
+ showErrorTost(result.error);
187
+ return;
188
+ }
24
189
 
25
- </script>
190
+ showSuccesTost(t('Field updated successfully'));
191
+ props.record[props.column.name] = result.record[props.column.name];
192
+ if (props.meta?.reviewedCheckboxesFieldName) {
193
+ props.record[props.meta?.reviewedCheckboxesFieldName] = result.record[props.meta?.reviewedCheckboxesFieldName];
194
+ }
195
+ isEditing.value = false;
196
+ } finally {
197
+ saving.value = false;
198
+ }
199
+ }
200
+ </script>
@@ -1,25 +1,200 @@
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" @click.stop>
3
+ <!-- Normal value display -->
4
+ <div v-if="!isEditing" class="flex items-center" :class="limitedText?.length > 50 ? 'min-w-48 max-w-full' : 'min-w-32'">
5
+ {{ limitedText? limitedText : '-' }}
6
+
7
+ <span v-if="meta?.reviewedCheckboxesFieldName && limitedText" class="flex items-center ml-2">
8
+ <Tooltip
9
+ >
10
+ <template #tooltip>
11
+ {{ record[meta?.reviewedCheckboxesFieldName]?.[props.column.name] ? t('Translation is reviewed') : t('Translation is not reviewed') }}
12
+ </template>
13
+ <IconCheckOutline
14
+ v-if="record[meta?.reviewedCheckboxesFieldName]?.[props.column.name]"
15
+ class="w-5 h-5 text-green-500"
16
+ />
17
+ <IconQuestionCircleSolid
18
+ v-else
19
+ class="w-5 h-5 text-yellow-500"
20
+ />
21
+ </Tooltip>
22
+ </span>
23
+
24
+ <button
25
+ v-if="!column.editReadonly"
26
+ @click="startEdit"
27
+ class="ml-2 opacity-0 group-hover:opacity-100 transition-opacity"
28
+ >
29
+ <IconPenSolid class="w-5 h-5 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300"/>
30
+ </button>
31
+ </div>
32
+
33
+ <!-- Edit mode -->
34
+ <div v-else class="flex flex-col gap-2">
35
+ <div class="flex items-center max-w-full gap-2"
36
+ :class="limitedText?.length > 50 ? 'min-w-72' : 'min-w-64'"
37
+ ref="inputHolder"
38
+ >
39
+ <ColumnValueInputWrapper
40
+ class="flex-grow"
41
+ ref="input"
42
+ :source="'edit'"
43
+ :column="column"
44
+ :currentValues="currentValues"
45
+ :mode="mode"
46
+ :columnOptions="columnOptions"
47
+ :unmasked="unmasked"
48
+ :setCurrentValue="setCurrentValue"
49
+ />
50
+ <div class="flex gap-1">
51
+ <button
52
+ @click="saveEdit"
53
+ :disabled="saving || (originalReviewed === reviewed && originalValue === currentValues[props.column.name])"
54
+ class="text-green-600 hover:text-green-700 dark:text-green-500 dark:hover:text-green-400 disabled:opacity-50"
55
+
56
+ >
57
+ <IconCheckOutline class="w-5 h-5" />
58
+ </button>
59
+ <button
60
+ @click="cancelEdit"
61
+ :disabled="saving"
62
+ class="text-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400"
63
+ >
64
+ <IconXOutline class="w-5 h-5" />
65
+ </button>
66
+ </div>
67
+ </div>
68
+ <div v-if="meta?.reviewedCheckboxesFieldName">
69
+ <label class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-0 cursor-pointer select-none">
70
+ <Checkbox
71
+ v-model="reviewed"
72
+ :disabled="saving"
73
+ />
74
+ {{ t('Translation is reviewed') }}
75
+ </label>
76
+
77
+ </div>
78
+ </div>
79
+
4
80
  </div>
5
81
  </template>
6
82
 
7
83
  <script setup lang="ts">
8
- import { computed } from 'vue';
9
- import { AdminForthResourceColumnCommon, AdminForthResourceCommon, AdminUser } from '@/types/Common';
84
+ import { ref, Ref, computed, nextTick } from 'vue';
85
+ import { IconPenSolid, IconCheckOutline, IconXOutline, IconQuestionCircleSolid } from '@iconify-prerendered/vue-flowbite';
86
+ import { callAdminForthApi } from '@/utils';
87
+ import { showErrorTost, showSuccesTost } from '@/composables/useFrontendApi';
88
+ import ColumnValueInputWrapper from '@/components/ColumnValueInputWrapper.vue';
89
+ import { useI18n } from 'vue-i18n';
90
+ import Tooltip from '@/afcl/Tooltip.vue';
91
+ import Checkbox from '@/afcl/Checkbox.vue';
10
92
 
93
+ const { t } = useI18n();
94
+ const props = defineProps(['column', 'record', 'resource', 'adminUser', 'meta']);
95
+ const isEditing = ref(false);
96
+ const editValue = ref(null);
97
+ const saving = ref(false);
98
+ const input = ref(null);
99
+ const columnOptions = ref({});
100
+ const mode = ref('edit');
101
+ const currentValues = ref({});
102
+ const unmasked = ref({});
103
+
104
+ const inputHolder = ref(null);
11
105
 
12
106
  const limitedText = computed(() => {
13
107
  const text = props.record[props.column.name];
14
108
  return text?.length > 50 ? text.slice(0, 50) + '...' : text;
15
109
  });
16
110
 
17
- const props = defineProps<{
18
- column: AdminForthResourceColumnCommon;
19
- record: any;
20
- meta: any;
21
- resource: AdminForthResourceCommon;
22
- adminUser: AdminUser;
23
- }>();
111
+ const reviewed: Ref<boolean> = ref(false);
112
+
113
+
114
+ const originalReviewed = ref(false);
115
+ const originalValue = ref(null);
116
+
117
+ async function startEdit() {
118
+ const value = props.record[props.column.name];
119
+ currentValues.value = {
120
+ [props.column.name]: props.column.isArray?.enabled
121
+ ? (Array.isArray(value) ? value : [value]).filter(v => v !== null && v !== undefined)
122
+ : value,
123
+ };
124
+ reviewed.value = props.record[props.meta?.reviewedCheckboxesFieldName]?.[props.column.name] || false;
125
+ originalReviewed.value = reviewed.value;
126
+ originalValue.value = value;
127
+ isEditing.value = true;
128
+ await nextTick();
129
+ if (inputHolder.value) {
130
+ inputHolder.value.querySelector('input, textarea, select')?.focus();
131
+ }
132
+ }
133
+
134
+ function cancelEdit() {
135
+ isEditing.value = false;
136
+ editValue.value = null;
137
+ }
138
+
139
+ function setCurrentValue(field, value, arrayIndex = undefined) {
140
+ if (arrayIndex !== undefined && props.column.isArray?.enabled) {
141
+ // Handle array updates
142
+ if (!Array.isArray(currentValues.value[field])) {
143
+ currentValues.value[field] = [];
144
+ }
145
+
146
+ const newArray = [...currentValues.value[field]];
147
+
148
+ if (arrayIndex >= newArray.length) {
149
+ // When adding a new item, always add null
150
+ newArray.push(null);
151
+ } else {
152
+ // For existing items, handle type conversion
153
+ if (props.column.isArray?.itemType && ['integer', 'float', 'decimal'].includes(props.column.isArray.itemType)) {
154
+ newArray[arrayIndex] = value !== null && value !== '' ? +value : null;
155
+ } else {
156
+ newArray[arrayIndex] = value;
157
+ }
158
+ }
159
+
160
+ // Assign the new array
161
+ currentValues.value[field] = newArray;
162
+ editValue.value = newArray;
163
+ } else {
164
+ // Handle non-array updates
165
+ currentValues.value[field] = value;
166
+ editValue.value = value;
167
+ }
168
+ }
169
+
170
+ async function saveEdit() {
171
+ saving.value = true;
172
+ try {
173
+ const result = await callAdminForthApi({
174
+ method: 'POST',
175
+ path: `/plugin/${props.meta.pluginInstanceId}/update-field`,
176
+ body: {
177
+ resourceId: props.resource.resourceId,
178
+ recordId: props.record._primaryKeyValue,
179
+ field: props.column.name,
180
+ value: currentValues.value[props.column.name],
181
+ reviewed: reviewed.value,
182
+ }
183
+ });
184
+
185
+ if (result.error) {
186
+ showErrorTost(result.error);
187
+ return;
188
+ }
24
189
 
25
- </script>
190
+ showSuccesTost(t('Field updated successfully'));
191
+ props.record[props.column.name] = result.record[props.column.name];
192
+ if (props.meta?.reviewedCheckboxesFieldName) {
193
+ props.record[props.meta?.reviewedCheckboxesFieldName] = result.record[props.meta?.reviewedCheckboxesFieldName];
194
+ }
195
+ isEditing.value = false;
196
+ } finally {
197
+ saving.value = false;
198
+ }
199
+ }
200
+ </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';
@@ -217,6 +222,9 @@ export default class I18nPlugin extends AdminForthPlugin {
217
222
  name: 'fully_translated',
218
223
  label: 'Fully translated',
219
224
  virtual: true,
225
+ filterOptions: {
226
+ multiselect: false,
227
+ },
220
228
  showIn: {
221
229
  show: true,
222
230
  list: true,
@@ -613,6 +621,17 @@ ${JSON.stringify(strings.reduce((acc, s) => {
613
621
  throw new Error(`Field ${this.trFieldNames[lang]} should be not required in resource ${resourceConfig.resourceId}`);
614
622
  }
615
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
+ }
616
635
  // ensure categoryFieldName defined and is string
617
636
  if (!this.options.categoryFieldName) {
618
637
  throw new Error(`categoryFieldName option is not defined. It is used to categorize translations and return only specific category e.g. to frontend`);
@@ -794,5 +813,47 @@ ${JSON.stringify(strings.reduce((acc, s) => {
794
813
  return translations;
795
814
  })
796
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
+ });
797
858
  }
798
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
 
@@ -245,6 +252,9 @@ export default class I18nPlugin extends AdminForthPlugin {
245
252
  name: 'fully_translated',
246
253
  label: 'Fully translated',
247
254
  virtual: true,
255
+ filterOptions: {
256
+ multiselect: false,
257
+ },
248
258
  showIn: {
249
259
  show: true,
250
260
  list: true,
@@ -729,6 +739,19 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
729
739
  }
730
740
  }
731
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
+
732
755
  // ensure categoryFieldName defined and is string
733
756
  if (!this.options.categoryFieldName) {
734
757
  throw new Error(`categoryFieldName option is not defined. It is used to categorize translations and return only specific category e.g. to frontend`);
@@ -937,6 +960,57 @@ JSON.stringify(strings.reduce((acc: object, s: { en_string: string }): object =>
937
960
  }
938
961
  });
939
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
+
940
1014
  }
941
1015
 
942
1016
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/i18n",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
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
  }