479 lines
14 KiB
Vue
479 lines
14 KiB
Vue
<template>
|
|
<section
|
|
class="h-screen-main w-full px-4 flex flex-col justify-between items-start"
|
|
>
|
|
<!-- Upload Content -->
|
|
<div class="w-full h-[calc(100%_-_64px)]">
|
|
<!-- File name -->
|
|
<!-- cursor-pointer -->
|
|
<div class="h-12 pl-1 border-b border-neutral-300 flex items-center">
|
|
<InputText
|
|
type="text"
|
|
v-model="fileName"
|
|
id="fileNameInput"
|
|
class="rounded !border-transparent !font-bold !text-base !py-1 min-w-[1px] w-auto"
|
|
@input="onInput"
|
|
@blur="onBlur"
|
|
maxlength="200"
|
|
title="Rename"
|
|
/>
|
|
</div>
|
|
<!-- Upload notification -->
|
|
<div class="flex justify-start items-center space-x-2 ml-2 py-2">
|
|
<span
|
|
class="material-symbols-outlined text-neutral-700 cursor-pointer"
|
|
v-tooltip.right="tooltipUpload"
|
|
>info</span
|
|
>
|
|
<span class="w-px h-7 bg-neutral-300"></span>
|
|
<!-- Upload text -->
|
|
<div>
|
|
<div v-if="!isDisabled"></div>
|
|
<div
|
|
v-else
|
|
class="flex justify-start items-center space-x-2 duration-700"
|
|
>
|
|
<p v-if="informData.length !== 0" class="text-primary text-sm">
|
|
Need to select
|
|
<span v-for="(item, index) in informData" :key="index"
|
|
>{{ item.label
|
|
}}<span v-if="index !== informData.length - 1">, </span></span
|
|
>.
|
|
</p>
|
|
<div v-if="repeatedData.length !== 0" class="duration-700">
|
|
<p v-if="repeatedData.length === 1" class="text-danger text-sm">
|
|
{{ repeatedData[0].label }} has been assigned.
|
|
</p>
|
|
<p v-else class="text-danger text-sm">
|
|
<span v-for="(item, index) in repeatedData" :key="index"
|
|
>{{ item.label
|
|
}}<span v-if="index !== repeatedData.length - 1"
|
|
>,
|
|
</span></span
|
|
>
|
|
have been assigned.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Upload table -->
|
|
<div
|
|
class="overflow-y-auto overflow-x-auto scrollbar max-h-[calc(100%_-_94px)]"
|
|
>
|
|
<table
|
|
class="text-sm border-separate border-spacing-0 h-full overflow-y-auto overflow-x-auto scrollbar"
|
|
>
|
|
<caption class="hidden">
|
|
Upload
|
|
</caption>
|
|
<thead class="sticky top-0 bg-neutral-10">
|
|
<tr class="hidden">
|
|
<th></th>
|
|
</tr>
|
|
<tr>
|
|
<td
|
|
v-for="(item, index) in uploadDetail?.columns"
|
|
:key="index"
|
|
class="border border-neutral-500 p-2 truncate max-w-[198px]"
|
|
>
|
|
{{ item }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td
|
|
v-for="(item, index) in uploadDetail?.columns"
|
|
:key="index"
|
|
class="px-2 py-1 bg-neutral-300 border border-neutral-500 max-w-[198px]"
|
|
>
|
|
<Select
|
|
v-model="selectedColumns[index]"
|
|
:options="columnType"
|
|
optionLabel="name"
|
|
placeholder=""
|
|
class="w-[180px] !border-neutral-500"
|
|
:data-type="item"
|
|
:inputId="index.toString()"
|
|
:inputClass="[selectedColumns[index]?.color, 'text-sm']"
|
|
>
|
|
<template #option="slotProps">
|
|
<div :class="slotProps.option.color" class="text-sm">
|
|
<span>{{ slotProps.option.name }}</span>
|
|
</div>
|
|
</template>
|
|
</Select>
|
|
</td>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(item, index) in uploadDetail?.data" :key="index">
|
|
<td
|
|
v-for="(itemDetail, key) in item"
|
|
:key="key"
|
|
class="border border-neutral-500 p-2 truncate max-w-[198px]"
|
|
>
|
|
{{ itemDetail }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<!-- Upload button -->
|
|
<div class="w-full text-right space-x-4 px-8 py-4">
|
|
<button type="button" class="btn btn-sm btn-neutral" @click="cancel">
|
|
Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-neutral" @click="reset">
|
|
Reset
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
@click="submit"
|
|
:disabled="isDisabled"
|
|
:class="isDisabled ? 'btn-disable' : 'btn-neutral'"
|
|
>
|
|
Upload
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
<script>
|
|
// The Lucia project.
|
|
// Copyright 2023-2026 DSP, inc. All rights reserved.
|
|
// Authors:
|
|
// chiayin.kuo@dsp.im (chiayin), 2023/1/31
|
|
// cindy.chang@dsp.im (Cindy Chang), 2024/5/30
|
|
// imacat.yang@dsp.im (imacat), 2023/9/23
|
|
/**
|
|
* @module views/Upload File upload page with column mapping
|
|
* dropdowns, file rename input, and validation before final
|
|
* upload submission.
|
|
*/
|
|
|
|
import { useFilesStore } from "@/stores/files";
|
|
|
|
export default {
|
|
beforeRouteEnter(to, from, next) {
|
|
// An uploadID is required to enter this page
|
|
next((vm) => {
|
|
const filesStore = useFilesStore();
|
|
if (filesStore.uploadId === null) {
|
|
vm.$router.push({ name: "Files", replace: true });
|
|
vm.$toast.default("Please upload your file.", { position: "bottom" });
|
|
}
|
|
});
|
|
},
|
|
};
|
|
</script>
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { storeToRefs } from "pinia";
|
|
import { useLoadingStore } from "@/stores/loading";
|
|
import {
|
|
uploadConfirm,
|
|
} from "@/module/alertModal.js";
|
|
|
|
const router = useRouter();
|
|
|
|
// Stores
|
|
const loadingStore = useLoadingStore();
|
|
const filesStore = useFilesStore();
|
|
const { isLoading } = storeToRefs(loadingStore);
|
|
const { uploadDetail, uploadId, uploadFileName } = storeToRefs(filesStore);
|
|
|
|
// Data
|
|
const tooltipUpload = {
|
|
value: `1. Case ID: A unique identifier for each case.
|
|
2. Activity: A process step executed by either a system (automated) or humans (manual).
|
|
3. Activity Instance ID: A unique identifier for a single occurrence of an activity.
|
|
4. Timestamp: The time of occurrence of a particular event, such as the start or end of an activity.
|
|
5. Status: Activity status, such as Start or Complete.
|
|
6. Attribute: A property that can be associated with a case to provide additional information about that case.`,
|
|
// Resource is not available yet
|
|
// 7. Resource: A resource refers to any entity that is required to carry out a business process. This can include people, equipment, software, or any other type of asset.
|
|
class: "!max-w-[400px] !text-[10px] !opacity-80",
|
|
autoHide: false,
|
|
};
|
|
|
|
const columnType = [
|
|
{
|
|
name: "Case ID*",
|
|
code: "case_id",
|
|
color: "!text-secondary",
|
|
value: "",
|
|
label: "Case ID",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "Timestamp*",
|
|
code: "timestamp",
|
|
color: "!text-secondary",
|
|
value: "",
|
|
label: "Timestamp",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "Status*",
|
|
code: "status",
|
|
color: "!text-secondary",
|
|
value: "",
|
|
label: "Status",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "Activity*",
|
|
code: "name",
|
|
color: "!text-secondary",
|
|
value: "",
|
|
label: "Activity",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "Activity Instance ID*",
|
|
code: "instance",
|
|
color: "!text-secondary",
|
|
value: "",
|
|
label: "Activity Instance ID",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "Case Attribute",
|
|
code: "case_attributes",
|
|
color: "!text-primary",
|
|
value: "",
|
|
label: "Case Attribute",
|
|
required: false,
|
|
},
|
|
// { name: 'Resource', code: '', color: '', value: '', label: 'Resource', required: false }, // Not available yet; may be added in the future
|
|
{
|
|
name: "Not Assigned",
|
|
code: "",
|
|
color: "!text-neutral-700",
|
|
value: "",
|
|
label: "Not Assigned",
|
|
required: false,
|
|
},
|
|
];
|
|
|
|
const selectedColumns = ref([]);
|
|
const informData = ref([]);
|
|
const repeatedData = ref([]);
|
|
const fileName = ref(uploadFileName.value);
|
|
const showEdit = ref(false);
|
|
|
|
// Computed
|
|
const isDisabled = computed(() => {
|
|
// 1. Length must match; every column must be assigned
|
|
// 2. Must not be null or undefined
|
|
const hasValue = !selectedColumns.value.includes(undefined);
|
|
const result = !(
|
|
selectedColumns.value.length === uploadDetail.value?.columns.length &&
|
|
informData.value.length === 0 &&
|
|
repeatedData.value.length === 0 &&
|
|
hasValue
|
|
);
|
|
return result;
|
|
});
|
|
|
|
// Watch
|
|
watch(
|
|
selectedColumns,
|
|
(newVal) => {
|
|
updateValidationData(newVal);
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
// Methods
|
|
/**
|
|
* Adjusts input width and trims whitespace on blur.
|
|
* @param {Event} e - The blur event from the file name input.
|
|
*/
|
|
function onBlur(e) {
|
|
const baseWidth = 20;
|
|
|
|
if (e.target.value === "") {
|
|
e.target.value = uploadFileName.value;
|
|
const textWidth = getTextWidth(e.target.value, e.target);
|
|
e.target.style.width = baseWidth + textWidth + "px";
|
|
} else if (e.target.value !== e.target.value.trim()) {
|
|
e.target.value = e.target.value.trim();
|
|
const textWidth = getTextWidth(e.target.value, e.target);
|
|
e.target.style.width = baseWidth + textWidth + "px";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dynamically resizes the input width as the user types.
|
|
* @param {Event} e - The input event from the file name input.
|
|
*/
|
|
function onInput(e) {
|
|
const baseWidth = 20;
|
|
const textWidth = getTextWidth(e.target.value, e.target);
|
|
|
|
e.target.style.width = baseWidth + textWidth + "px";
|
|
}
|
|
|
|
/**
|
|
* Measures the pixel width of text using a hidden span element.
|
|
* @param {string} text - The text to measure.
|
|
* @param {HTMLElement} e - The input element for font style reference.
|
|
* @returns {number} The text width in pixels.
|
|
*/
|
|
function getTextWidth(text, e) {
|
|
// Replace spaces with non-breaking spaces
|
|
const processedText = text.replaceAll(" ", "\u00a0");
|
|
const hiddenSpan = document.createElement("span");
|
|
|
|
hiddenSpan.textContent = processedText;
|
|
hiddenSpan.style.font = globalThis.getComputedStyle(e).font;
|
|
hiddenSpan.style.visibility = "hidden";
|
|
document.body.appendChild(hiddenSpan);
|
|
const width = hiddenSpan.getBoundingClientRect().width;
|
|
hiddenSpan.remove();
|
|
|
|
return width;
|
|
}
|
|
|
|
/**
|
|
* Updates validation state (missing required and duplicate columns).
|
|
* @param {Array} data - The currently selected column type assignments.
|
|
*/
|
|
function updateValidationData(data) {
|
|
const nameOccurrences = {};
|
|
const noSortedRepeatedData = []; // Unsorted duplicate selections
|
|
const selectedData = []; // Already selected data
|
|
|
|
informData.value = []; // Not yet selected data
|
|
repeatedData.value = []; // Duplicate selections
|
|
|
|
data.forEach((item) => {
|
|
const { name, code } = item;
|
|
|
|
if (nameOccurrences[name]) {
|
|
// 'Not Assigned' and 'Case Attribute' are excluded from validation
|
|
if (!code || code === "case_attributes") return;
|
|
nameOccurrences[name]++;
|
|
// Each duplicate option should only appear once
|
|
if (nameOccurrences[name] === 2) {
|
|
noSortedRepeatedData.push(item);
|
|
}
|
|
// Sort according to the dropdown menu order
|
|
repeatedData.value = columnType.filter((column) =>
|
|
noSortedRepeatedData.includes(column),
|
|
);
|
|
} else {
|
|
nameOccurrences[name] = 1;
|
|
selectedData.push(name);
|
|
informData.value = columnType.filter((item) =>
|
|
item.required ? !selectedData.includes(item.name) : false,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Resets all column selections to default. */
|
|
function reset() {
|
|
// Do not add to browser history
|
|
selectedColumns.value = [];
|
|
}
|
|
|
|
/** Navigates back to the Files page without uploading. */
|
|
function cancel() {
|
|
// Do not add to browser history
|
|
router.push({ name: "Files", replace: true });
|
|
}
|
|
|
|
/** Submits the column mapping and triggers the second-stage upload. */
|
|
async function submit() {
|
|
// Post API Data
|
|
const fetchData = {
|
|
timestamp: "",
|
|
case_id: "",
|
|
name: "",
|
|
instance: "",
|
|
status: "",
|
|
case_attributes: [],
|
|
};
|
|
// Assign values
|
|
const haveValueData = selectedColumns.value.map((column, i) => {
|
|
if (column && uploadDetail.value?.columns?.[i]) {
|
|
return {
|
|
name: column.name,
|
|
code: column.code,
|
|
color: column.color,
|
|
value: uploadDetail.value.columns[i],
|
|
};
|
|
}
|
|
});
|
|
|
|
// Get the desired file name to change
|
|
uploadFileName.value = fileName.value;
|
|
// Set the data for the second-stage upload
|
|
haveValueData.forEach((column) => {
|
|
if (column !== undefined) {
|
|
switch (column.code) {
|
|
case "timestamp":
|
|
fetchData.timestamp = column.value;
|
|
break;
|
|
case "case_id":
|
|
fetchData.case_id = column.value;
|
|
break;
|
|
case "name":
|
|
fetchData.name = column.value;
|
|
break;
|
|
case "instance":
|
|
fetchData.instance = column.value;
|
|
break;
|
|
case "status":
|
|
fetchData.status = column.value;
|
|
break;
|
|
case "case_attributes":
|
|
fetchData.case_attributes.push(column.value);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
uploadConfirm(fetchData);
|
|
}
|
|
|
|
// Mounted
|
|
onMounted(async () => {
|
|
// Watch only once
|
|
const unwatch = watch(
|
|
fileName,
|
|
(newValue) => {
|
|
if (newValue) {
|
|
const inputElement = document.getElementById("fileNameInput");
|
|
if (!inputElement) return;
|
|
const baseWidth = 20;
|
|
const textWidth = getTextWidth(fileName.value, inputElement);
|
|
inputElement.style.width = baseWidth + textWidth + "px";
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
showEdit.value = true;
|
|
if (uploadId.value) await filesStore.getUploadDetail();
|
|
if (uploadDetail.value?.columns) {
|
|
selectedColumns.value = Array.from(
|
|
{ length: uploadDetail.value.columns.length },
|
|
() => columnType[columnType.length - 1],
|
|
); // Default to "Not Assigned"
|
|
}
|
|
unwatch();
|
|
isLoading.value = false;
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
// Clear uploadID when leaving the page
|
|
uploadId.value = null;
|
|
uploadFileName.value = null;
|
|
});
|
|
</script>
|