Rename single-word Vue files to multi-word names and update references
Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
477
src/views/Upload/UploadPage.vue
Normal file
477
src/views/Upload/UploadPage.vue
Normal file
@@ -0,0 +1,477 @@
|
||||
<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]"
|
||||
>
|
||||
<Dropdown
|
||||
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>
|
||||
</Dropdown>
|
||||
</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 {
|
||||
uploadFailedFirst,
|
||||
uploadSuccess,
|
||||
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.replace(/ /g, "\u00a0");
|
||||
const hiddenSpan = document.createElement("span");
|
||||
|
||||
hiddenSpan.innerHTML = processedText;
|
||||
hiddenSpan.style.font = window.getComputedStyle(e).font;
|
||||
hiddenSpan.style.visibility = "hidden";
|
||||
document.body.appendChild(hiddenSpan);
|
||||
const width = hiddenSpan.getBoundingClientRect().width;
|
||||
document.body.removeChild(hiddenSpan);
|
||||
|
||||
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");
|
||||
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();
|
||||
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>
|
||||
Reference in New Issue
Block a user