@coopenomics/desktop 2025.11.29-2 → 2025.12.2-2

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": "@coopenomics/desktop",
3
- "version": "2025.11.29-2",
3
+ "version": "2025.12.2-2",
4
4
  "description": "A Desktop Project",
5
5
  "productName": "Desktop App",
6
6
  "author": "Alex Ant <dacom.dark.sun@gmail.com>",
@@ -25,9 +25,9 @@
25
25
  "start": "node -r ./alias-resolver.js dist/ssr/index.js"
26
26
  },
27
27
  "dependencies": {
28
- "@coopenomics/controller": "2025.11.29-2",
29
- "@coopenomics/notifications": "2025.11.29-2",
30
- "@coopenomics/sdk": "2025.11.29-2",
28
+ "@coopenomics/controller": "2025.12.2-2",
29
+ "@coopenomics/notifications": "2025.12.2-2",
30
+ "@coopenomics/sdk": "2025.12.2-2",
31
31
  "@dicebear/collection": "^9.0.1",
32
32
  "@dicebear/core": "^9.0.1",
33
33
  "@editorjs/code": "^2.9.3",
@@ -59,7 +59,7 @@
59
59
  "@wharfkit/wallet-plugin-privatekey": "^1.1.0",
60
60
  "axios": "^1.2.1",
61
61
  "compression": "^1.7.4",
62
- "cooptypes": "2025.11.29-2",
62
+ "cooptypes": "2025.12.2-2",
63
63
  "dompurify": "^3.1.7",
64
64
  "dotenv": "^16.4.5",
65
65
  "email-regex": "^5.0.0",
@@ -123,5 +123,5 @@
123
123
  "npm": ">= 6.13.4",
124
124
  "yarn": ">= 1.21.1"
125
125
  },
126
- "gitHead": "27badfb88a87da6e4d201d0931d6a4de3d7553c1"
126
+ "gitHead": "e552840cc42ff1e352880c51cc24587ed2c97c6c"
127
127
  }
@@ -23,7 +23,6 @@
23
23
  v-if='isInstall',
24
24
  :schema='extension.schema',
25
25
  v-model:config='data',
26
- :instructions='extension.instructions',
27
26
  :form-ref='myFormRef'
28
27
  )
29
28
  </template>
