complexqa_frontend_core 1.24.1 → 1.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "complexqa_frontend_core",
3
- "version": "1.24.1",
3
+ "version": "1.25.1",
4
4
  "description": "core of web ",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,55 @@
1
+ import { ApiAbstractElementClass } from './api_abstract_element_class.js';
2
+ import { typeAttachment } from '../types/family_elements/typeAttachment.js';
3
+
4
+ /**
5
+ * CRUD / Search / delete_file для attachment.
6
+ *
7
+ * @version v.0.1 (13/08/2026)
8
+ */
9
+ export class AttachmentApi extends ApiAbstractElementClass
10
+ {
11
+ module_prefix = 'attachment';
12
+
13
+ constructor(options)
14
+ {
15
+ super(options);
16
+ this.set_element_class_instance(typeAttachment);
17
+ }
18
+
19
+
20
+ /**
21
+ * Delete file in File App and unlink all attachments for that file_uuid.
22
+ *
23
+ * POST /web_api/attachment/delete_file
24
+ *
25
+ * @param {object} payload
26
+ * @param {string} payload.file_uuid
27
+ * @returns {Promise<{response: *, payload: *}>}
28
+ */
29
+ async delete_file(payload)
30
+ {
31
+ if (!this.module_prefix)
32
+ {
33
+ throw new Error('Error #attachment-delete-file-1152 in AttachmentApi');
34
+ }
35
+
36
+ if (this.stage === 'development')
37
+ {
38
+ return {
39
+ response: payload,
40
+ payload : payload,
41
+ };
42
+ }
43
+
44
+ let url = `/${ this.api_prefix }/${ this.module_prefix }/delete_file`;
45
+
46
+ payload = this.handle_request_payload(payload);
47
+ let response = this.api_request(url, payload);
48
+ response = this.handle_response(response);
49
+
50
+ return {
51
+ response: response,
52
+ payload : payload,
53
+ };
54
+ }
55
+ }
@@ -114,4 +114,26 @@ export class FileAppApi extends ApiAbstractClass
114
114
  payload : payload,
115
115
  };
116
116
  }
117
+
118
+
119
+ /**
120
+ * POST /web_api/file_app/delete_file
121
+ *
122
+ * @param {object} payload
123
+ * @param {string} payload.file_uuid
124
+ *
125
+ * @returns {Promise<{payload: object, response: object}>}
126
+ */
127
+ async delete_file(payload)
128
+ {
129
+ const url = `/${ this.api_prefix }/${ this.module_prefix }/delete_file`;
130
+ payload = this.handle_request_payload(payload);
131
+ let response = this.api_request(url, payload);
132
+ response = await this.handle_response(response, false);
133
+
134
+ return {
135
+ response: response,
136
+ payload : payload,
137
+ };
138
+ }
117
139
  }
@@ -13,6 +13,7 @@ import { TaskApi } from "./task_api.js";
13
13
  import { TestAccountApi } from "./test_account_api.js";
14
14
  import { BugApi } from "./bug_api.js";
15
15
  import { BugRelationApi } from "./bug_relation_api.js";
16
+ import { AttachmentApi } from "./attachment_api.js";
16
17
  import { TagApi } from "./tag_api.js";
17
18
  import { TagRelationApi } from "./tag_relation_api.js";
18
19
  import { TagColorApi } from "./tag/tag_color_api.js";
@@ -170,6 +171,7 @@ export class Api
170
171
  this.test_account = new TestAccountApi(payload);
171
172
  this.bug = new BugApi(payload);
172
173
  this.bug_relation = new BugRelationApi(payload);
174
+ this.attachment = new AttachmentApi(payload);
173
175
  this.tag = new TagApi(payload);
174
176
  this.tag_relation = new TagRelationApi(payload);
175
177
  this.event = new EventApi(payload);
@@ -4,6 +4,7 @@ import { typeTestRun } from '../../../types/family_elements/typeTestRun.js';
4
4
  import { typeBug } from '../../../types/family_elements/typeBug.js';
5
5
  import { typeBugRelation } from '../../../types/family_relation/typeBugRelation.js';
6
6
  import { typeTag } from '../../../types/family_elements/typeTag.js';
7
+ import { typeAttachment } from '../../../types/family_elements/typeAttachment.js';
7
8
  import { typeTagRelation } from '../../../types/family_relation/typeTagRelation.js';
8
9
  import { typeTestRunResult } from '../../../types/family_elements/typeTestRunResult.js';
