Merged PR 207: Fix Canvas and picklists
Fix Canvas and picklists
This commit is contained in:
@@ -8,20 +8,22 @@
|
||||
<MultiSelect
|
||||
v-if="dataSource === 'videoGroups'"
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
|
||||
:options="safeOptions"
|
||||
optionLabel="name"
|
||||
:filter="true"
|
||||
:placeholder="placeholder || 'Select VideoGroups'"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
class="w-full md:w-80"
|
||||
:virtualScrollerOptions="{ itemSize: 50 }"
|
||||
class="w-full"
|
||||
panelClass="video-groups-panel"
|
||||
@change="onSelectionChange"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex align-items-center">
|
||||
<i class="pi pi-video mr-2"></i>
|
||||
<div>
|
||||
<div>{{ slotProps.option.name }}</div>
|
||||
<div class="flex align-items-center p-3 hover:bg-gray-50">
|
||||
<i class="pi pi-video mr-3 text-blue-600"></i>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm">{{ slotProps.option.name }}</div>
|
||||
<small class="text-muted" v-if="slotProps.option.description">
|
||||
{{ slotProps.option.description }}
|
||||
</small>
|
||||
@@ -34,20 +36,22 @@
|
||||
<MultiSelect
|
||||
v-else-if="dataSource === 'ksDocuments'"
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
|
||||
:options="safeOptions"
|
||||
optionLabel="fileName"
|
||||
:filter="true"
|
||||
:placeholder="placeholder || 'Select Documents'"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
class="w-full md:w-80"
|
||||
:virtualScrollerOptions="{ itemSize: 50 }"
|
||||
class="w-full"
|
||||
panelClass="ks-documents-panel"
|
||||
@change="onSelectionChange"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex align-items-center">
|
||||
<i :class="getFileIcon(slotProps.option)" class="mr-2"></i>
|
||||
<div>
|
||||
<div>{{ slotProps.option.fileName }}</div>
|
||||
<div class="flex align-items-center p-3 hover:bg-gray-50">
|
||||
<i :class="getFileIcon(slotProps.option)" class="mr-3"></i>
|
||||
<div class="flex-1">
|
||||
<div class="font-medium text-sm">{{ slotProps.option.fileName }}</div>
|
||||
<small class="text-muted" v-if="slotProps.option.description">
|
||||
{{ slotProps.option.description }}
|
||||
</small>
|
||||
@@ -60,8 +64,8 @@
|
||||
<Dropdown
|
||||
v-else-if="multiple === false"
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
|
||||
:options="safeOptions"
|
||||
optionLabel="name"
|
||||
:filter="true"
|
||||
:placeholder="placeholder || 'Select an option'"
|
||||
:disabled="disabled"
|
||||
@@ -72,15 +76,17 @@
|
||||
|
||||
<!-- MultiSelect generico per altri tipi -->
|
||||
<MultiSelect
|
||||
v-else
|
||||
v-else-if="safeOptions && safeOptions.length > 0"
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
|
||||
:options="safeOptions"
|
||||
optionLabel="name"
|
||||
:filter="true"
|
||||
:placeholder="placeholder || 'Select options'"
|
||||
:disabled="disabled"
|
||||
:loading="loading"
|
||||
class="w-full md:w-80"
|
||||
:virtualScrollerOptions="{ itemSize: 50 }"
|
||||
class="w-full"
|
||||
panelClass="generic-multiselect-panel"
|
||||
@change="onSelectionChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -175,6 +181,11 @@ const selectedValue = computed({
|
||||
}
|
||||
});
|
||||
|
||||
// Computed property per garantire che le options siano sempre un array valido
|
||||
const safeOptions = computed(() => {
|
||||
return Array.isArray(props.options) ? props.options : [];
|
||||
});
|
||||
|
||||
const getFileIcon = (document) => {
|
||||
if (!document) return 'pi pi-file';
|
||||
|
||||
@@ -226,3 +237,52 @@ const onSelectionChange = (event) => {
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* Personalizzazione delle panel dei MultiSelect per migliorare la visualizzazione */
|
||||
.ks-documents-panel .p-multiselect-panel,
|
||||
.video-groups-panel .p-multiselect-panel,
|
||||
.generic-multiselect-panel .p-multiselect-panel {
|
||||
min-width: 350px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.ks-documents-panel .p-multiselect-items,
|
||||
.video-groups-panel .p-multiselect-items,
|
||||
.generic-multiselect-panel .p-multiselect-items {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.ks-documents-panel .p-multiselect-item,
|
||||
.video-groups-panel .p-multiselect-item,
|
||||
.generic-multiselect-panel .p-multiselect-item {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ks-documents-panel .p-multiselect-item:last-child,
|
||||
.video-groups-panel .p-multiselect-item:last-child,
|
||||
.generic-multiselect-panel .p-multiselect-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ks-documents-panel .p-multiselect-item:hover,
|
||||
.video-groups-panel .p-multiselect-item:hover,
|
||||
.generic-multiselect-panel .p-multiselect-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Stili per le icone dei file */
|
||||
.ks-documents-panel .pi {
|
||||
font-size: 1.2rem;
|
||||
min-width: 1.5rem;
|
||||
}
|
||||
|
||||
.video-groups-panel .pi-video {
|
||||
font-size: 1.2rem;
|
||||
min-width: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<ProgressSpinner style="width: 50px; height: 50px; margin-top: 50px" strokeWidth="3" fill="transparent" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex items-center justify-between p-2">
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading_data" class="flex justify-center">
|
||||
@@ -31,8 +30,8 @@
|
||||
<tr v-for="(input, index) in filteredInputs" :key="index">
|
||||
<th v-if="index === 'MultiFileUpload'" class="border border-gray-300 px-4 py-2">Files Uploaded</th>
|
||||
<th v-else-if="index === 'SingleFileUpload'" class="border border-gray-300 px-4 py-2 bg-gray-500 text-white">Parameter</th>
|
||||
<th v-else-if="index === 'input_multiselect_name'">
|
||||
{{ scenario.inputs && Array.isArray(scenario.inputs) ? scenario.inputs.find((i) => i.name === 'input_multiselect')?.label : null }}
|
||||
<th v-else-if="index.includes('input_multiselect') && index.endsWith('_name')">
|
||||
{{ scenario.inputs && Array.isArray(scenario.inputs) ? scenario.inputs.find((i) => i.name === index.replace('_name', ''))?.label : null }}
|
||||
</th>
|
||||
<th v-else class="border border-gray-300 px-4 py-2">
|
||||
{{ index.replace(/_/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) }}
|
||||
@@ -227,8 +226,14 @@ const retrieveScenarioExec = (id) => {
|
||||
});
|
||||
};
|
||||
const filteredInputs = computed(() => {
|
||||
const { input_multiselect_id, ...rest } = inputs.value;
|
||||
return rest;
|
||||
const filtered = {};
|
||||
for (const [key, value] of Object.entries(inputs.value)) {
|
||||
// Escludi tutti i campi che contengono "input_multiselect" e finiscono con "_id"
|
||||
if (!(key.includes('input_multiselect') && key.endsWith('_id'))) {
|
||||
filtered[key] = value;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const extractFiles = async (base64String, type, zip) => {
|
||||
@@ -319,7 +324,6 @@ const getFileNamesInput = (zipData) => {
|
||||
return files;
|
||||
};
|
||||
|
||||
|
||||
const downloadFile = async (filePath) => {
|
||||
try {
|
||||
let relativePath = filePath;
|
||||
@@ -354,22 +358,22 @@ const downloadCodegenieFile = (base64String) => {
|
||||
for (let i = 0; i < binaryLength; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
|
||||
// Creazione di un Blob dal file binario
|
||||
const blob = new Blob([bytes], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
|
||||
|
||||
|
||||
// Creazione di un URL per il Blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
|
||||
// Creazione di un elemento anchor per il download
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'sf_document-' + execution_id.value + '.docx';
|
||||
|
||||
|
||||
// Simulazione di un click per scaricare il file
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
|
||||
// Pulizia del DOM
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
:maxFileSize="20971520"
|
||||
v-model:files="uploadedFiles"
|
||||
@before-send="onBeforeSend"
|
||||
|
||||
>
|
||||
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
|
||||
<div class="pt-4">
|
||||
@@ -90,7 +89,6 @@
|
||||
:maxFileSize="20971520"
|
||||
v-model:files="uploadedFiles"
|
||||
@before-send="onBeforeSend"
|
||||
|
||||
>
|
||||
<template #content="{ files, uploadedFiles, removeUploadedFileCallback, removeFileCallback }">
|
||||
<div class="pt-4">
|
||||
@@ -131,6 +129,20 @@
|
||||
</FileUpload>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="input.type === 'multiselect'" class="mt-4">
|
||||
<DynamicPicker
|
||||
v-model="formData[input.name]"
|
||||
:input-name="input.name"
|
||||
:label="input.label"
|
||||
:data-source="input.dataSource || 'videoGroups'"
|
||||
:options="getOptionsForInput(input)"
|
||||
:disabled="loadingStore.exectuion_loading"
|
||||
:loading="loadingOptionsFor[input.dataSource || 'videoGroups'] || false"
|
||||
:show-status="input.dataSource === 'ksDocuments'"
|
||||
no-margin
|
||||
@change="onDynamicPickerChange(input.name, $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<label :for="input.name"
|
||||
><b>{{ input.label }}</b></label
|
||||
@@ -189,7 +201,7 @@
|
||||
<button class="p-button p-button-primary" @click="addToCanvas">Add to Canvas</button>
|
||||
</div>
|
||||
<div>
|
||||
<MdPreview class="editor" v-model="scenario_output" language="en-US" />
|
||||
<MdPreview class="editor" v-model="scenario_output" language="en-US" />
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
@@ -204,8 +216,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import DynamicPicker from '@/components/DynamicPicker.vue';
|
||||
import { KSDocumentService } from '@/service/KSDocumentService';
|
||||
import { ScenarioService } from '@/service/ScenarioService';
|
||||
import { KsVideoGroupStore } from '@/stores/KsVideoGroupStore';
|
||||
import { LoadingStore } from '@/stores/LoadingStore';
|
||||
import { UserPrefStore } from '@/stores/UserPrefStore';
|
||||
import { useAuth } from '@websanova/vue-auth/src/v3.js';
|
||||
import axios from 'axios';
|
||||
import JsonEditorVue from 'json-editor-vue';
|
||||
@@ -216,17 +232,20 @@ import 'md-editor-v3/lib/style.css';
|
||||
import moment from 'moment';
|
||||
import { usePrimeVue } from 'primevue/config';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import Select from 'primevue/select';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { computed, defineEmits, onMounted, ref } from 'vue';
|
||||
import { computed, defineEmits, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { JellyfishLoader } from 'vue3-spinner';
|
||||
|
||||
const props = defineProps(['scenario'])
|
||||
const emit = defineEmits(['add'])
|
||||
const props = defineProps(['scenario']);
|
||||
const emit = defineEmits(['add']);
|
||||
const loadingStore = LoadingStore();
|
||||
const ksVideoGroupStore = KsVideoGroupStore();
|
||||
const userPrefStore = UserPrefStore();
|
||||
const toast = useToast();
|
||||
const zip = ref(null);
|
||||
const router = useRouter();
|
||||
@@ -246,6 +265,10 @@ const debug_modal = ref(false);
|
||||
let pollingInterval = null;
|
||||
const folderName = ref('');
|
||||
const fileNamesOutput = ref([]);
|
||||
// Aggiunte per gestire le opzioni dei multiselect
|
||||
const videoGroups = ref([]);
|
||||
const ksDocuments = ref([]);
|
||||
const loadingOptionsFor = reactive({});
|
||||
// URL di upload
|
||||
const uploadUrlBase = import.meta.env.VITE_BACKEND_URL;
|
||||
const uploadUrl = ref('');
|
||||
@@ -258,6 +281,9 @@ const acceptedFormats = ref('.docx');
|
||||
// :url="`http://localhost:8081/uploadListFiles/${folderName}`"
|
||||
const auth = useAuth();
|
||||
|
||||
// Stato per tracciare se il componente è montato
|
||||
const isMounted = ref(false);
|
||||
|
||||
// Stato per l'ID univoco della cartella
|
||||
const uniqueFolderId = ref(generateUniqueId());
|
||||
const confirm = useConfirm();
|
||||
@@ -285,7 +311,7 @@ function stopTimer() {
|
||||
const onBeforeSend = (event) => {
|
||||
const { xhr } = event; // Estraggo l'oggetto XMLHttpRequest
|
||||
console.log('xhr', xhr);
|
||||
var token = auth.token()
|
||||
var token = auth.token();
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + token); // Imposta il tipo di contenuto
|
||||
};
|
||||
|
||||
@@ -306,6 +332,7 @@ const isInputFilled = computed(() => {
|
||||
|
||||
onMounted(() => {
|
||||
console.log('MdScenarioExecutionDialog montato!');
|
||||
isMounted.value = true;
|
||||
const timestamp = Date.now(); //Ottiene il timestamp corrente
|
||||
const randomNumber = Math.floor(Math.random() * 1000);
|
||||
folderName.value = `${timestamp}_${randomNumber}`;
|
||||
@@ -313,8 +340,21 @@ onMounted(() => {
|
||||
uploadUrlPR.value = uploadUrl.value + '/PR';
|
||||
uploadUrlOther.value = uploadUrl.value + '/OTHER';
|
||||
console.log('Upload URL:', uploadUrl);
|
||||
|
||||
// Carica le opzioni per i multiselect
|
||||
loadOptionsForScenario();
|
||||
});
|
||||
|
||||
// Cleanup quando il componente viene smontato
|
||||
onUnmounted(() => {
|
||||
isMounted.value = false;
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
}
|
||||
if (timerInterval.value) {
|
||||
clearInterval(timerInterval.value);
|
||||
}
|
||||
});
|
||||
|
||||
const getInputComponent = (type) => {
|
||||
switch (type) {
|
||||
@@ -324,11 +364,133 @@ const getInputComponent = (type) => {
|
||||
return Textarea;
|
||||
case 'select':
|
||||
return Select;
|
||||
case 'multiselect':
|
||||
return MultiSelect;
|
||||
default:
|
||||
return InputText;
|
||||
}
|
||||
};
|
||||
|
||||
// Funzione per ottenere le opzioni per gli input multiselect
|
||||
const getOptionsForInput = (input) => {
|
||||
// Basato sul dataSource, restituisce le opzioni appropriate
|
||||
let options = [];
|
||||
|
||||
switch (input.dataSource) {
|
||||
case 'videoGroups':
|
||||
options = videoGroups.value || [];
|
||||
break;
|
||||
case 'ksDocuments':
|
||||
options = ksDocuments.value || [];
|
||||
break;
|
||||
default:
|
||||
options = [];
|
||||
}
|
||||
|
||||
// Assicuriamoci che sia sempre un array
|
||||
return Array.isArray(options) ? options : [];
|
||||
};
|
||||
|
||||
const onDynamicPickerChange = (inputName, value) => {
|
||||
console.log(`Dynamic picker changed for ${inputName}:`, value);
|
||||
formData.value[inputName] = value;
|
||||
};
|
||||
|
||||
// Funzione per caricare i video groups
|
||||
const loadVideoGroups = async () => {
|
||||
try {
|
||||
if (!isMounted.value) return;
|
||||
|
||||
if (!userPrefStore.selectedProject?.id) {
|
||||
console.log('No selected project, skipping video groups load');
|
||||
videoGroups.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
await ksVideoGroupStore.fetchKsVideoGroup(userPrefStore.selectedProject.id);
|
||||
|
||||
if (!isMounted.value) return; // Check again after async call
|
||||
|
||||
videoGroups.value = [...(ksVideoGroupStore.ksVideoGroup || [])];
|
||||
console.log('Video groups loaded:', videoGroups.value);
|
||||
} catch (error) {
|
||||
console.error('Error loading video groups:', error);
|
||||
if (isMounted.value) {
|
||||
videoGroups.value = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Funzione per caricare i documenti KS
|
||||
const loadKsDocuments = async () => {
|
||||
try {
|
||||
if (!isMounted.value) return;
|
||||
|
||||
console.log('Starting KS documents load...');
|
||||
const docsResponse = await KSDocumentService.getKSDocuments();
|
||||
console.log('KS documents response:', docsResponse);
|
||||
|
||||
if (!isMounted.value) return; // Check again after async call
|
||||
|
||||
ksDocuments.value = docsResponse.data || [];
|
||||
console.log('KS documents loaded:', ksDocuments.value.length, 'items');
|
||||
|
||||
// Debug: verifica struttura dei documenti
|
||||
if (ksDocuments.value.length > 0) {
|
||||
console.log('First KS document structure:', ksDocuments.value[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading KS documents:', error);
|
||||
if (isMounted.value) {
|
||||
ksDocuments.value = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
// Carica le opzioni necessarie basate sui dataSource presenti negli inputs dello scenario
|
||||
const loadOptionsForScenario = async () => {
|
||||
if (!props.scenario.inputs) return;
|
||||
|
||||
console.log('Loading options for scenario inputs...');
|
||||
|
||||
// Trova tutti i dataSource unici negli input multiselect
|
||||
const dataSources = new Set();
|
||||
props.scenario.inputs.forEach((input) => {
|
||||
if (input.type === 'multiselect' && input.dataSource) {
|
||||
dataSources.add(input.dataSource);
|
||||
}
|
||||
});
|
||||
|
||||
// Crea le funzioni di caricamento per ogni dataSource
|
||||
const loadingPromises = Array.from(dataSources).map(async (dataSource) => {
|
||||
try {
|
||||
// Imposta lo stato di loading per questo dataSource
|
||||
loadingOptionsFor[dataSource] = true;
|
||||
console.log(`Loading options for dataSource: ${dataSource}`);
|
||||
|
||||
switch (dataSource) {
|
||||
case 'videoGroups':
|
||||
await loadVideoGroups();
|
||||
break;
|
||||
|
||||
case 'ksDocuments':
|
||||
await loadKsDocuments();
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown dataSource: ${dataSource}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading options for ${dataSource}:`, error);
|
||||
} finally {
|
||||
// Reset lo stato di loading per questo dataSource
|
||||
loadingOptionsFor[dataSource] = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Aspetta che tutti i caricamenti siano completati
|
||||
await Promise.all(loadingPromises);
|
||||
};
|
||||
|
||||
const execScenario = () => {
|
||||
if (numberPrFiles.value !== 1 && reqMultiFile.value) {
|
||||
toast.add({
|
||||
@@ -344,9 +506,30 @@ const execScenario = () => {
|
||||
|
||||
loadingStore.exectuion_loading = true;
|
||||
|
||||
// Crea una copia dei dati del form
|
||||
const processedData = { ...formData.value };
|
||||
|
||||
// Elabora tutti i multiselect dinamici
|
||||
if (props.scenario.inputs) {
|
||||
props.scenario.inputs.forEach((input) => {
|
||||
if (input.type === 'multiselect' && processedData[input.name]) {
|
||||
// Trasforma array di oggetti in array di IDs/nomi
|
||||
if (Array.isArray(processedData[input.name])) {
|
||||
if (input.dataSource === 'videoGroups') {
|
||||
processedData[input.name + '_names'] = processedData[input.name].map((item) => item.name || item);
|
||||
processedData[input.name + '_ids'] = processedData[input.name].map((item) => item.id || item);
|
||||
} else if (input.dataSource === 'ksDocuments') {
|
||||
processedData[input.name + '_names'] = processedData[input.name].map((item) => item.fileName || item);
|
||||
processedData[input.name + '_ids'] = processedData[input.name].map((item) => item.id || item);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const data = {
|
||||
scenario_id: props.scenario.id,
|
||||
inputs: { ...formData.value }
|
||||
inputs: processedData
|
||||
};
|
||||
|
||||
axios
|
||||
@@ -711,11 +894,24 @@ const formatSize = (bytes) => {
|
||||
};
|
||||
|
||||
const addToCanvas = () => {
|
||||
console.log("Added");
|
||||
emit('add',scenario_output.value)
|
||||
}
|
||||
</script>
|
||||
console.log('Added');
|
||||
emit('add', scenario_output.value);
|
||||
};
|
||||
|
||||
// Ricarica le opzioni quando lo scenario cambia
|
||||
watch(
|
||||
() => props.scenario,
|
||||
async (newScenario) => {
|
||||
if (!isMounted.value) return;
|
||||
|
||||
if (newScenario && newScenario.inputs) {
|
||||
formData.value = { ...newScenario.inputs.reduce((acc, input) => ({ ...acc, [input.name]: '' }), {}) };
|
||||
await loadOptionsForScenario();
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.editor ol {
|
||||
|
||||
Reference in New Issue
Block a user