diff --git a/package.json b/package.json index 8a6f290..ff3df50 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "postcss": "^8.4.14", "tailwindcss": "^3.0.24", "typescript": "^4.5.4", - "vite": "^2.9.18" + "vite": "^7.3.1" }, "dependencies": { "@editorjs/editorjs": "^2.24.3", @@ -29,10 +29,11 @@ "buffer": "^6.0.3", "country-flag-icons": "^1.5.5", "events": "^3.3.0", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "notyf": "^3.10.0", "shepherd.js": "^10.0.0", "stream-browserify": "^3.0.0", - "tippy.js": "^6.3.7" + "tippy.js": "^6.3.7", + "util": "^0.12.5" } -} +} \ No newline at end of file diff --git a/src/components/Table/DAOTable.xml b/src/components/Table/DAOTable.xml index ceb3d38..1a5ac6e 100644 --- a/src/components/Table/DAOTable.xml +++ b/src/components/Table/DAOTable.xml @@ -63,7 +63,7 @@

Clear filters

- +
diff --git a/src/components/TranslatorModal.ts b/src/components/TranslatorModal.ts index 38c322c..3c5d68c 100644 --- a/src/components/TranslatorModal.ts +++ b/src/components/TranslatorModal.ts @@ -1,22 +1,22 @@ -import { Component, onWillUpdateProps, useState, xml } from "@odoo/owl"; +import {Component, onWillUpdateProps, useState, xml} from "@odoo/owl"; import notyf from "../notifications"; -import { models, Translator } from "../models"; +import {models, Translator} from "../models"; import TranslationSkills from "./TranslationSkills"; import Modal from "./Modal"; import Loader from './Loader'; import _ from "../i18n"; type State = { - loading: boolean; - active: boolean; - translator?: Translator; - title?: string; + loading: boolean; + active: boolean; + translator?: Translator; + title?: string; } class TranslatorModal extends Component { - static template = xml` + static template = xml`
@@ -55,69 +55,73 @@ class TranslatorModal extends Component {

Translation Skills

- +
`; - static props = { - translatorId: { type: Number, optional: true }, - onClose: { type: Function, optional: true }, - }; + static props = { + translatorId: {type: Number, optional: true}, + onClose: {type: Function, optional: true}, + }; - static components = { - Modal, - Loader, - TranslationSkills, - }; + static components = { + Modal, + Loader, + TranslationSkills, + }; - _ = _; + _ = _; - state = useState({ - loading: false, - active: false, - translator: undefined, - title: undefined, - }); - - setup(): void { - this.fetchTranslator(this.props.translatorId); - onWillUpdateProps((nextProps) => { - this.fetchTranslator(nextProps.translatorId); + state = useState({ + loading: false, + active: false, + translator: undefined, + title: undefined, }); - } - onClose() { - this.props.onClose(); - setTimeout(() => { - this.state.translator = undefined; - this.state.title = undefined; - }, 300); - } + setup(): void { + this.fetchTranslator(this.props.translatorId); + onWillUpdateProps((nextProps) => { + this.fetchTranslator(nextProps.translatorId); + }); + } + + onClose() { + this.props.onClose(); + setTimeout(() => { + this.state.translator = undefined; + this.state.title = undefined; + }, 300); + } - fetchTranslator(translatorId: number) { - if (translatorId) { - this.state.loading = true; - this.state.active = true; - models.translators.find(translatorId).then((user) => { - if (!user) { - notyf.error(_('User not found')); - this.state.active = false; - this.state.loading = false; - this.props.onClose(); + fetchTranslator(translatorId: number) { + if (translatorId) { + this.state.loading = true; + this.state.active = true; + models.translators.find(translatorId).then((user) => { + if (!user) { + notyf.error(_('User not found')); + this.state.active = false; + this.state.loading = false; + this.props.onClose(); + } else { + this.state.loading = false; + this.state.translator = user; + this.state.title = user.name; + } + }); } else { - this.state.loading = false; - this.state.translator = user; - this.state.title = user.name; + // Keep internal state while modal is closing + this.state.active = false; + this.state.loading = false; } - }); - } else { - // Keep internal state while modal is closing - this.state.active = false; - this.state.loading = false; } - } } export default TranslatorModal; \ No newline at end of file diff --git a/src/models/LetterDAO.ts b/src/models/LetterDAO.ts index b0198ff..140e047 100644 --- a/src/models/LetterDAO.ts +++ b/src/models/LetterDAO.ts @@ -5,247 +5,263 @@ type Status = 'done' | 'to do' | 'to validate' | 'in progress'; type Priority = 0 | 1 | 2 | 3 | 4; interface BaseElement { - type: 'paragraph' | 'pageBreak'; - id: number | string; -}; + type: 'paragraph' | 'pageBreak'; + id: number | string; +} export interface Paragraph extends BaseElement { - type: 'paragraph'; - content: string; - source: string; - comments?: string; -}; + type: 'paragraph'; + content: string; + source: string; + comments?: string; +} export interface PageBreak extends BaseElement { - type: 'pageBreak'; -}; + type: 'pageBreak'; +} export type Element = Paragraph | PageBreak; export type Person = { - preferredName: string; - sex: string; - age: number; - ref: string; + preferredName: string; + sex: string; + age: number; + ref: string; }; export type Letter = { - id: number | string; - status: Status; - priority: Priority; - title: string; - source: string; - target: string; - unreadComments: boolean; - translatorId?: number; - lastUpdate?: Date; - pdfUrl?: string; - date: Date; - translatedElements: Element[]; - child: Person, - sponsor: Person, - translationIssue: string; + id: number | string; + status: Status; + priority: Priority; + title: string; + source: string; + target: string; + unreadComments: boolean; + translatorId?: number; + lastUpdate?: Date; + pdfUrl?: string; + date: Date; + translatedElements: Element[]; + child: Person, + sponsor: Person, + translationIssue: string; }; export const letterFieldsMapping: FieldsMapping = { - status: { field: 'translation_status' }, - title: { field: 'name' }, - priority: { field: 'translation_priority', format: 'number' }, - unreadComments: { field: 'unread_comments', format: 'boolean' }, - date: { field: 'scanned_date', format: (v) => new Date(v) }, - source: { field: 'src_translation_lang_id.name' }, - target: { field: 'translation_language_id.name' }, - translatorId: { field: 'new_translator_id', format: 'number' }, - translationIssue: {field: 'translation_issue'} + status: { field: 'translation_status' }, + title: { field: 'name' }, + priority: { field: 'translation_priority', format: 'number' }, + unreadComments: { field: 'unread_comments', format: 'boolean' }, + date: { field: 'scanned_date', format: (v) => new Date(v) }, + source: { field: 'src_translation_lang_id.name' }, + target: { field: 'translation_language_id.name' }, + translatorId: { field: 'new_translator_id', format: 'number' }, + translationIssue: {field: 'translation_issue'} }; const int = (val: string | number) => typeof val === 'string' ? parseInt(val, 10) : val; type LetterDAOApi = { - /** - * Takes a letter object coming from an xml-rpc request and sanitizes it - * @param data - */ - cleanLetter(data: Letter | undefined): Letter | undefined; - - /** - * This method will be called when an administrator replied to a comment writen by a translator - * on a letter. It should send an email or do something - * @param letter - * @param reply - * @returns Promise, true if successful, false otherwise - */ - replyToComments(letter: Letter, reply: string): Promise; - - /** - * Delete the given letter object - * @param letter - */ - deleteLetter(letter: Letter): Promise; - - /** - * Put the given letter back in the to do state and restart its - * translation process - * @param letter - */ - makeTranslatable(letter: Letter): Promise; - - /** - * Updates the given letter, either creating or updating it - * @param letter - */ - update(letter: Letter): Promise; - - /** - * Submits the given letter - * @param letter - */ - submit(letter: Letter): Promise; - - /** - * Reports an issue regarding a letter - */ - reportIssue(letterId: string | number, issueType: string, message: string): Promise; - - /** - * Mark all comments as read - * @param letter - */ - markCommentsAsRead(letter: Letter): Promise; + /** + * Takes a letter object coming from an xml-rpc request and sanitizes it + * @param data + */ + cleanLetter(data: Letter | undefined): Letter | undefined; + + /** + * This method will be called when an administrator replied to a comment writen by a translator + * on a letter. It should send an email or do something + * @param letter + * @param reply + * @returns Promise, true if successful, false otherwise + */ + replyToComments(letter: Letter, reply: string): Promise; + + /** + * Delete the given letter object + * @param letter + */ + deleteLetter(letter: Letter): Promise; + + /** + * Put the given letter back in the to do state and restart its + * translation process + * @param letter + */ + makeTranslatable(letter: Letter): Promise; + + /** + * Updates the given letter, either creating or updating it + * @param letter + */ + update(letter: Letter): Promise; + + /** + * Submits the given letter + * @param letter + */ + submit(letter: Letter): Promise; + + /** + * Reports an issue regarding a letter + */ + reportIssue(letterId: string | number, issueType: string, message: string): Promise; + + /** + * Mark all comments as read + * @param letter + */ + markCommentsAsRead(letter: Letter): Promise; }; const LetterDAO: BaseDAO & LetterDAOApi = { - async listIds(params) { - const searchParams = generateSearchDomain(params.search, letterFieldsMapping); - const ids = await OdooAPI.execute_kw('correspondence', 'search', searchParams); - if (!ids) { - console.error('Unable to retrieve letter ids', params.search); - return []; - } else { - return ids; - } - }, - - async find(id) { - return this.cleanLetter( - await OdooAPI.execute_kw('correspondence', 'get_letter_info', [int(id)]) - ); - }, - - async list(params) { - const searchParams = generateSearchQuery(params, letterFieldsMapping); - // Add global state - // @ts-ignore - searchParams[0].push(['state', '=', 'Global Partner translation queue']); - const [letterIds, total] = await Promise.all([ - OdooAPI.execute_kw('correspondence', 'search', searchParams), - OdooAPI.execute_kw('correspondence', 'search', [...searchParams, true]) as Promise - ]); - - const rawLetters = await OdooAPI.execute_kw('correspondence', 'list_letters', [letterIds]); - const data = (rawLetters || []).map(it => this.cleanLetter(it)) as Letter[]; - return { - data, - total, - }; - }, - - async replyToComments(letter, html) { - try { - await OdooAPI.execute_kw('correspondence', 'reply_to_comments', [letter.id, html]); - return true; - } catch (e) { - console.error(e); - return false; - } - }, - - async deleteLetter(letter) { - try { - await OdooAPI.execute_kw('correspondence', 'remove_local_translate', [letter.id]); - return true; - } catch (e) { - console.error(e); - return false; - } - }, - - async makeTranslatable(letter) { - try { - await OdooAPI.execute_kw('correspondence', 'resubmit_to_translation', [[letter.id]]); - return true; - } catch (e) { - console.error(e); - return false; - } - }, - - async update(letter) { - try { - const res = await OdooAPI.execute_kw('correspondence', 'save_translation', [ - letter.id, - letter.translatedElements, - letter.translatorId, - ]); - return res || false; - } catch (e) { - console.error(e); - return false; + async listIds(params) { + const searchParams = generateSearchDomain(params.search, letterFieldsMapping); + const ids = await OdooAPI.execute_kw('correspondence', 'search', searchParams); + if (!ids) { + console.error('Unable to retrieve letter ids', params.search); + return []; + } else { + return ids; + } + }, + + async find(id) { + return this.cleanLetter( + await OdooAPI.execute_kw('correspondence', 'get_letter_info', [int(id)]) + ); + }, + + async list(params) { + const searchParams = generateSearchQuery(params, letterFieldsMapping); + + if (!Array.isArray(searchParams[0])) { + searchParams[0] = []; + } + + searchParams[0].push(['state', '=', 'Global Partner translation queue']); + + const domainOnly = [searchParams[0]]; + + try { + const [letterIds, total] = await Promise.all([ + OdooAPI.execute_kw('correspondence', 'search', searchParams), + OdooAPI.execute_kw('correspondence', 'search_count', domainOnly) as Promise + ]); + + if (!letterIds || (letterIds as number[]).length === 0) { + return { data: [], total: total || 0 }; + } + + const rawLetters = await OdooAPI.execute_kw('correspondence', 'list_letters', [letterIds]); + const data = (rawLetters || []).map(it => this.cleanLetter(it)).filter(it => it !== undefined) as Letter[]; + + return { + data, + total: total || 0, + }; + } catch (error) { + console.error("Error fetching letters list:", error); + throw error; + } + }, + + async replyToComments(letter, html) { + try { + await OdooAPI.execute_kw('correspondence', 'reply_to_comments', [letter.id, html]); + return true; + } catch (e) { + console.error(e); + return false; + } + }, + + async deleteLetter(letter) { + try { + await OdooAPI.execute_kw('correspondence', 'remove_local_translate', [letter.id]); + return true; + } catch (e) { + console.error(e); + return false; + } + }, + + async makeTranslatable(letter) { + try { + await OdooAPI.execute_kw('correspondence', 'resubmit_to_translation', [[letter.id]]); + return true; + } catch (e) { + console.error(e); + return false; + } + }, + + async update(letter) { + try { + const res = await OdooAPI.execute_kw('correspondence', 'save_translation', [ + letter.id, + letter.translatedElements, + letter.translatorId, + ]); + return res || false; + } catch (e) { + console.error(e); + return false; + } + }, + + async submit(letter) { + try { + await OdooAPI.execute_kw('correspondence', 'submit_translation', [ + letter.id, + letter.translatedElements, + letter.translatorId, + ]); + + return true; + } catch (e) { + console.error(e); + return false; + } + }, + + async reportIssue(letterId, type, message) { + try { + await OdooAPI.execute_kw('correspondence', 'raise_translation_issue', [int(letterId), type, message]); + return true; + } catch (e) { + console.error(e); + return false; + } + }, + + async markCommentsAsRead(letter) { + try { + await OdooAPI.execute_kw('correspondence', 'mark_comments_read', [letter.id]); + return true; + } catch (e) { + console.error(e); + return false; + } + }, + + cleanLetter(letter) { + if (!letter) { + return undefined; + } + + return { + ...letter, + status: letter.status || 'to do', + date: new Date(OdooAPI.ifNoneElse(letter.date, Date.now())), + // @ts-ignore + lastUpdate: OdooAPI.ifNoneElse(letter.lastUpdate) ? new Date(letter.lastUpdate) : undefined, + translatorId: OdooAPI.ifNoneElse(letter.translatorId), + translatedElements: OdooAPI.ifNoneElse(letter.translatedElements, []), + }; } - }, - - async submit(letter) { - try { - await OdooAPI.execute_kw('correspondence', 'submit_translation', [ - letter.id, - letter.translatedElements, - letter.translatorId, - ]); - - return true; - } catch (e) { - console.error(e); - return false; - } - }, - - async reportIssue(letterId, type, message) { - try { - await OdooAPI.execute_kw('correspondence', 'raise_translation_issue', [int(letterId), type, message]); - return true; - } catch (e) { - console.error(e); - return false; - } - }, - - async markCommentsAsRead(letter) { - try { - await OdooAPI.execute_kw('correspondence', 'mark_comments_read', [letter.id]); - return true; - } catch (e) { - console.error(e); - return false; - } - }, - - cleanLetter(letter) { - if (!letter) { - return undefined; - } - - return { - ...letter, - status: letter.status || 'to do', - date: new Date(OdooAPI.ifNoneElse(letter.date, Date.now())), - // @ts-ignore - lastUpdate: OdooAPI.ifNoneElse(letter.lastUpdate) ? new Date(letter.lastUpdate) : undefined, - translatorId: OdooAPI.ifNoneElse(letter.translatorId), - translatedElements: OdooAPI.ifNoneElse(letter.translatedElements, []), - }; - } }; export default LetterDAO; \ No newline at end of file diff --git a/src/models/TranslatorDAO.ts b/src/models/TranslatorDAO.ts index 0e62d43..f9e11f5 100644 --- a/src/models/TranslatorDAO.ts +++ b/src/models/TranslatorDAO.ts @@ -65,23 +65,41 @@ const TranslatorDAO: BaseDAO & TranslatorDAOApi = { async find(id) { return this.cleanTranslator( - await OdooAPI.execute_kw('translation.user', 'get_user_info', [id]) + await OdooAPI.execute_kw('translation.user', 'get_user_info', [id]) ); }, async list(params) { const searchParams = generateSearchQuery(params, translatorFieldsMapping); - const [translatorIds, total] = await Promise.all([ - OdooAPI.execute_kw('translation.user', 'search', searchParams), - OdooAPI.execute_kw('translation.user', 'search', [...searchParams, true]) as Promise - ]); - const rawTranslators = await OdooAPI.execute_kw('translation.user', 'list_users', [translatorIds]); - const data = (rawTranslators || []).map(it => this.cleanTranslator(it)).filter(it => it !== undefined) as Translator[]; - return { - total, - data, - }; + // 1. Sécuriser le domaine de recherche + if (!Array.isArray(searchParams[0])) { + searchParams[0] = []; + } + + const domainOnly = [searchParams[0]]; + + try { + const [translatorIds, total] = await Promise.all([ + OdooAPI.execute_kw('translation.user', 'search', searchParams), + OdooAPI.execute_kw('translation.user', 'search_count', domainOnly) as Promise + ]); + + if (!translatorIds || (translatorIds as number[]).length === 0) { + return { data: [], total: total || 0 }; + } + + const rawTranslators = await OdooAPI.execute_kw('translation.user', 'list_users', [translatorIds]); + const data = (rawTranslators || []).map(it => this.cleanTranslator(it)).filter(it => it !== undefined) as Translator[]; + + return { + total: total || 0, + data, + }; + } catch (error) { + console.error("Error fetching translators list:", error); + throw error; + } }, async listIds(params) { diff --git a/src/pages/Home/home.xml b/src/pages/Home/home.xml index 6857cf3..2d93e3f 100644 --- a/src/pages/Home/home.xml +++ b/src/pages/Home/home.xml @@ -27,16 +27,16 @@
- - + +

Thank you.

diff --git a/src/pages/Home/index.ts b/src/pages/Home/index.ts index fe93b8a..07d6556 100644 --- a/src/pages/Home/index.ts +++ b/src/pages/Home/index.ts @@ -61,9 +61,9 @@ export default class Home extends Component { }, { // Defined, tutorial is called from within setup, post refresh, meaning the translator is fetched - text: this.currentTranslator.data?.skills.length === 0 - ? _('You currently have no skills defined, let us begin by registering one or more') - : _('It seems you already have translation skills defined, let us see how you can manage them'), + text: (this.currentTranslator.data?.skills?.length ?? 0) === 0 + ? _('You currently have no skills defined, let us begin by registering one or more') + : _('It seems you already have translation skills defined, let us see how you can manage them') }, { beforeShowPromise: () => new Promise((resolve) => { @@ -109,7 +109,7 @@ export default class Home extends Component { this.state.loading = true; if (!this.currentTranslator.data) { - this.currentTranslator.refresh(); + await this.currentTranslator.refresh(); } await Promise.all([ @@ -160,50 +160,52 @@ export default class Home extends Component { if (!this.currentTranslator.data) return; - // Retrieve letters to validate for each skill of the current translator - const lettersToValidate = await Promise.all(this.currentTranslator.data.skills.map(async (skill) => { - const lettersToValidate = await models.letters.list({ - search: [ - { column: 'status', term: 'to validate' }, - { column: 'source', term: skill.source }, - { column: 'target', term: skill.target }, - ], - }); - - return { - skill, - letters: lettersToValidate.data, - } - })); - - // Sort the letters to have only the one that have a skill as unverified and a letter already waiting to be validated - this.state.lettersAwaitingValidation = lettersToValidate.filter((item) => { - return (!item.skill.verified); - }).filter(item => item.letters.length > 0); - - }; + const lettersToValidate = await Promise.all( + (this.currentTranslator.data.skills || []).map(async (skill) => { + const lettersToValidate = await models.letters.list({ + search: [ + { column: 'status', term: 'to validate' }, + { column: 'source', term: skill.source }, + { column: 'target', term: skill.target }, + ], + }); + + return { + skill, + letters: lettersToValidate.data, + }; + }) + ); + + this.state.lettersAwaitingValidation = lettersToValidate + .filter(item => !item.skill.verified) + .filter(item => item.letters.length > 0); + } async fetchLetters() { if (!this.currentTranslator.data) return; - const skillLetters = await Promise.all(this.currentTranslator.data.skills.map(async (skill) => { - const skillLetters = await models.letters.list({ - sortBy: ['priority desc','date asc'], - pageSize: 5, - pageNumber: 0, - search: [ - { column: 'status', term: 'to do' }, - { column: 'source', term: skill.source }, - { column: 'target', term: skill.target }, - { column: 'translationIssue', term: false, operator: '=' }, - ], - }); - - return { - skill, - total: skillLetters.total, - letters: skillLetters.data, - }; - })); + + const skillLetters = await Promise.all( + (this.currentTranslator.data.skills || []).map(async (skill) => { + const skillLetters = await models.letters.list({ + sortBy: ['priority desc','date asc'], + pageSize: 5, + pageNumber: 0, + search: [ + { column: 'status', term: 'to do' }, + { column: 'source', term: skill.source }, + { column: 'target', term: skill.target }, + { column: 'translationIssue', term: false, operator: '=' }, + ], + }); + + return { + skill, + total: skillLetters.total, + letters: skillLetters.data, + }; + }) + ); this.state.skillLetters = skillLetters.sort((a, b) => { if (a.skill.verified) return 1; diff --git a/vite.config.js b/vite.config.js index a52146d..934b612 100644 --- a/vite.config.js +++ b/vite.config.js @@ -23,6 +23,7 @@ export default defineConfig(({ mode }) => { alias: { stream: "stream-browserify", events: "events", + util: "util", }, }, optimizeDeps: {