@@ -15,15 +15,11 @@ export async function useInitAppProcess(router: Router) {
15
15
  applyThemeFromStorage();
16
16
  const system = useSystemStore();
17
17
 
18
- // Загружаем системную информацию только на клиенте
19
- // На сервере SSR это создает лишнюю нагрузку и задержку рендеринга
20
- if (!isServer) {
21
- try {
22
- await system.loadSystemInfo();
23
- } catch (error) {
24
- console.warn('Failed to load initial system info, backend might be unavailable:', error);
25
- // Продолжаем инициализацию даже при недоступности бэкенда
26
- }
18
+ try {
19
+ await system.loadSystemInfo();
20
+ } catch (error) {
21
+ console.warn('Failed to load initial system info, backend might be unavailable:', error);
22
+ // Продолжаем инициализацию даже при недоступности бэкенда
27
23
  }
28
24
 
29
25
  // Запускаем мониторинг системной информации для отслеживания статуса
@@ -19,15 +19,21 @@ div.settings-form
19
19
  template(v-slot:prepend v-if="item.property.description?.prepend")
20
20
  span {{ item.property.description.prepend }}
21
21
 
22
- // Слот для append, если указано
23
- template(v-slot:append v-if="item.property.description?.append")
24
- span {{ item.property.description.append }}
22
+ // Слот для append, если указано или нужна иконка видимости пароля/копирования
23
+ template(v-slot:append)
24
+ span(v-if="item.property.description?.append") {{ item.property.description.append }}
25
+ q-icon.cursor-pointer(
26
+ v-if="shouldShowCopyIcon(item.property) || item.property.description?.password"
27
+ :name="getIconName(item.propertyName)"
28
+ @click="handleIconClick(item.propertyName)"
29
+ )
25
30
 
26
31
  </template>
27
32
  <script lang="ts" setup>
28
33
  import { defineProps, defineEmits, reactive, watch, computed } from 'vue';
29
- import { QInput, QCheckbox, QSelect } from 'quasar';
34
+ import { QInput, QCheckbox, QSelect, copyToClipboard } from 'quasar';
30
35
  import type { IExtensionConfigSchema, ISchemaProperty } from 'src/entities/Extension/model';
36
+ import { SuccessAlert } from 'src/shared/api/alerts';
31
37
 
32
38
  // Устанавливаем имя компонента для рекурсивного вызова
33
39
  defineOptions({
@@ -38,10 +44,86 @@ div.settings-form
38
44
  const props = defineProps<{
39
45
  schema: IExtensionConfigSchema;
40
46
  modelValue: Record<string, any>;
47
+ /** Режим установки - включает генерацию значений для полей с generator */
48
+ installMode?: boolean;
41
49
  }>();
42
50
 
43
51
  const emit = defineEmits(['update:modelValue']);
44
52
 
53
+ // Состояние видимости паролей для каждого поля
54
+ const passwordVisibility = reactive<Record<string, boolean>>({});
55
+
56
+ // Функция переключения видимости пароля
57
+ function togglePasswordVisibility(propertyName: string) {
58
+ passwordVisibility[propertyName] = !passwordVisibility[propertyName];
59
+ }
60
+
61
+
62
+ // Получение имени иконки для поля
63
+ function getIconName(propertyName: string): string | undefined {
64
+ const property = visibleProperties.value.find(p => p.propertyName === propertyName)?.property;
65
+
66
+ if (shouldShowCopyIcon(property)) {
67
+ return 'content_copy'; // Иконка копирования
68
+ } else if (property?.description?.password) {
69
+ return passwordVisibility[propertyName] ? 'visibility' : 'visibility_off';
70
+ }
71
+
72
+ return undefined;
73
+ }
74
+
75
+ // Определяем, нужно ли показывать иконку копирования
76
+ function shouldShowCopyIcon(property: ISchemaProperty | undefined): boolean {
77
+ if (!property?.description) return false;
78
+
79
+ // В режиме установки показываем копирование для полей с generator или copyable
80
+ if (props.installMode) {
81
+ return !!(property.description.generator || property.description.copyable);
82
+ }
83
+
84
+ // В обычном режиме показываем копирование только для полей с copyable
85
+ return !!property.description.copyable;
86
+ }
87
+
88
+ // Обработка клика по иконке
89
+ function handleIconClick(propertyName: string) {
90
+ const property = visibleProperties.value.find(p => p.propertyName === propertyName)?.property;
91
+
92
+ if (shouldShowCopyIcon(property)) {
93
+ // Копируем значение поля в буфер обмена
94
+ const valueToCopy = data[propertyName] || props.modelValue?.[propertyName] || '';
95
+ copyToClipboard(valueToCopy);
96
+
97
+ // Показываем уведомление об успешном копировании
98
+ SuccessAlert('Значение скопировано в буфер обмена');
99
+ } else if (property?.description?.password) {
100
+ // Переключаем видимость пароля
101
+ togglePasswordVisibility(propertyName);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Генерирует криптографически стойкую случайную строку
107
+ * @returns Hex-строка длиной 64 символа (256 бит)
108
+ */
109
+ function generateRandomSecret(): string {
110
+ const array = new Uint8Array(32);
111
+ crypto.getRandomValues(array);
112
+ return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
113
+ }
114
+
115
+ /**
116
+ * Генерирует значение для поля на основе типа генератора
117
+ */
118
+ function generateValue(generatorType: string): any {
119
+ switch (generatorType) {
120
+ case 'randomSecret':
121
+ return generateRandomSecret();
122
+ default:
123
+ return null;
124
+ }
125
+ }
126
+
45
127
  // Вычисляемое свойство для видимых свойств
46
128
  const visibleProperties = computed(() => {
47
129
  return Object.entries(props.schema.properties)
@@ -55,16 +137,27 @@ div.settings-form
55
137
  // Инициализируем реактивные данные
56
138
  const data = reactive<Record<string, any>>({});
57
139
 
58
- // Функция для установки значений по умолчанию
59
- function setDefaults(schema: ISchemaProperty, obj: any) {
140
+ // Функция для установки значений по умолчанию (с генерацией при необходимости)
141
+ function setDefaults(schema: ISchemaProperty, obj: any, sourceValues?: Record<string, any>) {
60
142
  if (schema.type === 'object' && schema.properties) {
61
143
  for (const key in schema.properties) {
62
144
  const property = schema.properties[key];
63
- if (property.default !== undefined) {
145
+ const existingValue = sourceValues?.[key];
146
+
147
+ // Если есть существующее значение (непустая строка для строковых полей)
148
+ if (existingValue !== undefined && existingValue !== null && existingValue !== '') {
149
+ obj[key] = existingValue;
150
+ } else if (props.installMode && property.description?.generator) {
151
+ // В режиме установки генерируем значение для полей с generator
152
+ obj[key] = generateValue(property.description.generator);
153
+ } else if (props.installMode && property.description?.default !== undefined) {
154
+ // В режиме установки устанавливаем значение из поля default
155
+ obj[key] = property.description.default;
156
+ } else if (property.default !== undefined) {
64
157
  obj[key] = property.default;
65
158
  } else if (property.type === 'object') {
66
159
  obj[key] = {};
67
- setDefaults(property, obj[key]);
160
+ setDefaults(property, obj[key], existingValue);
68
161
  } else {
69
162
  obj[key] = null;
70
163
  }
@@ -74,11 +167,15 @@ div.settings-form
74
167
 
75
168
  // Инициализация данных
76
169
  if (props.modelValue && Object.keys(props.modelValue).length > 0) {
77
- Object.assign(data, props.modelValue);
170
+ // Есть существующие данные - используем их, но проверяем на генерацию
171
+ setDefaults(props.schema, data, props.modelValue);
78
172
  } else {
79
173
  setDefaults(props.schema, data);
80
174
  }
81
175
 
176
+ // Эмитим начальные данные (важно для сгенерированных значений)
177
+ emit('update:modelValue', { ...data });
178
+
82
179
  // Отслеживание изменений и обновление данных
83
180
  watch(
84
181
  data,
@@ -113,7 +210,7 @@ div.settings-form
113
210
  }
114
211
 
115
212
  function getComponentProps(property: ISchemaProperty, propertyName: string) {
116
- const props: Record<string, any> = {
213
+ const componentProps: Record<string, any> = {
117
214
  modelValue: data[propertyName],
118
215
  'onUpdate:modelValue': (value: any) => {
119
216
  data[propertyName] = property.type === 'number' ? parseFloat(value) : value;
@@ -128,46 +225,61 @@ div.settings-form
128
225
 
129
226
  if (typeof minLength === 'number') {
130
227
  rules.push((val: string) => val.length >= minLength || `Минимальная длина: ${minLength}`);
131
- props.minLength = minLength;
228
+ componentProps.minLength = minLength;
132
229
  }
133
230
 
134
231
  if (typeof maxLength === 'number') {
135
232
  rules.push((val: string) => val.length <= maxLength || `Максимальная длина: ${maxLength}`);
136
- props.maxLength = maxLength;
233
+ componentProps.maxLength = maxLength;
137
234
  }
138
235
 
139
- props.rules = rules
236
+ componentProps.rules = rules
140
237
 
141
238
  // Добавляем маску и другие настройки, если они указаны
142
239
  if (property.description?.mask) {
143
- props.mask = property.description.mask;
240
+ componentProps.mask = property.description.mask;
144
241
  if (property.description?.fillMask !== undefined) {
145
- props.fillMask = property.description.fillMask;
242
+ componentProps.fillMask = property.description.fillMask;
146
243
  }
147
244
  }
148
245
 
246
+ // Поддержка readonly
247
+ if (property.description?.readonly) {
248
+ componentProps.readonly = true;
249
+ }
250
+
149
251
  // Установка типа поля в зависимости от типа property
150
252
  if (property.type === 'number') {
151
- props.type = 'number'; // Поле будет восприниматься как числовое, разрешены только цифры
253
+ componentProps.type = 'number'; // Поле будет восприниматься как числовое, разрешены только цифры
152
254
  } else if (property.type === 'string') {
153
- props.type = 'text'; // Поле для строк
255
+ // Проверка на пароль
256
+ if (property.description?.password) {
257
+ // В режиме установки показываем пароль как обычный текст для копирования
258
+ if (props.installMode) {
259
+ componentProps.type = 'text';
260
+ } else {
261
+ componentProps.type = passwordVisibility[propertyName] ? 'text' : 'password';
262
+ }
263
+ } else {
264
+ componentProps.type = 'text'; // Поле для строк
265
+ }
154
266
  // Проверка на многосстрочный ввод
155
267
  if (property.description?.maxRows) {
156
- props.type = 'textarea';
157
- props.autogrow = true; // Автоматический рост поля при вводе
158
- props.rows = property.description?.maxRows; // Установка максимального количества строк
268
+ componentProps.type = 'textarea';
269
+ componentProps.autogrow = true; // Автоматический рост поля при вводе
270
+ componentProps.rows = property.description?.maxRows; // Установка максимального количества строк
159
271
  }
160
272
  }
161
273
 
162
274
  if (property.enum) {
163
- props.options = property.enum;
275
+ componentProps.options = property.enum;
164
276
  }
165
277
 
166
278
  if (property.type === 'object') {
167
- props.schema = property;
279
+ componentProps.schema = property;
168
280
  }
169
281
 
170
- return props;
282
+ return componentProps;
171
283
  }
172
284
 
173
285
  function getLabel(property: ISchemaProperty, propertyName: string | number) {
@@ -2,15 +2,9 @@
2
2
  .info-card
3
3
  q-form(ref='formRef')
4
4
  div(v-if='schema && !isEmpty')
5
- ClientOnly
6
- template(#default)
7
- vue-markdown.description.q-mt-md(
8
- v-if='instructions',
9
- :source='instructions'
10
- )
11
5
  div
12
6
  p.text-h5 Настройки
13
- ZodForm.q-mt-lg(:schema='schema', :model-value='config', @update:model-value='$emit("update:config", $event)')
7
+ ZodForm.q-mt-lg(:schema='schema', :model-value='config', :install-mode='true', @update:model-value='$emit("update:config", $event)')
14
8
  div(v-else)
15
9
  .q-pa-md
16
10
  p.text-h5 Установка расширения
@@ -18,20 +12,13 @@
18
12
  </template>
19
13
 
20
14
  <script lang="ts" setup>
21
- import { computed, defineAsyncComponent } from 'vue';
15
+ import { computed } from 'vue';
22
16
  import { ZodForm } from 'src/shared/ui/ZodForm';
23
- import { ClientOnly } from 'src/shared/ui/ClientOnly';
24
17
  import { isExtensionSchemaEmpty } from 'src/shared/lib/utils';
25
18
 
26
- // Клиентский компонент для markdown, загружаемый только на клиенте
27
- const VueMarkdown = defineAsyncComponent(() =>
28
- import('vue-markdown-render').then((mod) => mod.default),
29
- );
30
-
31
19
  interface Props {
32
20
  schema?: any;
33
21
  config: any;
34
- instructions?: string;
35
22
  formRef?: any;
36
23
  }
37
24