fix: switch between code and visual for custom forms
This commit is contained in:
@@ -33,44 +33,80 @@
|
||||
|
||||
<!-- Режим визуального конструктора -->
|
||||
<template v-if="activeMode === 'visual'">
|
||||
<!-- Панель доступных полей -->
|
||||
<div class="tw:w-64 tw:flex-shrink-0 tw:h-full tw:overflow-hidden">
|
||||
<FieldsPanel class="tw:h-full" />
|
||||
</div>
|
||||
|
||||
<!-- Основная зона конструктора -->
|
||||
<div class="tw:flex-1 tw:flex tw:gap-4 tw:overflow-hidden">
|
||||
<!-- Зона формы -->
|
||||
<div class="tw:flex-1 tw:flex tw:flex-col tw:border tw:border-gray-200 tw:rounded-lg tw:overflow-hidden tw:bg-white tw:relative">
|
||||
<!-- Кнопка очистки (абсолютно позиционирована) -->
|
||||
<div class="tw:absolute tw:top-4 tw:right-4 tw:z-20">
|
||||
<Button
|
||||
label="Очистить"
|
||||
icon="fa fa-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
text
|
||||
v-tooltip.left="'Удалить все поля и очистить форму'"
|
||||
@click="clearForm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Контент (FormCanvas) занимает все оставшееся пространство -->
|
||||
<div class="tw:flex-1 tw:overflow-y-auto tw:relative">
|
||||
<FormCanvas class="tw:min-h-full" />
|
||||
<!-- Если форма кастомная, показываем предупреждение вместо редактора -->
|
||||
<div v-if="isCustom" class="tw:flex-1 tw:flex tw:items-center tw:justify-center">
|
||||
<div class="tw:max-w-2xl tw:p-8 tw:bg-yellow-50 tw:border-2 tw:border-yellow-200 tw:rounded-lg">
|
||||
<div class="tw:flex tw:items-start tw:gap-4">
|
||||
<i class="fa fa-exclamation-triangle tw:text-3xl tw:text-yellow-600"></i>
|
||||
<div>
|
||||
<h3 class="tw:text-lg tw:font-bold tw:text-yellow-800 tw:mb-2">
|
||||
Форма является кастомной
|
||||
</h3>
|
||||
<p class="tw:text-yellow-700 tw:mb-4">
|
||||
Эта форма была создана или изменена вручную в редакторе кода и не может быть отображена в визуальном редакторе.
|
||||
</p>
|
||||
<p class="tw:text-yellow-700 tw:mb-4">
|
||||
Для работы с этой формой используйте режим "Код". Если вы хотите создать новую форму в визуальном редакторе, необходимо сбросить текущую форму.
|
||||
</p>
|
||||
<Button
|
||||
label="Перейти в режим кода"
|
||||
icon="fa fa-code"
|
||||
@click="activeMode = 'code'"
|
||||
class="tw:mr-2"
|
||||
/>
|
||||
<Button
|
||||
label="Сбросить и создать новую"
|
||||
icon="fa fa-trash"
|
||||
severity="danger"
|
||||
outlined
|
||||
@click="showResetConfirmation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель настроек (справа) -->
|
||||
<div class="tw:w-80 tw:flex-shrink-0 tw:h-full tw:overflow-y-auto">
|
||||
<Panel class="tw:min-h-full">
|
||||
<template #header>
|
||||
<span>Настройки поля</span>
|
||||
</template>
|
||||
<FieldSettings />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Обычный визуальный редактор для некстомных форм -->
|
||||
<template v-else>
|
||||
<!-- Панель доступных полей -->
|
||||
<div class="tw:w-64 tw:flex-shrink-0 tw:h-full tw:overflow-hidden">
|
||||
<FieldsPanel class="tw:h-full" />
|
||||
</div>
|
||||
|
||||
<!-- Основная зона конструктора -->
|
||||
<div class="tw:flex-1 tw:flex tw:gap-4 tw:overflow-hidden">
|
||||
<!-- Зона формы -->
|
||||
<div class="tw:flex-1 tw:flex tw:flex-col tw:border tw:border-gray-200 tw:rounded-lg tw:overflow-hidden tw:bg-white tw:relative">
|
||||
<!-- Кнопка очистки (абсолютно позиционирована) -->
|
||||
<div class="tw:absolute tw:top-4 tw:right-4 tw:z-20">
|
||||
<Button
|
||||
label="Очистить"
|
||||
icon="fa fa-trash"
|
||||
severity="danger"
|
||||
size="small"
|
||||
text
|
||||
v-tooltip.left="'Удалить все поля и очистить форму'"
|
||||
@click="clearForm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Контент (FormCanvas) занимает все оставшееся пространство -->
|
||||
<div class="tw:flex-1 tw:overflow-y-auto tw:relative">
|
||||
<FormCanvas class="tw:min-h-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Панель настроек (справа) -->
|
||||
<div class="tw:w-80 tw:flex-shrink-0 tw:h-full tw:overflow-y-auto">
|
||||
<Panel class="tw:min-h-full">
|
||||
<template #header>
|
||||
<span>Настройки поля</span>
|
||||
</template>
|
||||
<FieldSettings />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Режим редактирования кода -->
|
||||
@@ -87,7 +123,12 @@
|
||||
<template v-if="activeMode === 'preview'">
|
||||
<div class="tw:flex-1 tw:flex tw:justify-center tw:overflow-auto">
|
||||
<div class="tw:w-full">
|
||||
<div v-if="jsonError" class="tw:p-4 tw:bg-red-50 tw:border tw:border-red-200 tw:rounded tw:text-red-700">
|
||||
<i class="fa fa-exclamation-circle tw:mr-2"></i>
|
||||
Ошибка в схеме: {{ jsonError }}
|
||||
</div>
|
||||
<FormRenderer
|
||||
v-else
|
||||
:schema="formSchema"
|
||||
submit-label="Отправить форму"
|
||||
@submit="handleFormSubmit"
|
||||
@@ -101,7 +142,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, provide, computed, onMounted } from 'vue';
|
||||
import { ref, provide, computed, onMounted, watch } from 'vue';
|
||||
import { Button, Panel, SelectButton, useConfirm, ConfirmPopup, ConfirmDialog } from 'primevue';
|
||||
import FieldsPanel from '@/components/FormBuilder/FieldsPanel.vue';
|
||||
import FormCanvas from '@/components/FormBuilder/FormCanvas.vue';
|
||||
@@ -109,6 +150,8 @@ import FieldSettings from '@/components/FormBuilder/FieldSettings.vue';
|
||||
import FormRenderer from '@/components/FormBuilder/FormRenderer.vue';
|
||||
import CodeEditor from '@/components/FormBuilder/CodeEditor.vue';
|
||||
import { toastBus } from '@/utils/toastHelper';
|
||||
import { saveRevision } from './utils/revisionManager.js';
|
||||
import { createEmptySchema } from './utils/schemaParser.js';
|
||||
|
||||
const formFields = defineModel({
|
||||
type: Array,
|
||||
@@ -120,13 +163,19 @@ const isCustom = defineModel('isCustom', {
|
||||
default: false
|
||||
});
|
||||
|
||||
// Локальные состояния (не сохраняются в БД)
|
||||
const dirtyFromCode = ref(false);
|
||||
const lastSyncedSchema = ref(null);
|
||||
|
||||
const activeMode = ref('visual');
|
||||
const jsonCode = ref('');
|
||||
const originalJsonCode = ref(''); // Для отслеживания изменений в режиме кода
|
||||
const jsonError = ref(null);
|
||||
const confirm = useConfirm();
|
||||
const selectButtonKey = ref(0);
|
||||
|
||||
// Алиас формы для сохранения ревизий (можно передавать через props, пока используем 'checkout')
|
||||
const formAlias = 'checkout';
|
||||
|
||||
const modes = [
|
||||
{ label: 'Визуальный', value: 'visual', icon: 'fa fa-th-large' },
|
||||
{ label: 'Код', value: 'code', icon: 'fa fa-code' },
|
||||
@@ -149,14 +198,49 @@ const formSchema = computed(() => {
|
||||
return formFields.value || [];
|
||||
});
|
||||
|
||||
// Инициализация режима при монтировании
|
||||
// Инициализация при монтировании
|
||||
onMounted(() => {
|
||||
initializeForm();
|
||||
});
|
||||
|
||||
// Инициализация формы при загрузке из БД
|
||||
function initializeForm() {
|
||||
const schema = formFields.value || [];
|
||||
|
||||
// Устанавливаем lastSyncedSchema равным загруженной схеме
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(schema));
|
||||
|
||||
// dirtyFromCode всегда false при загрузке
|
||||
dirtyFromCode.value = false;
|
||||
|
||||
// Определяем начальный режим на основе isCustom
|
||||
if (isCustom.value) {
|
||||
activeMode.value = 'code';
|
||||
jsonCode.value = JSON.stringify(formFields.value, null, 2);
|
||||
originalJsonCode.value = jsonCode.value;
|
||||
jsonCode.value = JSON.stringify(schema, null, 2);
|
||||
} else {
|
||||
activeMode.value = 'visual';
|
||||
jsonCode.value = JSON.stringify(schema, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Отслеживание изменений в визуальном редакторе
|
||||
watch(formFields, (newSchema) => {
|
||||
// Если мы в визуальном режиме и форма не кастомная, обновляем lastSyncedSchema
|
||||
if (activeMode.value === 'visual' && !dirtyFromCode.value && !isCustom.value) {
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(newSchema));
|
||||
// Обновляем jsonCode для синхронизации
|
||||
jsonCode.value = JSON.stringify(newSchema, null, 2);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// Отслеживание изменений isCustom для переинициализации
|
||||
watch(isCustom, () => {
|
||||
// При изменении isCustom извне (например, при загрузке из БД) переинициализируем
|
||||
if (activeMode.value === 'code' && !isCustom.value) {
|
||||
// Если isCustom стал false, но мы в режиме кода, это значит форма была сброшена
|
||||
// Синхронизируем состояния
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(formFields.value));
|
||||
dirtyFromCode.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -188,106 +272,169 @@ function handleModeChange(newMode) {
|
||||
// Пытаемся распарсить JSON перед уходом
|
||||
if (jsonError.value) {
|
||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка JSON', detail: 'Исправьте ошибки в JSON перед переключением режима' });
|
||||
// Не обновляем activeMode, остаемся в code
|
||||
cancelModeSwitch();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonCode.value);
|
||||
const parsed = JSON.parse(jsonCode.value);
|
||||
|
||||
if (hasDuplicateNames(parsed)) {
|
||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка валидации', detail: 'В схеме есть поля с одинаковыми именами (name). Исправьте их перед переключением.' });
|
||||
cancelModeSwitch();
|
||||
return;
|
||||
}
|
||||
if (hasDuplicateNames(parsed)) {
|
||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка валидации', detail: 'В схеме есть поля с одинаковыми именами (name). Исправьте их перед переключением.' });
|
||||
cancelModeSwitch();
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, были ли изменения в коде
|
||||
let hasChanges = false;
|
||||
try {
|
||||
// Сравниваем распарсенные объекты, чтобы избежать ложных срабатываний из-за форматирования
|
||||
const originalObj = JSON.parse(originalJsonCode.value);
|
||||
hasChanges = JSON.stringify(parsed) !== JSON.stringify(originalObj);
|
||||
} catch (e) {
|
||||
hasChanges = true; // Если не смогли распарсить оригинал, считаем что изменения были (или ошибка)
|
||||
}
|
||||
// Если переключаемся в визуальный режим
|
||||
if (newMode === 'visual') {
|
||||
// Проверяем, нужно ли показывать предупреждение
|
||||
const needsWarning = isCustom.value || dirtyFromCode.value;
|
||||
|
||||
// Если переключаемся в визуальный режим
|
||||
if (newMode === 'visual') {
|
||||
if (hasChanges) {
|
||||
// Если были изменения, показываем предупреждение
|
||||
confirm.require({
|
||||
group: 'modeSwitch',
|
||||
header: 'Предупреждение',
|
||||
message: 'Вы внесли изменения в код вручную.\n\nПосле ручного изменения визуальный редактор может работать некорректно или не отображать все настройки.\n\nВы можете остаться в режиме кода или очистить форму и начать заново в визуальном редакторе.',
|
||||
icon: 'fa fa-exclamation-triangle',
|
||||
acceptLabel: 'Очистить и перейти',
|
||||
rejectLabel: 'Остаться в коде',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: () => {
|
||||
formFields.value = [];
|
||||
selectedFieldId.value = null;
|
||||
isCustom.value = false;
|
||||
jsonCode.value = '[]';
|
||||
if (needsWarning) {
|
||||
// Сохраняем ревизию перед деструктивной операцией
|
||||
saveRevision(formAlias, formFields.value, 'reset_to_visual');
|
||||
|
||||
// Успешный переход
|
||||
activeMode.value = 'visual';
|
||||
},
|
||||
reject: () => {
|
||||
// Остаемся в коде
|
||||
// activeMode.value НЕ обновляем, он остался 'code'
|
||||
cancelModeSwitch();
|
||||
}
|
||||
});
|
||||
// Показываем предупреждение
|
||||
confirm.require({
|
||||
group: 'modeSwitch',
|
||||
header: 'Предупреждение',
|
||||
message: isCustom.value
|
||||
? 'Загруженная форма является кастомной и может содержать неподдерживаемые элементы.\n\nОткрытие визуального редактора приведет к полному сбросу формы. Все нестандартные настройки будут потеряны.'
|
||||
: 'Форма была изменена вручную в редакторе кода.\n\nВизуальный редактор не поддерживает все конструкции, и для его открытия потребуется полностью сбросить форму и создать новую. Все нестандартные настройки будут потеряны.',
|
||||
icon: 'fa fa-exclamation-triangle',
|
||||
acceptLabel: 'Сбросить и открыть визуальный редактор',
|
||||
rejectLabel: 'Отменить',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: () => {
|
||||
resetFormToVisual();
|
||||
activeMode.value = 'visual';
|
||||
},
|
||||
reject: () => {
|
||||
cancelModeSwitch();
|
||||
}
|
||||
});
|
||||
|
||||
// Не обновляем activeMode сразу, ждем решения пользователя
|
||||
return;
|
||||
} else {
|
||||
// Если изменений не было, просто переключаемся и сбрасываем флаг кастомного режима
|
||||
isCustom.value = false;
|
||||
formFields.value = parsed;
|
||||
}
|
||||
} else {
|
||||
// Переход в Preview - разрешаем обновление модели из кода
|
||||
console.log('Updating formFields for Preview:', parsed);
|
||||
formFields.value = parsed;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Ошибка парсинга при переключении:', e);
|
||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка', detail: 'Некорректный JSON' });
|
||||
cancelModeSwitch();
|
||||
return;
|
||||
} else {
|
||||
// Если isCustom=false и dirtyFromCode=false, просто переключаемся
|
||||
// Обновляем схему из кода
|
||||
formFields.value = parsed;
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(parsed));
|
||||
dirtyFromCode.value = false;
|
||||
}
|
||||
} else if (newMode === 'preview') {
|
||||
// Переход в Preview - обновляем модель из кода
|
||||
formFields.value = parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Ошибка парсинга при переключении:', e);
|
||||
toastBus.emit('show', { severity: 'error', summary: 'Ошибка', detail: 'Некорректный JSON' });
|
||||
cancelModeSwitch();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Если переключаемся В режим кода
|
||||
if (newMode === 'code') {
|
||||
isCustom.value = true;
|
||||
// Обновляем jsonCode из текущей схемы
|
||||
jsonCode.value = JSON.stringify(formFields.value, null, 2);
|
||||
originalJsonCode.value = jsonCode.value; // Сохраняем исходное состояние
|
||||
// Обновляем lastSyncedSchema, если мы пришли из визуального редактора
|
||||
if (activeMode.value === 'visual') {
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(formFields.value));
|
||||
dirtyFromCode.value = false;
|
||||
}
|
||||
// isCustom не меняем автоматически при переходе в режим кода
|
||||
// Он установится в true только когда пользователь реально изменит код (через handleJsonInput)
|
||||
}
|
||||
|
||||
// Если переключаемся В визуальный режим из preview
|
||||
if (newMode === 'visual' && activeMode.value === 'preview') {
|
||||
// Никаких проверок, просто переключаемся
|
||||
}
|
||||
|
||||
// Обновляем режим для успешных переходов
|
||||
activeMode.value = newMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сбрасывает форму для визуального редактора
|
||||
*/
|
||||
function resetFormToVisual() {
|
||||
// Сохраняем ревизию (уже сохранена выше, но на всякий случай)
|
||||
saveRevision(formAlias, formFields.value, 'reset_to_visual');
|
||||
|
||||
// Создаем пустую схему
|
||||
const emptySchema = createEmptySchema();
|
||||
|
||||
// Обновляем все состояния
|
||||
formFields.value = emptySchema;
|
||||
selectedFieldId.value = null;
|
||||
isCustom.value = false;
|
||||
jsonCode.value = JSON.stringify(emptySchema, null, 2);
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(emptySchema));
|
||||
dirtyFromCode.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Показывает подтверждение сброса формы
|
||||
*/
|
||||
function showResetConfirmation() {
|
||||
// Сохраняем ревизию перед деструктивной операцией
|
||||
saveRevision(formAlias, formFields.value, 'reset_to_visual');
|
||||
|
||||
confirm.require({
|
||||
group: 'modeSwitch',
|
||||
header: 'Подтверждение сброса',
|
||||
message: 'Вы уверены, что хотите сбросить кастомную форму и создать новую в визуальном редакторе?\n\nВсе текущие настройки будут потеряны.',
|
||||
icon: 'fa fa-exclamation-triangle',
|
||||
acceptLabel: 'Сбросить и создать новую',
|
||||
rejectLabel: 'Отменить',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: () => {
|
||||
resetFormToVisual();
|
||||
activeMode.value = 'visual';
|
||||
},
|
||||
reject: () => {
|
||||
// Отменяем действие
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function handleJsonInput() {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonCode.value);
|
||||
|
||||
if (hasDuplicateNames(parsed)) {
|
||||
jsonError.value = 'Ошибка: найдены поля с одинаковыми именами (name)';
|
||||
jsonError.value = 'Ошибка: найдены поля с одинаковыми именами (name)';
|
||||
} else {
|
||||
jsonError.value = null;
|
||||
jsonError.value = null;
|
||||
}
|
||||
|
||||
// Обновляем модель сразу, чтобы изменения не терялись
|
||||
console.log('handleJsonInput: Updating formFields', parsed);
|
||||
formFields.value = parsed;
|
||||
|
||||
// Проверяем, изменилась ли схема относительно lastSyncedSchema
|
||||
if (lastSyncedSchema.value !== null) {
|
||||
const currentSchemaStr = JSON.stringify(parsed);
|
||||
const lastSyncedStr = JSON.stringify(lastSyncedSchema.value);
|
||||
const hasChanges = currentSchemaStr !== lastSyncedStr;
|
||||
dirtyFromCode.value = hasChanges;
|
||||
|
||||
// Если есть изменения и форма еще не кастомная, устанавливаем isCustom=true
|
||||
if (hasChanges && !isCustom.value) {
|
||||
isCustom.value = true;
|
||||
}
|
||||
} else {
|
||||
// Если lastSyncedSchema еще не установлен, считаем что изменения есть
|
||||
dirtyFromCode.value = true;
|
||||
if (!isCustom.value) {
|
||||
isCustom.value = true;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
jsonError.value = e.message;
|
||||
// При ошибке парсинга не обновляем dirtyFromCode и isCustom
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,9 +449,18 @@ function clearForm(event) {
|
||||
acceptClass: 'p-button-danger p-button-sm',
|
||||
rejectClass: 'p-button-secondary p-button-sm p-button-text',
|
||||
accept: () => {
|
||||
formFields.value = [];
|
||||
// Сохраняем ревизию перед очисткой
|
||||
saveRevision(formAlias, formFields.value, 'clear_form');
|
||||
|
||||
const emptySchema = createEmptySchema();
|
||||
formFields.value = emptySchema;
|
||||
selectedFieldId.value = null;
|
||||
jsonCode.value = '[]';
|
||||
jsonCode.value = JSON.stringify(emptySchema, null, 2);
|
||||
lastSyncedSchema.value = JSON.parse(JSON.stringify(emptySchema));
|
||||
dirtyFromCode.value = false;
|
||||
// После очистки в визуальном редакторе форма не кастомная
|
||||
isCustom.value = false;
|
||||
|
||||
toastBus.emit('show', { severity: 'success', summary: 'Успешно', detail: 'Форма очищена' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,11 +58,16 @@
|
||||
<!-- Предпросмотр поля -->
|
||||
<div class="tw:relative tw:z-0">
|
||||
<FormKit
|
||||
v-if="field.$formkit"
|
||||
:key="`${field.id}-${field.prefixIcon}-${field.suffixIcon}`"
|
||||
:type="field.$formkit"
|
||||
v-bind="getFieldProps(field)"
|
||||
:name="field.name || `field_${field.id}`"
|
||||
/>
|
||||
<div v-else class="tw:text-red-500 tw:text-sm">
|
||||
<i class="fa fa-exclamation-triangle tw:mr-2"></i>
|
||||
Поле без типа $formkit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Утилита для управления ревизиями схемы формы
|
||||
* Сохраняет предыдущие версии перед деструктивными операциями
|
||||
*/
|
||||
|
||||
const REVISION_STORAGE_KEY = 'formBuilder_revisions';
|
||||
const MAX_REVISIONS = 10;
|
||||
|
||||
/**
|
||||
* Сохраняет ревизию схемы перед деструктивной операцией
|
||||
* @param {string} formAlias - Алиас формы (например, 'checkout')
|
||||
* @param {Array} schema - Схема формы для сохранения
|
||||
* @param {string} reason - Причина сохранения (например, 'reset_to_visual')
|
||||
*/
|
||||
export function saveRevision(formAlias, schema, reason = 'unknown') {
|
||||
try {
|
||||
const revisions = getRevisions(formAlias);
|
||||
const revision = {
|
||||
id: `rev_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
schema: JSON.parse(JSON.stringify(schema)), // Deep clone
|
||||
timestamp: new Date().toISOString(),
|
||||
reason,
|
||||
};
|
||||
|
||||
revisions.unshift(revision);
|
||||
|
||||
// Ограничиваем количество ревизий
|
||||
if (revisions.length > MAX_REVISIONS) {
|
||||
revisions.splice(MAX_REVISIONS);
|
||||
}
|
||||
|
||||
const storage = getAllRevisions();
|
||||
storage[formAlias] = revisions;
|
||||
localStorage.setItem(REVISION_STORAGE_KEY, JSON.stringify(storage));
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения ревизии:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все ревизии для формы
|
||||
* @param {string} formAlias - Алиас формы
|
||||
* @returns {Array} Массив ревизий
|
||||
*/
|
||||
export function getRevisions(formAlias) {
|
||||
try {
|
||||
const storage = getAllRevisions();
|
||||
return storage[formAlias] || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка получения ревизий:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все ревизии для всех форм
|
||||
* @returns {Object} Объект с ревизиями по алиасам форм
|
||||
*/
|
||||
function getAllRevisions() {
|
||||
try {
|
||||
const stored = localStorage.getItem(REVISION_STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
} catch (error) {
|
||||
console.error('Ошибка чтения ревизий из localStorage:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Восстанавливает схему из ревизии
|
||||
* @param {string} formAlias - Алиас формы
|
||||
* @param {string} revisionId - ID ревизии
|
||||
* @returns {Array|null} Схема формы или null, если ревизия не найдена
|
||||
*/
|
||||
export function restoreRevision(formAlias, revisionId) {
|
||||
try {
|
||||
const revisions = getRevisions(formAlias);
|
||||
const revision = revisions.find(r => r.id === revisionId);
|
||||
if (revision) {
|
||||
return JSON.parse(JSON.stringify(revision.schema)); // Deep clone
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Ошибка восстановления ревизии:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет ревизию
|
||||
* @param {string} formAlias - Алиас формы
|
||||
* @param {string} revisionId - ID ревизии
|
||||
*/
|
||||
export function deleteRevision(formAlias, revisionId) {
|
||||
try {
|
||||
const revisions = getRevisions(formAlias);
|
||||
const filtered = revisions.filter(r => r.id !== revisionId);
|
||||
const storage = getAllRevisions();
|
||||
storage[formAlias] = filtered;
|
||||
localStorage.setItem(REVISION_STORAGE_KEY, JSON.stringify(storage));
|
||||
} catch (error) {
|
||||
console.error('Ошибка удаления ревизии:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все ревизии для формы
|
||||
* @param {string} formAlias - Алиас формы
|
||||
*/
|
||||
export function clearRevisions(formAlias) {
|
||||
try {
|
||||
const storage = getAllRevisions();
|
||||
delete storage[formAlias];
|
||||
localStorage.setItem(REVISION_STORAGE_KEY, JSON.stringify(storage));
|
||||
} catch (error) {
|
||||
console.error('Ошибка очистки ревизий:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Утилита для работы со схемами форм
|
||||
* Упрощенная логика: если isCustom=true, схема несовместима с визуальным редактором
|
||||
*/
|
||||
|
||||
/**
|
||||
* Проверяет совместимость схемы с визуальным редактором
|
||||
* @param {boolean} isCustom - Флаг кастомной формы
|
||||
* @returns {boolean} true если схема совместима, false если нет
|
||||
*/
|
||||
export function isSchemaCompatible(isCustom) {
|
||||
// Если форма кастомная, она несовместима с визуальным редактором
|
||||
return !isCustom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает пустую схему для визуального редактора
|
||||
* @returns {Array} Пустая схема
|
||||
*/
|
||||
export function createEmptySchema() {
|
||||
return [];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user