Code owners
Assign users and groups as approvers for specific file changes. Learn more.
FilesView.vue 34.38 KiB
<template>
<div class="DataSource">
<!-- Files View Header -->
<FilesViewHeader
v-model="searchTerm"
:is-uploading="isUploading"
@clickFileSelect="clickFileSelect"
/>
<b-row>
<!-- Files View Table -->
<b-table
id="resourceViewTable"
ref="adaptTable"
:fields="headers"
:items="folderContents"
:busy="isBusy"
:locale="$i18n.locale"
:filter="searchTerm"
:per-page="perPage"
:current-page="currentPage"
:filter-included-fields="filterFields"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
selectable
striped
bordered
outlined
hover
small
responsive
head-variant="dark"
class="adaptTable"
show-empty
:empty-text="$t('page.resource.emptyTableText')"
:empty-filtered-text="$t('page.resource.emptyFilterText')"
@filtered="filtered"
@row-selected="onRowSelected"
>
<!-- Loading Spinner for Busy State -->
<div slot="table-busy" class="text-center text-danger my-2">
<b-spinner class="align-middle" />
<strong style="margin-left: 1%">
{{ $t("page.resource.loading") }}
</strong>
</div>
<!-- Column "Name" -->
<template #head(name)="row">
<b-form-checkbox v-model="selectAll" @change="allSelect(row)" />
<span>{{ $t("page.resource.fileName") }}</span>
</template>
<!-- Column "Last Modified" -->
<template #head(lastModified)="row">
<span>{{ row.label }}</span>
</template>
<!-- Column "Size" -->
<template #head(size)="row">
<span>{{ row.label }}</span>
</template>
<!-- -->
<template #head()="row">
<template v-if="visibleColumns.includes(row.field.key)">
<span>{{ row.label }}</span>
</template>
<template v-else>
<span class="additionalColumnHeader">
{{ row.label }}
<b-icon icon="x" @click="removeColumn(row)" />
</span>
</template>
</template>
<!-- Column "Add Column"/Filter -->
<template #head(addColumn)>
<b-dropdown id="addColumnDropDown" size="sm" right :no-caret="true">
<!-- Button Template + Icon -->
<template #button-content>
<b-icon icon="funnel" />
</template>
<!-- Checkbox Options -->
<b-form-checkbox
v-for="column in columns"
:key="column.key"
v-model="column.active"
stacked
>
{{ column.label }}
</b-form-checkbox>
</b-dropdown>
</template>
<!-- Row "Name" -->
<template #cell(name)="row">
<span class="d-flex justify-between fileViewEntryWrapper">
<span class="d-inline-flex gap-2">
<!-- File Icon and Checkbox -->
<span class="checkFile">
<!-- Show a checkbox for elements that are not read-only -->
<b-form-checkbox
v-if="!row.item.readOnly"
v-model="row.rowSelected"
class="tableCheck"
@change="select(row)"
/>
<!-- Icon -->
<b-icon
:icon="row.item.type === 'Tree' ? 'folder' : 'file-earmark'"
class="ml-1"
/>
</span>
<a
v-if="!editableDataUrl || row.item.type === 'Tree'"
class="fileViewEntry"
@click="triggerNavigation(row.item)"
>
{{ row.item.name }}
</a>
<span v-else class="fileViewEntry"> {{ row.item.name }} </span>
</span>
<!-- Row File "..." Menu -->
<b-dropdown
class="dotMenu"
left
variant="link"
toggle-class="text-decoration-none"
size="sm"
:no-caret="true"
:disabled="editableDataUrl && resource && resource.archived"
>
<!-- Row File "..." Button Template + Icon -->
<template #button-content>
<b-icon icon="three-dots-vertical" />
</template>
<!-- Option "Download" -->
<b-dropdown-item
v-if="!editableDataUrl && row.item.type === 'Leaf'"
@click="openFile(row.item)"
>
{{ $t("page.resource.metadataManagerBtnDownload") }}
</b-dropdown-item>
<!-- Option "Delete" -->
<b-dropdown-item
v-if="!isGuest"
:disabled="
(resource && resource.archived) ||
(resourceTypeInformation &&
!resourceTypeInformation.canDelete)
"
@click="showModalDeleteFile(row.item)"
>
{{ $t("buttons.delete") }}
</b-dropdown-item>
</b-dropdown>
</span>
</template>
<!-- Pagination -->
<template #table-caption>
<b-row class="text-right" align-v="center" no-gutters>
<b-col align-self="center" class="p-0" />
<b-col align-self="center" class="p-0">
<b-pagination
id="pagination"
v-model="currentPage"
:total-rows="rows"
:per-page="perPage"
aria-controls="resourceViewTable"
/>
</b-col>
<b-col align-self="center" class="p-0">
<b-form-select
v-model="perPage"
:options="paginationPerPageOptions"
style="max-width: 5rem"
/>
</b-col>
</b-row>
</template>
</b-table>
</b-row>
</div>
</template>
<script lang="ts">
import { defineComponent, type PropType, reactive } from "vue";
// import the store for current module
import useResourceStore from "../../store";
import useProjectStore from "@/modules/project/store";
import FilesViewHeader from "./FilesViewHeader.vue";
import MetadataManagerUtil from "../../utils/MetadataManagerUtil";
import { FileUtil } from "../../utils/FileUtil";
import { v4 as uuidv4 } from "uuid";
import { parseRDFDefinition } from "../../utils/linkedData";
import factory from "rdf-ext";
import {
TreeDto2FileInformation,
TreeDto2FolderInformation,
treeMapper,
} from "@/mapping/tree";
import type { BFormRow, BTable } from "bootstrap-vue";
import type { Dataset, Literal, Quad } from "@rdfjs/types";
import type {
ProjectDto,
ResourceTypeInformationDto,
FileTreeDto,
MetadataTreeDto,
} from "@coscine/api-client/dist/types/Coscine.Api";
import type {
BilingualLabels,
CustomTableField,
FileInformation,
FolderContent,
FolderInformation,
ReadOnlyFolderInformation,
VisitedResourceObject,
} from "../../types";
export default defineComponent({
components: {
FilesViewHeader,
},
props: {
folderContents: {
default: () => {
return [];
},
type: Array as PropType<FolderContent[]>,
},
fileListEdit: {
default: () => {
return [];
},
type: Array as PropType<FolderContent[]>,
},
isUploading: {
default: false,
type: Boolean,
},
dirTrail: {
required: true,
type: String,
},
dirCrumbs: {
required: true,
type: Array as PropType<string[]>,
},
},
emits: {
clickFileSelect: () => true,
fileListEdit: (_: FolderContent[]) => true,
folderContents: (_: FolderContent[]) => true,
showDetail: (_: boolean) => true,
showModalDelete: (_: FolderContent[]) => true,
},
setup() {
const resourceStore = useResourceStore();
const projectStore = useProjectStore();
return { resourceStore, projectStore };
},
data() {
return {
currentPage: 1,
filteredRows: -1,
isBusy: true,
isLoadingSearchResults: false,
perPage: 10,
selectAll: false,
searchTerm: "",
sortBy: "",
sortDesc: false,
};
},
computed: {
isGuest(): boolean | undefined {
return this.projectStore.currentUserRoleIsGuest;
},
applicationProfile(): Dataset | null {
return this.resourceStore.currentFullApplicationProfile;
},
project(): null | ProjectDto {
return this.projectStore.currentProject;
},
resource(): null | VisitedResourceObject {
return this.resourceStore.currentResource;
},
classes(): { [className: string]: BilingualLabels } {
return this.resourceStore.classes;
},
resourceTypeInformation(): ResourceTypeInformationDto | undefined {
return this.resourceStore.enabledResourceTypes?.find(
(resourceType) => resourceType.id === this.resource?.type?.id,
);
},
editableDataUrl(): boolean {
return (
this.resourceTypeInformation?.resourceContent?.metadataView
?.editableDataUrl === true
);
},
locale(): string {
// Define as computed property to translate RCV Table
return this.$root.$i18n.locale;
},
columns(): CustomTableField[] {
const columns: CustomTableField[] = [];
const oldColumns = this.loadFromLocalStorage();
if (!this.applicationProfile) return oldColumns;
// keys a, b, name, active
const shaclPath = factory.namedNode("http://www.w3.org/ns/shacl#path");
const shaclName = factory.namedNode("http://www.w3.org/ns/shacl#name");
const paths = this.applicationProfile.match(undefined, shaclPath);
for (const path of paths) {
// Read the active value from the localStorage
const identifier = path.object.value;
// Find the name of the column in the preferred language or a default if not available.
const names = this.applicationProfile.match(path.subject, shaclName);
const name = this.getColumnName(names);
// Determine if the column should be active based on stored settings.
const activeColumn = this.isColumnActive(identifier, oldColumns);
if (name) {
columns.push(this.createColumn(name, identifier, path, activeColumn));
}
}
return columns;
},
defaultHeaders(): Array<CustomTableField> {
// Define as computed property to have table
// header text react on language changes.
return [
{
label: this.$t("page.resource.fileName").toString(),
key: "name",
sortable: true,
active: true,
},
{
label: this.$t("page.resource.lastModified").toString(),
key: "lastModified",
sortable: true,
active: true,
formatter: (value, key, item: FolderContent) => {
return this.renderDate(item);
},
filterByFormatted: true,
},
{
label: this.$t("page.resource.size").toString(),
key: "size",
sortable: true,
active: true,
formatter: (value, key, item: FolderContent) => {
return this.renderSize(item);
},
filterByFormatted: true,
},
];
},
filterFields(): string[] {
const filterFields = this.defaultHeaders.map((header) => header.key);
filterFields.push(
...this.visibleColumns.filter(
(column) => !filterFields.includes(column),
),
);
filterFields.push(
...this.columns
.filter((column) => column.active)
.map((column) => column.key),
);
return filterFields;
},
headers(): Array<CustomTableField> {
const headers: CustomTableField[] = [...this.defaultHeaders];
for (const column of this.visibleColumns) {
if (!this.filterFields.includes(column)) {
headers.push({
label: this.$t(column).toString(),
key: column,
sortable: true,
active: true,
});
}
}
headers.push(...this.columns.filter((column) => column.active));
headers.push({
label: "add",
key: "addColumn",
sortable: false,
active: true,
});
return headers;
},
paginationPerPageOptions(): { value: number; text: string }[] {
return [
{ value: 10, text: "10" },
{ value: 50, text: "50" },
{ value: 100, text: "100" },
{ value: this.rows, text: this.$t("page.resource.all").toString() },
];
},
rows() {
return this.filteredRows >= 0
? this.filteredRows
: this.folderContents.length;
},
visibleColumns(): Array<string> {
let visibleColumns: Array<string> = [];
const collectedColumns =
this.resourceTypeInformation?.resourceContent?.entriesView?.columns
?.always;
if (collectedColumns) {
visibleColumns = Array.from(collectedColumns);
}
return visibleColumns;
},
},
watch: {
searchTerm() {
this.saveInLocalStorage();
},
headers() {
this.saveInLocalStorage();
},
async resource() {
await this.navigateTree();
},
sortBy() {
this.saveInLocalStorage();
},
sortDesc() {
this.saveInLocalStorage();
},
async dirTrail() {
// Reacts to route changes from 'triggerNavigation()'
await this.navigateTree();
},
},
async created() {
this.isBusy = true;
if (this.resource) {
await this.navigateTree();
}
this.isBusy = false;
},
methods: {
/**
* Retrieves the column name from the provided names list.
* It prioritizes names in the current locale.
*
* @param {Dataset<Quad, Quad>} names - The list of potential names.
* @returns {string | null} - The retrieved name or null.
*/
getColumnName(names: Dataset<Quad, Quad>): string | null {
let name: string | null = null;
for (const nameEntry of names) {
name = nameEntry.object.value;
if (
(nameEntry.object as Literal).language === this.$root.$i18n.locale
) {
break;
}
}
return name;
},
/**
* Determines if a column should be active based on the provided identifier and the stored columns.
* @param {string} identifier - The identifier of the column to check.
* @param {CustomTableField[]} oldColumns - The collection of stored old columns.
* @returns {boolean} - True if the column should be active, false otherwise.
*/
isColumnActive(
identifier: string,
oldColumns: CustomTableField[],
): boolean {
for (const oldColumn of oldColumns) {
if (oldColumn.key === identifier) {
return oldColumn.active;
}
}
return false;
},
/**
* Creates a column based on the provided name, identifier and path.
* @param {string} name - The name of the column.
* @param {string} identifier - The identifier of the column.
* @param {Quad} path - The path of the column.
* @param {boolean} activeColumn - True if the column should be active, false otherwise.
* @returns {CustomTableField} - The created column.
*/
createColumn(
name: string,
identifier: string,
path: Quad,
activeColumn: boolean,
): CustomTableField {
return reactive({
label: name,
key: identifier,
sortable: true,
active: activeColumn,
formatter: (value, key, item: FolderContent) => {
return this.formatColumnValue(
item.metadata[item.latestVersion],
path,
);
},
sortByFormatted: true,
filterByFormatted: true,
});
},
/**
* Formats the column value based on the provided metadata and path.
* @param {Dataset<Quad, Quad>} metadata - The metadata containing information to format.
* @param {Quad} path - The RDF quad defining the property path for the column.
* @returns {string} - The formatted column value, where multiple values are separated by a line break ("<br>").
*/
formatColumnValue(
metadata: Dataset<Quad, Quad> | null,
path: Quad,
): string {
// Extract all matching entries from the metadata using the specified path.
const entries = metadata?.match(undefined, path.object);
// If there are no matching entries, return an empty string.
if (!entries?.size) return "";
const returnList: string[] = [];
for (const entry of entries) {
const entryObject = entry.object;
let entryValue = entryObject.value;
// If the entry object is a literal, further formatting might be needed.
if (entryObject.termType === "Literal") {
// If the datatype of the literal indicates a date, format it accordingly.
if (entryObject.datatype.value.endsWith("date")) {
const date = entryObject.value;
const dateObject = new Date(date);
entryValue = dateObject.toLocaleDateString(this.$i18n.locale);
}
}
// If the entry object is a named node, it might need to be translated into a more readable form.
else if (entryObject.termType === "NamedNode") {
// Loop through all available classes to find a match for the named node's value.
for (const classValue of Object.values(this.classes)) {
// Depending on the user's locale, choose the appropriate language list.
const langClassList =
this.$i18n.locale === "de" ? classValue.de : classValue.en;
// If a language-specific list is available, search for the named node's value in it.
if (langClassList) {
const foundEntry = langClassList.find(
(langClassEntry) => langClassEntry.value === entryValue,
);
// If a match is found, replace the entryValue with the more readable name.
if (foundEntry && foundEntry.name) {
entryValue = foundEntry.name;
break; // Exit the loop once a match is found.
}
}
}
}
// Add the processed entry value to the list of results.
returnList.push(entryValue);
}
// Join the list of results with line breaks and return the final formatted value.
return returnList.join("<br>");
},
/**
* Navigate the folder structure based on the current folder's path.
* This method serves to identify whether the current folder is actually a file,
* opens it accordingly, and then navigates the directory structure.
*/
async navigateTree() {
// Start with the current folder as the reference
let currentFolder = this.dirTrail;
// Check if the current folder might actually be a file and handle it
if (this.isProbablyFile(currentFolder)) {
// TODO: Change to open modal
await this.openFile({
name: currentFolder.substring(currentFolder.lastIndexOf("/") + 1),
path: currentFolder,
} as FileInformation);
// Remove the file name from the path to get the folder path, then navigate to it
currentFolder = this.dirCrumbs.join("");
this.triggerNavigation({ path: currentFolder } as FolderInformation);
}
// Finally, open the current folder to display its contents
await this.openFolder({ path: currentFolder } as FolderInformation);
},
/**
* Navigates to the specified folder or resource based on the given `entry`.
* If the provided `entry.path` is different from the current `dirTrail`,
* a new route will be pushed to the Vue router.
*
* @param {FolderContent} entry - The folder or file information object to navigate to.
*/
triggerNavigation(entry: FolderContent) {
this.$router.push({
name: "resource-page",
params: {
dirTrail: entry.path,
},
});
},
/**
* Determines if a given path represents a file based on its structure.
* Files typically do not end with a slash `/`.
*
* @param {string} path - The path to evaluate.
* @returns {boolean} - True if the path might represent a file, false otherwise.
*/
isProbablyFile(path: string | undefined): boolean | undefined {
return path !== "" && !path?.endsWith("/");
},
/**
* Splits a path into its constituent directories.
*
* @param {string} path - The path to split.
* @returns {string[]} - An array representing the folder's hierarchical structure.
*/
splitPathIntoDirectories(path: string): string[] {
const directories = [];
let combinedPath = "";
const splitFolders = path.split("/");
for (const folder of splitFolders) {
if (combinedPath && !folder) continue;
combinedPath += folder + "/";
directories.push(combinedPath);
}
return directories;
},
/**
* Saves table preferences in local storage.
*/
async saveInLocalStorage() {
await this.resourceStore.setStoredColumns({
columns: this.columns,
filter: this.searchTerm,
sortBy: this.sortBy,
sortDesc: this.sortDesc,
});
},
/**
* Loads table preferences from local storage.
* @returns {Array<CustomTableField>} - Array of table fields.
*/
loadFromLocalStorage(): Array<CustomTableField> {
let storedColumns: Array<CustomTableField> = [];
const currentStoredColumns = this.resourceStore.currentStoredColumns;
if (currentStoredColumns) {
// If the stored columns are just an array (legacy format)
if (Array.isArray(currentStoredColumns)) {
return currentStoredColumns;
}
// Retrieve and set stored preferences
storedColumns = currentStoredColumns.columns;
this.searchTerm = currentStoredColumns.filter;
this.sortBy = currentStoredColumns.sortBy;
this.sortDesc = currentStoredColumns.sortDesc;
}
return storedColumns;
},
/**
* Deactivates a column based on the provided row.
* @param {BFormRow} row - Row which represents a column.
*/
removeColumn(row: BFormRow) {
for (const column of this.columns) {
if (column.key === row.column) {
column.active = false;
}
}
},
/**
* Emits an event to show the delete modal for a specific file.
* @param {FolderContent} file - File to delete.
*/
showModalDeleteFile(file: FolderContent) {
this.$emit("showModalDelete", [file]);
},
/**
* Asynchronously opens the specified file, fetching its blob content and triggering a download.
* @async
* @param {FileInformation} file - Information about the file to be opened.
*/
async openFile(file: FileInformation) {
if (this.project?.id && this.resource?.id) {
// Download the file as a blob (binary data)
await this.resourceStore.download(
this.project.id,
this.resource.id,
file.path,
file.name,
);
}
},
/**
* Asynchronously opens the given folder, fetching its file tree and metadata tree.
* @async
* @param {FolderInformation} folder - Information about the folder to be opened.
*/
async openFolder(folder: FolderInformation) {
if (!this.project?.id || !this.resource?.id) return;
this.isBusy = true;
// Empty file list, since the context changes
// (Other workaround would be to keep the editing files open)
this.emitEmptyFileList();
// Determine the path of the folder and fetch its file and metadata trees
const path = this.determinePath(folder);
const [fileTree, metadataTree] = await Promise.all([
this.resourceStore.getFileTree(this.project.id, this.resource.id, path),
this.resourceStore.getMetadataTree(
this.project.id,
this.resource.id,
path,
),
]);
// Create a temporary folder to store the entries
const folderContents = await this.constructFolderContents(
fileTree,
metadataTree,
);
this.isBusy = false;
this.$emit("folderContents", folderContents);
},
/**
* Emits an event to notify of an empty file list.
*/
emitEmptyFileList() {
this.$emit("fileListEdit", []);
},
/**
* Determines the correct path for the folder.
* @param {FolderInformation} folder - Information about the folder for which path needs to be determined.
* @returns {string} The determined path.
*/
determinePath(folder: FolderInformation): string {
return folder.path; // !== "" ? folder.path : "/";
},
/**
* Constructs the folder content based on the provided file and metadata trees.
* @async
* @param {FileTreeDto[] | null | undefined} fileTree - File tree of the folder.
* @param {MetadataTreeDto[] | null | undefined} metadataTree - Metadata tree of the folder.
* @returns {FolderContent[]} An array containing folder content.
*/
async constructFolderContents(
fileTree: FileTreeDto[] | null | undefined,
metadataTree: MetadataTreeDto[] | null | undefined,
): Promise<FolderContent[]> {
const folderContents = [];
if (this.dirCrumbs.length > 0) {
// Add navigate up folder entry (..)
folderContents.push(this.createNavigateUpFolder());
}
// Iterate over the file tree and map the entries for the files view table
for (const fileEntry of fileTree || []) {
const content = this.mapFileEntryToContent(fileEntry);
// Don't add hidden files/folders
if (content && !fileEntry.hidden) {
const metadataEntry = metadataTree?.find(
(md) => md.path === fileEntry.path,
);
if (metadataEntry?.version) {
const version = Number(metadataEntry.version);
content.latestVersion = version;
content.currentVersion = version;
}
if (
metadataEntry?.definition?.type &&
metadataEntry?.definition?.content
) {
content.metadata[content.latestVersion] = await parseRDFDefinition(
metadataEntry.definition.content,
metadataEntry.definition.type,
);
}
if (metadataEntry?.availableVersions) {
content.versions = metadataEntry.availableVersions.map((x) =>
Number(x),
);
}
folderContents.push(content);
}
}
return folderContents;
},
/**
* Creates a representation for the "navigate up" folder.
* @returns {FolderContent} A representation of the "navigate up" folder.
*/
createNavigateUpFolder(): FolderContent {
// Make a shallow copy of the array and copy all but the last element as it is the current folder itself
const currentDirCrumbs = this.dirCrumbs.slice(0, -1);
const path = currentDirCrumbs.join("");
const identifier = uuidv4();
const version = +new Date();
return {
id: identifier,
name: "..",
path: path,
readOnly: true,
type: "Tree",
parentDirectory: "",
versions: [version],
latestVersion: version,
currentVersion: version,
metadata: { [version]: factory.dataset() as unknown as Dataset },
} satisfies ReadOnlyFolderInformation;
},
/**
* Maps a file entry to its corresponding folder content.
* @param {FileTreeDto} fileEntry - The file data transfer object to map.
* @returns {FolderContent | undefined} The corresponding folder content or undefined if the type is unknown.
*/
mapFileEntryToContent(fileEntry: FileTreeDto): FolderContent | undefined {
let content: FolderContent | undefined = undefined;
switch (fileEntry.type) {
case "Tree":
content = treeMapper.map(TreeDto2FolderInformation, fileEntry);
break;
case "Leaf":
content = treeMapper.map(TreeDto2FileInformation, fileEntry);
break;
default:
console.error("Unknown content type: ", fileEntry.type);
break;
}
return content;
},
/**
* Handles when a row is selected, updating the UI and emitting necessary events.
* @param {FolderContent[]} items - Selected items.
*/
onRowSelected(items: FolderContent[]) {
this.$emit("showDetail", items.length > 0);
this.selectAll = items.length === this.folderContents.length;
// Filter out read-only entires like the navigation up folder ".."
const selectedFiles = items.filter((item) => !item.readOnly);
this.updateFileListEdit(selectedFiles);
},
/**
* Selects or deselects all rows.
* @param {BTable} row - Reference to the table.
*/
allSelect(row: BTable) {
if (this.selectAll) {
row.selectAllRows();
} else {
row.clearSelected();
}
},
/**
* Handler for the filtered elements
* @param elements filtered elements
*/
filtered(elements: FolderContent[]) {
this.currentPage = 1;
this.filteredRows = elements.length;
},
/**
* Toggles the selection of a specific row.
* @param {{ rowSelected: boolean; index: number }} row - Info about the row's status and its index.
*/
select(row: { rowSelected: boolean; index: number }) {
const tableRef = this.$refs.adaptTable as BTable;
row.rowSelected
? tableRef.selectRow(row.index)
: tableRef.unselectRow(row.index);
},
/**
* Emits an event for file selection.
*/
clickFileSelect() {
this.$emit("clickFileSelect");
},
/**
* Updates the file list for editing based on current selections.
*/
async updateFileListEdit(selectedFiles: FolderContent[]) {
const existingFilesSet = new Set(this.fileListEdit.map((f) => f.name));
const newFileListEdit: FileInformation[] = [];
for (const file of selectedFiles) {
// Check if file already exists in the list.
// Keep already entered metadata values, if another entry selected.
if (
existingFilesSet.has(file.name) &&
this.dirTrail === file.parentDirectory
) {
const existingFile = this.fileListEdit.find(
(f) => f.name === file.name,
);
if (existingFile) {
newFileListEdit.push(existingFile as FileInformation);
}
continue;
}
// If file doesn't exist, fetch metadata and add
const loadedMetadata = await MetadataManagerUtil.loadMetadataForFile(
file,
this.project,
this.resource,
);
const version = file.latestVersion;
newFileListEdit.push({
...file,
uploading: false,
metadata: { [version]: loadedMetadata },
info: file.type === "Tree" ? undefined : file.info,
size: file.type === "Tree" ? 0 : file.size,
} as FileInformation);
}
this.$emit("fileListEdit", newFileListEdit);
this.$emit("showDetail", selectedFiles.length > 0);
},
/**
* Converts last modified date into a locale string format.
* @param {FolderContent} item - Item to retrieve the date from.
* @returns {string} - Formatted date string.
*/
renderDate(item: FolderContent): string {
return item.lastModified
? new Date(item.lastModified).toLocaleDateString(this.$i18n.locale)
: "";
},
/**
* Converts size into a readable string format.
* @param {FolderContent} item - Item to retrieve the size from.
* @returns {string} - Formatted size string.
*/
renderSize(item: FolderContent): string {
return item.type === "Leaf" ? FileUtil.formatBytes(item.size) : "";
},
},
});
</script>
<style>
#resourceViewTable thead th:first-of-type div {
display: inline;
position: relative;
margin-left: 5px;
}
#resourceViewTable thead th:first-of-type div label {
cursor: pointer;
}
#resourceViewTable thead th:last-of-type {
text-align: center;
}
#resourceViewTable td,
#resourceViewTable th {
vertical-align: middle;
cursor: initial;
white-space: nowrap;
}
#resourceViewTable td,
#resourceViewTable tr td:not(:first-child):not(:last-child),
#resourceViewTable tr th:not(:first-child):not(:last-child) {
position: relative;
}
#resourceViewTable td a {
padding-left: 14px;
padding-right: 30px;
vertical-align: middle;
}
#resourceViewTable tr td:nth-child(2),
#resourceViewTable tr th:nth-child(2) {
max-width: 170px !important;
min-width: 170px !important;
width: 170px !important;
}
#resourceViewTable tr td:last-child,
#resourceViewTable tr th:last-child {
max-width: 55px !important;
min-width: 55px !important;
width: 55px !important;
}
#resourceViewTable tr td:not(:first-child):not(:last-child),
#resourceViewTable tr th:not(:first-child):not(:last-child) {
max-width: 200px;
text-overflow: ellipsis;
overflow: hidden;
}
#resourceViewTable i,
#resourceViewTable input,
#resourceViewTable div.tableCheck.custom-checkbox label,
.checkFile {
vertical-align: top;
}
</style>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#resourceViewTable {
overflow: visible;
}
#resourceViewTable :deep(.btn-link) {
padding-left: 5px !important;
padding-right: 5px !important;
background: none !important;
min-width: auto;
}
#resourceViewTable th:last-of-type,
.center-align {
text-align: center;
}
#resourceViewTable th > i {
position: absolute;
right: 10px;
top: 8px;
}
#resourceViewTable th .additionalColumnHeader {
margin-right: 20px;
}
.adaptTable {
overflow: auto;
position: absolute;
top: 65px;
bottom: 0px;
left: 10px;
right: 10px;
width: auto;
}
.fileViewEntryWrapper {
align-items: center;
}
.fileViewEntry {
padding: 0.05rem 0.5rem;
}
a.fileViewEntry {
cursor: pointer;
}
.DataSource {
height: inherit;
margin-top: 7px;
}
.tableCheck {
text-align: center;
margin-right: -0.5em;
position: absolute;
top: 15px;
left: 17px;
cursor: pointer;
}
#pagination {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
#pagination :deep(li.page-item.active button) {
z-index: 0;
}
#resourceViewTable .tableCheck,
#resourceViewTable .dotMenu {
visibility: hidden;
}
#resourceViewTable tr:hover .tableCheck,
#resourceViewTable tr:hover .dotMenu,
#resourceViewTable .b-table-row-selected .tableCheck {
visibility: visible;
}
.leftButton {
height: calc(1.4em + 0.75rem + 2px);
}
</style>