Skip to content

Commit c7769e7

Browse files
committed
feat: enhance BulkActionButton and TranslationJobViewComponent for improved task handling and UI updates
1 parent 15cfb0d commit c7769e7

3 files changed

Lines changed: 169 additions & 26 deletions

File tree

custom/BulkActionButton.vue

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,13 @@
1919
]"
2020
>
2121
<template #trigger>
22-
<button
23-
v-if="checkboxes.length > 0"
24-
class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-lightListViewButtonText focus:outline-none bg-lightListViewButtonBackground rounded-default border border-lightListViewButtonBorder hover:bg-lightListViewButtonBackgroundHover hover:text-lightListViewButtonTextHover focus:z-10 focus:ring-4 focus:ring-lightListViewButtonFocusRing dark:focus:ring-darkListViewButtonFocusRing dark:bg-darkListViewButtonBackground dark:text-darkListViewButtonText dark:border-darkListViewButtonBorder dark:hover:text-darkListViewButtonTextHover dark:hover:bg-darkListViewButtonBackgroundHover"
25-
>
26-
<IconLanguageOutline class="w-5 h-5" />
27-
{{ t('Translate Selected') }} {{ `(${checkboxes.length})` }}
28-
<div class="text-white bg-gradient-to-r from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800
29-
font-medium rounded-sm text-xs px-1 ml-1 text-center ">
30-
AI
22+
<button class="flex items-center justify-center w-full">
23+
<IconLanguageOutline class="text-gray-500 dark:text-gray-400 w-5 h-5" />
24+
<div class="flex items-end justify-start gap-2 cursor-pointer">
25+
<p class="text-justify max-h-[18px] truncate max-w-[60vw] md:max-w-none">{{ t('Translate filtered') }}</p>
26+
<div class="flex items-center justify-center text-white bg-gradient-to-r h-[18px] from-purple-500 via-purple-600 to-purple-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-purple-300 dark:focus:ring-purple-800 font-medium rounded-md text-sm px-1 text-center">
27+
{{t('AI')}}
28+
</div>
3129
</div>
3230
</button>
3331
</template>
@@ -53,13 +51,17 @@
5351
import { IconLanguageOutline } from '@iconify-prerendered/vue-flowbite';
5452
import { useI18n } from 'vue-i18n';
5553
import { Dialog, Button, Checkbox } from '@/afcl';
56-
import { computed, onMounted, ref, watch } from 'vue';
54+
import { computed, onMounted, ref, onUnmounted } from 'vue';
5755
import { callAdminForthApi } from '@/utils';
5856
import { useAdminforth } from '@/adminforth';
5957
import { getCountryCodeFromLangCode } from './langCommon';
6058
import { getName, overwrite } from 'country-list';
6159
import ISO6391 from 'iso-639-1';
6260
import 'flag-icon-css/css/flag-icons.min.css';
61+
import websocket from '@/websocket';
62+
import { useFiltersStore } from '@/stores/filters';
63+
64+
const filtersStore = useFiltersStore();
6365
6466
const { t } = useI18n();
6567
const adminforth = useAdminforth();
@@ -84,10 +86,17 @@
8486
const noneChecked = computed(() => Object.values(checkedLanguages.value).every(value => !value));
8587
8688
onMounted(() => {
89+
websocket.subscribe('/translation_progress', (data) => {
90+
adminforth.list.refresh();
91+
});
8792
for (const lang of props.meta.supportedLanguages) {
8893
checkedLanguages.value[lang] = true;
8994
}
9095
});
96+
97+
onUnmounted( () => {
98+
websocket.unsubscribe('/translation_progress');
99+
} )
91100
92101
function selectAll() {
93102
for (const lang of props.meta.supportedLanguages) {
@@ -106,12 +115,13 @@
106115
}
107116
108117
async function runTranslation() {
118+
const listOfIds = await getListOfIds();
109119
try {
110120
const res = await callAdminForthApi({
111121
path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
112122
method: 'POST',
113123
body: {
114-
selectedIds: props.checkboxes,
124+
selectedIds: listOfIds,
115125
selectedLanguages: Object.keys(checkedLanguages.value).filter(lang => checkedLanguages.value[lang]),
116126
},
117127
silentError: true,
@@ -128,4 +138,26 @@
128138
}
129139
}
130140
141+
async function getListOfIds() {
142+
const filters = filtersStore.getFilters();
143+
let res;
144+
try {
145+
res = await callAdminForthApi({
146+
path: `/plugin/${props.meta.pluginInstanceId}/get_filtered_ids`,
147+
method: 'POST',
148+
body: { filters },
149+
silentError: true,
150+
});
151+
} catch (e) {
152+
console.error('Failed to get records for filtered selector:', e);
153+
return [];
154+
}
155+
if (!res?.ok || !res?.recordIds) {
156+
console.error('Failed to get records for filtered selector, response error:', res);
157+
return [];
158+
}
159+
return res.recordIds;
160+
}
161+
162+
131163
</script>

custom/TranslationJobViewComponent.vue

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,39 @@
99
<span class=" text-gray-500">{{ t('Total translation token used:') }}</span>
1010
<span class="text-lg font-semibold text-gray-900 dark:text-gray-100">{{ new Number(props.job.state?.totalUsedTokens).toLocaleString() || 0 }}</span>
1111
</div>
12+
</div>
13+
<div class="grid grid-cols-3 gap-2">
14+
<div class="bg-gray-50 hover:bg-gray-100 transition-all px-2 py-2 rounded-md border max-w-64 w-full flex items-center gap-2" v-for="(task, index) in translationTasks" :key="index">
15+
{{ task.state?.taskName }} to
16+
<span class="flag-icon"
17+
:class="`flag-icon-${getCountryCodeFromLangCode(task.state?.lang)}`"
18+
></span>
19+
<component
20+
:is="getCustomComponent({file: '@@/plugins/BackgroundJobsPlugin/StateToIcon.vue'})"
21+
:status="task.status"
22+
/>
23+
</div>
1224
</div>
1325
</template>
1426

1527

1628
<script setup lang="ts">
29+
import { AdminForthComponentDeclarationFull } from 'adminforth';
1730
import { useI18n } from 'vue-i18n'
31+
import { onMounted, onUnmounted, ref } from 'vue';
32+
import websocket from '@/websocket';
33+
import { getCountryCodeFromLangCode } from './langCommon';
34+
import { getCustomComponent } from '@/utils';
35+
import { off } from 'process';
36+
1837
const { t } = useI18n();
1938
39+
const translationTasks = ref<{state: Record<string, any>, status: string}[]>([]);
40+
2041
const props = defineProps<{
2142
meta: any;
22-
getJobTasks: (limit?: number, offset?: number) => Promise<{state: Record<string, any>, status: string}[]>;
43+
getJobTasks: (limit?: number, offset?: number) => Promise<
44+
{tasks: {state: Record<string, any>, status: string}[], total: number}>;
2345
job: {
2446
id: string;
2547
name: string;
@@ -34,4 +56,27 @@ const props = defineProps<{
3456
};
3557
}>();
3658
59+
onMounted(async () => {
60+
const {tasks, total} = await props.getJobTasks(20, 0);
61+
translationTasks.value = tasks;
62+
63+
websocket.subscribe(`/background-jobs-task-update/${props.job.id}`, (data: { taskIndex: number, status?: string, state?: Record<string, any> }) => {
64+
65+
if (data.state) {
66+
translationTasks.value[data.taskIndex].state = data.state;
67+
}
68+
if (data.status) {
69+
translationTasks.value[data.taskIndex].status = data.status;
70+
}
71+
72+
});
73+
74+
75+
});
76+
77+
onUnmounted(() => {
78+
websocket.unsubscribe(`/background-jobs-task-update/${props.job.id}`);
79+
});
80+
81+
3782
</script>

index.ts

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock } from "adminforth";
1+
import AdminForth, { AdminForthPlugin, Filters, suggestIfTypo, AdminForthDataTypes, RAMLock, filtersTools, AdminForthFilterOperators } from "adminforth";
22
import type { IAdminForth, IHttpServer, AdminForthComponentDeclaration, AdminForthResourceColumn, AdminForthResource, BeforeLoginConfirmationFunction, AdminForthConfigMenuItem, AdminUser } from "adminforth";
33
import type { PluginOptions, SupportedLanguage } from './types.js';
44
import iso6391 from 'iso-639-1';
@@ -481,12 +481,11 @@ export default class I18nPlugin extends AdminForthPlugin {
481481
if (!resourceConfig.options.pageInjections.list) {
482482
resourceConfig.options.pageInjections.list = {};
483483
}
484-
if (!resourceConfig.options.pageInjections.list.beforeActionButtons) {
485-
resourceConfig.options.pageInjections.list.beforeActionButtons = [];
484+
if (!resourceConfig.options.pageInjections.list.threeDotsDropdownItems) {
485+
resourceConfig.options.pageInjections.list.threeDotsDropdownItems = [];
486486
}
487487

488-
(resourceConfig.options.pageInjections.list.beforeActionButtons as AdminForthComponentDeclaration[]).push(pageInjection);
489-
488+
(resourceConfig.options.pageInjections.list.threeDotsDropdownItems as AdminForthComponentDeclaration[]).push(pageInjection);
490489

491490
// if there is menu item with resourceId, add .badge function showing number of untranslated strings
492491
const addBadgeCountToMenuItem = (menuItem: AdminForthConfigMenuItem) => {
@@ -658,7 +657,7 @@ export default class I18nPlugin extends AdminForthPlugin {
658657

659658
}
660659

661-
async translateToLang (
660+
async getTranslateToLangTasks (
662661
langIsoCode: SupportedLanguage,
663662
strings: { en_string: string, category: string }[],
664663
plurals=false,
@@ -754,8 +753,9 @@ export default class I18nPlugin extends AdminForthPlugin {
754753
\`\`\``;
755754
const stringBanchCopy = [...stringBanch];
756755
generationTasksInitialData.push(
757-
{
756+
{
758757
state: {
758+
taskName: `Translate ${strings.length} strings`,
759759
prompt: promptToGenerate,
760760
strings: strings.filter(s => stringBanchCopy.includes(s.en_string)),
761761
translations: translations.filter(t => stringBanchCopy.includes(t.en_string)),
@@ -826,10 +826,10 @@ export default class I18nPlugin extends AdminForthPlugin {
826826
async ([lang, strings]: [SupportedLanguage, { en_string: string, category: string }[]]) => {
827827
// first translate without plurals
828828
const stringsWithoutPlurals = strings.filter(s => !s.en_string.includes('|'));
829-
const noPluralTranslationsTasks = await this.translateToLang(lang, stringsWithoutPlurals, false, translations, updateStrings, needToTranslateByLang, adminUser);
829+
const noPluralTranslationsTasks = await this.getTranslateToLangTasks(lang, stringsWithoutPlurals, false, translations, updateStrings, needToTranslateByLang, adminUser);
830830

831831
const stringsWithPlurals = strings.filter(s => s.en_string.includes('|'));
832-
const pluralTranslationsTasks = await this.translateToLang(lang, stringsWithPlurals, true, translations, updateStrings, needToTranslateByLang, adminUser);
832+
const pluralTranslationsTasks = await this.getTranslateToLangTasks(lang, stringsWithPlurals, true, translations, updateStrings, needToTranslateByLang, adminUser);
833833
generationTasksInitialData = generationTasksInitialData.concat(noPluralTranslationsTasks || []).concat(pluralTranslationsTasks || []);
834834
}
835835
)
@@ -914,6 +914,7 @@ export default class I18nPlugin extends AdminForthPlugin {
914914
//handler function
915915
handler: async ({ jobId, setTaskStateField, getTaskStateField }) => {
916916
const initialState: {
917+
taskName: string,
917918
prompt?: string,
918919
strings?: { en_string: string, category: string }[],
919920
translations?: any[],
@@ -938,16 +939,19 @@ export default class I18nPlugin extends AdminForthPlugin {
938939
);
939940
}
940941
afLogger.debug(`Translation task for language ${initialState.lang} completed.`);
941-
if (initialState.failedToTranslate.length > 0) {
942-
afLogger.error(`Failed to translate some strings for language ${initialState.lang} in plugin ${this.constructor.name}:, ${initialState.failedToTranslate}`);
943-
}
942+
944943
const stateToSave = {
945-
strings: initialState.strings,
944+
taskName: initialState.taskName,
946945
lang: initialState.lang,
947946
failedToTranslate: initialState.failedToTranslate,
948947
}
949-
950948
await setTaskStateField(stateToSave);
949+
950+
this.adminforth.websocket.publish('/translation_progress', {});
951+
if (initialState.failedToTranslate.length > 0) {
952+
afLogger.error(`Failed to translate some strings for language ${initialState.lang} in plugin ${this.constructor.name}:, ${initialState.failedToTranslate}`);
953+
throw new Error(`Failed to translate some strings for language ${initialState.lang}, check job details for more info`);
954+
}
951955
},
952956
//limit of tasks, that are running in parallel
953957
parallelLimit: this.options.parallelTranslationLimit || 20,
@@ -1276,6 +1280,68 @@ export default class I18nPlugin extends AdminForthPlugin {
12761280
}
12771281
});
12781282

1283+
server.endpoint({
1284+
method: 'POST',
1285+
path: `/plugin/${this.pluginInstanceId}/get_filtered_ids`,
1286+
handler: async ({ body, adminUser, headers, query, cookies, requestUrl }) => {
1287+
const resource = this.resourceConfig;
1288+
1289+
for (const hook of resource.hooks?.list?.beforeDatasourceRequest || []) {
1290+
const filterTools = filtersTools.get(body);
1291+
body.filtersTools = filterTools;
1292+
const resp = await hook({
1293+
resource,
1294+
query: body,
1295+
adminUser,
1296+
//@ts-ignore
1297+
filtersTools: filterTools,
1298+
extra: {
1299+
body, query, headers, cookies, requestUrl
1300+
},
1301+
adminforth: this.adminforth,
1302+
});
1303+
if (!resp || (!resp.ok && !resp.error)) {
1304+
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
1305+
}
1306+
if (resp.error) {
1307+
return { error: resp.error };
1308+
}
1309+
}
1310+
const filters = body.filters;
1311+
1312+
const normalizedFilters = { operator: AdminForthFilterOperators.AND, subFilters: [] };
1313+
if (filters) {
1314+
if (typeof filters !== 'object') {
1315+
throw new Error(`Filter should be an array or an object`);
1316+
}
1317+
if (Array.isArray(filters)) {
1318+
// if filters are an array, they will be connected with "AND" operator by default
1319+
normalizedFilters.subFilters = filters;
1320+
} else if (filters.field) {
1321+
// assume filter is a SingleFilter
1322+
normalizedFilters.subFilters = [filters];
1323+
} else if (filters.subFilters) {
1324+
// assume filter is a AndOr filter
1325+
normalizedFilters.operator = filters.operator;
1326+
normalizedFilters.subFilters = filters.subFilters;
1327+
} else {
1328+
// wrong filter
1329+
throw new Error(`Wrong filter object value: ${JSON.stringify(filters)}`);
1330+
}
1331+
}
1332+
1333+
const records = await this.adminforth.resource(this.resourceConfig.resourceId).list(normalizedFilters);
1334+
if (!records) {
1335+
return { ok: true, recordIds: [] };
1336+
}
1337+
const primaryKeyColumn = this.resourceConfig.columns.find((col) => col.primaryKey);
1338+
1339+
const recordIds = records.map(record => record[primaryKeyColumn.name]);
1340+
1341+
return { ok: true, recordIds }
1342+
}
1343+
});
1344+
12791345
}
12801346

12811347
}

0 commit comments

Comments
 (0)