9
10
  import { typeTestAccount } from '../../../types/family_elements/typeTestAccount.js';
@@ -33,6 +34,7 @@ export const TABLE_ELEMENT_MODELS = {
33
34
  test_run : typeTestRun,
34
35
  bug : typeBug,
35
36
  bug_relation : typeBugRelation,
37
+ attachment : typeAttachment,
36
38
  tag : typeTag,
37
39
  tag_relation : typeTagRelation,
38
40
  test_run_result: typeTestRunResult,
package/publish/index.js CHANGED
@@ -17,6 +17,7 @@ export { typeTestRunResult } from './types/family_elements/typeTestRunResult.js'
17
17
  export { typeTestAccount } from './types/family_elements/typeTestAccount.js'
18
18
  export {typeBug} from './types/family_elements/typeBug.js';
19
19
  export { typeTag } from './types/family_elements/typeTag.js';
20
+ export { typeAttachment } from './types/family_elements/typeAttachment.js';
20
21
 
21
22
  export { familyRelation } from './types/core/1_familyRelation.js';
22
23
  export { typeBugRelation } from './types/family_relation/typeBugRelation.js';
@@ -75,13 +75,53 @@ export class FileAppService extends abstractService
75
75
  }
76
76
 
77
77
 
78
+ /**
79
+ * Whether entity_type is present for File App binding.
80
+ * Empty / null / 0 → omit from token and upload init.
81
+ *
82
+ * @param {*} value
83
+ * @returns {boolean}
84
+ */
85
+ static #has_entity_type(value)
86
+ {
87
+ if (value === null || value === undefined)
88
+ {
89
+ return false;
90
+ }
91
+
92
+ const s = String(value).trim();
93
+
94
+ return s !== '' && s !== '0';
95
+ }
96
+
97
+
98
+ /**
99
+ * Whether entity_id is present for File App binding.
100
+ * Empty / null / 0 / NaN → omit from token and upload init.
101
+ *
102
+ * @param {*} value
103
+ * @returns {boolean}
104
+ */
105
+ static #has_entity_id(value)
106
+ {
107
+ if (value === null || value === undefined || value === '')
108
+ {
109
+ return false;
110
+ }
111
+
112
+ const n = Number(value);
113
+
114
+ return !Number.isNaN(n) && n !== 0;
115
+ }
116
+
117
+
78
118
  /**
79
119
  * Upload browser File/Blob to File App (token via Main App bridge).
80
120
  *
81
121
  * @param {object} payload
82
122
  * @param {File|Blob} payload.file
83
- * @param {string} payload.entity_type
84
- * @param {number} payload.entity_id
123
+ * @param {string} [payload.entity_type] — omit / null / 0 for unbound upload (attachments)
124
+ * @param {number} [payload.entity_id] — omit / null / 0 for unbound upload (attachments)
85
125
  * @param {string} payload.category
86
126
  * @param {string} [payload.original_name]
87
127
  * @param {string} [payload.file_uuid] — re-upload / new version for existing file
@@ -97,20 +137,24 @@ export class FileAppService extends abstractService
97
137
  throw new Error('FileAppService.upload_file: file is required');
98
138
  }
99
139
 
100
- const entity_type = String(payload.entity_type || '');
101
- const entity_id = Number(payload.entity_id);
102
- const category = String(payload.category || '');
103
- const mime_type = file.type || 'application/octet-stream';
104
- const size_bytes = Number(file.size || 0);
105
- const original_name = String(payload.original_name || file.name || 'upload');
106
- const api = FileAppService.#get_api_service();
107
-
108
- const token_wrap = await api.file_app.issue_access_token({
109
- scopes : [ 'file:upload', 'file:read' ],
110
- entity_type,
111
- entity_id,
140
+ const has_entity_type = FileAppService.#has_entity_type(payload.entity_type);
141
+ const has_entity_id = FileAppService.#has_entity_id(payload.entity_id);
142
+ const entity_type = has_entity_type ? String(payload.entity_type) : undefined;
143
+ const entity_id = has_entity_id ? Number(payload.entity_id) : undefined;
144
+ const category = String(payload.category || '');
145
+ const mime_type = file.type || 'application/octet-stream';
146
+ const size_bytes = Number(file.size || 0);
147
+ const original_name = String(payload.original_name || file.name || 'upload');
148
+ const api = FileAppService.#get_api_service();
149
+
150
+ const token_payload = {
151
+ scopes: [ 'file:upload', 'file:read' ],
112
152
  size_bytes,
113
- });
153
+ ...(has_entity_type ? { entity_type } : {}),
154
+ ...(has_entity_id ? { entity_id } : {}),
155
+ };
156
+
157
+ const token_wrap = await api.file_app.issue_access_token(token_payload);
114
158
 
115
159
  const token_data = await token_wrap?.response;
116
160
 
@@ -130,15 +174,17 @@ export class FileAppService extends abstractService
130
174
  throw new ApiException({ message: 'File App access token was not issued' });
131
175
  }
132
176
 
133
- const init_response = await axios.post(`${ file_app_url }/api/v1/uploads`, {
177
+ const init_payload = {
134
178
  category,
135
179
  mime_type,
136
180
  size_bytes,
137
181
  original_name,
138
- entity_type,
139
- entity_id,
182
+ ...(has_entity_type ? { entity_type } : {}),
183
+ ...(has_entity_id ? { entity_id } : {}),
140
184
  ...(payload.file_uuid ? { file_uuid: String(payload.file_uuid) } : {}),
141
- }, {
185
+ };
186
+
187
+ const init_response = await axios.post(`${ file_app_url }/api/v1/uploads`, init_payload, {
142
188
  headers: {
143
189
  Authorization: `Bearer ${ access_token }`,
144
190
  Accept : 'application/json',
@@ -183,8 +229,8 @@ export class FileAppService extends abstractService
183
229
  const wire = {
184
230
  ...confirmed,
185
231
  category : confirmed.category || category,
186
- entity_type: confirmed.entity_type || entity_type,
187
- entity_id : confirmed.entity_id ?? entity_id,
232
+ entity_type: confirmed.entity_type ?? entity_type ?? null,
233
+ entity_id : confirmed.entity_id ?? entity_id ?? null,
188
234
  mime_type : confirmed.mime_type || mime_type,
189
235
  };
190
236
 
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Переводы для typeAttachment.
3
+ *
4
+ * @see typeAttachment
5
+ */
6
+ export const typeAttachment_i18n = {
7
+ attribute_name_translate_matrix: {
8
+ en: {
9
+ attachment_id: 'ID',
10
+ team_id: 'Team',
11
+ project_id: 'Project',
12
+ element_type: 'Element type',
13
+ element_id: 'Element',
14
+ file_uuid: 'File',
15
+ caption: 'Caption',
16
+ sort_order: 'Sort order',
17
+ user_id: 'User',
18
+ created_at: 'Created',
19
+ updated_at: 'Updated',
20
+ file: 'File',
21
+ },
22
+ ru: {
23
+ attachment_id: 'ID',
24
+ team_id: 'Команда',
25
+ project_id: 'Проект',
26
+ element_type: 'Тип элемента',
27
+ element_id: 'Элемент',
28
+ file_uuid: 'Файл',
29
+ caption: 'Подпись',
30
+ sort_order: 'Порядок',
31
+ user_id: 'Пользователь',
32
+ created_at: 'Создан',
33
+ updated_at: 'Обновлён',
34
+ file: 'Файл',
35
+ },
36
+ de: {
37
+ attachment_id: 'ID',
38
+ team_id: 'Team',
39
+ project_id: 'Projekt',
40
+ element_type: 'Elementtyp',
41
+ element_id: 'Element',
42
+ file_uuid: 'Datei',
43
+ caption: 'Beschriftung',
44
+ sort_order: 'Sortierung',
45
+ user_id: 'Benutzer',
46
+ created_at: 'Erstellt',
47
+ updated_at: 'Aktualisiert',
48
+ file: 'Datei',
49
+ },
50
+ ar: {
51
+ attachment_id: 'ID',
52
+ team_id: 'الفريق',
53
+ project_id: 'المشروع',
54
+ element_type: 'نوع العنصر',
55
+ element_id: 'العنصر',
56
+ file_uuid: 'الملف',
57
+ caption: 'التسمية',
58
+ sort_order: 'ترتيب الفرز',
59
+ user_id: 'المستخدم',
60
+ created_at: 'تاريخ الإنشاء',
61
+ updated_at: 'تاريخ التحديث',
62
+ file: 'الملف',
63
+ },
64
+ es: {
65
+ attachment_id: 'ID',
66
+ team_id: 'Equipo',
67
+ project_id: 'Proyecto',
68
+ element_type: 'Tipo de elemento',
69
+ element_id: 'Elemento',
70
+ file_uuid: 'Archivo',
71
+ caption: 'Leyenda',
72
+ sort_order: 'Orden',
73
+ user_id: 'Usuario',
74
+ created_at: 'Creado',
75
+ updated_at: 'Actualizado',
76
+ file: 'Archivo',
77
+ },
78
+ fr: {
79
+ attachment_id: 'ID',
80
+ team_id: 'Équipe',
81
+ project_id: 'Projet',
82
+ element_type: 'Type d\'élément',
83
+ element_id: 'Élément',
84
+ file_uuid: 'Fichier',
85
+ caption: 'Légende',
86
+ sort_order: 'Ordre',
87
+ user_id: 'Utilisateur',
88
+ created_at: 'Créé',
89
+ updated_at: 'Mis à jour',
90
+ file: 'Fichier',
91
+ },
92
+ hy: {
93
+ attachment_id: 'ID',
94
+ team_id: 'Թիմ',
95
+ project_id: 'Նախագիծ',
96
+ element_type: 'Էլեմենտի տեսակ',
97
+ element_id: 'Էլեմենտ',
98
+ file_uuid: 'Ֆայլ',
99
+ caption: 'Ստորագրություն',
100
+ sort_order: 'Դասավորություն',
101
+ user_id: 'Օգտատեր',
102
+ created_at: 'Ստեղծված',
103
+ updated_at: 'Թարմացված',
104
+ file: 'Ֆայլ',
105
+ },
106
+ it: {
107
+ attachment_id: 'ID',
108
+ team_id: 'Team',
109
+ project_id: 'Progetto',
110
+ element_type: 'Tipo di elemento',
111
+ element_id: 'Elemento',
112
+ file_uuid: 'File',
113
+ caption: 'Didascalia',
114
+ sort_order: 'Ordine',
115
+ user_id: 'Utente',
116
+ created_at: 'Creato',
117
+ updated_at: 'Aggiornato',
118
+ file: 'File',
119
+ },
120
+ pl: {
121
+ attachment_id: 'ID',
122
+ team_id: 'Zespół',
123
+ project_id: 'Projekt',
124
+ element_type: 'Typ elementu',
125
+ element_id: 'Element',
126
+ file_uuid: 'Plik',
127
+ caption: 'Podpis',
128
+ sort_order: 'Kolejność',
129
+ user_id: 'Użytkownik',
130
+ created_at: 'Utworzono',
131
+ updated_at: 'Zaktualizowano',
132
+ file: 'Plik',
133
+ },
134
+ pt: {
135
+ attachment_id: 'ID',
136
+ team_id: 'Equipe',
137
+ project_id: 'Projeto',
138
+ element_type: 'Tipo de elemento',
139
+ element_id: 'Elemento',
140
+ file_uuid: 'Arquivo',
141
+ caption: 'Legenda',
142
+ sort_order: 'Ordem',
143
+ user_id: 'Usuário',
144
+ created_at: 'Criado',
145
+ updated_at: 'Atualizado',
146
+ file: 'Arquivo',
147
+ },
148
+ tt: {
149
+ attachment_id: 'ID',
150
+ team_id: 'Команда',
151
+ project_id: 'Проект',
152
+ element_type: 'Элемент тибы',
153
+ element_id: 'Элемент',
154
+ file_uuid: 'Файл',
155
+ caption: 'Язма',
156
+ sort_order: 'Тәртип',
157
+ user_id: 'Кулланучы',
158
+ created_at: 'Ясалган',
159
+ updated_at: 'Яңартылган',
160
+ file: 'Файл',
161
+ },
162
+ },
163
+
164
+ enum_value_translate_matrix: {
165
+ element_type: {
166
+ en: {
167
+ bug: 'Bug',
168
+ },
169
+ ru: {
170
+ bug: 'Баг',
171
+ },
172
+ de: {
173
+ bug: 'Fehler',
174
+ },
175
+ ar: {
176
+ bug: 'الخلل',
177
+ },
178
+ es: {
179
+ bug: 'Defecto',
180
+ },
181
+ fr: {
182
+ bug: 'Anomalie',
183
+ },
184
+ hy: {
185
+ bug: 'Խաուխ',
186
+ },
187
+ it: {
188
+ bug: 'Difetto',
189
+ },
190
+ pl: {
191
+ bug: 'Błąd',
192
+ },
193
+ pt: {
194
+ bug: 'Defeito',
195
+ },
196
+ tt: {
197
+ bug: 'Хата',
198
+ },
199
+ },
200
+ },
201
+ };
@@ -0,0 +1,119 @@
1
+ import { familyGeneralElement } from "../core/0_familyGeneralElement.js";
2
+ import { is_object } from "../../utils/utils.js";
3
+ import _ from 'underscore';
4
+ import { typeAttachment_i18n } from "../element_translate/typeAttachment.i18n.js";
5
+
6
+ /**
7
+ * First-class link entity: element ↔ File App file (`file_uuid`).
8
+ *
9
+ * Not a family_relation — polymorphic target via `element_type` + `element_id`,
10
+ * file identity via public `file_uuid` (not File App internal `file_id`).
11
+ *
12
+ * @see docs/Attachments/attachment-docs.MD
13
+ *
14
+ * @version v.0.1 (13/08/2026)
15
+ */
16
+ export class typeAttachment extends familyGeneralElement
17
+ {
18
+ attachment_id;
19
+ team_id;
20
+ project_id;
21
+ element_type;
22
+ element_id;
23
+ file_uuid;
24
+ caption;
25
+ sort_order;
26
+ user_id;
27
+ created_at;
28
+ updated_at;
29
+
30
+ /** Hydrated via search options.add: ['files'] */
31
+ file;
32
+
33
+ primary_key = 'attachment_id';
34
+ api_key = 'attachment';
35
+
36
+ structure_scheme = {
37
+ attachment_id: 'integer | require',
38
+ team_id : 'integer | require',
39
+ project_id : 'integer | require',
40
+ element_type : 'string | enum | require',
41
+ element_id : 'integer | require',
42
+ file_uuid : 'string | require',
43
+ caption : 'string | optional',
44
+ sort_order : 'integer | optional',
45
+ user_id : 'integer | optional',
46
+ created_at : 'timestamp | optional',
47
+ updated_at : 'timestamp | optional',
48
+ };
49
+
50
+ create_scheme = [
51
+ 'project_id',
52
+ 'element_type',
53
+ 'element_id',
54
+ 'file_uuid',
55
+ 'caption',
56
+ 'sort_order',
57
+ ];
58
+
59
+ available_enum_values = {
60
+ element_type: typeAttachment.get_element_type_values(),
61
+ };
62
+
63
+ enum_value_translate_matrix = typeAttachment_i18n.enum_value_translate_matrix;
64
+
65
+ attribute_name_translate_matrix = typeAttachment_i18n.attribute_name_translate_matrix;
66
+
67
+
68
+ /**
69
+ * Canonical element_type values (WITHOUT `_id` suffix). MVP: bug only.
70
+ *
71
+ * @returns {string[]}
72
+ */
73
+ static get_element_type_values()
74
+ {
75
+ return [ 'bug' ];
76
+ }
77
+
78
+
79
+ /**
80
+ * @param {object|false|undefined} data
81
+ * @returns {typeAttachment}
82
+ */
83
+ constructor(data = false)
84
+ {
85
+ super();
86
+
87
+ if (data && is_object(data))
88
+ {
89
+ _.mapObject(data, (value, key) =>
90
+ {
91
+ if (this.hasOwnProperty(key))
92
+ {
93
+ this[ key ] = value;
94
+ }
95
+ });
96
+ }
97
+
98
+ return this;
99
+ }
100
+
101
+
102
+ /**
103
+ * @returns {string}
104
+ */
105
+ get_element_name()
106
+ {
107
+ if (this.caption)
108
+ {
109
+ return String(this.caption);
110
+ }
111
+
112
+ if (this.file_uuid)
113
+ {
114
+ return String(this.file_uuid);
115
+ }
116
+
117
+ return String(this.attachment_id ?? '');
118
+ }
119
+ }
@@ -58,6 +58,7 @@ export class familyEvent extends familyGeneralElement
58
58
  'task',
59
59
  'bug',
60
60
  'bug_relation',
61
+ 'attachment',
61
62
  'tag',
62
63
  'tag_relation',
63
64
  'test_account',
@@ -21,8 +21,8 @@ export class typeFileScreenshot extends typeFile
21
21
  *
22
22
  * @param {object} payload
23
23
  * @param {File|Blob} payload.file
24
- * @param {string} payload.entity_type
25
- * @param {number} payload.entity_id
24
+ * @param {string} [payload.entity_type] — omit / null / 0 for unbound upload (attachments)
25
+ * @param {number} [payload.entity_id] — omit / null / 0 for unbound upload (attachments)
26
26
  * @param {string} [payload.original_name]
27
27
  *
28
28
  * @returns {Promise<typeFileScreenshot>}